cmpnd 0.49.0__tar.gz → 0.51.0__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 (380) hide show
  1. {cmpnd-0.49.0 → cmpnd-0.51.0}/PKG-INFO +1 -1
  2. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_http.py +13 -11
  3. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/__init__.py +19 -1
  4. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/programs.py +50 -9
  5. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/top.py +84 -28
  6. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/shell.py +15 -6
  7. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/deployment.py +109 -13
  8. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/optimization.py +12 -3
  9. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/packaging.py +12 -6
  10. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/programs.py +25 -0
  11. {cmpnd-0.49.0 → cmpnd-0.51.0}/docs/cli.md +53 -25
  12. {cmpnd-0.49.0 → cmpnd-0.51.0}/docs/client.md +11 -4
  13. {cmpnd-0.49.0 → cmpnd-0.51.0}/pyproject.toml +1 -1
  14. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_authored_metric_contract.py +1 -1
  15. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_authored_optimize.py +4 -2
  16. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_api_client.py +15 -0
  17. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_top.py +4 -4
  18. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_client_program_names.py +2 -2
  19. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_deploy_program_id.py +4 -4
  20. cmpnd-0.51.0/tests/test_program_names_in_deploy.py +627 -0
  21. {cmpnd-0.49.0 → cmpnd-0.51.0}/uv.lock +1 -1
  22. {cmpnd-0.49.0 → cmpnd-0.51.0}/.gitignore +0 -0
  23. {cmpnd-0.49.0 → cmpnd-0.51.0}/AGENTS.md +0 -0
  24. {cmpnd-0.49.0 → cmpnd-0.51.0}/CLAUDE.md +0 -0
  25. {cmpnd-0.49.0 → cmpnd-0.51.0}/README.md +0 -0
  26. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/__init__.py +0 -0
  27. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_assessments.py +0 -0
  28. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_authored_import.py +0 -0
  29. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_deployments.py +0 -0
  30. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_exporter_pool.py +0 -0
  31. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_gepa_patch.py +0 -0
  32. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_program_patch.py +0 -0
  33. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_rlm_patch.py +0 -0
  34. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_runtime_tools.py +0 -0
  35. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_timespec.py +0 -0
  36. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_traces.py +0 -0
  37. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/_valsets.py +0 -0
  38. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/authored.py +0 -0
  39. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/callback.py +0 -0
  40. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/api_client.py +0 -0
  41. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/__init__.py +0 -0
  42. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/admin.py +0 -0
  43. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/assessments.py +0 -0
  44. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/auth.py +0 -0
  45. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/datasets.py +0 -0
  46. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/deployments.py +0 -0
  47. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/evals.py +0 -0
  48. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/login.py +0 -0
  49. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/optimizations.py +0 -0
  50. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/org.py +0 -0
  51. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  52. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/signatures.py +0 -0
  53. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  54. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/traces.py +0 -0
  55. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/commands/valsets.py +0 -0
  56. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/credentials.py +0 -0
  57. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/deprecation.py +0 -0
  58. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/pagination.py +0 -0
  59. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/trace_render.py +0 -0
  60. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/tree_help.py +0 -0
  61. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/cli/verbs.py +0 -0
  62. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/client.py +0 -0
  63. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/configuration.py +0 -0
  64. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/context.py +0 -0
  65. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/dataset_sync.py +0 -0
  66. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/datasets.py +0 -0
  67. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/decorators.py +0 -0
  68. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/deployment_metadata.py +0 -0
  69. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/deployment_runs.py +0 -0
  70. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/eval_handler.py +0 -0
  71. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/execution.py +0 -0
  72. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/exporter.py +0 -0
  73. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/exporter_http.py +0 -0
  74. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/helpers.py +0 -0
  75. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/identity.py +0 -0
  76. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/imaging.py +0 -0
  77. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/ir/__init__.py +0 -0
  78. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/ir/decode.py +0 -0
  79. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/ir/encode.py +0 -0
  80. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/ir/hash.py +0 -0
  81. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/ir/program.py +0 -0
  82. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/ir/reflection.py +0 -0
  83. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/ir/types.py +0 -0
  84. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/ir/xxh64.py +0 -0
  85. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/lm_configuration.py +0 -0
  86. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/mcp_servers.py +0 -0
  87. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/models.py +0 -0
  88. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/named_programs.py +0 -0
  89. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/optimizers/__init__.py +0 -0
  90. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/optimizers/gepa.py +0 -0
  91. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  92. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/optimizers/resume.py +0 -0
  93. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/optimizers/tracker.py +0 -0
  94. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/references.py +0 -0
  95. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/retry.py +0 -0
  96. {cmpnd-0.49.0 → cmpnd-0.51.0}/cmpnd/traces.py +0 -0
  97. {cmpnd-0.49.0 → cmpnd-0.51.0}/docs/2026-04-02-code-review.md +0 -0
  98. {cmpnd-0.49.0 → cmpnd-0.51.0}/docs/custom-proposer.md +0 -0
  99. {cmpnd-0.49.0 → cmpnd-0.51.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  100. {cmpnd-0.49.0 → cmpnd-0.51.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  101. {cmpnd-0.49.0 → cmpnd-0.51.0}/docs/reading-traces.md +0 -0
  102. {cmpnd-0.49.0 → cmpnd-0.51.0}/docs/sdk-instrumentation.md +0 -0
  103. {cmpnd-0.49.0 → cmpnd-0.51.0}/examples/generalizing_proposer.py +0 -0
  104. {cmpnd-0.49.0 → cmpnd-0.51.0}/examples/local_ollama.py +0 -0
  105. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/LINEAGE.md +0 -0
  106. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/README.md +0 -0
  107. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  108. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  109. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
  110. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  111. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  112. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  113. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/cmpnd_ingest/py.typed +0 -0
  114. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  115. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/cmpnd_ingest/retry.py +0 -0
  116. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  117. {cmpnd-0.49.0 → cmpnd-0.51.0}/ingest/pyproject.toml +0 -0
  118. {cmpnd-0.49.0 → cmpnd-0.51.0}/scripts/audit-sdist.sh +0 -0
  119. {cmpnd-0.49.0 → cmpnd-0.51.0}/scripts/capture_review_run.py +0 -0
  120. {cmpnd-0.49.0 → cmpnd-0.51.0}/scripts/check-pip-install.sh +0 -0
  121. {cmpnd-0.49.0 → cmpnd-0.51.0}/scripts/check_release_artifacts.py +0 -0
  122. {cmpnd-0.49.0 → cmpnd-0.51.0}/scripts/fixtures/review_run.json.gz +0 -0
  123. {cmpnd-0.49.0 → cmpnd-0.51.0}/scripts/gepa_resume_bug.py +0 -0
  124. {cmpnd-0.49.0 → cmpnd-0.51.0}/scripts/release-stress.sh +0 -0
  125. {cmpnd-0.49.0 → cmpnd-0.51.0}/scripts/require_version_bump.py +0 -0
  126. {cmpnd-0.49.0 → cmpnd-0.51.0}/scripts/seed_review_run.py +0 -0
  127. {cmpnd-0.49.0 → cmpnd-0.51.0}/scripts/test-in-linux.sh +0 -0
  128. {cmpnd-0.49.0 → cmpnd-0.51.0}/stubs/dspy/__init__.pyi +0 -0
  129. {cmpnd-0.49.0 → cmpnd-0.51.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  130. {cmpnd-0.49.0 → cmpnd-0.51.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  131. {cmpnd-0.49.0 → cmpnd-0.51.0}/stubs/dspy/utils/__init__.pyi +0 -0
  132. {cmpnd-0.49.0 → cmpnd-0.51.0}/stubs/dspy/utils/callback.pyi +0 -0
  133. {cmpnd-0.49.0 → cmpnd-0.51.0}/stubs/dspy/utils/dummies.pyi +0 -0
  134. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/__init__.py +0 -0
  135. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/_deployment_signature_helpers.py +0 -0
  136. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/_exporter_helpers.py +0 -0
  137. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/_fake_clock.py +0 -0
  138. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/_identity_helpers.py +0 -0
  139. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/_resolve_refs.py +0 -0
  140. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/_traced_run.py +0 -0
  141. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/conftest.py +0 -0
  142. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/__init__.py +0 -0
  143. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/committee_task.py +0 -0
  144. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/conftest.py +0 -0
  145. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  146. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  147. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/fake_lm.py +0 -0
  148. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/helpers.py +0 -0
  149. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/target_capabilities.py +0 -0
  150. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_assessments_cli.py +0 -0
  151. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_capture_prompts.py +0 -0
  152. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_composite_module.py +0 -0
  153. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  154. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_deploy_execute.py +0 -0
  155. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_deploy_pandas.py +0 -0
  156. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_deploy_programs.py +0 -0
  157. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_edge_cases.py +0 -0
  158. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_example_hash.py +0 -0
  159. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_gepa_optimization.py +0 -0
  160. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_interpreter_spans.py +0 -0
  161. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_lm_accuracy.py +0 -0
  162. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_module_tracing.py +0 -0
  163. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_optimization_lineage.py +0 -0
  164. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  165. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_optimize_failures.py +0 -0
  166. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_optimize_loop.py +0 -0
  167. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  168. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  169. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_parallel_export.py +0 -0
  170. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_project_attribution.py +0 -0
  171. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_react_tool_spans.py +0 -0
  172. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_save_load.py +0 -0
  173. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_trace_structure.py +0 -0
  174. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_traces_read.py +0 -0
  175. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_user_attribution.py +0 -0
  176. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/e2e/test_valsets_cli.py +0 -0
  177. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  178. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  179. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  180. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  181. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  182. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  183. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  184. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  185. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  186. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/10_react.json +0 -0
  187. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/11_refine.json +0 -0
  188. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/12_retrieve.json +0 -0
  189. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/13_knn.json +0 -0
  190. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/14_parallel.json +0 -0
  191. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/identity_vectors/README.md +0 -0
  192. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/ingest_conformance.py +0 -0
  193. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/__init__.py +0 -0
  194. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/authored_optimize/judge.py +0 -0
  195. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  196. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/conftest.py +0 -0
  197. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  198. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/hosted_optimize_probe.py +0 -0
  199. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/optimization_run_probe.py +0 -0
  200. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  201. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/test_logs_renders_traces.py +0 -0
  202. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/test_optimization_run.py +0 -0
  203. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/test_optimizing_progress.py +0 -0
  204. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/integration/test_unhappy_path.py +0 -0
  205. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/load/__init__.py +0 -0
  206. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/load/__main__.py +0 -0
  207. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/load/drain.py +0 -0
  208. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/load/ingest.py +0 -0
  209. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/load/latency_transport.py +0 -0
  210. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/load/rlm_shape.py +0 -0
  211. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/load/rlm_size_report.py +0 -0
  212. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/load/sweep.py +0 -0
  213. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/load/test_rlm_shape.py +0 -0
  214. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/load/test_rlm_size_report.py +0 -0
  215. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/__init__.py +0 -0
  216. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/conftest.py +0 -0
  217. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/seed_parity.py +0 -0
  218. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_auth.py +0 -0
  219. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_datasets.py +0 -0
  220. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_deployments.py +0 -0
  221. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_evals.py +0 -0
  222. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_health.py +0 -0
  223. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_modules.py +0 -0
  224. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_optimizations.py +0 -0
  225. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_programs.py +0 -0
  226. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_signatures.py +0 -0
  227. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_trace_streaming.py +0 -0
  228. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/parity/test_traces.py +0 -0
  229. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/span_reference_fixture.py +0 -0
  230. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/__init__.py +0 -0
  231. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/_provider.py +0 -0
  232. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/_readiness.py +0 -0
  233. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/_reliability.py +0 -0
  234. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/_result.py +0 -0
  235. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/mint_bedrock_token.py +0 -0
  236. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_deploy_programs.py +0 -0
  237. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_multi_client_stress.py +0 -0
  238. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_optimize_client_committee.py +0 -0
  239. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_optimize_loop.py +0 -0
  240. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  241. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_optimize_stress.py +0 -0
  242. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_provider_roles.py +0 -0
  243. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_readiness.py +0 -0
  244. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_real_provider_twins.py +0 -0
  245. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_reliability.py +0 -0
  246. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_reliability_classify.py +0 -0
  247. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_rlm_wasm.py +0 -0
  248. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/stress/test_trace_search.py +0 -0
  249. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_adapter_reentrancy.py +0 -0
  250. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_assessments_api.py +0 -0
  251. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_authored.py +0 -0
  252. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_authored_bundle_collisions.py +0 -0
  253. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_authored_named_models.py +0 -0
  254. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_callback.py +0 -0
  255. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_candidate_lineage.py +0 -0
  256. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/__init__.py +0 -0
  257. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/conftest.py +0 -0
  258. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_admin.py +0 -0
  259. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_assessments.py +0 -0
  260. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_auth.py +0 -0
  261. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_credentials.py +0 -0
  262. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_datasets.py +0 -0
  263. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_deployment_runs.py +0 -0
  264. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_deployments.py +0 -0
  265. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_evals.py +0 -0
  266. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_login.py +0 -0
  267. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_main.py +0 -0
  268. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_optimizations.py +0 -0
  269. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_org.py +0 -0
  270. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_pagination.py +0 -0
  271. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_parity.py +0 -0
  272. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_programs.py +0 -0
  273. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_provider_credentials.py +0 -0
  274. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_schema_sample.py +0 -0
  275. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_shell.py +0 -0
  276. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_signatures.py +0 -0
  277. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_tool_servers.py +0 -0
  278. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_traces.py +0 -0
  279. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  280. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_tree_help.py +0 -0
  281. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cli/test_valsets.py +0 -0
  282. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_client.py +0 -0
  283. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_client_floor.py +0 -0
  284. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_clustering/__init__.py +0 -0
  285. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_clustering/conftest.py +0 -0
  286. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_clustering/test_edge_cases.py +0 -0
  287. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  288. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_clustering/test_type_conversion.py +0 -0
  289. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_cmp551_grouping_identity.py +0 -0
  290. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_configuration.py +0 -0
  291. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_context.py +0 -0
  292. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_context_propagation.py +0 -0
  293. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_dataset_polling.py +0 -0
  294. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_dataset_sync.py +0 -0
  295. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_datasets.py +0 -0
  296. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_decision_client_spans.py +0 -0
  297. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_decorators.py +0 -0
  298. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_deploy.py +0 -0
  299. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  300. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_deployment_metadata.py +0 -0
  301. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_deployment_runs.py +0 -0
  302. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_deployments_read.py +0 -0
  303. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_eval.py +0 -0
  304. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_eval_example_attribution.py +0 -0
  305. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_eval_start.py +0 -0
  306. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_eval_trace_grouping.py +0 -0
  307. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_exception_paths.py +0 -0
  308. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_execute.py +0 -0
  309. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_execute_run_identity.py +0 -0
  310. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_exporter.py +0 -0
  311. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_exporter_program_names.py +0 -0
  312. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_exporter_queue.py +0 -0
  313. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_exporter_real.py +0 -0
  314. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_fake_lm.py +0 -0
  315. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_gepa_callback.py +0 -0
  316. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_gepa_integration.py +0 -0
  317. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_gepa_output_normalization.py +0 -0
  318. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_gepa_resume.py +0 -0
  319. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_identity.py +0 -0
  320. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_imaging.py +0 -0
  321. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_import_weight.py +0 -0
  322. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_ingest_conformance.py +0 -0
  323. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_ingest_second_producer.py +0 -0
  324. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_integration.py +0 -0
  325. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_interpreter_callback.py +0 -0
  326. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_ir_encoder_properties.py +0 -0
  327. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_ir_golden_vectors.py +0 -0
  328. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_ir_roundtrip.py +0 -0
  329. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_lm15_native_calls.py +0 -0
  330. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_lm_configuration.py +0 -0
  331. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_lm_usage_shapes.py +0 -0
  332. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_mcp_tools.py +0 -0
  333. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_models.py +0 -0
  334. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_multi_client_stress_readiness.py +0 -0
  335. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_optimization_progress.py +0 -0
  336. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_optimization_start.py +0 -0
  337. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_optimize.py +0 -0
  338. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_optimize_budget.py +0 -0
  339. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_optimize_lift.py +0 -0
  340. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_optimize_lms.py +0 -0
  341. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_optimize_proposer.py +0 -0
  342. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_optimize_reflection_lm.py +0 -0
  343. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_optimize_terminal_failure.py +0 -0
  344. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_packaging.py +0 -0
  345. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_packaging_callable_source.py +0 -0
  346. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_packaging_mcp_tools.py +0 -0
  347. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_packaging_program_source.py +0 -0
  348. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_poll_until.py +0 -0
  349. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_program_identity.py +0 -0
  350. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_program_lineage_persist.py +0 -0
  351. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_programs_create.py +0 -0
  352. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_project_payload.py +0 -0
  353. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_real_provider_twins_readiness.py +0 -0
  354. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_reflector_properties.py +0 -0
  355. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_reflector_smoke.py +0 -0
  356. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_release_stress_script.py +0 -0
  357. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_repro_reactv2_bugs.py +0 -0
  358. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_require_version_bump.py +0 -0
  359. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_retry.py +0 -0
  360. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_reversible_failures.py +0 -0
  361. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_rlm_patch.py +0 -0
  362. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_run_attribution_paths.py +0 -0
  363. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_runtime_tools.py +0 -0
  364. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_schemas.py +0 -0
  365. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_seed_review_run.py +0 -0
  366. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_signature_inputs.py +0 -0
  367. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_span_references.py +0 -0
  368. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_span_sent_once.py +0 -0
  369. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_system_one.py +0 -0
  370. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_trace_context_concurrency.py +0 -0
  371. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_trace_fact_wire_contract.py +0 -0
  372. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_trace_facts.py +0 -0
  373. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_trace_render.py +0 -0
  374. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_traces_read.py +0 -0
  375. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_tracked_gepa.py +0 -0
  376. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_tracker_enrich.py +0 -0
  377. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_truncation.py +0 -0
  378. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_upload_auth_headers.py +0 -0
  379. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_valsets_api.py +0 -0
  380. {cmpnd-0.49.0 → cmpnd-0.51.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.49.0
3
+ Version: 0.51.0
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>
@@ -234,23 +234,25 @@ class ApiClient:
234
234
  # which is the difference a user needs most and had the least of.
235
235
  raise error(server_detail(response) or "Resource not found.")
236
236
  if response.status_code >= 500:
237
- # 5xx usually means the server is up but blew up handling the
238
- # request — a database hiccup, an unhandled exception, etc.
239
- # Include the response body excerpt so the user has something
240
- # to act on. The actual exception lives in the operator's
241
- # server logs, which we can't reach from here.
237
+ # 5xx means the cmpnd platform failed to handle the request — a
238
+ # database hiccup, an unhandled exception, a hop between its own
239
+ # services. A failure at something the user runs (their tool
240
+ # server, their provider) answers 4xx with a sentence naming it,
241
+ # so this hint names the platform and nothing of theirs. The cause
242
+ # lives in the platform's logs, which we can't reach from here.
242
243
  body_excerpt = (response.text or "").strip()[:200]
243
244
  msg = f"Server error ({response.status_code}) for {method} {path}"
244
245
  if body_excerpt:
245
246
  msg += f": {body_excerpt}"
246
- msg += "\n If you operate this server, check its logs for the underlying exception."
247
+ msg += (
248
+ "\n The cmpnd platform failed to handle this request. If you run this cmpnd "
249
+ "instance yourself, its logs hold the cause."
250
+ )
247
251
  raise error(msg, exit_code=2)
248
252
  if response.status_code >= 400:
249
- try:
250
- detail = response.json()
251
- except Exception:
252
- detail = response.text
253
- raise error(f"Request failed ({response.status_code}): {detail}")
253
+ # The server's own sentence: a 4xx says what to change, and the
254
+ # JSON envelope around it is noise.
255
+ raise error(f"Request failed ({response.status_code}): {server_detail(response)}")
254
256
 
255
257
  if return_response:
256
258
  return response
@@ -88,6 +88,24 @@ Auth: set CMPND_API_KEY and (optionally) CMPND_ENDPOINT, or pass --api-key /
88
88
  """
89
89
 
90
90
 
91
+ def parse(parser: argparse.ArgumentParser, argv: list[str]) -> argparse.Namespace:
92
+ """Parse ``argv``, and give a late positional the one word argparse could not place.
93
+
94
+ argparse fills optional positionals from the first run of bare words, so in
95
+ ``cmpnd deploy program.py --entrypoint qa capital-qa`` the program name comes too
96
+ late and is left over. A command that sets ``_late_positional`` takes one such
97
+ word into that argument when it is still empty. Anything else left over is the
98
+ usual usage error.
99
+ """
100
+ args, extras = parser.parse_known_args(argv)
101
+ late = getattr(args, "_late_positional", None)
102
+ if late and len(extras) == 1 and not extras[0].startswith("-") and getattr(args, late) is None:
103
+ setattr(args, late, extras.pop())
104
+ if extras:
105
+ parser.error("unrecognized arguments: " + " ".join(extras))
106
+ return args
107
+
108
+
91
109
  def main() -> None:
92
110
  parser = tree_help.TreeHelpParser(
93
111
  prog="cmpnd",
@@ -165,7 +183,7 @@ def main() -> None:
165
183
  # Top-level Docker-style shortcuts (human-readable by default; --json opts in)
166
184
  top.register(subparsers)
167
185
 
168
- args = parser.parse_args()
186
+ args = parse(parser, sys.argv[1:])
169
187
 
170
188
  if not args.resource:
171
189
  # Bare `cmpnd` drops into an interactive Python shell with platform
@@ -21,15 +21,24 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
21
21
 
22
22
  create = sub.add_parser(
23
23
  "create",
24
- help="Create a program and print its id",
24
+ help="Create a program with a name",
25
25
  description=(
26
- "Ask the platform for a new program id (p_…) and print it on stdout. Declare it "
27
- "with `cmpnd deploy --program-id`, `cmpnd.configure(program_id=...)` or "
28
- "CMPND_PROGRAM_ID so deployments, traces and optimization runs carry it. Each "
29
- "call creates a new program."
26
+ "Create a program with PROGRAM_NAME in your organization and print its id on stdout. "
27
+ "A program name that already has a program is refused. Deploy under the program name with "
28
+ "`cmpnd deploy SOURCE PROGRAM_NAME`, and declare it in Python with "
29
+ "cmpnd.Program(PROGRAM_NAME, module=..., lm=...). Without PROGRAM_NAME, --name creates an "
30
+ "unnamed program with a display name, as earlier releases did."
30
31
  ),
31
32
  )
32
- create.add_argument("--name", default=None, help="Display name (need not be unique)")
33
+ create.add_argument(
34
+ "name", nargs="?", default=None, metavar="PROGRAM_NAME", help="Program name, unique in your organization"
35
+ )
36
+ create.add_argument(
37
+ "--name",
38
+ dest="display_name",
39
+ default=None,
40
+ help="Display name of an unnamed program (need not be unique)",
41
+ )
33
42
  create.add_argument("--json", action="store_true", help="Emit the created program as JSON instead of its id")
34
43
  create.set_defaults(func=handle_create)
35
44
 
@@ -88,11 +97,13 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
88
97
 
89
98
  def handle_create(client: api_client.ApiClient, args: Any) -> Any:
90
99
  """Create a program. Prints the bare id on stdout so a script can capture it
91
- (``PROGRAM_ID=$(cmpnd programs create --name triage)``) and says what it made on
92
- stderr; ``--json`` returns the whole row instead, printed like the group's reads."""
100
+ (``PROGRAM_ID=$(cmpnd programs create triage)``) and says what it made on stderr;
101
+ ``--json`` returns serve's whole answer instead, printed like the group's reads."""
93
102
  from ... import programs
94
103
 
95
- row = programs.send_create(client, name=args.name)
104
+ if args.name is not None:
105
+ return _create_named(client, args)
106
+ row = programs.send_create(client, name=args.display_name)
96
107
  if getattr(args, "json", False):
97
108
  return row
98
109
  print(row["id"])
@@ -101,6 +112,36 @@ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
101
112
  return None
102
113
 
103
114
 
115
+ def _create_named(client: api_client.ApiClient, args: Any) -> Any:
116
+ """Create the program named ``args.name``, and refuse a name that already has one."""
117
+ from ... import programs
118
+ from . import top
119
+
120
+ if args.display_name is not None:
121
+ raise api_client.CliError("give a program name or --name, not both: --name is the older display name")
122
+ try:
123
+ answer = programs.send_resolve(client, args.name)
124
+ except ValueError as exc:
125
+ raise api_client.CliError(str(exc)) from exc
126
+ if answer is None:
127
+ raise api_client.CliError(
128
+ "This server has no program names, so it cannot create a program from a program name. A serve from "
129
+ "before program names, such as self-host release 2026-001, needs no program first: "
130
+ f"`{top._hint_program_name('deploy', source='SOURCE', name='PROGRAM_NAME')}` deploys there without "
131
+ "a program id."
132
+ )
133
+ if not answer.get("created"):
134
+ raise api_client.CliError(
135
+ f"the program name {args.name!r} already has a program, {answer['program_id']}. "
136
+ f"Deploy under it with `{top._hint_program_name('deploy', source='SOURCE', name=args.name)}`."
137
+ )
138
+ if getattr(args, "json", False):
139
+ return answer
140
+ print(answer["program_id"])
141
+ print(f"created program {answer['program_id']} named {args.name}", file=sys.stderr)
142
+ return None
143
+
144
+
104
145
  def handle_list(client: api_client.ApiClient, args: Any) -> Any:
105
146
  params: dict[str, str] = pagination.wire(args, "per_page", as_str=True)
106
147
  params["order_by"] = args.order_by
@@ -194,6 +194,19 @@ def _hint_run(deployment_id: str, body: str = '{"...": "..."}') -> str:
194
194
  return f"cmpnd run {short} '{body}'"
195
195
 
196
196
 
197
+ def _hint_program_name(verb: str, *, source: str | None = None, name: str = "capital-qa") -> str:
198
+ """How to name the program a deploy or optimize runs under, as this front-end spells it."""
199
+ if verb == "optimize":
200
+ source = source or "task.py"
201
+ if _SHELL_MODE:
202
+ return f'optimize("gepa", "{source}", program="{name}")'
203
+ return f"cmpnd optimize gepa {source} {name}"
204
+ source = source or "program.py"
205
+ if _SHELL_MODE:
206
+ return f'deploy("{source}", program="{name}")'
207
+ return f"cmpnd deploy {source} {name}"
208
+
209
+
197
210
  def _hint_status(deployment_id: str, follow: bool = False) -> str:
198
211
  short = _short_id(deployment_id)
199
212
  if _SHELL_MODE:
@@ -830,15 +843,49 @@ def _program_id_arg(value: str | None) -> str | None:
830
843
  raise api_client.CliError(str(exc)) from exc
831
844
 
832
845
 
846
+ def _program_name_arg(value: str | None) -> str | None:
847
+ """The program name a deploy or optimize names, checked before a file is staged."""
848
+ if value is None:
849
+ return None
850
+ from ... import programs
851
+
852
+ try:
853
+ return programs.validate_program_name(value)
854
+ except ValueError as exc:
855
+ raise api_client.CliError(str(exc)) from exc
856
+
857
+
858
+ def _program_args(args: Any, verb: str, *, needs_name: bool = True) -> tuple[str | None, str | None]:
859
+ """The program name and program id a deploy or optimize names, checked before a file is staged.
860
+
861
+ The name follows the source file. ``--program-id`` is the older input: it still
862
+ works, warns, and cannot be given with a name. A command with no name and no id
863
+ from either input warns that it will need a name, unless ``needs_name`` is false
864
+ because a deployment it reuses already names its program.
865
+ """
866
+ name = _program_name_arg(getattr(args, "program_name", None))
867
+ flag = getattr(args, "program_id", None)
868
+ example = _hint_program_name(verb)
869
+ id_input = "program_id=" if _SHELL_MODE else "--program-id"
870
+ if name is not None and flag is not None:
871
+ raise api_client.CliError(f"give a program name or {id_input}, not both")
872
+ if flag is not None:
873
+ print(f"warning: {id_input} is deprecated; give a program name instead, as in `{example}`.", file=sys.stderr)
874
+ program_id = _program_id_arg(flag)
875
+ if name is None and program_id is None and needs_name:
876
+ print(f"warning: this command has no program name; give one, as in `{example}`.", file=sys.stderr)
877
+ return name, program_id
878
+
879
+
833
880
  def cmd_deploy(client: api_client.ApiClient, args: Any) -> None:
834
881
  """Stage authored Python and deploy the checked bundle.
835
882
 
836
883
  A fresh child imports the staged bundle to inspect the declared entrypoint.
837
884
  The CLI process does not execute the file. ``--run '{json}'`` chains into
838
- the new deployment as a one-shot smoke test. ``--program-id`` (or
839
- ``CMPND_PROGRAM_ID``) names the program, from ``cmpnd programs create``:
885
+ the new deployment as a one-shot smoke test. The name after the source names the
886
+ program, which the platform makes the first time your organization uses the name:
840
887
 
841
- cat <<'PY' | cmpnd deploy - --program-id p_0123456789Ab --run '{"question": "2+2?"}'
888
+ cat <<'PY' | cmpnd deploy - capital-qa --run '{"question": "2+2?"}'
842
889
  import dspy
843
890
  lm = dspy.LM("openai/gemma4:e4b", temperature=0.0, max_tokens=64)
844
891
  dspy.configure(lm=lm)
@@ -849,7 +896,7 @@ def cmd_deploy(client: api_client.ApiClient, args: Any) -> None:
849
896
 
850
897
  from ... import authored, configuration, deployment
851
898
 
852
- program_id = _program_id_arg(getattr(args, "program_id", None))
899
+ program_name, program_id = _program_args(args, "deploy")
853
900
  configuration.configure(api_key=client._api_key, endpoint=client._endpoint, program_id=program_id)
854
901
  try:
855
902
  if args.source == "-" or args.source is None:
@@ -874,7 +921,9 @@ def cmd_deploy(client: api_client.ApiClient, args: Any) -> None:
874
921
  metadata = _parse_deploy_metadata(getattr(args, "metadata", None))
875
922
  print(_c("deploying…", "dim"), file=sys.stderr)
876
923
  try:
877
- deployed = deployment.deploy_bundle(prepared.bundle, prepared.facts, metadata=metadata, program_id=program_id)
924
+ deployed = deployment.deploy_bundle(
925
+ prepared.bundle, prepared.facts, metadata=metadata, program_id=program_id, program_name=program_name
926
+ )
878
927
  except ValueError as exc:
879
928
  raise api_client.CliError(str(exc)) from exc
880
929
 
@@ -924,7 +973,7 @@ def cmd_optimize(client: api_client.ApiClient, args: Any) -> Any:
924
973
  The file binds ``metric`` always, ``trainset`` and ``valset``, ``program`` unless
925
974
  ``--deployment-id`` names one, and ``proposer`` when the run brings its own::
926
975
 
927
- cat <<'PY' | cmpnd optimize gepa - --program-id p_0123456789Ab --reflection-model openai/gpt-4o --auto light
976
+ cat <<'PY' | cmpnd optimize gepa - capital-qa --reflection-model openai/gpt-4o --auto light
928
977
  import dspy
929
978
  dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))
930
979
  program = dspy.Predict("question -> answer")
@@ -941,7 +990,7 @@ def cmd_optimize(client: api_client.ApiClient, args: Any) -> Any:
941
990
  from ... import authored, configuration, optimization
942
991
 
943
992
  deployment_id = getattr(args, "deployment_id", None)
944
- program_id = _program_id_arg(getattr(args, "program_id", None))
993
+ program_name, program_id = _program_args(args, "optimize", needs_name=deployment_id is None)
945
994
  configuration.configure(api_key=client._api_key, endpoint=client._endpoint, program_id=program_id)
946
995
  try:
947
996
  if args.source == "-" or args.source is None:
@@ -972,6 +1021,7 @@ def cmd_optimize(client: api_client.ApiClient, args: Any) -> Any:
972
1021
  optimizer=args.optimizer,
973
1022
  deployment_id=deployment_id,
974
1023
  program_id=program_id,
1024
+ program_name=program_name,
975
1025
  # The flag is named for what a command line can carry — a model name,
976
1026
  # never an LM object — and the parameter for what the Python surface
977
1027
  # accepts. Each layer's name says what that layer takes.
@@ -1089,6 +1139,26 @@ def cmd_run(client: api_client.ApiClient, args: Any) -> None:
1089
1139
  # --------------------------------------------------------------------------
1090
1140
 
1091
1141
 
1142
+ def _add_program_options(parser: ArgumentParser, role: str) -> None:
1143
+ """The program name, a positional after the source, and the ``--program-id`` it replaces."""
1144
+ parser.add_argument(
1145
+ "program_name",
1146
+ nargs="?",
1147
+ metavar="PROGRAM_NAME",
1148
+ default=None,
1149
+ help=f"Program name {role}. The platform makes the program the first time "
1150
+ "your organization uses the program name",
1151
+ )
1152
+ parser.add_argument(
1153
+ "--program-id",
1154
+ dest="program_id",
1155
+ default=None,
1156
+ help="Deprecated; give a program name instead. Program id (p_…), defaults to CMPND_PROGRAM_ID",
1157
+ )
1158
+ # argparse leaves a name given after other options unplaced; cli.parse places it.
1159
+ parser.set_defaults(_late_positional="program_name")
1160
+
1161
+
1092
1162
  def _add_source_options(parser: ArgumentParser) -> None:
1093
1163
  """The two options that decide what a bundle holds, on every verb that stages one.
1094
1164
 
@@ -1247,20 +1317,13 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
1247
1317
  ),
1248
1318
  epilog=(
1249
1319
  "Examples:\n"
1250
- " cmpnd programs create --name qa # prints the id to deploy under\n"
1251
- " cmpnd deploy program.py --program-id p_0123456789Ab\n"
1252
- ' cmpnd deploy - --program-id p_0123456789Ab --run \'{"question":"2+2?"}\' < program.py\n'
1320
+ " cmpnd deploy program.py capital-qa\n"
1321
+ ' cmpnd deploy - capital-qa --run \'{"question":"2+2?"}\' < program.py\n'
1253
1322
  ),
1254
1323
  formatter_class=raw,
1255
1324
  )
1256
1325
  dp.add_argument("source", nargs="?", help="Path to a .py file, or '-' to read stdin")
1257
- dp.add_argument(
1258
- "--program-id",
1259
- dest="program_id",
1260
- default=None,
1261
- help="Program id (p_…) the deployment belongs to, from `cmpnd programs create`. "
1262
- "Defaults to CMPND_PROGRAM_ID. Required by every serve after self-host release 2026-001",
1263
- )
1326
+ _add_program_options(dp, "of the deployment")
1264
1327
  dp.add_argument(
1265
1328
  "--entrypoint",
1266
1329
  default="program",
@@ -1307,10 +1370,10 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
1307
1370
  ),
1308
1371
  epilog=(
1309
1372
  "Examples:\n"
1310
- " cmpnd optimize gepa task.py --program-id p_0123456789Ab --reflection-model openai/gpt-4o --auto light\n"
1311
- " cmpnd optimize gepa task.py --program-id p_0123456789Ab --include judge.py "
1373
+ " cmpnd optimize gepa task.py capital-qa --reflection-model openai/gpt-4o --auto light\n"
1374
+ " cmpnd optimize gepa task.py capital-qa --include judge.py "
1312
1375
  "--reflection-model openai/gpt-4o --auto light\n"
1313
- " cmpnd optimize gepa - --program-id p_0123456789Ab --reflection-model openai/gpt-4o "
1376
+ " cmpnd optimize gepa - capital-qa --reflection-model openai/gpt-4o "
1314
1377
  "--max-metric-calls 60 < task.py\n"
1315
1378
  " cmpnd optimize gepa task.py --reflection-model openai/gpt-4o --auto light --deployment-id d_a1b2c3d4\n"
1316
1379
  ),
@@ -1339,14 +1402,7 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
1339
1402
  default=None,
1340
1403
  help="Reuse an existing ready deployment as the starting program",
1341
1404
  )
1342
- opt.add_argument(
1343
- "--program-id",
1344
- dest="program_id",
1345
- default=None,
1346
- help="Program id (p_…) being optimized, from `cmpnd programs create`; defaults to "
1347
- "CMPND_PROGRAM_ID. Required by every serve after self-host release 2026-001 when "
1348
- "the file's program is deployed here, optional with --deployment-id",
1349
- )
1405
+ _add_program_options(opt, "of the program to optimize")
1350
1406
  opt.add_argument(
1351
1407
  "--reflection-model",
1352
1408
  dest="reflection_model",
@@ -486,12 +486,14 @@ def _make_namespace(client: api_client.ApiClient, endpoint: str) -> dict[str, An
486
486
  source_root: str | None = None,
487
487
  include: list[str] | None = None,
488
488
  program_id: str | None = None,
489
+ program: str | None = None,
489
490
  ) -> Deployment:
490
491
  """Deploy authored Python from a file or ``"-"`` for stdin.
491
492
 
492
- ``program_id`` (``p_…``, from ``cmpnd programs create``) names the program
493
- the deployment belongs to; it defaults to ``CMPND_PROGRAM_ID``. serve
494
- decides whether a deployment needs one. A fresh child imports the staged bundle. The shell
493
+ ``program`` names the program the deployment belongs to, which the platform
494
+ makes the first time your organization uses the name. ``program_id``
495
+ (``p_…``) is the older way to name it, and defaults to ``CMPND_PROGRAM_ID``.
496
+ A name wins over an id. A fresh child imports the staged bundle. The shell
495
497
  does not execute the file in its own process. Returns the
496
498
  new Deployment entity so you can chain ``.watch()`` / ``.run({...})``
497
499
  directly; also invalidates ``deps`` so the new deployment shows up
@@ -499,7 +501,9 @@ def _make_namespace(client: api_client.ApiClient, endpoint: str) -> dict[str, An
499
501
  """
500
502
  from .. import authored, configuration, deployment
501
503
 
502
- declared = commands.top._program_id_arg(program_id)
504
+ program_name, declared = commands.top._program_args(
505
+ _args(program_name=program, program_id=program_id), "deploy"
506
+ )
503
507
  configuration.configure(api_key=client._api_key, endpoint=client._endpoint, program_id=declared)
504
508
  if source in ("-", None):
505
509
  prepared = authored.prepare_stdin(
@@ -519,7 +523,9 @@ def _make_namespace(client: api_client.ApiClient, endpoint: str) -> dict[str, An
519
523
  )
520
524
  sys.stderr.write(_c("deploying…\n", "dim"))
521
525
  sys.stderr.flush()
522
- deployed = deployment.deploy_bundle(prepared.bundle, prepared.facts, program_id=declared)
526
+ deployed = deployment.deploy_bundle(
527
+ prepared.bundle, prepared.facts, program_id=declared, program_name=program_name
528
+ )
523
529
 
524
530
  record = client.get(f"/api/v1/deployments/{deployed.deployment_id}")
525
531
  deps.refresh() # new deployment becomes visible in the registry
@@ -536,6 +542,7 @@ def _make_namespace(client: api_client.ApiClient, endpoint: str) -> dict[str, An
536
542
  patience: int = 2,
537
543
  seed: int = 0,
538
544
  program_id: str | None = None,
545
+ program: str | None = None,
539
546
  ) -> Any:
540
547
  """Optimize a DSPy program: ``optimize("gepa", "task.py")``.
541
548
 
@@ -546,7 +553,8 @@ def _make_namespace(client: api_client.ApiClient, endpoint: str) -> dict[str, An
546
553
  than executed here, so what scores the run is the file's own bytes. ``include``
547
554
  adds the siblings it imports and the files it reads. Returns the best
548
555
  ``DeployedProgram`` (call it directly; ``.score`` is the best score found);
549
- ``deps`` is refreshed so the result deployment shows up.
556
+ ``deps`` is refreshed so the result deployment shows up. The ``program``
557
+ argument names the program being optimized, as ``deploy`` takes it.
550
558
  """
551
559
  result = commands.top.cmd_optimize(
552
560
  client,
@@ -555,6 +563,7 @@ def _make_namespace(client: api_client.ApiClient, endpoint: str) -> dict[str, An
555
563
  optimizer=optimizer,
556
564
  deployment_id=deployment_id,
557
565
  program_id=program_id,
566
+ program_name=program,
558
567
  reflection_model=reflection_model,
559
568
  source_root=source_root,
560
569
  include=list(include or []),
@@ -26,6 +26,7 @@ from . import (
26
26
  mcp_servers,
27
27
  models,
28
28
  packaging,
29
+ programs,
29
30
  retry,
30
31
  )
31
32
 
@@ -92,8 +93,8 @@ def _raise_http_error(response: httpx.Response, action: str) -> None:
92
93
  """Convert an HTTP error response into an actionable RuntimeError.
93
94
 
94
95
  Replaces ``response.raise_for_status()`` so callers see a useful
95
- message instead of a raw httpx traceback. 5xx points the user at
96
- server logs and the local-dev preflight; 4xx surfaces the response
96
+ message instead of a raw httpx traceback. 5xx says the platform
97
+ failed and points a self-hosting operator at its logs; 4xx surfaces the response
97
98
  body so e.g. ``{"error": "Invalid 'since' timestamp"}`` round-trips
98
99
  to the terminal.
99
100
  """
@@ -105,8 +106,8 @@ def _raise_http_error(response: httpx.Response, action: str) -> None:
105
106
  if body:
106
107
  msg += f"\n Body: {body}"
107
108
  msg += (
108
- "\n The server reached, but errored handling the request. "
109
- "If you operate it, check its logs for the underlying exception."
109
+ "\n The cmpnd platform failed to handle this request. "
110
+ "If you run this cmpnd instance yourself, its logs hold the cause."
110
111
  )
111
112
  else:
112
113
  msg = f"Failed to {action}: HTTP {response.status_code}"
@@ -356,8 +357,8 @@ def _resolve_mcp_tool_references(client: httpx.Client, config, references: list[
356
357
 
357
358
 
358
359
  MISSING_PROGRAM_ID = (
359
- "this server requires a program id: pass program_id= (`cmpnd deploy --program-id`), or set "
360
- "cmpnd.configure(program_id=...) or CMPND_PROGRAM_ID. Create one with `cmpnd programs create`."
360
+ "this server requires a program name: give one after the source, as in `cmpnd deploy SOURCE PROGRAM_NAME`, or "
361
+ "deploy a cmpnd.Program(PROGRAM_NAME, module=..., lm=...) with cmpnd.deploy()."
361
362
  )
362
363
 
363
364
  # The error serve answers, with a 400, to a create that names no program. A serve
@@ -376,6 +377,55 @@ def declared_program_id(explicit: str | None, config: configuration.Config) -> s
376
377
  return identity.validate_program_id(chosen) if chosen is not None else None
377
378
 
378
379
 
380
+ def program_for(program_id: str | None, program_name: str | None, config: configuration.Config) -> str | None:
381
+ """The program a deployment is created under, when the caller may name it by name or id.
382
+
383
+ A name wins over an explicit or configured id, and the SDK warns that the id is
384
+ not used. The name resolves to its program id with ``PUT``, which makes the program
385
+ the first time the organization uses the name. On a server with no program names,
386
+ the deployment goes without an id, and the SDK warns.
387
+ """
388
+ if program_name is None:
389
+ return declared_program_id(program_id, config)
390
+ programs.validate_program_name(program_name)
391
+ unused = program_id if program_id is not None else config.program_id
392
+ if unused is not None:
393
+ logger.warning("cmpnd: the deploy uses program name %r, and program id %s is not used.", program_name, unused)
394
+ return _program_by_name(program_name, config)
395
+
396
+
397
+ def _program_by_name(name: str, config: configuration.Config) -> str | None:
398
+ """The program id serve gives ``name``, or None when the server has no program names."""
399
+ answer = programs.send_resolve(_http.ApiClient(config.api_key, config.endpoint), name)
400
+ if answer is None:
401
+ logger.warning(
402
+ "cmpnd: program name %r is not used: the server has no program names, so the deploy has no program id.",
403
+ name,
404
+ )
405
+ return None
406
+ return answer["program_id"]
407
+
408
+
409
+ def _program_module(program: Any) -> Any:
410
+ """The module a program deploys: the module inside it, and inside each program it holds.
411
+
412
+ A program that overrides ``forward`` runs its own code around its module, and
413
+ packaging cannot rebuild that code yet, so it is refused.
414
+ """
415
+ from . import named_programs
416
+
417
+ module = program
418
+ while isinstance(module, named_programs.Program):
419
+ if named_programs.runs_itself(module):
420
+ raise ValueError(
421
+ f"{type(module).__name__} overrides forward, and deploying a cmpnd.Program that runs its own "
422
+ "code is not supported yet. Move that code into a dspy.Module, and deploy a cmpnd.Program "
423
+ "that holds it."
424
+ )
425
+ module = module.module
426
+ return module
427
+
428
+
379
429
  def _refused_for_missing_program_id(response: httpx.Response, deploy_body: dict[str, Any]) -> bool:
380
430
  """Whether serve refused a create that named no program because it needs one."""
381
431
  if response.status_code != 400 or "program_id" in deploy_body:
@@ -393,8 +443,14 @@ def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
393
443
  Packages the module into a source ZIP, uploads it through the platform API to the
394
444
  deploy stack, and polls until the program is parsed and ready.
395
445
 
446
+ A ``cmpnd.Program`` deploys its module under the program's name and on the
447
+ program's LM. The name resolves to its program id, which the platform makes the
448
+ first time your organization uses the name. A predictor with its own LM keeps it,
449
+ and the deployment's model is the LM the packaged source names.
450
+
396
451
  Args:
397
- module: A DSPy module (e.g. dspy.Predict, dspy.ChainOfThought).
452
+ module: A DSPy module (e.g. dspy.Predict, dspy.ChainOfThought), or a
453
+ ``cmpnd.Program``.
398
454
  metric: Optional metric callable for optimization support.
399
455
  timeout: Seconds to wait for the program to become ready (default 300),
400
456
  measured from the moment the upload completes. A transient blip
@@ -415,7 +471,8 @@ def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
415
471
  id (``cmpnd.configure(program_id=...)`` or ``CMPND_PROGRAM_ID``) is
416
472
  used. With neither, the create names no program, and serve decides:
417
473
  every serve with program ids refuses it, and a serve from before
418
- them, such as self-host release 2026-001, accepts it.
474
+ them, such as self-host release 2026-001, accepts it. A
475
+ ``cmpnd.Program``'s name wins over this id.
419
476
 
420
477
  Returns:
421
478
  A DeployedProgram that can be called with the same inputs as the
@@ -428,8 +485,29 @@ def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
428
485
  TimeoutError: If the program isn't ready within timeout seconds.
429
486
  """
430
487
  config = configuration.require_config()
431
- program = declared_program_id(program_id, config)
488
+ from . import named_programs
489
+
490
+ if not isinstance(module, named_programs.Program):
491
+ return _deploy_module(module, metric, timeout, metadata, declared_program_id(program_id, config), config)
492
+ inner = _program_module(module)
493
+ program = program_for(program_id, module.name, config)
494
+ import dspy
495
+
496
+ # Applied the same way the program applies it when it runs, so a nested program's
497
+ # LM is not used, and a predictor's own LM is.
498
+ with dspy.context(lm=module.lm, **{named_programs.PROGRAM_LM_SETTING: module.lm}):
499
+ return _deploy_module(inner, metric, timeout, metadata, program, config)
432
500
 
501
+
502
+ def _deploy_module(
503
+ module: Any,
504
+ metric: Any,
505
+ timeout: float,
506
+ metadata: Any,
507
+ program: str | None,
508
+ config: configuration.Config,
509
+ ) -> execution.DeployedProgram:
510
+ """Package and deploy a live module under ``program``, which the caller already chose."""
433
511
  # Resolve the program's MCP tool references against the org's registry, before
434
512
  # anything is packaged and before a deployment row exists. A program with no MCP
435
513
  # tools asks the registry nothing.
@@ -499,9 +577,8 @@ def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
499
577
  # the LM explicitly and so rides this same path; raw AWS creds never do.
500
578
  # When no key is forwardable, the server decides whether it can resolve a
501
579
  # provider and fails the deploy loudly if not — we do not guess here.
502
- import dspy
503
-
504
- lm = dspy.settings.lm
580
+ # The same LM the packaged source names: a predictor's own LM, else the context's.
581
+ lm = packaging._extract_lm_object(module)
505
582
  if lm is not None:
506
583
  if getattr(lm, "model", None):
507
584
  deploy_body["litellm_model"] = lm.model
@@ -522,14 +599,33 @@ def deploy_bundle(
522
599
  metadata: Any = None,
523
600
  *,
524
601
  program_id: str | None = None,
602
+ program_name: str | None = None,
525
603
  ) -> execution.DeployedProgram:
526
604
  """Upload a checked authored-source bundle without packaging a live module.
527
605
 
528
606
  ``program_id`` works as it does for :func:`deploy`: the explicit id, else the
529
607
  configured one, else none, and serve decides whether a deployment needs one.
608
+ ``program_name`` names the program instead, and wins over both ids.
609
+ """
610
+ config = configuration.require_config()
611
+ return deploy_bundle_under(
612
+ zip_bytes, facts, program_for(program_id, program_name, config), timeout=timeout, metadata=metadata
613
+ )
614
+
615
+
616
+ def deploy_bundle_under(
617
+ zip_bytes: bytes,
618
+ facts: authored.ProgramFacts,
619
+ program: str | None,
620
+ *,
621
+ timeout: float = 300,
622
+ metadata: Any = None,
623
+ ) -> execution.DeployedProgram:
624
+ """Upload a checked bundle under ``program``, which the caller already chose.
625
+
626
+ None deploys without a program id, with no fallback to the configured one.
530
627
  """
531
628
  config = configuration.require_config()
532
- program = declared_program_id(program_id, config)
533
629
  deploy_body = facts.deploy_body()
534
630
  if program is not None:
535
631
  deploy_body["program_id"] = program