cmpnd 0.33.2__tar.gz → 0.33.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cmpnd-0.33.2 → cmpnd-0.33.4}/PKG-INFO +1 -1
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/callback.py +55 -9
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/context.py +14 -15
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/helpers.py +41 -5
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/models.py +0 -2
- {cmpnd-0.33.2 → cmpnd-0.33.4}/docs/sdk-instrumentation.md +7 -5
- {cmpnd-0.33.2 → cmpnd-0.33.4}/pyproject.toml +1 -1
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/_traced_run.py +13 -3
- cmpnd-0.33.4/tests/conftest.py +8 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/load/rlm_size_report.py +9 -3
- cmpnd-0.33.4/tests/test_lm15_native_calls.py +157 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_lm_usage_shapes.py +20 -7
- cmpnd-0.33.4/tests/test_signature_inputs.py +154 -0
- cmpnd-0.33.4/tests/test_trace_context_concurrency.py +153 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/uv.lock +1 -1
- cmpnd-0.33.2/tests/conftest.py +0 -3
- {cmpnd-0.33.2 → cmpnd-0.33.4}/.gitignore +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/AGENTS.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/CLAUDE.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/README.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_assessments.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_authored_import.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_deployments.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_http.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_runtime_tools.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_traces.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/authored.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/client.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/configuration.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/datasets.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/decorators.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/deployment.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/deployment_metadata.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/execution.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/exporter.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/identity.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/imaging.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/lm_configuration.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/optimization.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/packaging.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/references.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/retry.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/cmpnd/traces.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/docs/cli.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/docs/client.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/docs/custom-proposer.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/docs/reading-traces.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/examples/local_ollama.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/README.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/cmpnd_ingest/trace_facts.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/ingest/pyproject.toml +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/scripts/release-stress.sh +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/stubs/dspy/utils/dummies.pyi +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/_deployment_signature_helpers.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/_fake_clock.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/_resolve_refs.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_assessments_cli.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/e2e/test_valsets_cli.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/authored_optimize/judge.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/conftest.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/load/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/load/__main__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/load/drain.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/load/ingest.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/load/sweep.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/load/test_rlm_size_report.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/conftest.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_health.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/span_reference_fixture.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/_provider.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/_readiness.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/_result.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_readiness.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/stress/test_trace_search.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_adapter_reentrancy.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_assessments_api.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_authored.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_authored_bundle_collisions.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_authored_metric_contract.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_authored_named_models.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_authored_optimize.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_callback.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_assessments.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_client.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_configuration.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_context.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_datasets.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_decorators.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_deploy.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_deployment_metadata.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_deployments_read.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_eval.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_eval_start.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_execute.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_execute_run_identity.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_exporter.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_gepa_output_normalization.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_identity.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_imaging.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_import_weight.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_integration.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_lm_configuration.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_models.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_optimize.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_packaging.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_poll_until.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_project_payload.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_real_provider_twins_readiness.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_release_stress_script.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_retry.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_run_attribution_paths.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_runtime_tools.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_schemas.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_span_references.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_span_sent_once.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_trace_fact_wire_contract.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_trace_facts.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_trace_render.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_traces_read.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_truncation.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/tests/test_xxh64.py +0 -0
- {cmpnd-0.33.2 → cmpnd-0.33.4}/todo.md +0 -0
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import contextvars
|
|
6
6
|
import logging
|
|
7
|
+
from collections.abc import Mapping
|
|
7
8
|
from datetime import datetime, timezone
|
|
8
9
|
from typing import Any
|
|
9
10
|
from uuid import UUID, uuid4
|
|
@@ -320,17 +321,49 @@ def _present_field(value: Any, key: str) -> tuple[bool, Any]:
|
|
|
320
321
|
return False, None
|
|
321
322
|
|
|
322
323
|
|
|
324
|
+
def _usage_reported(usage: Any) -> bool:
|
|
325
|
+
"""Whether a usage value reports anything.
|
|
326
|
+
|
|
327
|
+
An empty mapping counts as nothing. DSPy >= 3.4 writes ``usage={}`` for a
|
|
328
|
+
call its own cache answered, and a reported zero is a mapping of zeros,
|
|
329
|
+
never an empty one.
|
|
330
|
+
"""
|
|
331
|
+
return usage is not None and not (isinstance(usage, Mapping) and not usage)
|
|
332
|
+
|
|
333
|
+
|
|
323
334
|
def _history_usage(entry: Any) -> tuple[bool, Any]:
|
|
324
335
|
"""Return whether history carried usage and the value it carried."""
|
|
325
336
|
response = _value_field(entry, "response")
|
|
326
337
|
response_declares_usage, response_usage = _present_field(response, "usage")
|
|
327
338
|
if response_declares_usage:
|
|
328
|
-
return response_usage
|
|
339
|
+
return _usage_reported(response_usage), response_usage
|
|
329
340
|
if isinstance(entry, dict):
|
|
330
341
|
usage = entry.get("usage")
|
|
331
|
-
return
|
|
342
|
+
return _usage_reported(usage), usage
|
|
332
343
|
usage = _value_field(entry, "usage")
|
|
333
|
-
return usage
|
|
344
|
+
return _usage_reported(usage), usage
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _lm15_cache_hit(entry: Any, response: Any) -> bool | None:
|
|
348
|
+
"""Recognize a DSPy cache hit on a DSPy >= 3.4 lm15-native call.
|
|
349
|
+
|
|
350
|
+
Nothing on the history entry or the ``Response`` says the cache answered.
|
|
351
|
+
DSPy's ``CallResult.load`` rebuilds the cached ``Response`` with its token
|
|
352
|
+
counts intact and gives the call an empty ``usage``, and the history entry
|
|
353
|
+
copies that empty ``usage``. A fresh native call derives the entry's
|
|
354
|
+
``usage`` from those same counts, so it is empty only when the
|
|
355
|
+
``Response`` has none. An empty entry ``usage`` beside a ``Response`` that
|
|
356
|
+
does carry counts is therefore a cache hit.
|
|
357
|
+
|
|
358
|
+
Returns ``None`` when the shapes do not establish one either way.
|
|
359
|
+
"""
|
|
360
|
+
if not isinstance(entry, dict) or entry.get("usage") != {}:
|
|
361
|
+
return None
|
|
362
|
+
usage = getattr(response, "usage", None)
|
|
363
|
+
if usage is None or isinstance(usage, Mapping):
|
|
364
|
+
return None
|
|
365
|
+
counts = (getattr(usage, name, None) for name in ("input_tokens", "output_tokens", "total_tokens"))
|
|
366
|
+
return True if any(isinstance(count, int) for count in counts) else None
|
|
334
367
|
|
|
335
368
|
|
|
336
369
|
def _usage_dialect(instance: Any, usage: Any) -> str | None:
|
|
@@ -388,15 +421,22 @@ def _response_text(response: Any, key: str) -> str | None:
|
|
|
388
421
|
return None
|
|
389
422
|
|
|
390
423
|
|
|
424
|
+
#: lm15 finish reasons whose chat spelling differs. lm15 names a tool-call stop
|
|
425
|
+
#: in the singular (``FinishReason`` in DSPy's vendored ``lm15/types.py``); its
|
|
426
|
+
#: ``stop``, ``length`` and ``content_filter`` are already the chat words.
|
|
427
|
+
_LM15_FINISH_REASONS = {"tool_call": "tool_calls"}
|
|
428
|
+
|
|
429
|
+
|
|
391
430
|
def _finish_reason(response: Any) -> str | None:
|
|
392
431
|
"""Read why the model stopped, from whichever response shape answered.
|
|
393
432
|
|
|
394
|
-
Chat puts it on a choice. DSPy's typed ``LMResponse`` puts it on an
|
|
395
|
-
The
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
433
|
+
Chat puts it on a choice. DSPy's typed ``LMResponse`` (3.3) puts it on an
|
|
434
|
+
output. The lm15 ``Response`` a DSPy >= 3.4 engine returns carries it at the
|
|
435
|
+
top level. The Responses API has none of these: it reports a ``status``
|
|
436
|
+
plus, when the answer was cut short, an ``incomplete_details.reason``. All of
|
|
437
|
+
them map onto the chat vocabulary, which is what serve's truncation warning
|
|
438
|
+
reads — a Responses ``max_output_tokens`` and a chat ``length`` are the same
|
|
439
|
+
fact and must land on the same word.
|
|
400
440
|
|
|
401
441
|
Returns ``None`` when the response reports nothing about it.
|
|
402
442
|
"""
|
|
@@ -415,6 +455,10 @@ def _finish_reason(response: Any) -> str | None:
|
|
|
415
455
|
if reason:
|
|
416
456
|
return str(reason)
|
|
417
457
|
|
|
458
|
+
reason = getattr(response, "finish_reason", None)
|
|
459
|
+
if isinstance(reason, str) and reason:
|
|
460
|
+
return _LM15_FINISH_REASONS.get(reason, reason)
|
|
461
|
+
|
|
418
462
|
incomplete = getattr(response, "incomplete_details", None)
|
|
419
463
|
reason = incomplete.get("reason") if isinstance(incomplete, dict) else getattr(incomplete, "reason", None)
|
|
420
464
|
if reason:
|
|
@@ -1381,6 +1425,8 @@ class _LMMixin(_CallbackBase):
|
|
|
1381
1425
|
cache_hit = _bool_field(response, "cache_hit")
|
|
1382
1426
|
if cache_hit is None:
|
|
1383
1427
|
cache_hit = _bool_field(_value_field(response, "_hidden_params"), "cache_hit")
|
|
1428
|
+
if cache_hit is None:
|
|
1429
|
+
cache_hit = _lm15_cache_hit(latest_entry, response)
|
|
1384
1430
|
observation.dspy_client_cache_hit = cache_hit
|
|
1385
1431
|
observation.usage_reported = usage_reported and cache_hit is not True
|
|
1386
1432
|
serialized_usage = models._safe_serialize(usage)
|
|
@@ -13,8 +13,12 @@ if TYPE_CHECKING:
|
|
|
13
13
|
# Current trace context
|
|
14
14
|
_current_trace: ContextVar[models.TraceBuilder | None] = ContextVar("cmpnd_current_trace", default=None)
|
|
15
15
|
|
|
16
|
-
# Stack of active spans (for nested spans)
|
|
17
|
-
|
|
16
|
+
# Stack of active spans (for nested spans). A tuple, replaced on every push and
|
|
17
|
+
# pop and never mutated: a context copy (``contextvars.copy_context()``, an
|
|
18
|
+
# asyncio task) shares the objects its parent context holds, so a mutable stack
|
|
19
|
+
# would let concurrent runs in sibling copies read each other's spans as their
|
|
20
|
+
# parent while each keeps its own trace.
|
|
21
|
+
_span_stack: ContextVar[tuple[models.SpanBuilder, ...]] = ContextVar("cmpnd_span_stack", default=())
|
|
18
22
|
|
|
19
23
|
# Current eval run context
|
|
20
24
|
_current_eval_run: ContextVar[models.EvalRunBuilder | None] = ContextVar("cmpnd_current_eval_run", default=None)
|
|
@@ -37,26 +41,21 @@ def set_current_trace(trace: models.TraceBuilder | None) -> None:
|
|
|
37
41
|
def get_current_span() -> models.SpanBuilder | None:
|
|
38
42
|
"""Get the current (innermost) span."""
|
|
39
43
|
stack = _span_stack.get()
|
|
40
|
-
if stack is None:
|
|
41
|
-
return None
|
|
42
44
|
return stack[-1] if stack else None
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
def push_span(span: models.SpanBuilder) -> None:
|
|
46
|
-
"""Push a span onto
|
|
47
|
-
|
|
48
|
-
if stack is None:
|
|
49
|
-
stack = []
|
|
50
|
-
_span_stack.set(stack)
|
|
51
|
-
stack.append(span)
|
|
48
|
+
"""Push a span onto this context's stack."""
|
|
49
|
+
_span_stack.set((*_span_stack.get(), span))
|
|
52
50
|
|
|
53
51
|
|
|
54
52
|
def pop_span() -> models.SpanBuilder | None:
|
|
55
|
-
"""Pop the innermost span from
|
|
53
|
+
"""Pop the innermost span from this context's stack."""
|
|
56
54
|
stack = _span_stack.get()
|
|
57
|
-
if stack:
|
|
58
|
-
return
|
|
59
|
-
|
|
55
|
+
if not stack:
|
|
56
|
+
return None
|
|
57
|
+
_span_stack.set(stack[:-1])
|
|
58
|
+
return stack[-1]
|
|
60
59
|
|
|
61
60
|
|
|
62
61
|
def get_current_eval_run() -> models.EvalRunBuilder | None:
|
|
@@ -119,6 +118,6 @@ def create_child_span(
|
|
|
119
118
|
def clear_context() -> None:
|
|
120
119
|
"""Clear all trace/span context. Useful for testing."""
|
|
121
120
|
_current_trace.set(None)
|
|
122
|
-
_span_stack.set(
|
|
121
|
+
_span_stack.set(())
|
|
123
122
|
_current_eval_run.set(None)
|
|
124
123
|
_current_optimization_run_id.set(None)
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from . import models
|
|
8
|
+
from . import identity, models
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def truncate_field(value: Any, max_size: int, depth: int = 0) -> Any:
|
|
@@ -21,8 +21,44 @@ def truncate_field(value: Any, max_size: int, depth: int = 0) -> Any:
|
|
|
21
21
|
return models.truncate_field(value, max_size, depth)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
"""
|
|
24
|
+
def signature_label(signature: Any) -> str:
|
|
25
|
+
"""Name a DSPy signature in a few words: its class name, else its fields.
|
|
26
|
+
|
|
27
|
+
A signature built from a string is a ``StringSignature`` whatever it holds,
|
|
28
|
+
so it is named by its ``in1, in2 -> out1`` summary instead.
|
|
29
|
+
"""
|
|
30
|
+
name = getattr(signature, "__name__", None)
|
|
31
|
+
if name and name != "StringSignature":
|
|
32
|
+
return name
|
|
33
|
+
return identity.build_signature_summary(signature) or str(name or "")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_signature(value: Any, dspy: Any) -> bool:
|
|
37
|
+
signature_type = getattr(dspy, "Signature", None)
|
|
38
|
+
return isinstance(value, type) and isinstance(signature_type, type) and issubclass(value, signature_type)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _input_value(value: Any, dspy: Any) -> Any:
|
|
42
|
+
"""Replace a signature class, alone or among positional args, by its label.
|
|
43
|
+
|
|
44
|
+
DSPy hands every adapter ``format``/``parse`` hook the predictor's
|
|
45
|
+
signature, and a ``Predict`` called with ``signature=`` gets one too. A
|
|
46
|
+
signature class's repr embeds its whole instructions and every field's
|
|
47
|
+
metadata, which the span already records once as its own attributes.
|
|
48
|
+
"""
|
|
49
|
+
if _is_signature(value, dspy):
|
|
50
|
+
return signature_label(value)
|
|
51
|
+
if isinstance(value, (list, tuple)) and any(_is_signature(item, dspy) for item in value):
|
|
52
|
+
return [signature_label(item) if _is_signature(item, dspy) else item for item in value]
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def clean_inputs(inputs: dict[str, Any], dspy: Any, max_field_size: int) -> dict[str, Any]:
|
|
57
|
+
"""Clean and prepare inputs for storage, with per-field truncation.
|
|
58
|
+
|
|
59
|
+
A signature class among the inputs is recorded by its label, not its repr
|
|
60
|
+
(``_input_value``).
|
|
61
|
+
"""
|
|
26
62
|
result = {}
|
|
27
63
|
|
|
28
64
|
# Merge kwargs into top level
|
|
@@ -32,10 +68,10 @@ def clean_inputs(inputs: dict[str, Any], _dspy: Any, max_field_size: int) -> dic
|
|
|
32
68
|
kwargs = inputs.get("kwargs", {})
|
|
33
69
|
for k, v in inputs.items():
|
|
34
70
|
if k != "kwargs":
|
|
35
|
-
serialized = models._safe_serialize(v)
|
|
71
|
+
serialized = models._safe_serialize(_input_value(v, dspy))
|
|
36
72
|
result[k] = truncate_field(serialized, max_field_size, depth=1)
|
|
37
73
|
for k, v in kwargs.items():
|
|
38
|
-
serialized = models._safe_serialize(v)
|
|
74
|
+
serialized = models._safe_serialize(_input_value(v, dspy))
|
|
39
75
|
result[k] = truncate_field(serialized, max_field_size, depth=1)
|
|
40
76
|
|
|
41
77
|
# Remove empty args
|
|
@@ -713,8 +713,6 @@ class SpanBuilder:
|
|
|
713
713
|
selected_demo_ids: list[str] = field(default_factory=list)
|
|
714
714
|
|
|
715
715
|
# Inputs/Outputs
|
|
716
|
-
# Initialised to {} by default; thin() sets these to None when stripping
|
|
717
|
-
# ADAPTER_FORMAT / ADAPTER_PARSE spans to reduce export payload size.
|
|
718
716
|
inputs: dict[str, Any] | None = field(default_factory=dict)
|
|
719
717
|
outputs: dict[str, Any] | None = field(default_factory=dict)
|
|
720
718
|
|
|
@@ -81,7 +81,7 @@ The hook methods, in the order you'd expect them to fire for a typical `dspy.Cha
|
|
|
81
81
|
|
|
82
82
|
- **Module lifecycle** — `on_module_start` (`callback.py:195`) creates a new `SpanBuilder`, and, if this is the root module call, also creates a new `TraceBuilder` via `_create_trace` (`callback.py:579`). `on_module_end` (`callback.py:307`) closes the span, attaches outputs, and, if it was the root, exports the trace. A call is root when no span is on the stack — or when the only span above it is the `Evaluate` span, which contains many independent program runs and parents none of them, so each run under a `dspy.Evaluate` roots its own trace. Such a root span is built with `create_child_span(..., root=True)` so it does not adopt the `Evaluate` span as its parent.
|
|
83
83
|
- **LM calls.** `on_lm_start` and `on_lm_end` wrap the provider call. The callback records the model, the current span token projections, and a versioned `lm_usage` evidence object. `capture_prompts` controls whether the prompt payload is recorded.
|
|
84
|
-
- **Adapter format/parse** — `on_adapter_format_start` / `_end` and `on_adapter_parse_start` / `_end`. Both route through the shared helpers `_start_adapter_span` and `_end_adapter_span`. The callback records one span per adapter call. DSPy's `Adapter.__init_subclass__` wraps `format` and `parse` on every subclass, so a `JSONAdapter.format` call fires the hooks twice and a `BAMLAdapter.format` call three times, each with a fresh `call_id` and the same instance. `_start_adapter_span` skips a call when the current span is the same method's adapter span on the same instance, and `_end_adapter_span` returns early for the skipped `call_id`. The outermost span is kept, since its output is what reaches the LM. `tests/test_adapter_reentrancy.py` runs the real adapters under DSPy's dispatch.
|
|
84
|
+
- **Adapter format/parse** — `on_adapter_format_start` / `_end` and `on_adapter_parse_start` / `_end`. Both route through the shared helpers `_start_adapter_span` and `_end_adapter_span`. The callback records one span per adapter call. DSPy's `Adapter.__init_subclass__` wraps `format` and `parse` on every subclass, so a `JSONAdapter.format` call fires the hooks twice and a `BAMLAdapter.format` call three times, each with a fresh `call_id` and the same instance. `_start_adapter_span` skips a call when the current span is the same method's adapter span on the same instance, and `_end_adapter_span` returns early for the skipped `call_id`. The outermost span is kept, since its output is what reaches the LM. `tests/test_adapter_reentrancy.py` runs the real adapters under DSPy's dispatch. An adapter span's inputs are the method's arguments, so they include the predictor's `signature`. `helpers.clean_inputs` records any signature class among a span's inputs by its label, the class name or the `in1, in2 -> out1` fields of a string signature. It never records the repr, which embeds the whole instructions and every field's metadata. The same rule covers a `Predict` called with `signature=`, as DSPy's RLM does on every turn from DSPy `main` on. The span records the signature's fields and instructions once, as its own attributes.
|
|
85
85
|
- **Tools** — `on_tool_start` (`callback.py:700`) / `on_tool_end` (`callback.py:733`). One span per `dspy.Tool` invocation, with the tool's args as inputs.
|
|
86
86
|
- **Interpreter (code execution)** — `_InterpreterMixin` (`callback.py:1119`), for the DSPy `CodeInterpreter` hooks DSPy >= 3.3.1 fires around a program running code in the interpreter (`RLM`, `ProgramOfThought`, `CodeAct`, `Flex` — every module reaching it through `_create_interpreter`; `ReAct` / `ReActV2` use no interpreter and fire none). `on_interpreter_execute_start` / `_end` emit one `INTERPRETER_EXECUTE` span per REPL cell — the code in, its captured stdout out, and the `SUBMIT()` result (a DSPy `FinalOutput`) recorded distinctly from stdout. `on_interpreter_tool_call_start` / `_end` synthesize a `TOOL` span for a tool called from interpreter code **only when no real `dspy.Tool` span covered the call** (a user tool routes through `Tool.__call__` and fires `on_tool_*`, so its row is suppressed to avoid a duplicate; the `llm_query` builtins fire no tool hook, so they get the synthesized span). `on_interpreter_startup_*` / `on_interpreter_shutdown_*` emit no span — their duration folds into an `interpreter.startup_ms` / `interpreter.shutdown_ms` attribute on the enclosing span, and an event with no enclosing span (a pooled pre-warm) is dropped.
|
|
87
87
|
- **Evaluate** — `on_evaluate_start` (`callback.py:773`) / `on_evaluate_end` (`callback.py:782`). These are thin forwarders that delegate to `EvalHandler` (see "Eval handling" below).
|
|
@@ -135,18 +135,18 @@ If you see `UNKNOWN` in the UI, `_get_span_type_for_module` couldn't match the D
|
|
|
135
135
|
|
|
136
136
|
## Context stack
|
|
137
137
|
|
|
138
|
-
DSPy's execution is threaded: a single program call can fan out to parallel LM calls, evaluators often run workers in threads, and the callback hooks can fire on any of them. The SDK tracks "which trace/span am I currently inside" using Python's `contextvars
|
|
138
|
+
DSPy's execution is threaded: a single program call can fan out to parallel LM calls, evaluators often run workers in threads, and the callback hooks can fire on any of them. The SDK tracks "which trace/span am I currently inside" using Python's `contextvars`. A fresh `threading.Thread` or pool thread starts with empty context; `contextvars.copy_context()` and every `asyncio` task start with a copy of the caller's.
|
|
139
139
|
|
|
140
140
|
Everything lives in `python-sdk/cmpnd/context.py`:
|
|
141
141
|
|
|
142
142
|
- `_current_trace` (`context.py:14`) — the active `TraceBuilder`.
|
|
143
|
-
- `_span_stack`
|
|
143
|
+
- `_span_stack` — an immutable tuple of `SpanBuilder`s. `push_span` and `pop_span` set a new tuple rather than mutating the old one, because a context copy shares the objects its parent context holds: a mutable stack would be one list shared by every sibling copy, so concurrent runs (GEPA's `DspyAdapter.batch_evaluate`, which runs each candidate in a `copy_context()`, or `asyncio.gather` over `acall`) would read each other's spans as their parent. `get_current_span` returns the innermost. `tests/test_trace_context_concurrency.py` pins this.
|
|
144
144
|
- `_current_eval_run` (`context.py:24`) — set while an `EvalHandler` is active.
|
|
145
145
|
- `_current_optimization_run_id` (`context.py:29`) — set while a patched `GEPA.compile` is running, so evals triggered inside it can be linked back.
|
|
146
146
|
|
|
147
147
|
`create_child_span` at `context.py:83` is the standard constructor: it reads the current trace + span, assigns `trace_id` and `parent_span_id`, and returns an unpushed `SpanBuilder`. The callback uses it for every child span; so does the `start_span` context manager below. Pass `root=True` to have it ignore the stack and leave `parent_span_id` unset, for a span that starts a trace beneath one that does not parent it.
|
|
148
148
|
|
|
149
|
-
The important consequence:
|
|
149
|
+
The important consequence: a span's `trace_id` comes from `_current_trace` and its `parent_span_id` from `_span_stack`, so the two stay consistent only while both are read from the same context. A thread that starts with empty context (DSPy's `ParallelExecutor` workers) roots its own traces, which is what `Evaluate` wants. A copied context starts inside whatever trace and span the caller had open.
|
|
150
150
|
|
|
151
151
|
The inverse also has to be designed for, and is easier to miss: work that stays on the *calling* thread inherits context the SDK may not want it to. `dspy.Evaluate(num_threads=1)` runs every example inline rather than on a worker, so the `Evaluate` span is still on the stack when each program run begins — which is why the root rule above names that span explicitly instead of relying on a worker thread's fresh contextvars to hide it. `tests/test_eval_trace_grouping.py` runs a real `Evaluate` over both paths to keep them from drifting.
|
|
152
152
|
|
|
@@ -709,7 +709,9 @@ Every LM span carries one `LMUsageObservationV1`. The observation preserves the
|
|
|
709
709
|
|
|
710
710
|
The queryable counters use `None` for an absent measurement and `0` for a reported zero. The legacy span counters keep their established defaults for compatibility. The evidence object also records requested and reported models, requested and effective service tiers, one unambiguous requested cache TTL, DSPy client-cache status, provider response identity, and an optional hosted egress call ID.
|
|
711
711
|
|
|
712
|
-
The callback mints `source_observation_id` when the LM call starts and records `observed_at` when it ends. Export retries serialize the same observation, including the same identity, timestamp, and SHA-256 payload digest. A DSPy client-cache hit remains an SDK observation, but `usage_reported` is false because no provider call occurred.
|
|
712
|
+
The callback mints `source_observation_id` when the LM call starts and records `observed_at` when it ends. Export retries serialize the same observation, including the same identity, timestamp, and SHA-256 payload digest. A DSPy client-cache hit remains an SDK observation, but `usage_reported` is false because no provider call occurred. An empty usage object also leaves `usage_reported` false, since it reports no count. On the LiteLLM path DSPy stamps `cache_hit` on the cached response, and `dspy_client_cache_hit` reads it there. An lm15-native call on DSPy 3.4 carries no such flag, so the SDK infers the hit from its shape: DSPy empties the history entry's `usage` while the cached `Response` keeps its token counts, which a fresh call never does (`_lm15_cache_hit` in `python-sdk/cmpnd/callback.py`).
|
|
713
|
+
|
|
714
|
+
The LM span's `lm.finish_reason` attribute records why the model stopped, in the chat vocabulary serve's truncation warning reads. Each response shape spells it differently. Chat puts it on the first choice and DSPy 3.3's typed `LMResponse` on the first output. The lm15 `Response` a DSPy 3.4 engine returns carries it at the top level and names a tool-call stop `tool_call`, which the SDK records as `tool_calls`. The Responses API reports a `status`, and its `max_output_tokens` cutoff lands as `length`. `_finish_reason` in `python-sdk/cmpnd/callback.py` holds the mapping.
|
|
713
715
|
|
|
714
716
|
`provider_cost` stays `None`. DSPy and LiteLLM can attach a calculated cost to history, but that value is not provider evidence. The SDK will populate this field only when an adapter can prove that the provider supplied both the amount and its currency.
|
|
715
717
|
|
|
@@ -5,7 +5,7 @@ name = "cmpnd"
|
|
|
5
5
|
# it's new) — no git tags. Bump with `uv version --bump {minor,patch}` or by
|
|
6
6
|
# hand. Runtime `cmpnd.__version__` reads installed metadata, so it tracks
|
|
7
7
|
# this automatically. See docs/plans/2026-07-02-sdk-pypi-tag-free-publish.md.
|
|
8
|
-
version = "0.33.
|
|
8
|
+
version = "0.33.4"
|
|
9
9
|
description = "DSPy observability and deployment SDK for cmpnd"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -10,6 +10,7 @@ callback hands to its exporter, whether it streams them or batches them.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import inspect
|
|
13
14
|
import json
|
|
14
15
|
from typing import Any
|
|
15
16
|
from unittest import mock
|
|
@@ -51,7 +52,7 @@ class CaptureExporter:
|
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
def make_callback(**kwargs: Any) -> Any:
|
|
54
|
-
with mock.patch("cmpnd.exporter.ensure_exporter_started"):
|
|
55
|
+
with mock.patch("cmpnd.exporter.ensure_exporter_started", autospec=True):
|
|
55
56
|
from cmpnd.callback import CmpndCallback
|
|
56
57
|
|
|
57
58
|
return CmpndCallback(**kwargs)
|
|
@@ -62,7 +63,7 @@ def run(program: Any, lm: Any, adapter: Any, callback: Any = None, **inputs: Any
|
|
|
62
63
|
exp = CaptureExporter()
|
|
63
64
|
exp.lm = lm
|
|
64
65
|
cb = callback or make_callback()
|
|
65
|
-
with mock.patch("cmpnd.exporter.get_exporter", return_value=exp):
|
|
66
|
+
with mock.patch("cmpnd.exporter.get_exporter", autospec=True, return_value=exp):
|
|
66
67
|
with dspy.context(lm=lm, adapter=adapter, callbacks=[cb]):
|
|
67
68
|
result = program(**inputs)
|
|
68
69
|
return result, exp
|
|
@@ -81,8 +82,17 @@ def run_rlm(
|
|
|
81
82
|
rlm = dspy.RLM("question -> answer", max_iters=turns)
|
|
82
83
|
adapter = adapter or dspy.ChatAdapter()
|
|
83
84
|
interpreter = FakeInterpreter(lambda t: interpreter_output(t, output_size))
|
|
85
|
+
# DSPy 3.4 renamed RLM.forward's caller-owned interpreter parameter from
|
|
86
|
+
# positional ``interpreter`` to keyword-only ``interpreter_factory``.
|
|
87
|
+
new_rlm_api = "interpreter_factory" in inspect.signature(getattr(rlm, "forward")).parameters
|
|
88
|
+
|
|
89
|
+
def _call_rlm(**kw: Any) -> Any:
|
|
90
|
+
if new_rlm_api:
|
|
91
|
+
return rlm(interpreter_factory=lambda: interpreter, **kw)
|
|
92
|
+
return rlm(interpreter, **kw)
|
|
93
|
+
|
|
84
94
|
return run(
|
|
85
|
-
|
|
95
|
+
_call_rlm,
|
|
86
96
|
DummyLM(rlm_answers(turns), adapter=adapter),
|
|
87
97
|
adapter,
|
|
88
98
|
callback=callback,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Shared pytest configuration for the cmpnd SDK test suite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# litellm imports openai._models during test collection. Initialize openai
|
|
6
|
+
# first to avoid the circular import in openai 1.109.1 when that module loads
|
|
7
|
+
# before openai's own __init__ has finished.
|
|
8
|
+
import openai # noqa: F401 (imported for its import-order side effect)
|
|
@@ -24,6 +24,7 @@ span, is not counted.
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
26
|
import argparse
|
|
27
|
+
import inspect
|
|
27
28
|
import json
|
|
28
29
|
import os
|
|
29
30
|
import subprocess
|
|
@@ -130,7 +131,7 @@ def run_rlm(turns: int, output_size: int, adapter: str = "chat") -> list[dict[st
|
|
|
130
131
|
from cmpnd.configuration import configure
|
|
131
132
|
|
|
132
133
|
configure(api_key="size-report", endpoint="http://localhost:8000")
|
|
133
|
-
with mock.patch("cmpnd.exporter.ensure_exporter_started"):
|
|
134
|
+
with mock.patch("cmpnd.exporter.ensure_exporter_started", autospec=True):
|
|
134
135
|
from cmpnd.callback import CmpndCallback
|
|
135
136
|
|
|
136
137
|
callback = CmpndCallback()
|
|
@@ -144,9 +145,14 @@ def run_rlm(turns: int, output_size: int, adapter: str = "chat") -> list[dict[st
|
|
|
144
145
|
|
|
145
146
|
interpreter = FakeInterpreter(output)
|
|
146
147
|
capture = _Capture()
|
|
147
|
-
|
|
148
|
+
# DSPy 3.4 takes a factory instead of the positional interpreter.
|
|
149
|
+
new_rlm_api = "interpreter_factory" in inspect.signature(getattr(rlm, "forward")).parameters
|
|
150
|
+
with mock.patch("cmpnd.exporter.get_exporter", autospec=True, return_value=capture):
|
|
148
151
|
with dspy.context(lm=lm, adapter=adapter_instance, callbacks=[callback]):
|
|
149
|
-
|
|
152
|
+
if new_rlm_api:
|
|
153
|
+
rlm(interpreter_factory=lambda: interpreter, question="Which rows changed?")
|
|
154
|
+
else:
|
|
155
|
+
rlm(interpreter, question="Which rows changed?")
|
|
150
156
|
return [s.to_api_dict() for s in capture.spans]
|
|
151
157
|
|
|
152
158
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""LM calls DSPy answers through its lm15 engines, driven end to end.
|
|
2
|
+
|
|
3
|
+
DSPy 3.4 runs an LM call through an engine that returns an lm15 ``Response``,
|
|
4
|
+
and the history entry the callback reads holds that ``Response`` rather than a
|
|
5
|
+
provider's chat or Responses object. The ``Response`` is a slots dataclass with
|
|
6
|
+
a top-level ``finish_reason`` in lm15's own vocabulary, and a DSPy cache hit on
|
|
7
|
+
this path leaves the entry's ``usage`` empty with nothing on the entry or the
|
|
8
|
+
``Response`` saying it was a hit.
|
|
9
|
+
|
|
10
|
+
These drive a real ``dspy.Predict`` over DSPy's own ``DummyLM``, which on 3.4
|
|
11
|
+
answers through ``DummyEngine`` and so takes the same native path a provider
|
|
12
|
+
call does. No shape is hand-built: if DSPy changes what the entry holds, these
|
|
13
|
+
see it.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
from unittest import mock
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
from cmpnd.configuration import configure
|
|
24
|
+
from cmpnd.context import clear_context
|
|
25
|
+
|
|
26
|
+
dspy = pytest.importorskip("dspy")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from dspy.clients.engines.dummy_engine import DummyEngine
|
|
30
|
+
from dspy.lm15 import Response
|
|
31
|
+
except ImportError: # DSPy < 3.4 answers DummyLM without an lm15 engine.
|
|
32
|
+
pytest.skip("needs DSPy>=3.4 (lm15 engines)", allow_module_level=True)
|
|
33
|
+
|
|
34
|
+
from dspy.utils.dummies import DummyLM # noqa: E402
|
|
35
|
+
|
|
36
|
+
from tests.test_eval_trace_grouping import RecordingExporter # noqa: E402
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class QA(dspy.Signature):
|
|
40
|
+
"""Answer the question."""
|
|
41
|
+
|
|
42
|
+
question: str = dspy.InputField()
|
|
43
|
+
answer: str = dspy.OutputField()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture(autouse=True)
|
|
47
|
+
def _sdk_configured():
|
|
48
|
+
configure(api_key="test_key", endpoint="http://localhost:8000")
|
|
49
|
+
yield
|
|
50
|
+
clear_context()
|
|
51
|
+
import cmpnd.configuration
|
|
52
|
+
from cmpnd.exporter import shutdown_exporter
|
|
53
|
+
|
|
54
|
+
cmpnd.configuration._config = None
|
|
55
|
+
shutdown_exporter()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.fixture
|
|
59
|
+
def memory_cache():
|
|
60
|
+
"""DSPy's cache in memory only, empty, and restored afterwards."""
|
|
61
|
+
from dspy.clients import configure_cache
|
|
62
|
+
|
|
63
|
+
saved = dspy.cache
|
|
64
|
+
configure_cache(enable_disk_cache=False, enable_memory_cache=True)
|
|
65
|
+
yield
|
|
66
|
+
dspy.cache = saved
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _FinishingEngine(DummyEngine):
|
|
70
|
+
"""``DummyEngine`` answering with a chosen lm15 finish reason."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, owner: Any, finish_reason: str) -> None:
|
|
73
|
+
super().__init__(owner)
|
|
74
|
+
self._finish_reason = finish_reason
|
|
75
|
+
|
|
76
|
+
def _complete_messages(self, messages: list[dict[str, Any]]) -> Response:
|
|
77
|
+
response = super()._complete_messages(messages)
|
|
78
|
+
return Response(
|
|
79
|
+
id=response.id,
|
|
80
|
+
model=response.model,
|
|
81
|
+
message=response.message,
|
|
82
|
+
finish_reason=self._finish_reason,
|
|
83
|
+
usage=response.usage,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _lm_spans(lm: Any, calls: int = 1) -> list[Any]:
|
|
88
|
+
"""Run ``Predict(QA)`` ``calls`` times on ``lm``; return every LM span, in order."""
|
|
89
|
+
from cmpnd.callback import CmpndCallback
|
|
90
|
+
|
|
91
|
+
with mock.patch("cmpnd.exporter.ensure_exporter_started", autospec=True):
|
|
92
|
+
callback = CmpndCallback()
|
|
93
|
+
recorder = RecordingExporter()
|
|
94
|
+
|
|
95
|
+
with (
|
|
96
|
+
mock.patch("cmpnd.exporter.get_exporter", autospec=True, return_value=recorder),
|
|
97
|
+
dspy.context(lm=lm, callbacks=[callback]),
|
|
98
|
+
):
|
|
99
|
+
for _ in range(calls):
|
|
100
|
+
dspy.Predict(QA)(question="What is the capital of France?")
|
|
101
|
+
|
|
102
|
+
spans = [s for t in recorder.traces for s in (getattr(t, "_batch_spans", None) or [])] + recorder.spans
|
|
103
|
+
lm_spans = [s for s in spans if s.span_type.value == "lm_call"]
|
|
104
|
+
assert len(lm_spans) == calls, f"expected {calls} LM spans, got {[s.name for s in spans]}"
|
|
105
|
+
return sorted(lm_spans, key=lambda s: s.start_time)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_history_holds_an_lm15_response():
|
|
109
|
+
"""The premise: DummyLM's history entry carries the native ``Response``."""
|
|
110
|
+
lm = DummyLM([{"answer": "Paris"}])
|
|
111
|
+
_lm_spans(lm)
|
|
112
|
+
assert isinstance(lm.history[-1]["response"], Response)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.parametrize(
|
|
116
|
+
("native", "recorded"),
|
|
117
|
+
[
|
|
118
|
+
("stop", "stop"),
|
|
119
|
+
("length", "length"),
|
|
120
|
+
# lm15 names a tool-call stop in the singular; serve reads the chat word.
|
|
121
|
+
("tool_call", "tool_calls"),
|
|
122
|
+
("content_filter", "content_filter"),
|
|
123
|
+
],
|
|
124
|
+
)
|
|
125
|
+
def test_finish_reason_on_a_native_call(native: str, recorded: str):
|
|
126
|
+
lm = DummyLM([{"answer": "Paris"}])
|
|
127
|
+
lm._engine_spec = _FinishingEngine(lm, native)
|
|
128
|
+
|
|
129
|
+
(span,) = _lm_spans(lm)
|
|
130
|
+
assert span.attributes.get("lm.finish_reason") == recorded
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_a_dspy_cache_hit_reports_no_usage(memory_cache: None):
|
|
134
|
+
"""A repeated identical call is answered from DSPy's cache.
|
|
135
|
+
|
|
136
|
+
The provider was not called, so there is no usage to report. The hit's
|
|
137
|
+
usage is unreported rather than zero, and the observation says DSPy's
|
|
138
|
+
cache answered.
|
|
139
|
+
"""
|
|
140
|
+
lm = DummyLM({"capital of France": {"answer": "Paris"}})
|
|
141
|
+
# DummyLM opts out of DSPy's cache so a scripted list is consumed in order.
|
|
142
|
+
# This test is about the cache, so it opts back in.
|
|
143
|
+
lm._cache_responses = True
|
|
144
|
+
|
|
145
|
+
first, second = _lm_spans(lm, calls=2)
|
|
146
|
+
|
|
147
|
+
assert lm.history[-1]["usage"] == {}, "premise: DSPy answered the second call from its cache"
|
|
148
|
+
fresh = first.to_api_dict()["lm_usage"]
|
|
149
|
+
hit = second.to_api_dict()["lm_usage"]
|
|
150
|
+
assert fresh["usage_reported"] is True
|
|
151
|
+
assert fresh["dspy_client_cache_hit"] is None
|
|
152
|
+
assert hit["usage_reported"] is False
|
|
153
|
+
assert hit["usage_raw"] is None
|
|
154
|
+
assert hit["prompt_tokens"] is None
|
|
155
|
+
assert hit["dspy_client_cache_hit"] is True
|
|
156
|
+
# The cached answer still says why the model stopped.
|
|
157
|
+
assert second.attributes.get("lm.finish_reason") == "stop"
|
|
@@ -39,7 +39,8 @@ def _typed_lm_types_available() -> bool:
|
|
|
39
39
|
|
|
40
40
|
``dspy.core.types`` (``LMUsage``, ``LMResponse``) arrived in DSPy 3.3.0.
|
|
41
41
|
Measured across the versions ``.github/workflows/ci-sdk.yml`` pins: absent on
|
|
42
|
-
3.1.3, 3.2.0 and 3.2.1, present on 3.3.0 and 3.3.1
|
|
42
|
+
3.1.3, 3.2.0 and 3.2.1, present on 3.3.0 and 3.3.1, and removed in 3.4.0b1,
|
|
43
|
+
whose ``dspy.core.types`` raises ``ImportError``. Probed by import rather
|
|
43
44
|
than by version number, so the answer stays right whatever the matrix pins
|
|
44
45
|
next.
|
|
45
46
|
|
|
@@ -98,7 +99,7 @@ def _span_for(usage: Any, response: Any = None) -> Any:
|
|
|
98
99
|
"""Drive on_lm_start/on_lm_end over one history entry; return the span."""
|
|
99
100
|
from cmpnd.callback import CmpndCallback
|
|
100
101
|
|
|
101
|
-
with mock.patch("cmpnd.exporter.ensure_exporter_started"):
|
|
102
|
+
with mock.patch("cmpnd.exporter.ensure_exporter_started", autospec=True):
|
|
102
103
|
callback = CmpndCallback()
|
|
103
104
|
recorder = _RecordingExporter()
|
|
104
105
|
|
|
@@ -106,7 +107,7 @@ def _span_for(usage: Any, response: Any = None) -> Any:
|
|
|
106
107
|
if response is not None:
|
|
107
108
|
entry["response"] = response
|
|
108
109
|
|
|
109
|
-
with mock.patch("cmpnd.exporter.get_exporter", return_value=recorder):
|
|
110
|
+
with mock.patch("cmpnd.exporter.get_exporter", autospec=True, return_value=recorder):
|
|
110
111
|
callback.on_lm_start(call_id="lm1", instance=_lm([entry]), inputs={})
|
|
111
112
|
callback.on_lm_end(call_id="lm1", outputs=None, exception=None)
|
|
112
113
|
|
|
@@ -118,13 +119,13 @@ def _span_for_entry(entry: Any, *, lm_kwargs: dict[str, Any] | None = None) -> A
|
|
|
118
119
|
"""Drive one complete history entry through the callback."""
|
|
119
120
|
from cmpnd.callback import CmpndCallback
|
|
120
121
|
|
|
121
|
-
with mock.patch("cmpnd.exporter.ensure_exporter_started"):
|
|
122
|
+
with mock.patch("cmpnd.exporter.ensure_exporter_started", autospec=True):
|
|
122
123
|
callback = CmpndCallback()
|
|
123
124
|
recorder = _RecordingExporter()
|
|
124
125
|
lm = _lm([entry])
|
|
125
126
|
lm.kwargs = lm_kwargs or {}
|
|
126
127
|
|
|
127
|
-
with mock.patch("cmpnd.exporter.get_exporter", return_value=recorder):
|
|
128
|
+
with mock.patch("cmpnd.exporter.get_exporter", autospec=True, return_value=recorder):
|
|
128
129
|
callback.on_lm_start(call_id="sdk-call-1", instance=lm, inputs={})
|
|
129
130
|
callback.on_lm_end(call_id="sdk-call-1", outputs=None, exception=None)
|
|
130
131
|
|
|
@@ -471,6 +472,18 @@ def test_dspy_cache_hit_is_not_provider_reported_usage():
|
|
|
471
472
|
assert observation["usage_raw"] is None
|
|
472
473
|
|
|
473
474
|
|
|
475
|
+
def test_an_empty_usage_object_is_not_reported_usage():
|
|
476
|
+
"""An empty mapping reports nothing, unlike a mapping of zeros.
|
|
477
|
+
|
|
478
|
+
DSPy >= 3.4 writes ``usage={}`` for a call its cache answered, with nothing
|
|
479
|
+
else on this entry to say so, so the empty object is all the callback sees.
|
|
480
|
+
"""
|
|
481
|
+
observation = _span_for_entry({"usage": {}}).to_api_dict()["lm_usage"]
|
|
482
|
+
assert observation["usage_reported"] is False
|
|
483
|
+
assert observation["usage_raw"] is None
|
|
484
|
+
assert observation["dspy_client_cache_hit"] is None
|
|
485
|
+
|
|
486
|
+
|
|
474
487
|
@_needs_typed_lm_types
|
|
475
488
|
def test_typed_history_preserves_response_usage_and_request_extensions():
|
|
476
489
|
from dspy.core.types import (
|
|
@@ -580,7 +593,7 @@ def test_prediction_usage_aggregates_on_every_shape(shape: str):
|
|
|
580
593
|
|
|
581
594
|
from cmpnd.callback import CmpndCallback
|
|
582
595
|
|
|
583
|
-
with mock.patch("cmpnd.exporter.ensure_exporter_started"):
|
|
596
|
+
with mock.patch("cmpnd.exporter.ensure_exporter_started", autospec=True):
|
|
584
597
|
callback = CmpndCallback()
|
|
585
598
|
|
|
586
599
|
span = mock.MagicMock()
|
|
@@ -608,7 +621,7 @@ def test_prediction_usage_sums_across_models():
|
|
|
608
621
|
|
|
609
622
|
from cmpnd.callback import CmpndCallback
|
|
610
623
|
|
|
611
|
-
with mock.patch("cmpnd.exporter.ensure_exporter_started"):
|
|
624
|
+
with mock.patch("cmpnd.exporter.ensure_exporter_started", autospec=True):
|
|
612
625
|
callback = CmpndCallback()
|
|
613
626
|
|
|
614
627
|
span = mock.MagicMock()
|