Metadata-Version: 2.5
Name: cmpnd
Version: 0.102.5
Summary: DSPy observability and deployment SDK for cmpnd
Project-URL: Homepage, https://cmpnd.ai
Author-email: cmpnd <hello@cmpnd.ai>
License: MIT
Keywords: dspy,llm,observability,tracing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: dspy<4,>=3.3.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.0
Requires-Dist: sniffio>=1.3.0
Requires-Dist: typing-extensions>=4.1.0
Requires-Dist: xxhash>=3.0.0
Provides-Extra: dev
Requires-Dist: basedpyright>=1.39; extra == 'dev'
Requires-Dist: boto3>=1.34; extra == 'dev'
Requires-Dist: gepa; extra == 'dev'
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: ipython>=8.20; extra == 'dev'
Requires-Dist: jsonschema>=4.0; extra == 'dev'
Requires-Dist: litellm; extra == 'dev'
Requires-Dist: litellm<1.102; (python_version >= '3.15') and extra == 'dev'
Requires-Dist: numpy>=2.3; extra == 'dev'
Requires-Dist: pandas>=2.2; extra == 'dev'
Requires-Dist: pillow>=10.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-rerunfailures>=14.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Requires-Dist: twine>=7.0; extra == 'dev'
Provides-Extra: fake-provider
Requires-Dist: cmpnd-fake-provider; extra == 'fake-provider'
Provides-Extra: images
Requires-Dist: pillow>=10.0; extra == 'images'
Provides-Extra: mcp
Requires-Dist: dspy>=3.3.1; extra == 'mcp'
Requires-Dist: mcp>=1.9; extra == 'mcp'
Provides-Extra: shell
Requires-Dist: ipython>=8.20; extra == 'shell'
Description-Content-Type: text/markdown

# cmpnd

The `cmpnd` package traces, deploys and optimizes DSPy programs on the cmpnd
platform. You give a program a name, and a client records its traces,
deployments and optimization runs under that name.

## Installation

```bash
uv add cmpnd      # or: pip install cmpnd
```

The interactive `cmpnd` shell uses IPython when you install the `shell` extra:
`uv add 'cmpnd[shell]'` or `pip install 'cmpnd[shell]'`. The plain install
leaves IPython out, and the shell then runs on the standard Python prompt.

## Two independent ways to use the SDK

The SDK has two ways to use it, and they are independent:

- `cmpnd.Client` is for a program you keep. A client holds its own account,
  tags and providers. It runs, deploys and optimizes programs, and it looks
  up your organization's providers.
- `cmpnd.configure()` with `cmpnd.auto_instrument()` is for sketching. It
  traces every DSPy call in the process.

The two do not share settings, and neither changes the other. A client never
reads `cmpnd.configure()`, and `cmpnd.configure()` never changes a client. Both
can run in one process. Pick the way that fits the job, and do not expect a
setting from one to apply to the other.

## Authentication

The `cmpnd` library works with the cmpnd service at
[platform.cmpnd.ai](https://platform.cmpnd.ai). Create an API key on the
platform under Settings → API keys. A client takes the key as
`cmpnd.Client(api_key=)`. The sketching path takes it as
`cmpnd.configure(api_key=)`. Each way reads only its own argument, and both
use `CMPND_API_KEY` when the argument is missing. For a self-hosted cmpnd
install, also pass the install's URL as `endpoint=` to the same call, or set
`CMPND_ENDPOINT`.

With no `api_key=` and no `CMPND_API_KEY`, the library uses the key that
`cmpnd login` stored for the endpoint, as the command line does. That is the
active organization's key (`cmpnd org switch`). A script on a machine where
you ran `cmpnd login` needs no key of its own.

## Quick start

A program name is the name your organization gives a program, such as
`question-answerer`. Traces, deployments and optimization runs under one
program name go to one program. The platform makes the program the first time
your organization uses the program name.

`@cmpnd.program(name)` above a `dspy.Module` class gives the class a program
name. Building the class takes its own arguments and a required `lm=`. The
`lm=` argument is the `dspy.LM` that the program runs on:

```python
import cmpnd
import dspy

class Answer(dspy.Signature):
    """Answer the question in one short sentence."""
    question: str = dspy.InputField()
    answer: str = dspy.OutputField()

@cmpnd.program("question-answerer")
class QA(dspy.Predict):
    def __init__(self):
        super().__init__(Answer)

qa1 = QA(lm=dspy.LM("openai/gpt-6-luna"))
```

A `cmpnd.Client` runs the program and records its trace under the program
name:

```python
gepa = dspy.GEPA(metric=metric, reflection_lm=dspy.LM("openai/gpt-6-luna"), auto="light")

with cmpnd.Client(api_key="ck_xxx", project_tags="my-project") as sdk:
    # the trace goes to the program "question-answerer"
    result = sdk.run_program(qa1, question="What is DSPy?")
    qa2 = sdk.optimize_program(qa1, gepa, trainset=train, valset=val)

cmpnd.trace_id(result)  # "a1b2c3d4-...", the trace of this run
```

`cmpnd.trace_id()` links a result to its platform trace, for example to attach
feedback later. A client records only the programs you run through it.

A client is active from construction. The `with` form flushes the client's
traces and closes it on exit. Without `with`, the client stays active until
you call `close()` or the process exits, and its traces go out at exit:

```python
sdk = cmpnd.Client(api_key="ck_xxx", project_tags="my-project")
sdk.run_program(qa2, question="What is DSPy?")

# optional: flushes the traces now, so a later read sees them
sdk.close()
```

The `cmpnd.Program` constructor gives a program name to a module you already
have, such as a `dspy.Predict`. It makes the same kind of
program as the decorator:

```python
qa3 = cmpnd.Program(
    "question-answerer", module=dspy.Predict("question -> answer"), lm=dspy.LM("openai/gpt-6-luna")
)
```

- A program name starts with an ASCII letter or digit, then has letters,
  digits, `.`, `_` or `-`, at most 64 characters. It does not start with `p_`.
  A bad name raises `ValueError`. Declaring a program makes no network call.
- `optimize_program` runs the optimizer in your process and returns a copy of
  the program that holds the optimized module. The copy keeps the program name
  and the LM.

## Sketching with `auto_instrument()`

`cmpnd.auto_instrument()` records a trace of every DSPy call in the process,
with no other change to your code. Use it to sketch a
[DSPy program](https://dspy.ai/) and see what it does:

```python
cmpnd.configure(api_key="ck_xxx", project_tags="my-project")
cmpnd.auto_instrument()

dspy.configure(lm=dspy.LM("openai/gpt-6-luna"))
dspy.ChainOfThought("question -> answer")(question="What is DSPy?")
```

Each program shape gets a program of its own, under a name the SDK generates
from its class and signature, such as `ChainOfThought-17e1f4089a24`. These
programs do not count toward your plan's program limit. The first such trace in
a process logs a warning that names the generated name and shows how to give the
program a real one: `@cmpnd.program(name)` or `cmpnd.Program(name, module=..., lm=...)`,
run through `cmpnd.Client`. Calling a `cmpnd.Program` directly while `auto_instrument()` is
on raises a `TypeError` that points to the client. The API reference below
lists the options of `cmpnd.configure()`.

## Deployed programs

Every deployment belongs to a program, and each program has a program name.
Give a DSPy module a program name with `cmpnd.Program`, then deploy it through a
client for server-side execution:

```python
import cmpnd
import dspy

sdk = cmpnd.Client()  # CMPND_API_KEY, else your cmpnd login
openai = sdk.get_provider("openai")  # a provider your organization added

qa4 = cmpnd.Program(
    "question-answerer", module=dspy.Predict("question -> answer"), lm=dspy.LM("openai/gpt-6-luna")
)
deployed = sdk.deploy_program(qa4, provider=openai)

result = sdk.run_deployment(deployed, question="What is DSPy?")  # runs server-side
print(result.answer)
```

`deploy_program()` packages the module's source, sends it to the platform and
waits until the platform has read it. It then returns a `DeployedProgram`.
`run_deployment()` runs it server-side on the client's account, and also takes
a deployment ID. A `DeployedProgram` is not callable, because a run needs a
client. `str(deployed)` and `deployed.deployment_id` return the deployment ID.
`deployed.artifact_metadata` holds the typed artifact record when the platform
has inspected it. An unknown interface leaves `input_fields` and
`output_fields` as `None`. A known interface with no fields returns empty
lists.

The platform makes the program the first time your organization uses the
program name. Later deploys under the same program name go to the same
program, and so do the traces of a client that runs it.

### Providers

A provider is a model provider's key that your organization sealed under a
name. A hosted program spends a provider, and the deploy names it. The program's code stays the code you run on your laptop:

```python
lm = dspy.LM("openai/deepseek-4.1-flash", api_base="https://gw.lunaroute.com/v1")  # unchanged

luna = sdk.get_provider("luna-exec")
deployed = sdk.deploy_program(program, provider=luna)
# deploy: default-lm: chat_completions openai/deepseek-4.1-flash → luna-exec (https://gw.lunaroute.com)
```

- `sdk.get_provider(name)` returns a `cmpnd.Provider` holding the name,
  the protocols its endpoint serves and its `api_base`. It holds no key. A name
  the organization does not hold raises `cmpnd.ProviderNotFound`, which lists
  the names it does hold and the nearest one. `sdk.list_providers()`
  returns them all.
- Both read the client's own account, the one it deploys and optimizes on.
- `provider=` takes a `cmpnd.Provider`, never a string. A string raises
  and shows the `client.get_provider(...)` line to write.
- These older spellings still work and warn with a `DeprecationWarning`:
  `credential=` (use `provider=`), `cmpnd.Credential` and
  `cmpnd.CredentialNotFound` (the same classes as `cmpnd.Provider` and
  `cmpnd.ProviderNotFound`), and `cmpnd.credentials.get(name)` and
  `cmpnd.credentials.list()`, which read the `cmpnd.configure()` account.
  Replace each of these calls with `sdk.get_provider(name)` or
  `sdk.list_providers()` on your client. Passing both `provider=` and
  `credential=` raises `TypeError`.
- An organization may have no default provider. Then every deploy and every
  optimization names its provider. The platform refuses one that names none,
  and the refusal lists the names the organization holds. An owner sets a
  default with `cmpnd providers default NAME`, and a deploy without
  `provider=` spends it.
- The deployment records the provider's name. Its runs spend the newest
  key sealed under that name, so `run_deployment()` takes no provider,
  and an owner who rotates a key needs no redeploy.
- The deploy prints the protocol, model and provider of each binding, as
  above. A server that prints none chose the provider from the model's
  provider prefix, and the SDK warns.
- A hosted call goes to the provider's `api_base`. The LM's own `api_base`
  serves your laptop run and is not sent.

Keys never leave your laptop. The SDK sends no key on the LM, and a key reaches
the platform only when an owner adds a provider, with `cmpnd providers add`,
`sdk.add_provider` or the providers page. `cmpnd deploy FILE`
refuses source whose import builds an LM carrying a key, and names the module,
the model and the field.

### Gateways

An OpenAI-compatible gateway, such as LunaRoute, is a provider of its own.
Add it with the gateway's provider type, or with `--type openai-compatible` and
the gateway's address:

```bash
cmpnd providers add luna-exec --type lunaroute < key.txt
cmpnd providers add my-gateway --type openai-compatible --base-url https://gateway.example.com/v1 < key.txt
```

Spell the gateway's models `openai/<model>`, such as
`openai/deepseek-4.1-flash`, and deploy with
`provider=sdk.get_provider("luna-exec")`. The `openai/` prefix selects
the Chat Completions wire protocol, and the gateway receives the model after
the first `/`. The provider decides the address and the key, so one gateway
serves every model it routes.

### The same source on your laptop and hosted

A program's logic runs unchanged on your laptop and hosted. On your laptop it
spends the key in your environment. Hosted, it spends a provider your
organization added. Keep the logic in one file and let a small entry point
choose where it runs.

The program file holds the logic and names its model. It holds no key:

```python
# qa.py
import dspy

lm = dspy.LM("openai/gpt-5-mini")  # no api_key: litellm reads OPENAI_API_KEY on your laptop
program = dspy.Predict("question -> answer")


def metric(gold, pred, trace=None, pred_name=None, pred_trace=None, lms=None):
    verdict = dspy.Predict("answer, gold -> correct: bool")(answer=pred.answer, gold=gold.answer, lm=lms["critic"])
    return dspy.Prediction(score=float(verdict.correct), feedback="correct" if verdict.correct else "wrong")
```

Add the key once for the organization:

```bash
printf '%s' "$OPENAI_API_KEY" | cmpnd providers add openai
```

`sdk.add_provider("openai", api_key=os.environ["OPENAI_API_KEY"])` does the same from Python.

The entry point is the only code that knows where the run happens:

```python
# main.py
import functools
import sys

import cmpnd
import dspy

from qa import lm, metric, program

HOSTED = "--hosted" in sys.argv

sdk = cmpnd.Client()
qa = cmpnd.Program("qa", module=program, lm=lm)
reflection = dspy.LM("openai/gpt-5", max_tokens=32000)
lms = {"critic": dspy.LM("openai/gpt-5-mini")}
train, val = load_examples()  # your dspy.Example lists

if HOSTED:
    openai = sdk.get_provider("openai")
    deployed = sdk.deploy_program(qa, provider=openai)
    print(sdk.run_deployment(deployed, question="What is DSPy?"))
    best = sdk.optimize_deployment(
        deployed,
        trainset=train,
        valset=val,
        metric=metric,  # the platform hands it lms
        provider=openai,  # every model below spends it
        reflection_lm=cmpnd.HostedLM(reflection),
        lms={name: cmpnd.HostedLM(model) for name, model in lms.items()},
        auto="light",
    )
else:
    print(sdk.run_program(qa, question="What is DSPy?"))  # spends $OPENAI_API_KEY
    gepa = dspy.GEPA(metric=functools.partial(metric, lms=lms), reflection_lm=reflection, auto="light")
    best = sdk.optimize_program(qa, gepa, trainset=train, valset=val)
```

`python main.py` runs everything on your laptop, and `python main.py --hosted`
runs it on the platform. `cmpnd deploy qa.py qa --provider openai` deploys
the same file from the terminal.

| | On your laptop | Hosted |
|---|---|---|
| Key | the provider's variable in your environment, such as `OPENAI_API_KEY` | the credential the call names, else the organization's default, if one is set |
| Address | the LM's `api_base`, else the provider's endpoint | the credential's `api_base` |
| Model | `gpt-5-mini`, the model after its first `/` | `gpt-5-mini` |
| The metric's `lms` | the dict the entry point binds | the run's `lms=`, which the platform hands it |

These rules keep the program file portable:

- **Leave the key off the program's LM** and let litellm read the provider's
  own variable. For a key in a variable litellm does not read, write
  `api_key=os.environ.get("NAME")`. cmpnd imports a deploy file with an empty
  environment, so the key is `None` there and present on your laptop.
- **Never write a key into the file, and never read it as
  `os.environ["NAME"]` at module scope.** The deploy refuses the first. The
  second raises `KeyError` hosted, where the environment holds no key.
- **Seal the credential at the address your laptop LM reaches.** A program
  that sets `api_base="https://gw.lunaroute.com/v1"` needs a credential sealed
  there, with `--preset lunaroute` or `--api-base`. Hosted calls go to the
  credential's address and ignore the LM's `api_base`. The deploy prints the
  address it chose, so you can compare the two.
- **Wrap a model you hand the hosted optimization in `cmpnd.HostedLM`.** The
  entry point builds `reflection_lm=` and each `lms=` entry, so those LMs may
  hold a key for the laptop run. The wrapper leaves that key on your machine.
  `optimize_deployment` refuses a key on an unwrapped LM, because there it would
  look spent. `cmpnd.HostedLM(model, provider=c)` makes a model spend a
  credential other than the run's.

A judge built on `dspy.experimental.TypeSafe` also runs unchanged in both
places. [`docs/sdk-instrumentation.md`](docs/sdk-instrumentation.md), under
"System One judges", describes the hosted side.

### Options

```python
sdk.deploy_program(
    program,           # required: a cmpnd.Program
    metric=None,       # optional: a metric callable, packaged for optimization
    timeout=300,       # optional: seconds to wait for the deployment to be ready
    metadata=None,     # optional: a JSON-serializable dict, at most 64 KiB, stored with the deployment
    provider=None,   # a cmpnd.Provider the program's model spends, else the org's default, if one is set
)
```

### How it works

1. The SDK packages the module's source, including its LM configuration,
   signature and types.
2. The platform reads the program, the model string and the sampling settings
   from that source.
3. `deploy_program()` returns the `DeployedProgram` when the deployment is
   ready.

## Optimization on the platform

`optimize_deployment()` optimizes a deployment through the platform. It returns
the best deployment as a `DeployedProgram`, whose `score` is the best score
found. Deploy the program first:

```python
import cmpnd
import dspy

sdk = cmpnd.Client()  # CMPND_API_KEY, else your cmpnd login
openai = sdk.get_provider("openai")

program = cmpnd.Program(
    "haiku", module=dspy.ChainOfThought("topic -> haiku"), lm=dspy.LM("openai/gpt-6-luna")
)
deployed = sdk.deploy_program(program, provider=openai)

# GEPA needs a five-argument feedback metric that returns dspy.Prediction(score=, feedback=)
def metric(example, pred, trace=None, pred_name=None, pred_trace=None):
    ok = example.season.lower() not in pred.haiku.lower()
    return dspy.Prediction(
        score=float(ok), feedback=None if ok else "Don't name the season."
    )

best = sdk.optimize_deployment(
    deployed,
    optimizer="gepa",
    trainset=train,
    valset=val,
    metric=metric,
    provider=openai,
    reflection_lm=dspy.LM("openai/gpt-6-luna"),
    auto="light",
)

print(best.score)
print(sdk.run_deployment(best, topic="late autumn").haiku)
```

`optimize_deployment()` also takes the ID of a ready deployment. The
optimization belongs to the deployment's program, and the program's model comes
from the deployment.

`labels=` labels every trace the run produces, its rollouts, metric calls and
reflections, so `sdk.list_traces(labels={"experiment": "tone-v3"})` finds them:
`labels={"experiment": "tone-v3", "teams": ["search"]}`. They follow the rules
of a run's labels: the SDK refuses a reserved `cmpnd.*` or `dspy.*` key and a
label past a cap before any network call.

`reflection_lm` is the model that proposes each new instruction. The search is
only as good as this model, so it is usually a strong model and not the one
being optimized. A `dspy.LM` passes its sampling settings to the provider. A
bare model name gives the same model with no sampling settings. A run spends
sealed credentials only, so the SDK refuses a key on the LM before any network
call. `cmpnd.HostedLM(lm)` keeps a key your laptop runs spend on this machine
and passes the LM's model and sampling.

`provider=` names the run's provider, which every binding that names none
spends: the program's model, the reflection model and each entry of `lms`. A
binding names its own provider through a wrapper for its protocol:

```python
luna = sdk.get_provider("luna-exec")      # serves chat_completions and system_one
openai = sdk.get_provider("openai")
best = sdk.optimize_deployment(
    deployed, trainset=train, valset=val, metric=metric,
    provider=luna,
    reflection_lm=cmpnd.HostedLM(dspy.LM("openai/gpt-5"), provider=openai),
    lms={"judge": cmpnd.SystemOne("lunaroute/djev", provider=luna)},
    auto="light",
)
```

`cmpnd.HostedLM(lm, provider=c)` wraps a chat LM, and
`cmpnd.SystemOne(model, provider=c)` declares a System One model. A binding
without a credential spends the run's, else the organization's default, if one
is set. The platform refuses a credential whose endpoint does not serve a
binding's protocol, and names the `provider=` to add.

The SDK checks the metric before any network call. GEPA writes each new
instruction from the textual feedback the metric returns, so it needs a
five-argument feedback metric. The SDK refuses a metric with fewer than five
positional arguments, because such a run would report success and learn
nothing. A `*args` metric cannot be checked, and the SDK accepts it with a
warning.

`gepa` is the default and the only optimizer the platform runs.
`optimize_deployment()` raises `ValueError` for the retired `bootstrap_fewshot`
and `random_search` before any network call, and the platform answers them with
a 404 that names gepa.

### A metric with its own model

A metric reaches a model the same way the program's predictors do. A metric
that builds a `dspy.Predict` runs on the program's own model. A metric that
needs a different model, such as a stronger judge, declares it in the
optimize call's `lms` argument and selects it by name through a sixth
parameter, `lms`:

```python
def faithfulness(gold, pred, trace, pred_name, pred_trace, lms=None):
    """Does the summary say only what the document says?"""
    verdict = dspy.Predict("document, summary -> score: float, feedback: str")(
        document=gold.document,
        summary=pred.summary,
        lm=lms["critic"],
    )
    return dspy.Prediction(score=verdict.score, feedback=verdict.feedback)


best = sdk.optimize_deployment(
    deployed, optimizer="gepa", trainset=train, valset=val, metric=faithfulness,
    provider=openai,
    reflection_lm=dspy.LM("openai/gpt-6-luna"),
    lms={"critic": dspy.LM("openai/gpt-6-luna")},
    max_metric_calls=300,
)
```

`lms` declares any model that is not one of the program's predictors. The
rules:

- A name the run did not declare raises at the metric and lists the declared
  names.
- `"default-lm"` is reserved for the program's own model, which comes from the
  deployment. Declaring it raises an error that names it.
- The platform refuses a binding it finds no credential for when it dispatches
  the run, before the run starts. These LMs, like `reflection_lm`, must not
  carry a key.

The `lms` parameter does not change the arity check. Five or more positional
arguments make a feedback metric, and a metric without `lms` never receives
it.

### Options

```python
sdk.optimize_deployment(
    deployed,                # required: a DeployedProgram or a deployment ID
    optimizer="gepa",        # optional: the only optimizer the platform runs
    trainset=train,          # required: training examples
    valset=val,              # required: validation examples
    metric=metric,           # required: the metric, arity-checked
    config=None,             # optional: optimizer config, allow-listed by the platform
    reflection_lm=None,      # required: a dspy.LM, a bare model name or a cmpnd.HostedLM
    lms=None,                # optional: name -> dspy.LM, cmpnd.HostedLM or cmpnd.SystemOne
    provider=None,         # the cmpnd.Provider every other binding spends, else the org's default, if one is set
    max_metric_calls=None,   # budget: the metric-call ceiling for the run
    auto=None,               # budget: "light", "medium" or "heavy"
    may_author_code=None,    # optional: False keeps each dspy.Flex at its deployed source
    seed=0,                  # optional: the search's random seed
    poll_interval=3.0,       # optional: seconds between status checks
    timeout=600.0,           # optional: seconds to wait for the run
)
```

Pass exactly one budget: `max_metric_calls`, `auto` or
`config["max_full_evals"]`. The SDK raises before any network call when the
budget is missing.

`optimize_deployment()` raises `cmpnd.OptimizeError` when the platform answers a step's
initiation with a 4xx, such as a 409 for missing data or a metric mismatch. It
raises `cmpnd.OptimizeFailed` when a step reaches `failed`.

The result's `optimization_run_id` names the run that produced it.
`continue_optimization()` starts a new run from one of that run's candidates,
reusing its data, with a budget of its own:

```python
better = sdk.continue_optimization(best, max_metric_calls=120)  # from the best candidate

for candidate in sdk.list_candidates(best.optimization_run_id):  # read each one first
    print(candidate["candidate_id"], candidate["aggregate_score"], candidate["instructions"])
other = sdk.continue_optimization(best, candidate="ca_...", auto="light")
```

`model=` runs the candidate on another student model, and `provider=` names
the credential that model spends. A loop over models is a model sweep:

```python
for model in ["openai/gpt-4o-mini", "openai/gpt-4.1-nano"]:
    swept = sdk.continue_optimization(best, model=model, auto="light")
    print(model, swept.score)
```

A refusal raises `cmpnd.OptimizeError`, whose `error_code` says why, such as
`not_completed`, `unknown_candidate` or `no_credential`.

## Command line

The package installs the `cmpnd` command. `cmpnd login` saves your API key for
the command line. `cmpnd -h` lists the commands, and `cmpnd COMMAND -h`
describes one:

```
usage: cmpnd [-h] [--version] [--api-key API_KEY] [--endpoint ENDPOINT]
             {auth,login,logout,org,credentials,tool-servers,optimizations,programs,traces,evals,datasets,deployments,ps,status,logs,inspect,schema,sample,run,deploy,optimize}
             ...

CMPND platform CLI — deploy, run, and inspect DSPy programs.

options:
  -h, --help           show this help message and exit
  --version            show program's version number and exit
  --api-key API_KEY    API key (or set CMPND_API_KEY)
  --endpoint ENDPOINT  API endpoint (or set CMPND_ENDPOINT)
```

Deploy a source file under a program name, or optimize the program a file
declares:

```bash
cmpnd deploy program.py question-answerer --provider openai
cmpnd optimize gepa task.py haiku --provider openai --reflection-model openai/gpt-6-luna --auto light
```

`cmpnd deploy` uploads the file as written. `cmpnd optimize` reads `program`,
`trainset`, `valset` and `metric` from the file, deploys the program under the
program name and prints the best deployment ID. `--provider` names the
provider the program's models spend, and `cmpnd providers list --text` shows
the names your organization holds.

`cmpnd programs rename PROGRAM NAME` gives a program a new name, or names one
that has none. PROGRAM is a program name or a `p_` id, and the program keeps
its id and its history. The old name is free at once, so update the code that
declares it.

A program calls tools on an MCP server once your organization seals the
server's key, registers the server and allows its tools:

```bash
cmpnd credentials create --kind mcp --server https://mcp.exa.ai/mcp --name exa-prod \
    --placement query_param --query-param-name exaApiKey < exa-key.txt
cmpnd tool-servers create --url https://mcp.exa.ai/mcp --credential exa-prod
cmpnd tool-servers allow <server_id> web_search_exa
```

[`docs/cli.md`](docs/cli.md) describes every command, including "Tool
servers".

## Custom spans

Trace non-DSPy code with the decorator:

```python
from cmpnd import trace, SpanType

@trace(name="fetch_documents", span_type=SpanType.RETRIEVE)
def fetch_documents(query: str) -> list[str]:
    return documents
```

Or with the context manager:

```python
from cmpnd import start_span, SpanType

def run_pipeline(query: str):
    with start_span("vector_search", span_type=SpanType.RETRIEVE) as span:
        span.set_attribute("index", "my-faiss-index")
        docs = search(query)
        span.set_outputs({"doc_count": len(docs)})

    return generate(query, docs)
```

## What gets traced

- Modules: the module type, signature name and instructions, input and output
  field names, and demo count.
- LM calls: the model name and provider, token usage and the request and
  response content.
- Adapters: format and parse operations.
- Tools: the tool name and description and each invocation's inputs and
  outputs.
- Evaluations: scores and the program under evaluation.

## Span types

`SpanType` has these members: `UNKNOWN`, `MODULE`, `PREDICT`,
`CHAIN_OF_THOUGHT`, `REACT`, `RLM`, `RETRIEVE`, `LM_CALL`, `ADAPTER_FORMAT`,
`ADAPTER_PARSE`, `TOOL`, `INTERPRETER_EXECUTE`, `SIGNATURE_VALIDATE`,
`DEMO_SELECT`, `OPTIMIZATION` and `EVALUATION`.

## API reference

### `cmpnd.configure()`

Sets the global API key, endpoint and options that `auto_instrument()` uses:

```python
cmpnd.configure(
    api_key="ck_xxx",                      # or CMPND_API_KEY, or your cmpnd login
    endpoint="https://platform.cmpnd.ai",  # optional, or CMPND_ENDPOINT
    project_tags="my-project",             # optional: one tag or a list of tags
    batch_size=100,                        # optional: spans per export batch
    flush_interval_seconds=5.0,            # optional: longest a running trace's spans wait to be sent
    max_queue_bytes=128 * 1024 * 1024,     # optional: memory unsent traces may hold, or CMPND_MAX_QUEUE_BYTES
    capture_inputs=True,                   # optional: record span inputs
    capture_outputs=True,                  # optional: record span outputs
    default_tags={"env": "prod"},          # optional: tags on every trace; a run's own tags override them
    labels={"env": "prod", "teams": ["search", "ranking"]},  # optional: labels on every trace
)
```

A label's value is a string or a list of strings. The SDK adds its own labels to
every trace, such as `dspy.module_class`, `dspy.version` and `cmpnd.sdk.version`,
and refuses a key under `cmpnd.` or `dspy.` from `labels=` with a `ValueError`.
`project` is yours to set, and defaults to the trace's `project_tags`. A trace
sends its tags, metadata and projects only as labels, so a tag or metadata key
under `cmpnd.` or `dspy.`, or one past a label cap, is dropped with a warning on
the `cmpnd` logger.

`api_key` and `endpoint` fall back to the `CMPND_API_KEY` and
`CMPND_ENDPOINT` environment variables, and `api_key` then to the key
`cmpnd login` stored for the endpoint. A call that passes only settings, such
as `cmpnd.configure(max_input_size=200_000)` in a program file, needs no key.
With no account configured or found, it returns `None` and keeps the settings
for the first `configure()` call that finds one. `project` is a deprecated alias for
`project_tags`. Passing both raises `ValueError`. `CMPND_MAX_INPUT_SIZE` and `CMPND_MAX_OUTPUT_SIZE` set the
captured input and output size limits, 65,536 bytes each by default.

### How much memory unsent traces may hold

Traces wait in memory until the server takes them, so an outage makes them pile
up. `max_queue_bytes` (or `CMPND_MAX_QUEUE_BYTES`) bounds the memory they may
hold, queued or still being sent, across every client of the process. Past it,
the SDK drops new records and says so in a warning that names the setting. It
drops spans first, and keeps an eighth of the budget for the records that start
and end traces. A trace whose start it sent always ends: when its finished body
no longer fits, the SDK sends the trace's outcome without its spans. `CMPND_MAX_QUEUE_BYTES` takes bytes or a binary unit, such as
`134217728` or `128MiB`, and both `cmpnd.configure()` and `cmpnd.Client()` read
it. A value that is neither raises a `SendingSettingsWarning` and leaves the
default.

The default, 128 MiB, is sized to bridge a two-minute outage for a program that
makes 50 calls a second of about 20 KB each: 120 s × 50/s × 20 KB = 120 MB. A
small Predict or ChainOfThought call sends 13 to 18 KB, but holds 39 to 45 KB of
Python objects while it waits, and the budget counts what it holds. For such a
program the default bridges about a minute. Raise it to bridge longer.
`max_queue_size`, 10,000 records in each of two queues, bounds the count too.

`configure()` and `cmpnd.Client()` also read the proxy and CA settings the SDK
sends traces and API calls with (`HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`, `SSL_CERT_FILE`,
`SSL_CERT_DIR`). They raise a `SendingSettingsWarning` for a setting that would
stop traces from reaching the server, such as a CA file that does not exist.

### `cmpnd.flush()`

Sends everything the process has queued, for every client, within a timeout.
Returns `True` only when the server accepted every record within the timeout,
and `False` when one timed out, was refused or was dropped. A record the SDK shed
because its queue was full counts as dropped, so a flush after a burst the SDK
could not hold returns `False`. Call it before a serverless handler
returns. `cmpnd.sender_health()` reports whether the background sender runs,
why it last failed to start, how many records it sent and dropped, and the bytes
unsent records hold against `max_queue_bytes`.

### Forking servers and serverless

The SDK sends traces from one background thread per process, with up to four
more that compress request bodies. The threads start on the process's first
trace, never at import or at `configure()`, and a trace never waits for them.
`configure()` and `cmpnd.Client()` import what the threads need and build the
TLS context they share, so the first trace pays for neither, and a server that
configures before it forks does that work once.

- **gunicorn, uWSGI, Celery prefork and `multiprocessing`.** Configure in the
  parent, before it forks, as `--preload` does. Each worker starts its own
  sender on its first trace. A worker never sends what its parent queued, and it
  never touches its parent's sockets. A worker that configures cmpnd
  itself, as a Celery `worker_process_init` handler does, reads its proxy
  settings from the environment; on macOS it skips the system proxy settings,
  and says so when they name a proxy the environment does not.
- **A process that forks to run another program**, as `subprocess`, `pty` and
  `multiprocessing` can, runs nothing of the SDK's in the child before the exec.
  So on macOS the child does not crash in CoreFoundation.
- **gevent and eventlet.** The sender runs on a real OS thread with its own
  event loop, so it never blocks the hub, and a greenlet that records never
  waits on a send. A gevent worker that patches `ssl` after its master
  configured cmpnd, as gunicorn's `-k gevent --preload` does, still sends over
  HTTPS. CI tests gevent. Eventlet support is best-effort and untested.
  Patch before the process records its first trace. The sender picks its
  selector when it starts, so a process that records and only then calls
  `monkey.patch_all()` keeps a sender that can lose the records after the
  patch. A forked worker starts a sender of its own, so gunicorn's
  `-k gevent --preload`, which patches in each worker, is unaffected.
- **Serverless functions.** The platform may freeze the process when the
  handler returns. Call `cmpnd.flush()` before it does.
- **A finished trace leaves at once.** The sender sends a trace as soon as it
  finishes, or as soon as the request before it returns, so an exit that skips
  the SDK's exit flush loses only the traces of about the last round trip.
- **At exit**, the SDK spends up to five seconds sending what is still queued.
  It does so in a `multiprocessing` child too, which leaves without running
  `atexit` handlers before Python 3.13. Two kinds of exit skip it:
  - `Pool.terminate()` kills the workers, and leaving a `with Pool()` block
    calls it. Call `pool.close()` and `pool.join()` first, or `cmpnd.flush()` at
    the end of the task.
  - Celery and billiard prefork children leave through `os._exit` and run no
    exit hook. Call `cmpnd.flush()` from a handler of Celery's
    `worker_process_shutdown` signal, or at the end of the task.
  - uvicorn re-raises the SIGTERM it stopped on once it has shut down, so its
    workers exit by the signal and run no exit hook. Flush in the app's
    lifespan shutdown:

    ```python
    from contextlib import asynccontextmanager

    import cmpnd
    from fastapi import FastAPI


    @asynccontextmanager
    async def lifespan(app: FastAPI):
        yield
        cmpnd.flush(5.0)


    app = FastAPI(lifespan=lifespan)
    ```

    The SDK does not install a SIGTERM handler of its own: a library's signal
    handler would replace the app's, and works only from the main thread.

### `cmpnd.auto_instrument()`

Registers the callback with DSPy, so every DSPy call records a trace. Each
trace goes to a program under a name the SDK generates for its root module.

### `cmpnd.Client()`

Takes most of the same arguments as `cmpnd.configure()` and keeps them apart from the
global config: a client never reads `cmpnd.configure()`. Use it as a context manager or
as a plain object.

- `run_program(program, **inputs)` runs a `cmpnd.Program` and records its trace
  under the program name.
- `optimize_program(program, optimizer, trainset=..., valset=...)` compiles a
  `cmpnd.Program` with a DSPy optimizer in your process, records every
  rollout, and returns a copy of the program that holds the optimized module.
  For `dspy.GEPA`, it also records the optimization run.
- `deploy_program(program)` deploys a `cmpnd.Program` under its program name
  and returns a `DeployedProgram`.
- `run_deployment(deployment, **inputs)` runs a deployment and returns a
  `dspy.Prediction`. The section below describes it.
- `optimize_deployment(deployment, ...)` optimizes a deployment on the platform
  and returns the best `DeployedProgram`, whose `.score` is the best score
  found. It raises `cmpnd.OptimizeError` or `cmpnd.OptimizeFailed` on failure.
- `list_candidates(run_id)` returns a run's candidates as dicts, each with its
  `ca_` id, score, parent and instructions.
- `continue_optimization(run, candidate=None, model=None, provider=None, ...)`
  runs a new optimization from one candidate of a completed run, with that
  run's data and a new budget, optionally on another student model, and returns
  its best `DeployedProgram`.
- `list_traces(**filters)` and `get_trace(trace_id)` read traces with the
  client's credentials. They flush the client's traces first.
- `list_deployments(status=None, since=None, limit=50, offset=0)` and
  `get_deployment(deployment_id)` read deployment records, as
  `cmpnd deployments list` and `show` do.
- `get_deployment_source(deployment_id, path=None)` returns the program file a
  deployment stores, as `cmpnd inspect --source` does. A continuation's
  deployment holds the program the continuation runs.
- `get_provider(name)` returns the organization's `cmpnd.Provider` under
  that name, and `list_providers()` returns them all, as
  `cmpnd providers list` does. Both read the client's own account.
- `add_provider(name, *, type=None, api_key, base_url=None, region=None)`,
  `rotate_provider_key(name, *, api_key)`, `set_default_provider(name)`,
  `clear_default_provider()` and `remove_provider(name)` change the
  organization's providers, as `cmpnd providers add`, `rotate`, `default` and
  `remove` do, for an owner or a key with `credentials:write`.
- `close()` flushes the client's traces and closes the client. A closed
  client raises `RuntimeError` on every method.

### `cmpnd.program()`

`@cmpnd.program(name)` above a `dspy.Module` class returns a `cmpnd.Program`
class with the same class name. Building it takes the class's own arguments
and a required `lm=` keyword. The decorator refuses a class whose
constructor already takes `lm`. Give an instance of it to `cmpnd.Program` instead.

### `cmpnd.Program()`

`cmpnd.Program(name, *, module, lm)` gives a DSPy module a program name and
the `dspy.LM` it runs on. `@cmpnd.program(name)` builds the same thing from a
`dspy.Module` class.

### `cmpnd.CmpndCallback`

The callback class, for manual registration:

```python
import dspy
import cmpnd

cmpnd.configure(api_key="ck_xxx")
dspy.configure(callbacks=[cmpnd.CmpndCallback()])
```

### `cmpnd.Client.run_deployment()`

Submits one durable run of a `DeployedProgram`, a `d_` deployment ID or a
`cmpnd.ProgramAddress`, waits for the result within the local timeout and
returns a `dspy.Prediction`. A fast result returns at once, and the SDK never
submits one run twice. A deployment of another account answers 404, and the
error says the deployment was not found.

A program name is an address, never a plain string. Run a program by its name
or tag with `sdk.run_deployment(cmpnd.ProgramAddress.parse("trend:canary"), ...)`.
Any other string raises `ValueError` before anything is sent, and the message
names the `ProgramAddress.parse` call to use. `submit_deployment_run()`,
`list_deployment_runs()` and `optimize_deployment()` take their deployment the
same way.

`timeout` is the whole submit-and-wait budget in seconds, 30 by default, or
`None` for no local limit. `entrypoint` and `lm` select a module and model and
travel beside the inputs. `metadata` and `tags` take `dict[str, str]` values
that the platform applies to the run's root trace. Names the platform sets,
such as `deployment_run_id`, raise `TypeError` before anything is sent. The
errors:

- A refused request raises `httpx.HTTPStatusError`.
- A lost or failed submission reply raises `RunSubmissionUncertain`.
- A connection to the platform that never opened, such as one refused, raises
  `RunNotSubmitted` after a few attempts. It names the endpoint, and nothing was
  sent, so submitting again is safe. It is a `RunSubmissionUncertain`.
- A spent budget raises `RunWaitTimeout`, which holds the run ID and, once a
  worker admitted the run, the `trace_id` of the trace it is streaming.
- An answer that expired before it was read raises `RunResultExpired`.

`sdk.deployment_run_id(result)` returns the run ID of a result, or of an
exception when the platform had assigned a run.

`sdk.submit_deployment_run(deployment, inputs)` returns the accepted handle
without waiting. `sdk.list_deployment_runs()`, `get_deployment_run(id)`,
`get_deployment_run_result(id)` and `wait_deployment_run(id)` find, inspect and
resume runs. `list_deployment_runs(deployment)` limits the list to one
deployment. `RunSubmissionUncertain` names the list command to check before you
send new work.

### `cmpnd.DeployedProgram`

The deployment that `deploy_program()` and `optimize_deployment()` return. Run
it with `run_deployment()`. `str()` and `.deployment_id` return its ID.
`.score` is its score when it came from `optimize_deployment()`, and `None`
otherwise.
`.deployment_signature` holds the typed deployment response, and
`.artifact_metadata` holds its metadata when available.

### `cmpnd.trace()`

A decorator that traces a function.

### `cmpnd.start_span()`

A context manager that opens a span.

### `cmpnd.get_current_trace()`

Returns the current trace, or `None`.

### `cmpnd.get_current_span()`

Returns the current span, or `None`.

### `cmpnd.trace_id()`

Returns the platform trace ID stamped on a prediction from an instrumented
program, or `None` when the prediction came from outside instrumentation. Use
it to attach feedback through
`POST /api/v1/traces/{cmpnd.trace_id(pred)}/metadata`.

### `cmpnd.flush_exporter()`

Publishes everything the background exporter has queued and leaves the
exporter running. Call it before a reader fetches a trace the program just
produced. It returns `True` when the drain finished within the timeout, 5
seconds by default, and `False` when it timed out or no exporter is running.

### `cmpnd.shutdown_exporter()`

Flushes and stops the background exporter.
