cmpnd 0.27.0__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.0 → cmpnd-0.27.2}/AGENTS.md +2 -2
  2. {cmpnd-0.27.0 → cmpnd-0.27.2}/PKG-INFO +1 -1
  3. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/__init__.py +4 -1
  4. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/_authored_import.py +26 -5
  5. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/authored.py +6 -0
  6. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/execution.py +67 -3
  7. cmpnd-0.27.2/cmpnd/lm_configuration.py +203 -0
  8. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/optimization.py +52 -3
  9. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/packaging.py +15 -8
  10. {cmpnd-0.27.0 → cmpnd-0.27.2}/docs/sdk-instrumentation.md +44 -1
  11. {cmpnd-0.27.0 → cmpnd-0.27.2}/pyproject.toml +1 -1
  12. cmpnd-0.27.2/scripts/release-stress.sh +14 -0
  13. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/_reliability.py +8 -0
  14. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_reliability_classify.py +12 -1
  15. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_rlm_wasm.py +9 -11
  16. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_execute.py +77 -1
  17. cmpnd-0.27.2/tests/test_lm_configuration.py +286 -0
  18. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_reflector_properties.py +1 -1
  19. cmpnd-0.27.2/tests/test_release_stress_script.py +14 -0
  20. {cmpnd-0.27.0 → cmpnd-0.27.2}/uv.lock +1 -1
  21. {cmpnd-0.27.0 → cmpnd-0.27.2}/.gitignore +0 -0
  22. {cmpnd-0.27.0 → cmpnd-0.27.2}/CLAUDE.md +0 -0
  23. {cmpnd-0.27.0 → cmpnd-0.27.2}/README.md +0 -0
  24. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/_assessments.py +0 -0
  25. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/_exporter_pool.py +0 -0
  26. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/_gepa_patch.py +0 -0
  27. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/_http.py +0 -0
  28. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/_program_patch.py +0 -0
  29. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/_rlm_patch.py +0 -0
  30. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/_timespec.py +0 -0
  31. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/_traces.py +0 -0
  32. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/_valsets.py +0 -0
  33. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/callback.py +0 -0
  34. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/__init__.py +0 -0
  35. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/api_client.py +0 -0
  36. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/__init__.py +0 -0
  37. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/admin.py +0 -0
  38. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/assessments.py +0 -0
  39. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/auth.py +0 -0
  40. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/datasets.py +0 -0
  41. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/deployments.py +0 -0
  42. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/evals.py +0 -0
  43. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/login.py +0 -0
  44. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/optimizations.py +0 -0
  45. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/org.py +0 -0
  46. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/programs.py +0 -0
  47. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/provider_credentials.py +0 -0
  48. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/signatures.py +0 -0
  49. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/tool_servers.py +0 -0
  50. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/top.py +0 -0
  51. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/traces.py +0 -0
  52. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/commands/valsets.py +0 -0
  53. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/credentials.py +0 -0
  54. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/deprecation.py +0 -0
  55. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/pagination.py +0 -0
  56. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/shell.py +0 -0
  57. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/trace_render.py +0 -0
  58. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/tree_help.py +0 -0
  59. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/cli/verbs.py +0 -0
  60. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/client.py +0 -0
  61. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/configuration.py +0 -0
  62. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/context.py +0 -0
  63. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/dataset_sync.py +0 -0
  64. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/datasets.py +0 -0
  65. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/decorators.py +0 -0
  66. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/deployment.py +0 -0
  67. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/eval_handler.py +0 -0
  68. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/exporter.py +0 -0
  69. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/exporter_http.py +0 -0
  70. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/helpers.py +0 -0
  71. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/identity.py +0 -0
  72. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/imaging.py +0 -0
  73. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/ir/__init__.py +0 -0
  74. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/ir/decode.py +0 -0
  75. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/ir/encode.py +0 -0
  76. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/ir/hash.py +0 -0
  77. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/ir/program.py +0 -0
  78. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/ir/reflection.py +0 -0
  79. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/ir/types.py +0 -0
  80. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/ir/xxh64.py +0 -0
  81. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/mcp_servers.py +0 -0
  82. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/models.py +0 -0
  83. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/optimizers/__init__.py +0 -0
  84. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/optimizers/gepa.py +0 -0
  85. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/optimizers/gepa_callback.py +0 -0
  86. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/optimizers/resume.py +0 -0
  87. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/optimizers/tracker.py +0 -0
  88. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/retry.py +0 -0
  89. {cmpnd-0.27.0 → cmpnd-0.27.2}/cmpnd/traces.py +0 -0
  90. {cmpnd-0.27.0 → cmpnd-0.27.2}/docs/2026-04-02-code-review.md +0 -0
  91. {cmpnd-0.27.0 → cmpnd-0.27.2}/docs/cli.md +0 -0
  92. {cmpnd-0.27.0 → cmpnd-0.27.2}/docs/client.md +0 -0
  93. {cmpnd-0.27.0 → cmpnd-0.27.2}/docs/custom-proposer.md +0 -0
  94. {cmpnd-0.27.0 → cmpnd-0.27.2}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  95. {cmpnd-0.27.0 → cmpnd-0.27.2}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  96. {cmpnd-0.27.0 → cmpnd-0.27.2}/docs/reading-traces.md +0 -0
  97. {cmpnd-0.27.0 → cmpnd-0.27.2}/examples/generalizing_proposer.py +0 -0
  98. {cmpnd-0.27.0 → cmpnd-0.27.2}/examples/local_ollama.py +0 -0
  99. {cmpnd-0.27.0 → cmpnd-0.27.2}/ingest/LINEAGE.md +0 -0
  100. {cmpnd-0.27.0 → cmpnd-0.27.2}/ingest/README.md +0 -0
  101. {cmpnd-0.27.0 → cmpnd-0.27.2}/ingest/cmpnd_ingest/__init__.py +0 -0
  102. {cmpnd-0.27.0 → cmpnd-0.27.2}/ingest/cmpnd_ingest/bodies.py +0 -0
  103. {cmpnd-0.27.0 → cmpnd-0.27.2}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  104. {cmpnd-0.27.0 → cmpnd-0.27.2}/ingest/cmpnd_ingest/protocols.py +0 -0
  105. {cmpnd-0.27.0 → cmpnd-0.27.2}/ingest/cmpnd_ingest/py.typed +0 -0
  106. {cmpnd-0.27.0 → cmpnd-0.27.2}/ingest/cmpnd_ingest/request_log.py +0 -0
  107. {cmpnd-0.27.0 → cmpnd-0.27.2}/ingest/cmpnd_ingest/retry.py +0 -0
  108. {cmpnd-0.27.0 → cmpnd-0.27.2}/ingest/pyproject.toml +0 -0
  109. {cmpnd-0.27.0 → cmpnd-0.27.2}/scripts/audit-sdist.sh +0 -0
  110. {cmpnd-0.27.0 → cmpnd-0.27.2}/scripts/capture_review_run.py +0 -0
  111. {cmpnd-0.27.0 → cmpnd-0.27.2}/scripts/check_release_artifacts.py +0 -0
  112. {cmpnd-0.27.0 → cmpnd-0.27.2}/scripts/fixtures/review_run.json.gz +0 -0
  113. {cmpnd-0.27.0 → cmpnd-0.27.2}/scripts/gepa_resume_bug.py +0 -0
  114. {cmpnd-0.27.0 → cmpnd-0.27.2}/scripts/require_version_bump.py +0 -0
  115. {cmpnd-0.27.0 → cmpnd-0.27.2}/scripts/seed_review_run.py +0 -0
  116. {cmpnd-0.27.0 → cmpnd-0.27.2}/scripts/test-in-linux.sh +0 -0
  117. {cmpnd-0.27.0 → cmpnd-0.27.2}/stubs/dspy/__init__.pyi +0 -0
  118. {cmpnd-0.27.0 → cmpnd-0.27.2}/stubs/dspy/primitives/__init__.pyi +0 -0
  119. {cmpnd-0.27.0 → cmpnd-0.27.2}/stubs/dspy/primitives/prediction.pyi +0 -0
  120. {cmpnd-0.27.0 → cmpnd-0.27.2}/stubs/dspy/utils/__init__.pyi +0 -0
  121. {cmpnd-0.27.0 → cmpnd-0.27.2}/stubs/dspy/utils/callback.pyi +0 -0
  122. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/__init__.py +0 -0
  123. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/_exporter_helpers.py +0 -0
  124. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/_fake_clock.py +0 -0
  125. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/_identity_helpers.py +0 -0
  126. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/conftest.py +0 -0
  127. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/__init__.py +0 -0
  128. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/committee_task.py +0 -0
  129. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/conftest.py +0 -0
  130. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  131. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  132. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/fake_lm.py +0 -0
  133. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/helpers.py +0 -0
  134. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/target_capabilities.py +0 -0
  135. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_assessments_cli.py +0 -0
  136. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_capture_prompts.py +0 -0
  137. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_composite_module.py +0 -0
  138. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_dataset_auto_creation.py +0 -0
  139. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_deploy_execute.py +0 -0
  140. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_deploy_programs.py +0 -0
  141. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_edge_cases.py +0 -0
  142. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_example_hash.py +0 -0
  143. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_gepa_optimization.py +0 -0
  144. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_interpreter_spans.py +0 -0
  145. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_lm_accuracy.py +0 -0
  146. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_module_tracing.py +0 -0
  147. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_optimization_lineage.py +0 -0
  148. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_optimization_metric_identity.py +0 -0
  149. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_optimize_failures.py +0 -0
  150. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_optimize_loop.py +0 -0
  151. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_optimize_review_committee.py +0 -0
  152. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  153. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_parallel_export.py +0 -0
  154. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_project_attribution.py +0 -0
  155. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_react_tool_spans.py +0 -0
  156. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_save_load.py +0 -0
  157. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_trace_structure.py +0 -0
  158. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_traces_read.py +0 -0
  159. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_user_attribution.py +0 -0
  160. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/e2e/test_valsets_cli.py +0 -0
  161. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/01_predict_qa.json +0 -0
  162. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  163. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  164. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  165. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  166. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  167. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/07_program_of_thought.json +0 -0
  168. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/08_opaque_module.json +0 -0
  169. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  170. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/10_react.json +0 -0
  171. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/11_refine.json +0 -0
  172. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/12_retrieve.json +0 -0
  173. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/13_knn.json +0 -0
  174. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/14_parallel.json +0 -0
  175. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/identity_vectors/README.md +0 -0
  176. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/ingest_conformance.py +0 -0
  177. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/__init__.py +0 -0
  178. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/authored_optimize/judge.py +0 -0
  179. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  180. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/conftest.py +0 -0
  181. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  182. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/hosted_optimize_probe.py +0 -0
  183. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/optimization_run_probe.py +0 -0
  184. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  185. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/test_logs_renders_traces.py +0 -0
  186. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/test_optimization_run.py +0 -0
  187. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/test_optimizing_progress.py +0 -0
  188. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/integration/test_unhappy_path.py +0 -0
  189. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/load/__init__.py +0 -0
  190. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/load/__main__.py +0 -0
  191. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/load/drain.py +0 -0
  192. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/load/ingest.py +0 -0
  193. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/load/latency_transport.py +0 -0
  194. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/load/rlm_shape.py +0 -0
  195. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/load/sweep.py +0 -0
  196. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/load/test_rlm_shape.py +0 -0
  197. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/__init__.py +0 -0
  198. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/conftest.py +0 -0
  199. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/seed_parity.py +0 -0
  200. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_auth.py +0 -0
  201. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_datasets.py +0 -0
  202. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_deployments.py +0 -0
  203. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_evals.py +0 -0
  204. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_health.py +0 -0
  205. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_modules.py +0 -0
  206. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_optimizations.py +0 -0
  207. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_programs.py +0 -0
  208. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_signatures.py +0 -0
  209. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_trace_streaming.py +0 -0
  210. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/parity/test_traces.py +0 -0
  211. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/__init__.py +0 -0
  212. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/_provider.py +0 -0
  213. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/_result.py +0 -0
  214. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/mint_bedrock_token.py +0 -0
  215. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_deploy_programs.py +0 -0
  216. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_multi_client_stress.py +0 -0
  217. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_optimize_client_committee.py +0 -0
  218. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_optimize_loop.py +0 -0
  219. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_optimize_loop_wasm.py +0 -0
  220. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_optimize_stress.py +0 -0
  221. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_provider_roles.py +0 -0
  222. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_real_provider_twins.py +0 -0
  223. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_reliability.py +0 -0
  224. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/stress/test_trace_search.py +0 -0
  225. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_assessments_api.py +0 -0
  226. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_authored.py +0 -0
  227. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_authored_bundle_collisions.py +0 -0
  228. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_authored_metric_contract.py +0 -0
  229. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_authored_named_models.py +0 -0
  230. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_authored_optimize.py +0 -0
  231. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_callback.py +0 -0
  232. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_candidate_lineage.py +0 -0
  233. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/__init__.py +0 -0
  234. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/conftest.py +0 -0
  235. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_admin.py +0 -0
  236. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_api_client.py +0 -0
  237. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_assessments.py +0 -0
  238. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_auth.py +0 -0
  239. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_credentials.py +0 -0
  240. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_datasets.py +0 -0
  241. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_deployments.py +0 -0
  242. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_evals.py +0 -0
  243. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_login.py +0 -0
  244. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_main.py +0 -0
  245. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_optimizations.py +0 -0
  246. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_org.py +0 -0
  247. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_pagination.py +0 -0
  248. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_parity.py +0 -0
  249. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_programs.py +0 -0
  250. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_provider_credentials.py +0 -0
  251. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_schema_sample.py +0 -0
  252. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_shell.py +0 -0
  253. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_signatures.py +0 -0
  254. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_tool_servers.py +0 -0
  255. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_top.py +0 -0
  256. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_traces.py +0 -0
  257. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_traces_search_wire.py +0 -0
  258. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_tree_help.py +0 -0
  259. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cli/test_valsets.py +0 -0
  260. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_client.py +0 -0
  261. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_clustering/__init__.py +0 -0
  262. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_clustering/conftest.py +0 -0
  263. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_clustering/test_edge_cases.py +0 -0
  264. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_clustering/test_hash_consistency.py +0 -0
  265. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_clustering/test_type_conversion.py +0 -0
  266. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_cmp551_grouping_identity.py +0 -0
  267. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_configuration.py +0 -0
  268. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_context.py +0 -0
  269. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_context_propagation.py +0 -0
  270. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_dataset_polling.py +0 -0
  271. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_dataset_sync.py +0 -0
  272. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_datasets.py +0 -0
  273. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_decorators.py +0 -0
  274. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_deploy.py +0 -0
  275. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_deploy_mcp_tool_references.py +0 -0
  276. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_eval.py +0 -0
  277. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_eval_example_attribution.py +0 -0
  278. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_eval_start.py +0 -0
  279. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_eval_trace_grouping.py +0 -0
  280. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_exception_paths.py +0 -0
  281. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_exporter.py +0 -0
  282. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_exporter_queue.py +0 -0
  283. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_exporter_real.py +0 -0
  284. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_fake_lm.py +0 -0
  285. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_gepa_callback.py +0 -0
  286. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_gepa_integration.py +0 -0
  287. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_gepa_resume.py +0 -0
  288. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_identity.py +0 -0
  289. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_imaging.py +0 -0
  290. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_import_weight.py +0 -0
  291. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_ingest_conformance.py +0 -0
  292. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_ingest_second_producer.py +0 -0
  293. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_integration.py +0 -0
  294. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_interpreter_callback.py +0 -0
  295. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_ir_encoder_properties.py +0 -0
  296. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_ir_golden_vectors.py +0 -0
  297. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_ir_roundtrip.py +0 -0
  298. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_lm_usage_shapes.py +0 -0
  299. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_mcp_tools.py +0 -0
  300. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_models.py +0 -0
  301. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_optimization_progress.py +0 -0
  302. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_optimization_start.py +0 -0
  303. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_optimize.py +0 -0
  304. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_optimize_budget.py +0 -0
  305. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_optimize_lift.py +0 -0
  306. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_optimize_lms.py +0 -0
  307. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_optimize_proposer.py +0 -0
  308. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_optimize_reflection_lm.py +0 -0
  309. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_optimize_terminal_failure.py +0 -0
  310. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_packaging.py +0 -0
  311. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_packaging_callable_source.py +0 -0
  312. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_packaging_mcp_tools.py +0 -0
  313. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_packaging_program_source.py +0 -0
  314. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_poll_until.py +0 -0
  315. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_program_lineage_persist.py +0 -0
  316. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_project_payload.py +0 -0
  317. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_reflector_smoke.py +0 -0
  318. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_repro_reactv2_bugs.py +0 -0
  319. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_require_version_bump.py +0 -0
  320. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_retry.py +0 -0
  321. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_reversible_failures.py +0 -0
  322. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_rlm_patch.py +0 -0
  323. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_schemas.py +0 -0
  324. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_seed_review_run.py +0 -0
  325. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_span_sent_once.py +0 -0
  326. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_trace_render.py +0 -0
  327. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_traces_read.py +0 -0
  328. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_tracked_gepa.py +0 -0
  329. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_tracker_enrich.py +0 -0
  330. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_truncation.py +0 -0
  331. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_valsets_api.py +0 -0
  332. {cmpnd-0.27.0 → cmpnd-0.27.2}/tests/test_xxh64.py +0 -0
  333. {cmpnd-0.27.0 → 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.0
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>
@@ -27,8 +27,9 @@ from .context import (
27
27
  from .datasets import DatasetNotFoundError, VersionNotFoundError, load_dataset
28
28
  from .decorators import start_span, trace
29
29
  from .deployment import DeployFailed, deploy, deploy_bundle
30
- from .execution import DeployedProgram, execute
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
@@ -93,6 +95,7 @@ __all__ = [
93
95
  "DeployFailed",
94
96
  "execute",
95
97
  "DeployedProgram",
98
+ "RetryableProviderError",
96
99
  # Optimize
97
100
  "optimize",
98
101
  "OptimizeError",
@@ -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(
@@ -1,7 +1,7 @@
1
1
  """Execute deployed DSPy programs through the Cmpnd platform.
2
2
 
3
- Calls the Rails execute endpoint, which proxies to the Lambda executor
4
- with injected provider credentials. End users only send program inputs.
3
+ Calls the serve execute endpoint, which forwards to the worker with scoped
4
+ provider access. End users only send program inputs.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -23,6 +23,55 @@ if TYPE_CHECKING:
23
23
  from dspy.primitives.prediction import Prediction
24
24
 
25
25
 
26
+ class RetryableProviderError(HTTPStatusError):
27
+ """A provider failure the platform identified as safe to retry.
28
+
29
+ ``provider_status`` is absent when the provider failed before returning an HTTP
30
+ response, such as on a connection failure or timeout.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ message: str,
36
+ *,
37
+ request: httpx.Request,
38
+ response: httpx.Response,
39
+ provider_status: int | None,
40
+ attempts: int | None,
41
+ retry_after: int | None,
42
+ ) -> None:
43
+ super().__init__(message, request=request, response=response)
44
+ self.provider_status = provider_status
45
+ self.attempts = attempts
46
+ self.retry_after = retry_after
47
+
48
+
49
+ def _nonnegative_int(value: object) -> int | None:
50
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
51
+ return value
52
+ return None
53
+
54
+
55
+ def _retryable_provider_failure(
56
+ resp: httpx.Response,
57
+ ) -> tuple[int | None, int | None, int | None] | None:
58
+ try:
59
+ body = resp.json()
60
+ except ValueError:
61
+ return None
62
+ if not isinstance(body, dict) or body.get("retryable") is not True:
63
+ return None
64
+ failure = body.get("failure")
65
+ if not isinstance(failure, dict) or failure.get("domain") != "provider":
66
+ return None
67
+ provider_status = _nonnegative_int(failure.get("upstream_status"))
68
+ return (
69
+ provider_status,
70
+ _nonnegative_int(failure.get("attempts")),
71
+ _nonnegative_int(body.get("retry_after")),
72
+ )
73
+
74
+
26
75
  class DeployedProgram:
27
76
  """A deployed DSPy program that can be called like a local module.
28
77
 
@@ -159,6 +208,7 @@ def execute(deployment_id, timeout=30, metadata=None, tags=None, **inputs):
159
208
  TypeError: If metadata or tags is provided and is not a ``dict[str, str]``.
160
209
  RuntimeError: If cmpnd is not configured, deployment is not ready,
161
210
  or the provider key is missing.
211
+ RetryableProviderError: If the platform reports a retryable provider failure.
162
212
  httpx.HTTPStatusError: For other HTTP errors (e.g. 502 executor error).
163
213
  """
164
214
  from dspy.primitives.prediction import Prediction
@@ -200,8 +250,22 @@ def execute(deployment_id, timeout=30, metadata=None, tags=None, **inputs):
200
250
  # sandbox refusal naming the missing module and the user's own file and line
201
251
  # was arriving as a bare "Server error '500 Internal Server Error'".
202
252
  detail = _upstream_detail(resp)
253
+ message = f"Execution failed with {resp.status_code} for {resp.request.url}" + (
254
+ f": {detail}" if detail else ""
255
+ )
256
+ provider_failure = _retryable_provider_failure(resp)
257
+ if provider_failure is not None:
258
+ provider_status, attempts, retry_after = provider_failure
259
+ raise RetryableProviderError(
260
+ message,
261
+ request=resp.request,
262
+ response=resp,
263
+ provider_status=provider_status,
264
+ attempts=attempts,
265
+ retry_after=retry_after,
266
+ )
203
267
  raise HTTPStatusError(
204
- f"Execution failed with {resp.status_code} for {resp.request.url}" + (f": {detail}" if detail else ""),
268
+ message,
205
269
  request=resp.request,
206
270
  response=resp,
207
271
  )
@@ -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__,