Skip to main content

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

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. 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:

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:

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:

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:

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 and see what it does:

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:

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:

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:

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:

# 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:

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:

# 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, under "System One judges", describes the hosted side.

Options

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:

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:

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:

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

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:

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:

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:

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:

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 describes every command, including "Tool servers".

Custom spans

Trace non-DSPy code with the decorator:

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:

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:

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:

      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:

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.

Metadata

Release files for cmpnd 0.102.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cmpnd 0.102.5
File Size Uploaded Provenance Actions
cmpnd-0.102.5.tar.gz 2.2 MB With provenance Details for cmpnd-0.102.5.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for cmpnd 0.102.5
File Interpreter ABI Platform Actions
cmpnd-0.102.5-py3-none-any.whl Python 3 none any Details for cmpnd-0.102.5-py3-none-any.whl

Total release size: 2.9 MB

Release files / cmpnd-0.102.5.tar.gz

Download URL cmpnd-0.102.5.tar.gz
Size 2.2 MB
Tags Source
SHA-256 checksum
How to use checksums
dba5d9bd45c1d99c3a43fb29906e1c3381aa7f25f2a7195aa31d93aaee03911b
BLAKE2b-256 checksum
How to use checksums
38ce37fc4b3d01031b4b6566776dc06ad3c1ddd49b8168568072ab02dc5375df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 13, 2026.

Transparency log

Release files / cmpnd-0.102.5-py3-none-any.whl

Download URL cmpnd-0.102.5-py3-none-any.whl
Size 623.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
57ec58822bd255e3d1c350fc39b5b65293adf913391beae5a1e4d3dbe00cd46e
BLAKE2b-256 checksum
How to use checksums
e72c0ffb66d14ace709543b32222b5ddb0994d797d7a3cc8ff6f452e5e5f3a7b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 13, 2026.

Transparency log

Security for cmpnd 0.102.5

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cmpnd-0.102.5.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 13, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.102.5 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page