cmpnd 0.27.1__tar.gz → 0.27.2__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.
Files changed (333) hide show
  1. {cmpnd-0.27.1 → cmpnd-0.27.2}/AGENTS.md +2 -2
  2. {cmpnd-0.27.1 → cmpnd-0.27.2}/PKG-INFO +1 -1
  3. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/__init__.py +2 -0
  4. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/_authored_import.py +26 -5
  5. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/authored.py +6 -0
  6. cmpnd-0.27.2/cmpnd/lm_configuration.py +203 -0
  7. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/optimization.py +52 -3
  8. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/packaging.py +15 -8
  9. {cmpnd-0.27.1 → cmpnd-0.27.2}/docs/sdk-instrumentation.md +44 -1
  10. {cmpnd-0.27.1 → cmpnd-0.27.2}/pyproject.toml +1 -1
  11. cmpnd-0.27.2/tests/test_lm_configuration.py +286 -0
  12. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_reflector_properties.py +1 -1
  13. {cmpnd-0.27.1 → cmpnd-0.27.2}/uv.lock +1 -1
  14. {cmpnd-0.27.1 → cmpnd-0.27.2}/.gitignore +0 -0
  15. {cmpnd-0.27.1 → cmpnd-0.27.2}/CLAUDE.md +0 -0
  16. {cmpnd-0.27.1 → cmpnd-0.27.2}/README.md +0 -0
  17. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/_assessments.py +0 -0
  18. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/_exporter_pool.py +0 -0
  19. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/_gepa_patch.py +0 -0
  20. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/_http.py +0 -0
  21. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/_program_patch.py +0 -0
  22. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/_rlm_patch.py +0 -0
  23. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/_timespec.py +0 -0
  24. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/_traces.py +0 -0
  25. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/_valsets.py +0 -0
  26. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/callback.py +0 -0
  27. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/__init__.py +0 -0
  28. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/api_client.py +0 -0
  29. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/__init__.py +0 -0
  30. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/admin.py +0 -0
  31. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/assessments.py +0 -0
  32. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/auth.py +0 -0
  33. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/datasets.py +0 -0
  34. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/deployments.py +0 -0
  35. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/evals.py +0 -0
  36. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/login.py +0 -0
  37. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/optimizations.py +0 -0
  38. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/org.py +0 -0
  39. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/programs.py +0 -0
  40. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/provider_credentials.py +0 -0
  41. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/signatures.py +0 -0
  42. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/tool_servers.py +0 -0
  43. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/top.py +0 -0
  44. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/traces.py +0 -0
  45. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/commands/valsets.py +0 -0
  46. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/credentials.py +0 -0
  47. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/deprecation.py +0 -0
  48. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/pagination.py +0 -0
  49. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/shell.py +0 -0
  50. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/trace_render.py +0 -0
  51. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/tree_help.py +0 -0
  52. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/cli/verbs.py +0 -0
  53. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/client.py +0 -0
  54. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/configuration.py +0 -0
  55. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/context.py +0 -0
  56. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/dataset_sync.py +0 -0
  57. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/datasets.py +0 -0
  58. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/decorators.py +0 -0
  59. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/deployment.py +0 -0
  60. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/eval_handler.py +0 -0
  61. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/execution.py +0 -0
  62. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/exporter.py +0 -0
  63. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/exporter_http.py +0 -0
  64. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/helpers.py +0 -0
  65. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/identity.py +0 -0
  66. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/imaging.py +0 -0
  67. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/ir/__init__.py +0 -0
  68. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/ir/decode.py +0 -0
  69. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/ir/encode.py +0 -0
  70. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/ir/hash.py +0 -0
  71. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/ir/program.py +0 -0
  72. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/ir/reflection.py +0 -0
  73. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/ir/types.py +0 -0
  74. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/ir/xxh64.py +0 -0
  75. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/mcp_servers.py +0 -0
  76. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/models.py +0 -0
  77. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/optimizers/__init__.py +0 -0
  78. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/optimizers/gepa.py +0 -0
  79. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/optimizers/gepa_callback.py +0 -0
  80. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/optimizers/resume.py +0 -0
  81. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/optimizers/tracker.py +0 -0
  82. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/retry.py +0 -0
  83. {cmpnd-0.27.1 → cmpnd-0.27.2}/cmpnd/traces.py +0 -0
  84. {cmpnd-0.27.1 → cmpnd-0.27.2}/docs/2026-04-02-code-review.md +0 -0
  85. {cmpnd-0.27.1 → cmpnd-0.27.2}/docs/cli.md +0 -0
  86. {cmpnd-0.27.1 → cmpnd-0.27.2}/docs/client.md +0 -0
  87. {cmpnd-0.27.1 → cmpnd-0.27.2}/docs/custom-proposer.md +0 -0
  88. {cmpnd-0.27.1 → cmpnd-0.27.2}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  89. {cmpnd-0.27.1 → cmpnd-0.27.2}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  90. {cmpnd-0.27.1 → cmpnd-0.27.2}/docs/reading-traces.md +0 -0
  91. {cmpnd-0.27.1 → cmpnd-0.27.2}/examples/generalizing_proposer.py +0 -0
  92. {cmpnd-0.27.1 → cmpnd-0.27.2}/examples/local_ollama.py +0 -0
  93. {cmpnd-0.27.1 → cmpnd-0.27.2}/ingest/LINEAGE.md +0 -0
  94. {cmpnd-0.27.1 → cmpnd-0.27.2}/ingest/README.md +0 -0
  95. {cmpnd-0.27.1 → cmpnd-0.27.2}/ingest/cmpnd_ingest/__init__.py +0 -0
  96. {cmpnd-0.27.1 → cmpnd-0.27.2}/ingest/cmpnd_ingest/bodies.py +0 -0
  97. {cmpnd-0.27.1 → cmpnd-0.27.2}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  98. {cmpnd-0.27.1 → cmpnd-0.27.2}/ingest/cmpnd_ingest/protocols.py +0 -0
  99. {cmpnd-0.27.1 → cmpnd-0.27.2}/ingest/cmpnd_ingest/py.typed +0 -0
  100. {cmpnd-0.27.1 → cmpnd-0.27.2}/ingest/cmpnd_ingest/request_log.py +0 -0
  101. {cmpnd-0.27.1 → cmpnd-0.27.2}/ingest/cmpnd_ingest/retry.py +0 -0
  102. {cmpnd-0.27.1 → cmpnd-0.27.2}/ingest/pyproject.toml +0 -0
  103. {cmpnd-0.27.1 → cmpnd-0.27.2}/scripts/audit-sdist.sh +0 -0
  104. {cmpnd-0.27.1 → cmpnd-0.27.2}/scripts/capture_review_run.py +0 -0
  105. {cmpnd-0.27.1 → cmpnd-0.27.2}/scripts/check_release_artifacts.py +0 -0
  106. {cmpnd-0.27.1 → cmpnd-0.27.2}/scripts/fixtures/review_run.json.gz +0 -0
  107. {cmpnd-0.27.1 → cmpnd-0.27.2}/scripts/gepa_resume_bug.py +0 -0
  108. {cmpnd-0.27.1 → cmpnd-0.27.2}/scripts/release-stress.sh +0 -0
  109. {cmpnd-0.27.1 → cmpnd-0.27.2}/scripts/require_version_bump.py +0 -0
  110. {cmpnd-0.27.1 → cmpnd-0.27.2}/scripts/seed_review_run.py +0 -0
  111. {cmpnd-0.27.1 → cmpnd-0.27.2}/scripts/test-in-linux.sh +0 -0
  112. {cmpnd-0.27.1 → cmpnd-0.27.2}/stubs/dspy/__init__.pyi +0 -0
  113. {cmpnd-0.27.1 → cmpnd-0.27.2}/stubs/dspy/primitives/__init__.pyi +0 -0
  114. {cmpnd-0.27.1 → cmpnd-0.27.2}/stubs/dspy/primitives/prediction.pyi +0 -0
  115. {cmpnd-0.27.1 → cmpnd-0.27.2}/stubs/dspy/utils/__init__.pyi +0 -0
  116. {cmpnd-0.27.1 → cmpnd-0.27.2}/stubs/dspy/utils/callback.pyi +0 -0
  117. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/__init__.py +0 -0
  118. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/_exporter_helpers.py +0 -0
  119. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/_fake_clock.py +0 -0
  120. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/_identity_helpers.py +0 -0
  121. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/conftest.py +0 -0
  122. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/__init__.py +0 -0
  123. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/committee_task.py +0 -0
  124. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/conftest.py +0 -0
  125. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  126. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  127. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/fake_lm.py +0 -0
  128. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/helpers.py +0 -0
  129. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/target_capabilities.py +0 -0
  130. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_assessments_cli.py +0 -0
  131. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_capture_prompts.py +0 -0
  132. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_composite_module.py +0 -0
  133. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_dataset_auto_creation.py +0 -0
  134. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_deploy_execute.py +0 -0
  135. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_deploy_programs.py +0 -0
  136. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_edge_cases.py +0 -0
  137. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_example_hash.py +0 -0
  138. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_gepa_optimization.py +0 -0
  139. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_interpreter_spans.py +0 -0
  140. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_lm_accuracy.py +0 -0
  141. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_module_tracing.py +0 -0
  142. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_optimization_lineage.py +0 -0
  143. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_optimization_metric_identity.py +0 -0
  144. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_optimize_failures.py +0 -0
  145. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_optimize_loop.py +0 -0
  146. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_optimize_review_committee.py +0 -0
  147. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  148. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_parallel_export.py +0 -0
  149. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_project_attribution.py +0 -0
  150. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_react_tool_spans.py +0 -0
  151. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_save_load.py +0 -0
  152. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_trace_structure.py +0 -0
  153. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_traces_read.py +0 -0
  154. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_user_attribution.py +0 -0
  155. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/e2e/test_valsets_cli.py +0 -0
  156. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/01_predict_qa.json +0 -0
  157. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  158. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  159. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  160. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  161. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  162. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/07_program_of_thought.json +0 -0
  163. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/08_opaque_module.json +0 -0
  164. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  165. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/10_react.json +0 -0
  166. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/11_refine.json +0 -0
  167. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/12_retrieve.json +0 -0
  168. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/13_knn.json +0 -0
  169. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/14_parallel.json +0 -0
  170. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/identity_vectors/README.md +0 -0
  171. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/ingest_conformance.py +0 -0
  172. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/__init__.py +0 -0
  173. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/authored_optimize/judge.py +0 -0
  174. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  175. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/conftest.py +0 -0
  176. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  177. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/hosted_optimize_probe.py +0 -0
  178. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/optimization_run_probe.py +0 -0
  179. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  180. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/test_logs_renders_traces.py +0 -0
  181. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/test_optimization_run.py +0 -0
  182. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/test_optimizing_progress.py +0 -0
  183. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/integration/test_unhappy_path.py +0 -0
  184. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/load/__init__.py +0 -0
  185. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/load/__main__.py +0 -0
  186. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/load/drain.py +0 -0
  187. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/load/ingest.py +0 -0
  188. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/load/latency_transport.py +0 -0
  189. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/load/rlm_shape.py +0 -0
  190. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/load/sweep.py +0 -0
  191. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/load/test_rlm_shape.py +0 -0
  192. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/__init__.py +0 -0
  193. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/conftest.py +0 -0
  194. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/seed_parity.py +0 -0
  195. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_auth.py +0 -0
  196. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_datasets.py +0 -0
  197. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_deployments.py +0 -0
  198. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_evals.py +0 -0
  199. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_health.py +0 -0
  200. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_modules.py +0 -0
  201. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_optimizations.py +0 -0
  202. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_programs.py +0 -0
  203. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_signatures.py +0 -0
  204. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_trace_streaming.py +0 -0
  205. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/parity/test_traces.py +0 -0
  206. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/__init__.py +0 -0
  207. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/_provider.py +0 -0
  208. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/_reliability.py +0 -0
  209. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/_result.py +0 -0
  210. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/mint_bedrock_token.py +0 -0
  211. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_deploy_programs.py +0 -0
  212. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_multi_client_stress.py +0 -0
  213. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_optimize_client_committee.py +0 -0
  214. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_optimize_loop.py +0 -0
  215. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_optimize_loop_wasm.py +0 -0
  216. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_optimize_stress.py +0 -0
  217. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_provider_roles.py +0 -0
  218. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_real_provider_twins.py +0 -0
  219. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_reliability.py +0 -0
  220. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_reliability_classify.py +0 -0
  221. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_rlm_wasm.py +0 -0
  222. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/stress/test_trace_search.py +0 -0
  223. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_assessments_api.py +0 -0
  224. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_authored.py +0 -0
  225. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_authored_bundle_collisions.py +0 -0
  226. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_authored_metric_contract.py +0 -0
  227. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_authored_named_models.py +0 -0
  228. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_authored_optimize.py +0 -0
  229. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_callback.py +0 -0
  230. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_candidate_lineage.py +0 -0
  231. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/__init__.py +0 -0
  232. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/conftest.py +0 -0
  233. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_admin.py +0 -0
  234. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_api_client.py +0 -0
  235. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_assessments.py +0 -0
  236. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_auth.py +0 -0
  237. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_credentials.py +0 -0
  238. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_datasets.py +0 -0
  239. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_deployments.py +0 -0
  240. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_evals.py +0 -0
  241. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_login.py +0 -0
  242. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_main.py +0 -0
  243. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_optimizations.py +0 -0
  244. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_org.py +0 -0
  245. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_pagination.py +0 -0
  246. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_parity.py +0 -0
  247. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_programs.py +0 -0
  248. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_provider_credentials.py +0 -0
  249. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_schema_sample.py +0 -0
  250. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_shell.py +0 -0
  251. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_signatures.py +0 -0
  252. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_tool_servers.py +0 -0
  253. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_top.py +0 -0
  254. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_traces.py +0 -0
  255. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_traces_search_wire.py +0 -0
  256. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_tree_help.py +0 -0
  257. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cli/test_valsets.py +0 -0
  258. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_client.py +0 -0
  259. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_clustering/__init__.py +0 -0
  260. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_clustering/conftest.py +0 -0
  261. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_clustering/test_edge_cases.py +0 -0
  262. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_clustering/test_hash_consistency.py +0 -0
  263. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_clustering/test_type_conversion.py +0 -0
  264. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_cmp551_grouping_identity.py +0 -0
  265. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_configuration.py +0 -0
  266. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_context.py +0 -0
  267. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_context_propagation.py +0 -0
  268. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_dataset_polling.py +0 -0
  269. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_dataset_sync.py +0 -0
  270. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_datasets.py +0 -0
  271. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_decorators.py +0 -0
  272. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_deploy.py +0 -0
  273. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_deploy_mcp_tool_references.py +0 -0
  274. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_eval.py +0 -0
  275. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_eval_example_attribution.py +0 -0
  276. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_eval_start.py +0 -0
  277. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_eval_trace_grouping.py +0 -0
  278. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_exception_paths.py +0 -0
  279. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_execute.py +0 -0
  280. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_exporter.py +0 -0
  281. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_exporter_queue.py +0 -0
  282. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_exporter_real.py +0 -0
  283. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_fake_lm.py +0 -0
  284. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_gepa_callback.py +0 -0
  285. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_gepa_integration.py +0 -0
  286. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_gepa_resume.py +0 -0
  287. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_identity.py +0 -0
  288. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_imaging.py +0 -0
  289. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_import_weight.py +0 -0
  290. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_ingest_conformance.py +0 -0
  291. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_ingest_second_producer.py +0 -0
  292. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_integration.py +0 -0
  293. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_interpreter_callback.py +0 -0
  294. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_ir_encoder_properties.py +0 -0
  295. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_ir_golden_vectors.py +0 -0
  296. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_ir_roundtrip.py +0 -0
  297. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_lm_usage_shapes.py +0 -0
  298. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_mcp_tools.py +0 -0
  299. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_models.py +0 -0
  300. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_optimization_progress.py +0 -0
  301. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_optimization_start.py +0 -0
  302. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_optimize.py +0 -0
  303. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_optimize_budget.py +0 -0
  304. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_optimize_lift.py +0 -0
  305. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_optimize_lms.py +0 -0
  306. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_optimize_proposer.py +0 -0
  307. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_optimize_reflection_lm.py +0 -0
  308. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_optimize_terminal_failure.py +0 -0
  309. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_packaging.py +0 -0
  310. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_packaging_callable_source.py +0 -0
  311. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_packaging_mcp_tools.py +0 -0
  312. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_packaging_program_source.py +0 -0
  313. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_poll_until.py +0 -0
  314. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_program_lineage_persist.py +0 -0
  315. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_project_payload.py +0 -0
  316. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_reflector_smoke.py +0 -0
  317. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_release_stress_script.py +0 -0
  318. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_repro_reactv2_bugs.py +0 -0
  319. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_require_version_bump.py +0 -0
  320. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_retry.py +0 -0
  321. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_reversible_failures.py +0 -0
  322. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_rlm_patch.py +0 -0
  323. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_schemas.py +0 -0
  324. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_seed_review_run.py +0 -0
  325. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_span_sent_once.py +0 -0
  326. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_trace_render.py +0 -0
  327. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_traces_read.py +0 -0
  328. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_tracked_gepa.py +0 -0
  329. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_tracker_enrich.py +0 -0
  330. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_truncation.py +0 -0
  331. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_valsets_api.py +0 -0
  332. {cmpnd-0.27.1 → cmpnd-0.27.2}/tests/test_xxh64.py +0 -0
  333. {cmpnd-0.27.1 → cmpnd-0.27.2}/todo.md +0 -0
@@ -130,7 +130,7 @@ not a copy — see the reasoning at the top of `cmpnd/exporter_http.py`).
130
130
 
131
131
  The transport sits under its own directory, with its own `pyproject.toml`
132
132
  (`cmpnd-ingest`), so a producer that is not a DSPy program can install it without
133
- dspy: its entire dependency set is httpx. The wasm worker's native GEPA child
133
+ dspy: its entire dependency set is httpx. The optimizer's search process
134
134
  (`search-process/`) is that producer, and a venv unable to supply dspy is
135
135
  what enforces its boundary. `ingest/README.md` has the full argument.
136
136
 
@@ -158,7 +158,7 @@ What that costs you, before touching either pyproject:
158
158
  - `cmpnd/callback.py` - DSPy instrumentation, signature extraction
159
159
  - `cmpnd/optimizers/tracker.py` - Optimization tracking
160
160
  - `cmpnd/packaging.py` - Source ZIP generation for deploy (ported from the retired `deploy/local_cmpnd.py`)
161
- - `cmpnd/deployment.py` - `deploy()` function: SDK -> serve -> AWS deploy stack
161
+ - `cmpnd/deployment.py` - `deploy()` function: package the module, create the deployment on serve, PUT the bundle to the upload slot serve provisioned (a presigned S3 PUT when hosted, the worker's own slot on-prem), poll the parser
162
162
 
163
163
  ## Reflector ↔ DSPy compatibility
164
164
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.27.1
3
+ Version: 0.27.2
4
4
  Summary: DSPy observability and deployment SDK for cmpnd
5
5
  Project-URL: Homepage, https://cmpnd.ai
6
6
  Author-email: cmpnd <hello@cmpnd.ai>
@@ -29,6 +29,7 @@ from .decorators import start_span, trace
29
29
  from .deployment import DeployFailed, deploy, deploy_bundle
30
30
  from .execution import DeployedProgram, RetryableProviderError, execute
31
31
  from .exporter import flush_exporter, shutdown_exporter
32
+ from .lm_configuration import configure_lm
32
33
  from .mcp_servers import (
33
34
  Bearer,
34
35
  McpToolReference,
@@ -57,6 +58,7 @@ _auto_instrument_callback: CmpndCallback | None = None
57
58
  __all__ = [
58
59
  # Configuration
59
60
  "configure",
61
+ "configure_lm",
60
62
  "get_config",
61
63
  "require_config",
62
64
  # Main callback
@@ -56,7 +56,9 @@ def _configured(request: dict[str, Any]) -> dict[str, int | None]:
56
56
  transport while letting the file change the settings it owns, and records the two
57
57
  trace limits the create call carries.
58
58
  """
59
- from . import configuration
59
+ from . import configuration, lm_configuration
60
+
61
+ lm_configuration.begin_authored()
60
62
 
61
63
  original_configure = configuration.configure
62
64
  authored_limits: dict[str, int | None] = {"max_input_size": None, "max_output_size": None}
@@ -115,7 +117,6 @@ def _program_facts(
115
117
  authored_limits: dict[str, int | None],
116
118
  ) -> dict[str, Any]:
117
119
  """Everything the deployment create call reads off the imported program."""
118
- import dspy
119
120
 
120
121
  from . import configuration, deployment, identity, mcp_servers, packaging
121
122
  from .authored import ProgramFacts
@@ -163,7 +164,9 @@ def _program_facts(
163
164
  "input_field_types": [identity.get_type_string(f.annotation) for f in signature.input_fields.values()],
164
165
  "output_field_types": [identity.get_type_string(f.annotation) for f in signature.output_fields.values()],
165
166
  }
166
- lm = dspy.settings.lm
167
+ from . import lm_configuration
168
+
169
+ lm = packaging._extract_lm_object(program)
167
170
  lm_kwargs = getattr(lm, "kwargs", {}) if lm is not None else {}
168
171
  facts = ProgramFacts(
169
172
  program_signature_hash=identity.compute_program_hash(program),
@@ -177,6 +180,7 @@ def _program_facts(
177
180
  max_output_size=authored_limits["max_output_size"],
178
181
  tool_references=tool_references,
179
182
  tool_declarations=tool_declarations,
183
+ lm_capabilities=lm_configuration.resolve({"default-lm": lm} if lm is not None else {}, require_all=False),
180
184
  **fields,
181
185
  )
182
186
  return asdict(facts)
@@ -184,12 +188,20 @@ def _program_facts(
184
188
 
185
189
  def _check(request: dict[str, Any]) -> dict[str, Any]:
186
190
  """The program bundle's answer: one import, the entrypoint's facts."""
191
+ from . import lm_configuration, packaging
192
+
187
193
  authored_limits = _configured(request)
188
194
  with _staged(request) as root:
189
195
  manifest = json.loads((root / "manifest.json").read_text())
190
196
  module = _import_member(str(manifest["program"]))
191
197
  program, kind = _program(module, str(manifest["entrypoint"]))
192
- return {"facts": _program_facts(program, kind, authored_limits)}
198
+ facts = _program_facts(program, kind, authored_limits)
199
+ bindings = dict(getattr(module, "lms", None) or {})
200
+ default_lm = packaging._extract_lm_object(program)
201
+ if default_lm is not None:
202
+ bindings["default-lm"] = default_lm
203
+ facts["lm_capabilities"] = lm_configuration.resolve(bindings)
204
+ return {"facts": facts}
193
205
 
194
206
 
195
207
  def _check_optimize(request: dict[str, Any]) -> dict[str, Any]:
@@ -199,7 +211,7 @@ def _check_optimize(request: dict[str, Any]) -> dict[str, Any]:
199
211
  importing it three times would run its module scope three times — which is neither
200
212
  what the author's own script does nor what the kernel does.
201
213
  """
202
- from . import optimization
214
+ from . import lm_configuration, optimization, packaging
203
215
 
204
216
  ask = request["optimize"]
205
217
  authored_limits = _configured(request)
@@ -242,6 +254,7 @@ def _check_optimize(request: dict[str, Any]) -> dict[str, Any]:
242
254
  f"Bind `{program_entrypoint} = ...` to optimize the file's own program, or pass an "
243
255
  "existing deployment id to optimize that."
244
256
  )
257
+ program = None
245
258
  if declared_program is not None:
246
259
  program, kind = _program(module, program_entrypoint)
247
260
  facts = _program_facts(program, kind, authored_limits)
@@ -254,6 +267,13 @@ def _check_optimize(request: dict[str, Any]) -> dict[str, Any]:
254
267
  optimization._check_proposer_call(declared_proposer)
255
268
  found_proposer = proposer_entrypoint
256
269
 
270
+ bindings = dict(getattr(module, "lms", None) or {})
271
+ if program is not None:
272
+ default_lm = packaging._extract_lm_object(program)
273
+ if default_lm is not None:
274
+ bindings["default-lm"] = default_lm
275
+ declarations = lm_configuration.resolve(bindings)
276
+
257
277
  work = Path(request["work_dir"])
258
278
  trainset = _rows(module, "trainset")
259
279
  valset = _rows(module, "valset")
@@ -266,6 +286,7 @@ def _check_optimize(request: dict[str, Any]) -> dict[str, Any]:
266
286
  "program_entrypoint": found_program,
267
287
  "proposer_entrypoint": found_proposer,
268
288
  "lms": declared_lms,
289
+ "lm_capabilities": declarations,
269
290
  "trainset_rows": len(trainset),
270
291
  "valset_rows": len(valset),
271
292
  }
@@ -59,6 +59,7 @@ class ProgramFacts:
59
59
  max_output_size: int | None = None
60
60
  tool_references: list[dict[str, Any]] = field(default_factory=list)
61
61
  tool_declarations: list[dict[str, Any]] = field(default_factory=list)
62
+ lm_capabilities: dict[str, Any] = field(default_factory=dict)
62
63
 
63
64
  @classmethod
64
65
  def minimal(cls) -> ProgramFacts:
@@ -177,6 +178,9 @@ def _prepare(
177
178
  }
178
179
  bundle = _zip(manifest, members)
179
180
  facts = _check_bundle(bundle, api_key=api_key, endpoint=endpoint)
181
+ if facts.lm_capabilities:
182
+ manifest["lm_capabilities"] = facts.lm_capabilities
183
+ bundle = _zip(manifest, members)
180
184
  if facts.tool_declarations:
181
185
  # The child pinned these declarations while importing this exact source.
182
186
  # Serve checks each schema hash against the registry again on create, so
@@ -350,6 +354,8 @@ def _prepare_optimize(
350
354
  manifest["proposer_entrypoint"] = facts.proposer_entrypoint
351
355
  if program_facts is not None and program_facts.tool_declarations:
352
356
  manifest["tools"] = program_facts.tool_declarations
357
+ if found.get("lm_capabilities"):
358
+ manifest["lm_capabilities"] = found["lm_capabilities"]
353
359
  program_bundle = _zip(manifest, members) if facts.program_entrypoint else None
354
360
  optimization_bundle = _zip(manifest, {**members, **produced})
355
361
  return PreparedOptimize(
@@ -0,0 +1,203 @@
1
+ """Hosted LM declarations, resolved by binding name and object identity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, TypedDict
6
+
7
+
8
+ class LMConfigurationError(ValueError):
9
+ """A hosted binding declaration cannot be resolved consistently."""
10
+
11
+
12
+ class LMCapabilities(TypedDict):
13
+ """Explicit adapter hints for a hosted model, independent of its provider."""
14
+
15
+ supported_params: list[str]
16
+ supports_response_schema: bool
17
+
18
+
19
+ def _validate_lm_capabilities(value: object) -> dict[str, LMCapabilities]:
20
+ if not isinstance(value, dict):
21
+ raise LMConfigurationError("lm_capabilities must map exact binding names to capability declarations")
22
+ result: dict[str, LMCapabilities] = {}
23
+ for binding, declaration in value.items():
24
+ if not isinstance(binding, str) or not binding or binding != binding.strip():
25
+ raise LMConfigurationError(
26
+ "lm_capabilities binding names must be nonempty strings without surrounding whitespace"
27
+ )
28
+ if not isinstance(declaration, dict):
29
+ raise LMConfigurationError(f"lm_capabilities[{binding!r}] must be a capability declaration")
30
+ unknown = declaration.keys() - {"supported_params", "supports_response_schema"}
31
+ if unknown:
32
+ raise LMConfigurationError(f"lm_capabilities[{binding!r}] has unknown fields: {sorted(unknown, key=str)}")
33
+ params = declaration.get("supported_params", [])
34
+ if not isinstance(params, list) or any(not isinstance(param, str) or not param for param in params):
35
+ raise LMConfigurationError(
36
+ f"lm_capabilities[{binding!r}].supported_params must be a list of nonempty strings"
37
+ )
38
+ schema = declaration.get("supports_response_schema", False)
39
+ if not isinstance(schema, bool):
40
+ raise LMConfigurationError(f"lm_capabilities[{binding!r}].supports_response_schema must be a boolean")
41
+ if schema and "response_format" not in params:
42
+ raise LMConfigurationError(
43
+ f"lm_capabilities[{binding!r}] requires response_format in supported_params for schema output"
44
+ )
45
+ result[binding] = {"supported_params": list(dict.fromkeys(params)), "supports_response_schema": schema}
46
+ return result
47
+
48
+
49
+ _objects: list[Any] = []
50
+ _authored = False
51
+ _names: dict[str, dict[str, Any]] = {}
52
+ _restored: dict[str, dict[str, Any]] = {}
53
+
54
+
55
+ def reset() -> None:
56
+ global _authored
57
+ _authored = False
58
+ _objects.clear()
59
+ _names.clear()
60
+ _restored.clear()
61
+
62
+
63
+ def begin_authored() -> None:
64
+ """Start the isolated importer or kernel scope before loading authored code."""
65
+ global _authored
66
+ reset()
67
+ _authored = True
68
+
69
+
70
+ def _object_declaration(lm: Any) -> Any:
71
+ declaration = getattr(lm, "_cmpnd_lm_capabilities", None)
72
+ if declaration is not None and declaration[0] == id(lm):
73
+ return declaration[1:]
74
+ return None
75
+
76
+
77
+ def _name(value: object) -> str:
78
+ if not isinstance(value, str) or not value or value != value.strip():
79
+ raise LMConfigurationError("an LM binding name must be a nonempty string without surrounding whitespace")
80
+ return value
81
+
82
+
83
+ def _model(lm: Any) -> str:
84
+ model = lm if isinstance(lm, str) else getattr(lm, "model", None)
85
+ if not isinstance(model, str) or not model:
86
+ raise LMConfigurationError("configure_lm needs an LM object with a nonempty model name")
87
+ return model
88
+
89
+
90
+ def _merge(target: dict[str, Any], name: str, declaration: dict[str, Any]) -> None:
91
+ if name in target and target[name] != declaration:
92
+ raise LMConfigurationError(f"conflicting LM capability declarations for binding {name!r}")
93
+ target[name] = declaration
94
+
95
+
96
+ def configure_lm(
97
+ lm: Any,
98
+ *,
99
+ supported_params: list[str] | None = None,
100
+ supports_response_schema: bool = False,
101
+ ) -> None:
102
+ """Declare capabilities once for an LM object or explicit hosted binding name.
103
+
104
+ Every binding created from the same object shares its declaration. Separate
105
+ objects remain independent, even when their settings match. Packaged
106
+ declarations retain their resolved model and cannot follow a model change.
107
+ """
108
+ hints = dict(
109
+ _validate_lm_capabilities(
110
+ {
111
+ "binding": {
112
+ "supported_params": supported_params if supported_params is not None else [],
113
+ "supports_response_schema": supports_response_schema,
114
+ }
115
+ }
116
+ )["binding"]
117
+ )
118
+ if isinstance(lm, str):
119
+ if not _authored:
120
+ raise LMConfigurationError(
121
+ "named LM declarations are supported only inside authored programs; pass an LM object"
122
+ )
123
+ _merge(_names, _name(lm), hints)
124
+ return
125
+ model = _model(lm)
126
+ declared = _object_declaration(lm)
127
+ if declared is not None and declared != (model, hints):
128
+ raise LMConfigurationError("conflicting LM capability declarations for the same LM object")
129
+ lm._cmpnd_lm_capabilities = (id(lm), model, hints)
130
+ if _authored and not any(obj is lm for obj in _objects):
131
+ _objects.append(lm)
132
+
133
+
134
+ def restore(declarations: object) -> None:
135
+ """Load model-pinned declarations from a packaged artifact."""
136
+ if not isinstance(declarations, dict):
137
+ raise LMConfigurationError("lm_capabilities must map binding names to declarations")
138
+ for name, declaration in declarations.items():
139
+ _name(name)
140
+ if not isinstance(declaration, dict):
141
+ raise LMConfigurationError(f"lm_capabilities[{name!r}] must be a capability declaration")
142
+ model = _model(declaration.get("model"))
143
+ hints = _validate_lm_capabilities({name: {k: v for k, v in declaration.items() if k != "model"}})[name]
144
+ _merge(_restored, name, {"model": model, **hints})
145
+
146
+
147
+ def resolve(bindings: dict[str, Any], *, require_all: bool = True) -> dict[str, Any]:
148
+ """Resolve aliases and names, rejecting conflicts before any LM call.
149
+
150
+ Packaging a program alone may defer named optimization declarations. The
151
+ optimization package resolves the full set and rejects names it cannot bind.
152
+ """
153
+ result: dict[str, Any] = {}
154
+ for name, lm in bindings.items():
155
+ _name(name)
156
+ if name not in _restored and name not in _names and _object_declaration(lm) is None:
157
+ continue
158
+ model = _model(lm)
159
+ if name in _restored:
160
+ stored = _restored[name]
161
+ if stored["model"] != model:
162
+ raise LMConfigurationError(
163
+ f"model changed for binding {name!r}; configure the replacement with a new declaration"
164
+ )
165
+ _merge(result, name, dict(stored))
166
+ if name in _names:
167
+ _merge(result, name, {"model": model, **_names[name]})
168
+ declaration = _object_declaration(lm)
169
+ if declaration is not None:
170
+ declared_model, hints = declaration
171
+ if declared_model != model:
172
+ raise LMConfigurationError(
173
+ f"model changed for binding {name!r}; configure the replacement with a new declaration"
174
+ )
175
+ _merge(result, name, {"model": model, **hints})
176
+ if require_all:
177
+ unresolved = sorted((_names.keys() | _restored.keys()) - bindings.keys())
178
+ if unresolved:
179
+ raise LMConfigurationError(f"unresolved LM capability binding declarations: {', '.join(unresolved)}")
180
+ if any(not any(obj is lm for lm in bindings.values()) for obj in _objects):
181
+ raise LMConfigurationError(
182
+ "unresolved LM object capability declaration; pass that object as a hosted binding"
183
+ )
184
+ return result
185
+
186
+
187
+ def resolve_call(identities: dict[str, Any], models: dict[str, Any]) -> dict[str, Any]:
188
+ """Resolve the objects loaded for one call against its granted model names."""
189
+ declarations = resolve(identities, require_all=False)
190
+ unresolved = _names.keys() - identities.keys() - _restored.keys()
191
+ if unresolved:
192
+ raise LMConfigurationError(f"unresolved LM capability binding declarations: {', '.join(sorted(unresolved))}")
193
+ for obj in _objects:
194
+ if not any(obj is value for value in identities.values()):
195
+ raise LMConfigurationError(
196
+ "unresolved LM object capability declaration; pass that object as a hosted binding"
197
+ )
198
+ for name, model in models.items():
199
+ if name in declarations and declarations[name]["model"] != model:
200
+ raise LMConfigurationError(
201
+ f"model changed for binding {name!r}; configure the replacement with a new declaration"
202
+ )
203
+ return {name: declaration for name, declaration in declarations.items() if name in models}
@@ -658,6 +658,7 @@ def _build_optimization_bundle(
658
658
  valset: list[Any],
659
659
  metric: Callable[..., Any],
660
660
  proposer: Callable[..., Any] | None = None,
661
+ lm_capabilities: dict[str, Any] | None = None,
661
662
  ) -> bytes:
662
663
  """Build the optimization-bundle ZIP: ``trainset.ndjson`` +
663
664
  ``valset.ndjson`` (one flattened example per line) + ``metric.py`` (the
@@ -679,6 +680,13 @@ def _build_optimization_bundle(
679
680
  "runs in a sandbox from its own source, so there is nothing else to send."
680
681
  )
681
682
 
683
+ if lm_capabilities:
684
+ preamble = "from cmpnd.lm_configuration import restore as _restore_lms\n"
685
+ preamble += f"_restore_lms({lm_capabilities!r})\n"
686
+ metric_source += "\n" + preamble
687
+ if proposer_source is not None:
688
+ proposer_source += "\n" + preamble
689
+
682
690
  def _ndjson(rows: list[Any]) -> str:
683
691
  return "\n".join(json.dumps(_example_to_dict(r)) for r in rows) + "\n"
684
692
 
@@ -919,7 +927,15 @@ def optimize(
919
927
 
920
928
  cfg = configuration.require_config()
921
929
 
922
- bundle = _build_optimization_bundle(trainset, valset, metric, proposer)
930
+ from . import lm_configuration
931
+
932
+ bindings = {name: entry["model"] for name, entry in (config.get("lms") or {}).items()}
933
+ bindings.update(lms or {})
934
+ default_lm = packaging._extract_lm_object(program)
935
+ if default_lm is not None:
936
+ bindings["default-lm"] = default_lm
937
+ declarations = lm_configuration.resolve(bindings)
938
+ bundle = _build_optimization_bundle(trainset, valset, metric, proposer, declarations)
923
939
 
924
940
  if deployment_id is None:
925
941
  deployment_id = str(deployment.deploy(program, metric=metric))
@@ -1083,9 +1099,18 @@ def optimize_authored(
1083
1099
  _refuse_conflicting_members(cfg, deployment_id, prepared.members)
1084
1100
  facts = _deployment_signature_facts(cfg, deployment_id)
1085
1101
 
1102
+ from . import lm_configuration
1103
+
1104
+ bindings = {name: entry["model"] for name, entry in (config.get("lms") or {}).items()}
1105
+ bindings.update(lms or {})
1106
+ if facts is not None and facts.litellm_model:
1107
+ bindings["default-lm"] = facts.litellm_model
1108
+ declarations = lm_configuration.resolve(bindings)
1109
+ bundle = _authored_capabilities_bundle(prepared.optimization_bundle, declarations)
1110
+
1086
1111
  best_deployment_id, best_score = _run_steps(
1087
1112
  cfg,
1088
- prepared.optimization_bundle,
1113
+ bundle,
1089
1114
  deployment_id,
1090
1115
  optimizer=optimizer,
1091
1116
  config=config,
@@ -1110,6 +1135,28 @@ def optimize_authored(
1110
1135
  )
1111
1136
 
1112
1137
 
1138
+ def _authored_capabilities_bundle(bundle: bytes, declarations: dict[str, Any]) -> bytes:
1139
+ """Add caller-resolved bindings without changing any authored source bytes."""
1140
+ if not declarations:
1141
+ return bundle
1142
+ from .lm_configuration import LMConfigurationError
1143
+
1144
+ output = io.BytesIO()
1145
+ with zipfile.ZipFile(io.BytesIO(bundle)) as archive:
1146
+ manifest = json.loads(archive.read("manifest.json"))
1147
+ merged = dict(manifest.get("lm_capabilities") or {})
1148
+ for name, declaration in declarations.items():
1149
+ if name in merged and merged[name] != declaration:
1150
+ raise LMConfigurationError(f"conflicting LM capability declarations for binding {name!r}")
1151
+ merged[name] = declaration
1152
+ manifest["lm_capabilities"] = merged
1153
+ with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as updated:
1154
+ for member in archive.infolist():
1155
+ body = json.dumps(manifest).encode() if member.filename == "manifest.json" else archive.read(member)
1156
+ updated.writestr(member, body)
1157
+ return output.getvalue()
1158
+
1159
+
1113
1160
  def _deployment_signature_facts(cfg: configuration.Config, deployment_id: str) -> authored.ProgramFacts | None:
1114
1161
  """Read an existing deployment's signature without making metadata a run prerequisite."""
1115
1162
  from . import identity
@@ -1120,7 +1167,8 @@ def _deployment_signature_facts(cfg: configuration.Config, deployment_id: str) -
1120
1167
  response = client.get(f"{cfg.endpoint}/api/v1/deployments/{deployment_id}", headers=headers)
1121
1168
  if not response.is_success:
1122
1169
  return None
1123
- value = response.json().get("program_signature_hash")
1170
+ deployment_row = response.json()
1171
+ value = deployment_row.get("program_signature_hash")
1124
1172
  if value is None or value == "":
1125
1173
  return None
1126
1174
  # Deployment responses use the 16-digit hex display form. Signature routes
@@ -1133,6 +1181,7 @@ def _deployment_signature_facts(cfg: configuration.Config, deployment_id: str) -
1133
1181
  row = response.json()
1134
1182
  return authored.ProgramFacts(
1135
1183
  program_signature_hash=signature_hash,
1184
+ litellm_model=deployment_row.get("litellm_model"),
1136
1185
  signature_name=row.get("signature_name"),
1137
1186
  input_fields=row.get("input_fields") or [],
1138
1187
  output_fields=row.get("output_fields") or [],
@@ -34,12 +34,11 @@ if TYPE_CHECKING:
34
34
  import dspy
35
35
 
36
36
 
37
- def _extract_lm_state(module: dspy.Module) -> dict[str, typing.Any] | None:
38
- """Extract LM config from module predictors or global dspy.settings.
37
+ def _extract_lm_object(module: dspy.Module) -> dspy.LM | None:
38
+ """Find the program's LM object without losing its identity.
39
39
 
40
- Walks the module tree looking for any object with a non-None ``.lm``
41
- attribute, calls ``.dump_state()`` on the first one found. Falls back
42
- to ``dspy.settings.lm`` if no module-level LM exists.
40
+ Walk the module tree for a non-None ``.lm`` attribute, then fall back to
41
+ ``dspy.settings.lm`` if no module-level LM exists.
43
42
  """
44
43
  seen: set[int] = set()
45
44
 
@@ -62,9 +61,12 @@ def _extract_lm_state(module: dspy.Module) -> dict[str, typing.Any] | None:
62
61
  lm = _find_lm(module)
63
62
  if lm is None:
64
63
  lm = dspy.settings.lm
65
- if lm is not None and hasattr(lm, "dump_state"):
66
- return lm.dump_state()
67
- return None
64
+ return lm
65
+
66
+
67
+ def _extract_lm_state(module: dspy.Module) -> dict[str, typing.Any] | None:
68
+ lm = _extract_lm_object(module)
69
+ return lm.dump_state() if lm is not None and hasattr(lm, "dump_state") else None
68
70
 
69
71
 
70
72
  class ImportEntry(typing.NamedTuple):
@@ -1248,7 +1250,12 @@ def generate_source_zip(
1248
1250
  if metric_source is not None:
1249
1251
  metric_filename = "metric_source.py"
1250
1252
 
1253
+ from . import lm_configuration
1254
+
1255
+ lm = _extract_lm_object(module)
1256
+ declarations = lm_configuration.resolve({"default-lm": lm} if lm is not None else {}, require_all=False)
1251
1257
  manifest = {
1258
+ "lm_capabilities": declarations,
1252
1259
  "dependency_versions": {
1253
1260
  "python": platform.python_version(),
1254
1261
  "dspy": dspy.__version__,
@@ -19,7 +19,7 @@ cmpnd.configure(api_key="ck_...", project="my-app")
19
19
  cmpnd.auto_instrument()
20
20
  ```
21
21
 
22
- - **`cmpnd.configure()`** — `python-sdk/cmpnd/config.py:42`. Reads `api_key` and `endpoint` from args or `CMPND_API_KEY` / `CMPND_ENDPOINT` env vars, builds a `Config` dataclass (`config.py:12`), and stores it in a `ContextVar`. This call does not start any threads. It also does not touch DSPy. You can call `configure()` in a library and let the user decide whether to instrument.
22
+ - **`cmpnd.configure()`** — `cmpnd/configuration.py`. Reads `api_key` and `endpoint` from args or `CMPND_API_KEY` / `CMPND_ENDPOINT` env vars, builds a `Config` dataclass (`configuration.py`), and stores it under a lock. This call does not start any threads. It also does not touch DSPy. You can call `configure()` in a library and let the user decide whether to instrument.
23
23
  - **`cmpnd.auto_instrument()`** — `python-sdk/cmpnd/__init__.py:272`. Requires `configure()` to have been called. It instantiates a `CmpndCallback`, appends it to `dspy.settings.callbacks` (deduplicating if already installed), and patches `dspy.GEPA.__init__` / `GEPA.compile` so optimization runs are tracked automatically. See [`./optimization-tracking.md`](../../docs/optimization-tracking.md) for what that patch does.
24
24
 
25
25
  The exporter thread isn't started here directly. It is lazily started on first export via `ensure_exporter_started()` in `python-sdk/cmpnd/exporter.py:41`.
@@ -30,6 +30,49 @@ For an explicit, scoped alternative that traces only the programs you run throug
30
30
 
31
31
  ---
32
32
 
33
+ ## Hosted LM capabilities
34
+
35
+ Declare hosted capabilities once for an LM object. Inside an authored program,
36
+ you can also name an explicit binding.
37
+ Every binding created from the same object shares its declaration. Separate LM
38
+ objects remain independent, even when their model names and sampling match.
39
+
40
+ ```python
41
+ import cmpnd
42
+ import dspy
43
+
44
+ lm = dspy.LM("provider/model", reasoning_effort="low")
45
+ cmpnd.configure_lm(
46
+ lm,
47
+ supported_params=["response_format"],
48
+ supports_response_schema=True,
49
+ )
50
+ dspy.configure(lm=lm, adapter=dspy.JSONAdapter())
51
+ ```
52
+
53
+ The object form follows the object into `default-lm` or the named `lms` passed to
54
+ optimization. The string form names a binding such as `default-lm`, `judge`, or
55
+ an authored proposer's LM. Call that form inside the authored source file, for
56
+ example `cmpnd.configure_lm("judge", supported_params=["response_format"])`.
57
+ Notebook and other live SDK callers pass LM objects. Object declarations belong
58
+ to those objects and apply only to workflows that use them, so independent
59
+ optimizations can run sequentially in one process.
60
+ Each binding has one model within a program or run.
61
+ Identical declarations are idempotent. Conflicting declarations and names that
62
+ an optimization cannot resolve raise an error before inference.
63
+
64
+ The SDK records resolved declarations in the program and optimization bundles.
65
+ Execute, metric, and authored proposer turns use those snapshots. A declaration
66
+ retains its resolved model, so replacing that model requires a new declaration.
67
+ An LM object's identity stays local to Python. The bundle carries its resolved
68
+ binding names and capabilities.
69
+
70
+ List `response_format` to request JSON-object mode. Add
71
+ `supports_response_schema=True` when the model and its configured provider
72
+ endpoint accept JSON schemas. Undeclared bindings keep prompt-only JSON. These
73
+ hints do not add a format the provider cannot accept or change local DSPy's
74
+ capability lookup.
75
+
33
76
  ## The callback
34
77
 
35
78
  `python-sdk/cmpnd/callback.py` implements `CmpndCallback`, which satisfies `dspy.utils.callback.BaseCallback` without inheriting it — `callback.py` must import on the standard library alone (`tests/test_import_weight.py`), so it cannot import dspy to get a base class. DSPy calls its hook methods synchronously on whatever thread is executing the module. The callback owns no long-lived state beyond a per-call span builder map; everything trace/span related flows through context vars (see next section).
@@ -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.27.1"
8
+ version = "0.27.2"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"