cmpnd 0.55.0__tar.gz → 0.57.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 (392) hide show
  1. {cmpnd-0.55.0 → cmpnd-0.57.0}/PKG-INFO +173 -11
  2. {cmpnd-0.55.0 → cmpnd-0.57.0}/README.md +172 -10
  3. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/__init__.py +8 -2
  4. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_authored_import.py +113 -4
  5. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/callback.py +3 -2
  6. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/programs.py +139 -0
  7. cmpnd-0.57.0/cmpnd/cli/commands/provider_credentials.py +532 -0
  8. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/top.py +19 -1
  9. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/verbs.py +5 -0
  10. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/client.py +54 -9
  11. cmpnd-0.57.0/cmpnd/credentials.py +199 -0
  12. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/deployment.py +105 -22
  13. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/helpers.py +116 -0
  14. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/lm_configuration.py +71 -15
  15. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/optimization.py +109 -53
  16. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/programs.py +1 -1
  17. {cmpnd-0.55.0 → cmpnd-0.57.0}/docs/cli.md +122 -24
  18. {cmpnd-0.55.0 → cmpnd-0.57.0}/docs/sdk-instrumentation.md +16 -5
  19. {cmpnd-0.55.0 → cmpnd-0.57.0}/pyproject.toml +1 -1
  20. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/capture_review_run.py +4 -1
  21. cmpnd-0.57.0/tests/_credential_route.py +96 -0
  22. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/committee_task.py +49 -54
  23. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/helpers.py +93 -0
  24. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +3 -3
  25. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/hosted_optimize_probe.py +51 -26
  26. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/test_logs_renders_traces.py +2 -2
  27. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/_provider.py +31 -24
  28. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_deploy_programs.py +7 -4
  29. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_optimize_loop.py +19 -18
  30. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_optimize_loop_wasm.py +23 -50
  31. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_optimize_stress.py +16 -18
  32. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_provider_roles.py +65 -3
  33. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_real_provider_twins.py +11 -9
  34. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_rlm_wasm.py +8 -13
  35. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_authored_named_models.py +19 -5
  36. cmpnd-0.57.0/tests/test_cli/test_programs.py +290 -0
  37. cmpnd-0.57.0/tests/test_cli/test_provider_credentials.py +660 -0
  38. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_top.py +31 -1
  39. cmpnd-0.57.0/tests/test_credential_kwargs.py +306 -0
  40. cmpnd-0.57.0/tests/test_credential_selection.py +320 -0
  41. cmpnd-0.57.0/tests/test_credential_values.py +193 -0
  42. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_deploy.py +55 -84
  43. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_optimize_lms.py +29 -15
  44. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_optimize_reflection_lm.py +18 -34
  45. cmpnd-0.57.0/tests/test_seal_lm_credential.py +153 -0
  46. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_system_one.py +31 -12
  47. {cmpnd-0.55.0 → cmpnd-0.57.0}/uv.lock +1 -1
  48. cmpnd-0.55.0/cmpnd/cli/commands/provider_credentials.py +0 -321
  49. cmpnd-0.55.0/tests/test_cli/test_programs.py +0 -143
  50. cmpnd-0.55.0/tests/test_cli/test_provider_credentials.py +0 -319
  51. {cmpnd-0.55.0 → cmpnd-0.57.0}/.gitignore +0 -0
  52. {cmpnd-0.55.0 → cmpnd-0.57.0}/AGENTS.md +0 -0
  53. {cmpnd-0.55.0 → cmpnd-0.57.0}/CLAUDE.md +0 -0
  54. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_assessments.py +0 -0
  55. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_deployments.py +0 -0
  56. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_exporter_pool.py +0 -0
  57. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_gepa_patch.py +0 -0
  58. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_http.py +0 -0
  59. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_program_patch.py +0 -0
  60. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_rlm_patch.py +0 -0
  61. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_runtime_tools.py +0 -0
  62. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_timespec.py +0 -0
  63. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_traces.py +0 -0
  64. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/_valsets.py +0 -0
  65. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/authored.py +0 -0
  66. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/__init__.py +0 -0
  67. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/api_client.py +0 -0
  68. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/__init__.py +0 -0
  69. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/admin.py +0 -0
  70. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/assessments.py +0 -0
  71. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/auth.py +0 -0
  72. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/datasets.py +0 -0
  73. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/deployments.py +0 -0
  74. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/evals.py +0 -0
  75. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/login.py +0 -0
  76. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/optimizations.py +0 -0
  77. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/org.py +0 -0
  78. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/signatures.py +0 -0
  79. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  80. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/traces.py +0 -0
  81. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/commands/valsets.py +0 -0
  82. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/credentials.py +0 -0
  83. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/deprecation.py +0 -0
  84. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/pagination.py +0 -0
  85. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/shell.py +0 -0
  86. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/trace_render.py +0 -0
  87. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/cli/tree_help.py +0 -0
  88. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/configuration.py +0 -0
  89. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/context.py +0 -0
  90. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/dataset_sync.py +0 -0
  91. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/datasets.py +0 -0
  92. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/decorators.py +0 -0
  93. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/deployment_metadata.py +0 -0
  94. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/deployment_runs.py +0 -0
  95. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/eval_handler.py +0 -0
  96. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/execution.py +0 -0
  97. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/exporter.py +0 -0
  98. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/exporter_http.py +0 -0
  99. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/identity.py +0 -0
  100. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/imaging.py +0 -0
  101. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/ir/__init__.py +0 -0
  102. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/ir/decode.py +0 -0
  103. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/ir/encode.py +0 -0
  104. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/ir/hash.py +0 -0
  105. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/ir/program.py +0 -0
  106. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/ir/reflection.py +0 -0
  107. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/ir/types.py +0 -0
  108. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/ir/xxh64.py +0 -0
  109. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/mcp_servers.py +0 -0
  110. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/models.py +0 -0
  111. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/named_programs.py +0 -0
  112. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/optimizers/__init__.py +0 -0
  113. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/optimizers/gepa.py +0 -0
  114. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  115. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/optimizers/resume.py +0 -0
  116. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/optimizers/tracker.py +0 -0
  117. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/packaging.py +0 -0
  118. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/references.py +0 -0
  119. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/retry.py +0 -0
  120. {cmpnd-0.55.0 → cmpnd-0.57.0}/cmpnd/traces.py +0 -0
  121. {cmpnd-0.55.0 → cmpnd-0.57.0}/docs/2026-04-02-code-review.md +0 -0
  122. {cmpnd-0.55.0 → cmpnd-0.57.0}/docs/client.md +0 -0
  123. {cmpnd-0.55.0 → cmpnd-0.57.0}/docs/custom-proposer.md +0 -0
  124. {cmpnd-0.55.0 → cmpnd-0.57.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  125. {cmpnd-0.55.0 → cmpnd-0.57.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  126. {cmpnd-0.55.0 → cmpnd-0.57.0}/docs/reading-traces.md +0 -0
  127. {cmpnd-0.55.0 → cmpnd-0.57.0}/examples/generalizing_proposer.py +0 -0
  128. {cmpnd-0.55.0 → cmpnd-0.57.0}/examples/local_ollama.py +0 -0
  129. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/LINEAGE.md +0 -0
  130. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/README.md +0 -0
  131. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  132. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  133. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
  134. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  135. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  136. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  137. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/cmpnd_ingest/py.typed +0 -0
  138. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  139. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/cmpnd_ingest/retry.py +0 -0
  140. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  141. {cmpnd-0.55.0 → cmpnd-0.57.0}/ingest/pyproject.toml +0 -0
  142. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/audit-sdist.sh +0 -0
  143. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/check-pip-install.sh +0 -0
  144. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/check_release_artifacts.py +0 -0
  145. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/fixtures/review_run.json.gz +0 -0
  146. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/gepa_resume_bug.py +0 -0
  147. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/release-stress.sh +0 -0
  148. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/require_version_bump.py +0 -0
  149. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/seed_review_run.py +0 -0
  150. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/selfhost_compat.py +0 -0
  151. {cmpnd-0.55.0 → cmpnd-0.57.0}/scripts/test-in-linux.sh +0 -0
  152. {cmpnd-0.55.0 → cmpnd-0.57.0}/stubs/dspy/__init__.pyi +0 -0
  153. {cmpnd-0.55.0 → cmpnd-0.57.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  154. {cmpnd-0.55.0 → cmpnd-0.57.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  155. {cmpnd-0.55.0 → cmpnd-0.57.0}/stubs/dspy/utils/__init__.pyi +0 -0
  156. {cmpnd-0.55.0 → cmpnd-0.57.0}/stubs/dspy/utils/callback.pyi +0 -0
  157. {cmpnd-0.55.0 → cmpnd-0.57.0}/stubs/dspy/utils/dummies.pyi +0 -0
  158. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/__init__.py +0 -0
  159. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/_deployment_signature_helpers.py +0 -0
  160. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/_exporter_helpers.py +0 -0
  161. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/_fake_clock.py +0 -0
  162. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/_identity_helpers.py +0 -0
  163. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/_resolve_refs.py +0 -0
  164. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/_traced_run.py +0 -0
  165. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/conftest.py +0 -0
  166. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/__init__.py +0 -0
  167. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/conftest.py +0 -0
  168. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  169. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  170. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/fake_lm.py +0 -0
  171. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/target_capabilities.py +0 -0
  172. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_assessments_cli.py +0 -0
  173. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_capture_prompts.py +0 -0
  174. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_composite_module.py +0 -0
  175. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  176. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_deploy_execute.py +0 -0
  177. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_deploy_pandas.py +0 -0
  178. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_deploy_programs.py +0 -0
  179. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_edge_cases.py +0 -0
  180. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_example_hash.py +0 -0
  181. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_gepa_optimization.py +0 -0
  182. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_interpreter_spans.py +0 -0
  183. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_lm_accuracy.py +0 -0
  184. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_module_tracing.py +0 -0
  185. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_optimization_lineage.py +0 -0
  186. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  187. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_optimize_failures.py +0 -0
  188. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_optimize_loop.py +0 -0
  189. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  190. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  191. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_parallel_export.py +0 -0
  192. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_project_attribution.py +0 -0
  193. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_react_tool_spans.py +0 -0
  194. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_save_load.py +0 -0
  195. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_trace_structure.py +0 -0
  196. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_traces_read.py +0 -0
  197. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_user_attribution.py +0 -0
  198. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/e2e/test_valsets_cli.py +0 -0
  199. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  200. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  201. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  202. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  203. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  204. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  205. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  206. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  207. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  208. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/10_react.json +0 -0
  209. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/11_refine.json +0 -0
  210. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/12_retrieve.json +0 -0
  211. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/13_knn.json +0 -0
  212. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/14_parallel.json +0 -0
  213. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/identity_vectors/README.md +0 -0
  214. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/ingest_conformance.py +0 -0
  215. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/__init__.py +0 -0
  216. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/authored_optimize/judge.py +0 -0
  217. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  218. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/conftest.py +0 -0
  219. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/optimization_run_probe.py +0 -0
  220. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  221. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/test_optimization_run.py +0 -0
  222. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/test_optimizing_progress.py +0 -0
  223. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/integration/test_unhappy_path.py +0 -0
  224. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/load/__init__.py +0 -0
  225. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/load/__main__.py +0 -0
  226. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/load/drain.py +0 -0
  227. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/load/ingest.py +0 -0
  228. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/load/latency_transport.py +0 -0
  229. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/load/rlm_shape.py +0 -0
  230. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/load/rlm_size_report.py +0 -0
  231. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/load/sweep.py +0 -0
  232. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/load/test_rlm_shape.py +0 -0
  233. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/load/test_rlm_size_report.py +0 -0
  234. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/__init__.py +0 -0
  235. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/conftest.py +0 -0
  236. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/seed_parity.py +0 -0
  237. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_auth.py +0 -0
  238. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_datasets.py +0 -0
  239. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_deployments.py +0 -0
  240. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_evals.py +0 -0
  241. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_health.py +0 -0
  242. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_modules.py +0 -0
  243. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_optimizations.py +0 -0
  244. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_programs.py +0 -0
  245. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_signatures.py +0 -0
  246. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_trace_streaming.py +0 -0
  247. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/parity/test_traces.py +0 -0
  248. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/span_reference_fixture.py +0 -0
  249. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/__init__.py +0 -0
  250. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/_readiness.py +0 -0
  251. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/_reliability.py +0 -0
  252. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/_result.py +0 -0
  253. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/mint_bedrock_token.py +0 -0
  254. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_multi_client_stress.py +0 -0
  255. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_optimize_client_committee.py +0 -0
  256. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_readiness.py +0 -0
  257. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_reliability.py +0 -0
  258. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_reliability_classify.py +0 -0
  259. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/stress/test_trace_search.py +0 -0
  260. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_adapter_reentrancy.py +0 -0
  261. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_assessments_api.py +0 -0
  262. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_authored.py +0 -0
  263. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_authored_bundle_collisions.py +0 -0
  264. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_authored_metric_contract.py +0 -0
  265. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_authored_optimize.py +0 -0
  266. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_callback.py +0 -0
  267. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_candidate_lineage.py +0 -0
  268. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/__init__.py +0 -0
  269. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/conftest.py +0 -0
  270. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_admin.py +0 -0
  271. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_api_client.py +0 -0
  272. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_assessments.py +0 -0
  273. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_auth.py +0 -0
  274. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_credentials.py +0 -0
  275. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_datasets.py +0 -0
  276. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_deployment_runs.py +0 -0
  277. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_deployments.py +0 -0
  278. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_evals.py +0 -0
  279. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_login.py +0 -0
  280. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_main.py +0 -0
  281. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_optimizations.py +0 -0
  282. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_org.py +0 -0
  283. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_pagination.py +0 -0
  284. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_parity.py +0 -0
  285. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_schema_sample.py +0 -0
  286. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_shell.py +0 -0
  287. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_signatures.py +0 -0
  288. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_tool_servers.py +0 -0
  289. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_traces.py +0 -0
  290. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  291. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_tree_help.py +0 -0
  292. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cli/test_valsets.py +0 -0
  293. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_client.py +0 -0
  294. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_client_floor.py +0 -0
  295. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_client_program_names.py +0 -0
  296. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_clustering/__init__.py +0 -0
  297. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_clustering/conftest.py +0 -0
  298. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_clustering/test_edge_cases.py +0 -0
  299. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  300. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_clustering/test_type_conversion.py +0 -0
  301. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_cmp551_grouping_identity.py +0 -0
  302. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_configuration.py +0 -0
  303. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_context.py +0 -0
  304. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_context_propagation.py +0 -0
  305. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_dataset_polling.py +0 -0
  306. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_dataset_sync.py +0 -0
  307. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_datasets.py +0 -0
  308. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_decision_client_spans.py +0 -0
  309. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_decorators.py +0 -0
  310. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  311. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_deploy_program_id.py +0 -0
  312. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_deployment_metadata.py +0 -0
  313. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_deployment_runs.py +0 -0
  314. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_deployments_read.py +0 -0
  315. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_eval.py +0 -0
  316. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_eval_example_attribution.py +0 -0
  317. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_eval_start.py +0 -0
  318. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_eval_trace_grouping.py +0 -0
  319. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_exception_paths.py +0 -0
  320. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_execute.py +0 -0
  321. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_execute_run_identity.py +0 -0
  322. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_exporter.py +0 -0
  323. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_exporter_program_names.py +0 -0
  324. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_exporter_queue.py +0 -0
  325. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_exporter_real.py +0 -0
  326. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_fake_lm.py +0 -0
  327. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_gepa_callback.py +0 -0
  328. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_gepa_integration.py +0 -0
  329. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_gepa_output_normalization.py +0 -0
  330. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_gepa_resume.py +0 -0
  331. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_identity.py +0 -0
  332. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_imaging.py +0 -0
  333. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_import_weight.py +0 -0
  334. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_ingest_conformance.py +0 -0
  335. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_ingest_second_producer.py +0 -0
  336. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_integration.py +0 -0
  337. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_interpreter_callback.py +0 -0
  338. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_ir_encoder_properties.py +0 -0
  339. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_ir_golden_vectors.py +0 -0
  340. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_ir_roundtrip.py +0 -0
  341. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_lm15_native_calls.py +0 -0
  342. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_lm_configuration.py +0 -0
  343. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_lm_usage_shapes.py +0 -0
  344. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_mcp_tools.py +0 -0
  345. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_models.py +0 -0
  346. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_multi_client_stress_readiness.py +0 -0
  347. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_optimization_progress.py +0 -0
  348. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_optimization_start.py +0 -0
  349. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_optimize.py +0 -0
  350. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_optimize_budget.py +0 -0
  351. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_optimize_lift.py +0 -0
  352. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_optimize_proposer.py +0 -0
  353. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_optimize_terminal_failure.py +0 -0
  354. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_packaging.py +0 -0
  355. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_packaging_callable_source.py +0 -0
  356. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_packaging_mcp_tools.py +0 -0
  357. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_packaging_program_source.py +0 -0
  358. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_poll_until.py +0 -0
  359. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_program_id_deprecation.py +0 -0
  360. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_program_identity.py +0 -0
  361. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_program_lineage_persist.py +0 -0
  362. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_program_names_in_deploy.py +0 -0
  363. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_programs_create.py +0 -0
  364. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_project_payload.py +0 -0
  365. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_real_provider_twins_readiness.py +0 -0
  366. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_reflector_properties.py +0 -0
  367. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_reflector_smoke.py +0 -0
  368. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_release_stress_script.py +0 -0
  369. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_replaced_names.py +0 -0
  370. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_repro_reactv2_bugs.py +0 -0
  371. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_require_version_bump.py +0 -0
  372. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_retry.py +0 -0
  373. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_reversible_failures.py +0 -0
  374. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_rlm_patch.py +0 -0
  375. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_run_attribution_paths.py +0 -0
  376. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_runtime_tools.py +0 -0
  377. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_schemas.py +0 -0
  378. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_seed_review_run.py +0 -0
  379. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_signature_inputs.py +0 -0
  380. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_span_references.py +0 -0
  381. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_span_sent_once.py +0 -0
  382. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_trace_context_concurrency.py +0 -0
  383. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_trace_fact_wire_contract.py +0 -0
  384. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_trace_facts.py +0 -0
  385. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_trace_render.py +0 -0
  386. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_traces_read.py +0 -0
  387. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_tracked_gepa.py +0 -0
  388. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_tracker_enrich.py +0 -0
  389. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_truncation.py +0 -0
  390. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_upload_auth_headers.py +0 -0
  391. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_valsets_api.py +0 -0
  392. {cmpnd-0.55.0 → cmpnd-0.57.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.55.0
3
+ Version: 0.57.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>
@@ -205,6 +205,145 @@ The platform makes the program the first time your organization uses the
205
205
  program name. Later deploys under the same program name go to the same
206
206
  program, and so do the traces of a client that runs it.
207
207
 
208
+ ### Credentials
209
+
210
+ A hosted program spends a credential your organization sealed, and the deploy
211
+ names it. The program's code stays the code you run on your laptop:
212
+
213
+ ```python
214
+ lm = dspy.LM("openai/deepseek-4.1-flash", api_base="https://gw.lunaroute.com/v1") # unchanged
215
+
216
+ luna = cmpnd.credentials.get("luna-exec")
217
+ deployed = sdk.deploy_program(program, credential=luna)
218
+ # deploy: default-lm: chat_completions openai/deepseek-4.1-flash → luna-exec (https://gw.lunaroute.com)
219
+ ```
220
+
221
+ - `cmpnd.credentials.get(name)` returns a `cmpnd.Credential` holding the name,
222
+ the protocols its endpoint serves and its `api_base`. It holds no key. A name
223
+ the organization does not hold raises `cmpnd.CredentialNotFound`, which lists
224
+ the names it does hold and the nearest one. `cmpnd.credentials.list()`
225
+ returns them all.
226
+ - `credential=` takes a `cmpnd.Credential`, never a string. A string raises
227
+ and shows the `cmpnd.credentials.get(...)` line to write.
228
+ - Without `credential=`, the deployment spends the organization's default,
229
+ which an owner sets with `cmpnd credentials default NAME`.
230
+ - The deployment records the credential's name. Each call spends the newest
231
+ credential sealed under that name, so an owner who rotates a key needs no
232
+ redeploy.
233
+ - The deploy prints the protocol, model and credential of each binding, as
234
+ above. A server that prints none chose the credential from the model's
235
+ provider prefix, and the SDK warns.
236
+ - A hosted call goes to the credential's `api_base`. The LM's own `api_base`
237
+ serves your laptop run and is not sent.
238
+
239
+ Keys never leave your laptop. The SDK sends no key on the LM, and a key reaches
240
+ the platform only through `cmpnd credentials create`. `cmpnd deploy FILE`
241
+ refuses source whose import builds an LM carrying a key, and names the module,
242
+ the model and the field.
243
+
244
+ ### The same source on your laptop and hosted
245
+
246
+ A program's logic runs unchanged on your laptop and hosted. On your laptop it
247
+ spends the key in your environment. Hosted, it spends a credential your
248
+ organization sealed. Keep the logic in one file and let a small entry point
249
+ choose where it runs.
250
+
251
+ The program file holds the logic and names its model. It holds no key:
252
+
253
+ ```python
254
+ # qa.py
255
+ import dspy
256
+
257
+ lm = dspy.LM("openai/gpt-5-mini") # no api_key: litellm reads OPENAI_API_KEY on your laptop
258
+ program = dspy.Predict("question -> answer")
259
+
260
+
261
+ def metric(gold, pred, trace=None, pred_name=None, pred_trace=None, lms=None):
262
+ verdict = dspy.Predict("answer, gold -> correct: bool")(answer=pred.answer, gold=gold.answer, lm=lms["critic"])
263
+ return dspy.Prediction(score=float(verdict.correct), feedback="correct" if verdict.correct else "wrong")
264
+ ```
265
+
266
+ Seal the key once for the organization and make it the default:
267
+
268
+ ```bash
269
+ printf '%s' "$OPENAI_API_KEY" | cmpnd credentials create --name openai --preset openai
270
+ cmpnd credentials default openai
271
+ ```
272
+
273
+ The entry point is the only code that knows where the run happens:
274
+
275
+ ```python
276
+ # main.py
277
+ import functools
278
+ import sys
279
+
280
+ import cmpnd
281
+ import dspy
282
+
283
+ from qa import lm, metric, program
284
+
285
+ HOSTED = "--hosted" in sys.argv
286
+
287
+ sdk = cmpnd.Client()
288
+ qa = cmpnd.Program("qa", module=program, lm=lm)
289
+ reflection = dspy.LM("openai/gpt-5", max_tokens=32000)
290
+ lms = {"critic": dspy.LM("openai/gpt-5-mini")}
291
+ train, val = load_examples() # your dspy.Example lists
292
+
293
+ if HOSTED:
294
+ deployed = sdk.deploy_program(qa) # spends the default credential, openai
295
+ print(sdk.run_deployment(deployed, question="What is DSPy?"))
296
+ best = sdk.optimize_deployment(
297
+ deployed,
298
+ trainset=train,
299
+ valset=val,
300
+ metric=metric, # the platform hands it lms
301
+ reflection_lm=cmpnd.HostedLM(reflection),
302
+ lms={name: cmpnd.HostedLM(model) for name, model in lms.items()},
303
+ auto="light",
304
+ )
305
+ else:
306
+ print(sdk.run_program(qa, question="What is DSPy?")) # spends $OPENAI_API_KEY
307
+ gepa = dspy.GEPA(metric=functools.partial(metric, lms=lms), reflection_lm=reflection, auto="light")
308
+ best = sdk.optimize_program(qa, gepa, trainset=train, valset=val)
309
+ ```
310
+
311
+ `python main.py` runs everything on your laptop, and `python main.py --hosted`
312
+ runs it on the platform. `cmpnd deploy qa.py qa` deploys the same file from the
313
+ terminal.
314
+
315
+ | | On your laptop | Hosted |
316
+ |---|---|---|
317
+ | Key | the provider's variable in your environment, such as `OPENAI_API_KEY` | the credential the call names, else the organization's default |
318
+ | Address | the LM's `api_base`, else the provider's endpoint | the credential's `api_base` |
319
+ | Model | `gpt-5-mini`, the model after its first `/` | `gpt-5-mini` |
320
+ | The metric's `lms` | the dict the entry point binds | the run's `lms=`, which the platform hands it |
321
+
322
+ These rules keep the program file portable:
323
+
324
+ - **Leave the key off the program's LM** and let litellm read the provider's
325
+ own variable. For a key in a variable litellm does not read, write
326
+ `api_key=os.environ.get("NAME")`. cmpnd imports a deploy file with an empty
327
+ environment, so the key is `None` there and present on your laptop.
328
+ - **Never write a key into the file, and never read it as
329
+ `os.environ["NAME"]` at module scope.** The deploy refuses the first. The
330
+ second raises `KeyError` hosted, where the environment holds no key.
331
+ - **Seal the credential at the address your laptop LM reaches.** A program
332
+ that sets `api_base="https://gw.lunaroute.com/v1"` needs a credential sealed
333
+ there, with `--preset lunaroute` or `--api-base`. Hosted calls go to the
334
+ credential's address and ignore the LM's `api_base`. The deploy prints the
335
+ address it chose, so you can compare the two.
336
+ - **Wrap a model you hand the hosted optimization in `cmpnd.HostedLM`.** The
337
+ entry point builds `reflection_lm=` and each `lms=` entry, so those LMs may
338
+ hold a key for the laptop run. The wrapper leaves that key on your machine.
339
+ `optimize_deployment` refuses a key on an unwrapped LM, because there it would
340
+ look spent. `cmpnd.HostedLM(model, credential=c)` makes a model spend a
341
+ credential other than the run's.
342
+
343
+ A judge built on `dspy.experimental.TypeSafe` also runs unchanged in both
344
+ places. [`docs/sdk-instrumentation.md`](docs/sdk-instrumentation.md), under
345
+ "System One judges", describes the hosted side.
346
+
208
347
  ### Options
209
348
 
210
349
  ```python
@@ -213,6 +352,7 @@ sdk.deploy_program(
213
352
  metric=None, # optional: a metric callable, packaged for optimization
214
353
  timeout=300, # optional: seconds to wait for the deployment to be ready
215
354
  metadata=None, # optional: a JSON-serializable dict, at most 64 KiB, stored with the deployment
355
+ credential=None, # optional: a cmpnd.Credential the program's model spends
216
356
  )
217
357
  ```
218
358
 
@@ -270,11 +410,32 @@ from the deployment.
270
410
  `reflection_lm` is the model that proposes each new instruction. The search is
271
411
  only as good as this model, so it is usually a strong model and not the one
272
412
  being optimized. A `dspy.LM` passes its sampling settings to the provider. A
273
- bare model name gives the same model with no sampling settings. The platform
274
- supplies the credential and the endpoint, so the SDK refuses an `api_key`,
275
- `api_base` or `base_url` on the LM before any network call. On a hosted org,
276
- both live on the provider credential (Account → Model Providers), keyed by the
277
- model's protocol. One `openai` credential serves every `openai/*` model.
413
+ bare model name gives the same model with no sampling settings. A run spends
414
+ sealed credentials only, so the SDK refuses a key on the LM before any network
415
+ call. `cmpnd.HostedLM(lm)` keeps a key your laptop runs spend on this machine
416
+ and passes the LM's model and sampling.
417
+
418
+ `credential=` names the run's credential, which every binding that names none
419
+ spends: the program's model, the reflection model and each entry of `lms`. A
420
+ binding names its own credential through a wrapper for its protocol:
421
+
422
+ ```python
423
+ luna = cmpnd.credentials.get("luna-exec") # serves chat_completions and system_one
424
+ openai = cmpnd.credentials.get("openai")
425
+ best = sdk.optimize_deployment(
426
+ deployed, trainset=train, valset=val, metric=metric,
427
+ credential=luna,
428
+ reflection_lm=cmpnd.HostedLM(dspy.LM("openai/gpt-5"), credential=openai),
429
+ lms={"judge": cmpnd.SystemOne("lunaroute/djev", credential=luna)},
430
+ auto="light",
431
+ )
432
+ ```
433
+
434
+ `cmpnd.HostedLM(lm, credential=c)` wraps a chat LM, and
435
+ `cmpnd.SystemOne(model, credential=c)` declares a System One model. A binding
436
+ without a credential spends the run's, or the organization's default. The
437
+ platform refuses a credential whose endpoint does not serve a binding's
438
+ protocol, and names the `credential=` to add.
278
439
 
279
440
  The SDK checks the metric before any network call. GEPA writes each new
280
441
  instruction from the textual feedback the metric returns, so it needs a
@@ -322,9 +483,9 @@ rules:
322
483
  names.
323
484
  - `"default-lm"` is reserved for the program's own model, which comes from the
324
485
  deployment. Declaring it raises an error that names it.
325
- - The platform refuses a model your org holds no credential for when it dispatches
326
- the run, before the run starts. These LMs, like `reflection_lm`, must not set
327
- `api_key`, `api_base` or `base_url`.
486
+ - The platform refuses a binding it finds no credential for when it dispatches
487
+ the run, before the run starts. These LMs, like `reflection_lm`, must not
488
+ carry a key.
328
489
 
329
490
  The `lms` parameter does not change the arity check. Five or more positional
330
491
  arguments make a feedback metric, and a metric without `lms` never receives
@@ -340,8 +501,9 @@ sdk.optimize_deployment(
340
501
  valset=val, # required: validation examples
341
502
  metric=metric, # required: the metric, arity-checked
342
503
  config=None, # optional: optimizer config, allow-listed by the platform
343
- reflection_lm=None, # required: a dspy.LM or a bare model name
344
- lms=None, # optional: name -> dspy.LM, the run's named models
504
+ reflection_lm=None, # required: a dspy.LM, a bare model name or a cmpnd.HostedLM
505
+ lms=None, # optional: name -> dspy.LM, cmpnd.HostedLM or cmpnd.SystemOne
506
+ credential=None, # optional: the cmpnd.Credential every other binding spends
345
507
  max_metric_calls=None, # budget: the metric-call ceiling for one step
346
508
  auto=None, # budget: "light", "medium" or "heavy"
347
509
  may_author_code=None, # optional: False keeps each dspy.Flex at its deployed source
@@ -154,6 +154,145 @@ The platform makes the program the first time your organization uses the
154
154
  program name. Later deploys under the same program name go to the same
155
155
  program, and so do the traces of a client that runs it.
156
156
 
157
+ ### Credentials
158
+
159
+ A hosted program spends a credential your organization sealed, and the deploy
160
+ names it. The program's code stays the code you run on your laptop:
161
+
162
+ ```python
163
+ lm = dspy.LM("openai/deepseek-4.1-flash", api_base="https://gw.lunaroute.com/v1") # unchanged
164
+
165
+ luna = cmpnd.credentials.get("luna-exec")
166
+ deployed = sdk.deploy_program(program, credential=luna)
167
+ # deploy: default-lm: chat_completions openai/deepseek-4.1-flash → luna-exec (https://gw.lunaroute.com)
168
+ ```
169
+
170
+ - `cmpnd.credentials.get(name)` returns a `cmpnd.Credential` holding the name,
171
+ the protocols its endpoint serves and its `api_base`. It holds no key. A name
172
+ the organization does not hold raises `cmpnd.CredentialNotFound`, which lists
173
+ the names it does hold and the nearest one. `cmpnd.credentials.list()`
174
+ returns them all.
175
+ - `credential=` takes a `cmpnd.Credential`, never a string. A string raises
176
+ and shows the `cmpnd.credentials.get(...)` line to write.
177
+ - Without `credential=`, the deployment spends the organization's default,
178
+ which an owner sets with `cmpnd credentials default NAME`.
179
+ - The deployment records the credential's name. Each call spends the newest
180
+ credential sealed under that name, so an owner who rotates a key needs no
181
+ redeploy.
182
+ - The deploy prints the protocol, model and credential of each binding, as
183
+ above. A server that prints none chose the credential from the model's
184
+ provider prefix, and the SDK warns.
185
+ - A hosted call goes to the credential's `api_base`. The LM's own `api_base`
186
+ serves your laptop run and is not sent.
187
+
188
+ Keys never leave your laptop. The SDK sends no key on the LM, and a key reaches
189
+ the platform only through `cmpnd credentials create`. `cmpnd deploy FILE`
190
+ refuses source whose import builds an LM carrying a key, and names the module,
191
+ the model and the field.
192
+
193
+ ### The same source on your laptop and hosted
194
+
195
+ A program's logic runs unchanged on your laptop and hosted. On your laptop it
196
+ spends the key in your environment. Hosted, it spends a credential your
197
+ organization sealed. Keep the logic in one file and let a small entry point
198
+ choose where it runs.
199
+
200
+ The program file holds the logic and names its model. It holds no key:
201
+
202
+ ```python
203
+ # qa.py
204
+ import dspy
205
+
206
+ lm = dspy.LM("openai/gpt-5-mini") # no api_key: litellm reads OPENAI_API_KEY on your laptop
207
+ program = dspy.Predict("question -> answer")
208
+
209
+
210
+ def metric(gold, pred, trace=None, pred_name=None, pred_trace=None, lms=None):
211
+ verdict = dspy.Predict("answer, gold -> correct: bool")(answer=pred.answer, gold=gold.answer, lm=lms["critic"])
212
+ return dspy.Prediction(score=float(verdict.correct), feedback="correct" if verdict.correct else "wrong")
213
+ ```
214
+
215
+ Seal the key once for the organization and make it the default:
216
+
217
+ ```bash
218
+ printf '%s' "$OPENAI_API_KEY" | cmpnd credentials create --name openai --preset openai
219
+ cmpnd credentials default openai
220
+ ```
221
+
222
+ The entry point is the only code that knows where the run happens:
223
+
224
+ ```python
225
+ # main.py
226
+ import functools
227
+ import sys
228
+
229
+ import cmpnd
230
+ import dspy
231
+
232
+ from qa import lm, metric, program
233
+
234
+ HOSTED = "--hosted" in sys.argv
235
+
236
+ sdk = cmpnd.Client()
237
+ qa = cmpnd.Program("qa", module=program, lm=lm)
238
+ reflection = dspy.LM("openai/gpt-5", max_tokens=32000)
239
+ lms = {"critic": dspy.LM("openai/gpt-5-mini")}
240
+ train, val = load_examples() # your dspy.Example lists
241
+
242
+ if HOSTED:
243
+ deployed = sdk.deploy_program(qa) # spends the default credential, openai
244
+ print(sdk.run_deployment(deployed, question="What is DSPy?"))
245
+ best = sdk.optimize_deployment(
246
+ deployed,
247
+ trainset=train,
248
+ valset=val,
249
+ metric=metric, # the platform hands it lms
250
+ reflection_lm=cmpnd.HostedLM(reflection),
251
+ lms={name: cmpnd.HostedLM(model) for name, model in lms.items()},
252
+ auto="light",
253
+ )
254
+ else:
255
+ print(sdk.run_program(qa, question="What is DSPy?")) # spends $OPENAI_API_KEY
256
+ gepa = dspy.GEPA(metric=functools.partial(metric, lms=lms), reflection_lm=reflection, auto="light")
257
+ best = sdk.optimize_program(qa, gepa, trainset=train, valset=val)
258
+ ```
259
+
260
+ `python main.py` runs everything on your laptop, and `python main.py --hosted`
261
+ runs it on the platform. `cmpnd deploy qa.py qa` deploys the same file from the
262
+ terminal.
263
+
264
+ | | On your laptop | Hosted |
265
+ |---|---|---|
266
+ | Key | the provider's variable in your environment, such as `OPENAI_API_KEY` | the credential the call names, else the organization's default |
267
+ | Address | the LM's `api_base`, else the provider's endpoint | the credential's `api_base` |
268
+ | Model | `gpt-5-mini`, the model after its first `/` | `gpt-5-mini` |
269
+ | The metric's `lms` | the dict the entry point binds | the run's `lms=`, which the platform hands it |
270
+
271
+ These rules keep the program file portable:
272
+
273
+ - **Leave the key off the program's LM** and let litellm read the provider's
274
+ own variable. For a key in a variable litellm does not read, write
275
+ `api_key=os.environ.get("NAME")`. cmpnd imports a deploy file with an empty
276
+ environment, so the key is `None` there and present on your laptop.
277
+ - **Never write a key into the file, and never read it as
278
+ `os.environ["NAME"]` at module scope.** The deploy refuses the first. The
279
+ second raises `KeyError` hosted, where the environment holds no key.
280
+ - **Seal the credential at the address your laptop LM reaches.** A program
281
+ that sets `api_base="https://gw.lunaroute.com/v1"` needs a credential sealed
282
+ there, with `--preset lunaroute` or `--api-base`. Hosted calls go to the
283
+ credential's address and ignore the LM's `api_base`. The deploy prints the
284
+ address it chose, so you can compare the two.
285
+ - **Wrap a model you hand the hosted optimization in `cmpnd.HostedLM`.** The
286
+ entry point builds `reflection_lm=` and each `lms=` entry, so those LMs may
287
+ hold a key for the laptop run. The wrapper leaves that key on your machine.
288
+ `optimize_deployment` refuses a key on an unwrapped LM, because there it would
289
+ look spent. `cmpnd.HostedLM(model, credential=c)` makes a model spend a
290
+ credential other than the run's.
291
+
292
+ A judge built on `dspy.experimental.TypeSafe` also runs unchanged in both
293
+ places. [`docs/sdk-instrumentation.md`](docs/sdk-instrumentation.md), under
294
+ "System One judges", describes the hosted side.
295
+
157
296
  ### Options
158
297
 
159
298
  ```python
@@ -162,6 +301,7 @@ sdk.deploy_program(
162
301
  metric=None, # optional: a metric callable, packaged for optimization
163
302
  timeout=300, # optional: seconds to wait for the deployment to be ready
164
303
  metadata=None, # optional: a JSON-serializable dict, at most 64 KiB, stored with the deployment
304
+ credential=None, # optional: a cmpnd.Credential the program's model spends
165
305
  )
166
306
  ```
167
307
 
@@ -219,11 +359,32 @@ from the deployment.
219
359
  `reflection_lm` is the model that proposes each new instruction. The search is
220
360
  only as good as this model, so it is usually a strong model and not the one
221
361
  being optimized. A `dspy.LM` passes its sampling settings to the provider. A
222
- bare model name gives the same model with no sampling settings. The platform
223
- supplies the credential and the endpoint, so the SDK refuses an `api_key`,
224
- `api_base` or `base_url` on the LM before any network call. On a hosted org,
225
- both live on the provider credential (Account → Model Providers), keyed by the
226
- model's protocol. One `openai` credential serves every `openai/*` model.
362
+ bare model name gives the same model with no sampling settings. A run spends
363
+ sealed credentials only, so the SDK refuses a key on the LM before any network
364
+ call. `cmpnd.HostedLM(lm)` keeps a key your laptop runs spend on this machine
365
+ and passes the LM's model and sampling.
366
+
367
+ `credential=` names the run's credential, which every binding that names none
368
+ spends: the program's model, the reflection model and each entry of `lms`. A
369
+ binding names its own credential through a wrapper for its protocol:
370
+
371
+ ```python
372
+ luna = cmpnd.credentials.get("luna-exec") # serves chat_completions and system_one
373
+ openai = cmpnd.credentials.get("openai")
374
+ best = sdk.optimize_deployment(
375
+ deployed, trainset=train, valset=val, metric=metric,
376
+ credential=luna,
377
+ reflection_lm=cmpnd.HostedLM(dspy.LM("openai/gpt-5"), credential=openai),
378
+ lms={"judge": cmpnd.SystemOne("lunaroute/djev", credential=luna)},
379
+ auto="light",
380
+ )
381
+ ```
382
+
383
+ `cmpnd.HostedLM(lm, credential=c)` wraps a chat LM, and
384
+ `cmpnd.SystemOne(model, credential=c)` declares a System One model. A binding
385
+ without a credential spends the run's, or the organization's default. The
386
+ platform refuses a credential whose endpoint does not serve a binding's
387
+ protocol, and names the `credential=` to add.
227
388
 
228
389
  The SDK checks the metric before any network call. GEPA writes each new
229
390
  instruction from the textual feedback the metric returns, so it needs a
@@ -271,9 +432,9 @@ rules:
271
432
  names.
272
433
  - `"default-lm"` is reserved for the program's own model, which comes from the
273
434
  deployment. Declaring it raises an error that names it.
274
- - The platform refuses a model your org holds no credential for when it dispatches
275
- the run, before the run starts. These LMs, like `reflection_lm`, must not set
276
- `api_key`, `api_base` or `base_url`.
435
+ - The platform refuses a binding it finds no credential for when it dispatches
436
+ the run, before the run starts. These LMs, like `reflection_lm`, must not
437
+ carry a key.
277
438
 
278
439
  The `lms` parameter does not change the arity check. Five or more positional
279
440
  arguments make a feedback metric, and a metric without `lms` never receives
@@ -289,8 +450,9 @@ sdk.optimize_deployment(
289
450
  valset=val, # required: validation examples
290
451
  metric=metric, # required: the metric, arity-checked
291
452
  config=None, # optional: optimizer config, allow-listed by the platform
292
- reflection_lm=None, # required: a dspy.LM or a bare model name
293
- lms=None, # optional: name -> dspy.LM, the run's named models
453
+ reflection_lm=None, # required: a dspy.LM, a bare model name or a cmpnd.HostedLM
454
+ lms=None, # optional: name -> dspy.LM, cmpnd.HostedLM or cmpnd.SystemOne
455
+ credential=None, # optional: the cmpnd.Credential every other binding spends
294
456
  max_metric_calls=None, # budget: the metric-call ceiling for one step
295
457
  auto=None, # budget: "light", "medium" or "heavy"
296
458
  may_author_code=None, # optional: False keeps each dspy.Flex at its deployed source
@@ -17,7 +17,7 @@ from typing import TYPE_CHECKING
17
17
 
18
18
  from cmpnd_ingest.client_floor import UpgradeRequiredWarning
19
19
 
20
- from . import authored, programs, traces
20
+ from . import authored, credentials, programs, traces
21
21
  from ._http import ApiError
22
22
  from .callback import CmpndCallback, trace_id
23
23
  from .client import Client
@@ -27,6 +27,7 @@ from .context import (
27
27
  get_current_span,
28
28
  get_current_trace,
29
29
  )
30
+ from .credentials import Credential, CredentialNotFound
30
31
  from .datasets import DatasetNotFoundError, VersionNotFoundError, load_dataset
31
32
  from .decorators import start_span, trace
32
33
  from .deployment import DeployFailed
@@ -39,7 +40,7 @@ from .deployment_metadata import (
39
40
  from .deployment_runs import RunResultExpired, RunSubmissionUncertain, RunWaitTimeout
40
41
  from .execution import DeployedProgram, RetryableProviderError
41
42
  from .exporter import flush_exporter, shutdown_exporter
42
- from .lm_configuration import SystemOne, configure_lm
43
+ from .lm_configuration import HostedLM, SystemOne, configure_lm
43
44
  from .mcp_servers import (
44
45
  Bearer,
45
46
  McpToolReference,
@@ -70,6 +71,7 @@ __all__ = [
70
71
  "configure",
71
72
  "configure_lm",
72
73
  "SystemOne",
74
+ "HostedLM",
73
75
  "get_config",
74
76
  "require_config",
75
77
  # Main callback
@@ -95,6 +97,10 @@ __all__ = [
95
97
  "traces",
96
98
  # Program name checks (cmpnd.programs.validate_program_name)
97
99
  "programs",
100
+ # The organization's provider credentials (cmpnd.credentials.get / list)
101
+ "credentials",
102
+ "Credential",
103
+ "CredentialNotFound",
98
104
  # A program declared by name
99
105
  "Program",
100
106
  "program",
@@ -15,6 +15,103 @@ from pathlib import Path
15
15
  from typing import Any
16
16
 
17
17
 
18
+ class BundleCarriesCredential(ValueError):
19
+ """The bundle's import built an LM that holds a provider credential."""
20
+
21
+
22
+ @contextlib.contextmanager
23
+ def _built_lms(root: Path):
24
+ """Record every dspy LM built inside the block, with the bundle module that built it.
25
+
26
+ The check reads objects the import built and parses no source. It wraps the
27
+ ``__init__`` of ``dspy.BaseLM`` and of every LM class that defines its own, because
28
+ ``dspy.LM`` before dspy 3.3 never calls ``BaseLM.__init__``. It names the innermost
29
+ module of the bundle on the stack, so an LM a helper module builds is named by that
30
+ module, and records each LM once, however many of the wrapped constructors it runs.
31
+ """
32
+ from dspy.clients.base_lm import BaseLM # pyright: ignore[reportMissingTypeStubs] # dspy re-exports BaseLM untyped
33
+
34
+ built: list[tuple[Any, str | None]] = []
35
+ seen: set[int] = set()
36
+
37
+ def lm_classes(cls: type) -> list[type]:
38
+ found = [cls]
39
+ for sub in cls.__subclasses__():
40
+ found.extend(lm_classes(sub))
41
+ return found
42
+
43
+ def recording(original: Any) -> Any:
44
+ def init(self: Any, *args: Any, **kwargs: Any) -> None:
45
+ original(self, *args, **kwargs)
46
+ if id(self) not in seen:
47
+ seen.add(id(self))
48
+ built.append((self, _bundle_module(root)))
49
+
50
+ return init
51
+
52
+ originals = {cls: cls.__dict__["__init__"] for cls in lm_classes(BaseLM) if "__init__" in cls.__dict__}
53
+ for cls, original in originals.items():
54
+ cls.__init__ = recording(original)
55
+ try:
56
+ yield built
57
+ finally:
58
+ for cls, original in originals.items():
59
+ cls.__init__ = original
60
+
61
+
62
+ def _bundle_module(root: Path) -> str | None:
63
+ """The name of the innermost module on the stack whose file sits in the staged bundle."""
64
+ frame = sys._getframe(2) # the caller of the LM's __init__
65
+ prefix = str(root.resolve()) + "/"
66
+ while frame is not None:
67
+ if str(Path(frame.f_code.co_filename).resolve()).startswith(prefix):
68
+ name = frame.f_globals.get("__name__")
69
+ return name if isinstance(name, str) else None
70
+ frame = frame.f_back
71
+ return None
72
+
73
+
74
+ def _refuse_bundled_credentials(built: list[tuple[Any, str | None]], entry: str) -> None:
75
+ """Refuse a bundle whose import built an LM that holds a provider credential.
76
+
77
+ The import ran with no environment, so a field set here came from the bundle: a
78
+ literal, or a file it ships, such as a ``.env`` it loads. The platform stores the
79
+ source it deploys, and a key belongs only in a sealed credential. The refusal names
80
+ the module, the model and the field, and never the value.
81
+ """
82
+ from . import helpers
83
+
84
+ findings: list[tuple[str, str, str]] = []
85
+ for lm, module in built:
86
+ model = str(getattr(lm, "model", None) or "?")
87
+ for field in helpers.credential_fields(getattr(lm, "kwargs", None)):
88
+ findings.append((module or entry, model, field))
89
+ if not findings:
90
+ return
91
+ lines = [f" module {module!r} builds an LM for {model!r} with {field} set" for module, model, field in findings]
92
+ module, model, field = findings[0]
93
+ holder, bracket, header = field.partition("[")
94
+ prefix = model.split("/", 1)[0].upper()
95
+ variable = f"{prefix}_API_KEY" if bracket or field == "api_key" else field.upper()
96
+ variable = "".join(char if char.isalnum() else "_" for char in variable)
97
+ if bracket:
98
+ header = header.removesuffix("]")
99
+ value = '"Bearer " + key' if "authorization" in header.lower() else "key"
100
+ advice = (
101
+ "Read the key from the environment instead, and send the header only where it is set, as "
102
+ f'{holder}={{{header}: {value}}} if (key := os.environ.get("{variable}")) else None.'
103
+ )
104
+ else:
105
+ advice = f'Read the key from the environment instead, as {field}=os.environ.get("{variable}").'
106
+ raise BundleCarriesCredential(
107
+ "the source carries a provider credential, and the platform stores the source it deploys:\n"
108
+ + "\n".join(lines)
109
+ + f"\n{advice} This check and the hosted kernel run with no "
110
+ "environment, so it yields None there, and your own runs keep their key. Seal the key for "
111
+ "hosted runs with `cmpnd credentials create --name NAME --preset PROVIDER`."
112
+ )
113
+
114
+
18
115
  def _program(module: Any, entrypoint: str) -> tuple[Any, str]:
19
116
  import dspy
20
117
 
@@ -188,10 +285,11 @@ def _check(request: dict[str, Any]) -> dict[str, Any]:
188
285
  from . import lm_configuration, packaging
189
286
 
190
287
  authored_limits = _configured(request)
191
- with _staged(request) as root:
288
+ with _staged(request) as root, _built_lms(root) as built:
192
289
  manifest = json.loads((root / "manifest.json").read_text())
193
290
  module = _import_member(str(manifest["program"]))
194
291
  program, kind = _program(module, str(manifest["entrypoint"]))
292
+ _refuse_bundled_credentials(built, module.__name__)
195
293
  facts = _program_facts(program, kind, authored_limits)
196
294
  bindings = dict(getattr(module, "lms", None) or {})
197
295
  default_lm = packaging._extract_lm_object(program)
@@ -212,7 +310,7 @@ def _check_optimize(request: dict[str, Any]) -> dict[str, Any]:
212
310
 
213
311
  ask = request["optimize"]
214
312
  authored_limits = _configured(request)
215
- with _staged(request):
313
+ with _staged(request) as root, _built_lms(root) as built:
216
314
  module = _import_member(str(ask["entry"]))
217
315
  declared_lms: dict[str, Any] | None = None
218
316
  if hasattr(module, "lms"):
@@ -256,6 +354,7 @@ def _check_optimize(request: dict[str, Any]) -> dict[str, Any]:
256
354
  program, kind = _program(module, program_entrypoint)
257
355
  facts = _program_facts(program, kind, authored_limits)
258
356
  found_program = program_entrypoint
357
+ _refuse_bundled_credentials(built, module.__name__)
259
358
 
260
359
  proposer_entrypoint = str(ask["proposer_entrypoint"])
261
360
  declared_proposer = getattr(module, proposer_entrypoint, None)
@@ -331,6 +430,8 @@ def main() -> int:
331
430
  answer = _check_optimize(request) if request.get("optimize") else _check(request)
332
431
  print(json.dumps(answer))
333
432
  return 0
433
+ except BundleCarriesCredential as exc:
434
+ print(json.dumps({"error": str(exc)}))
334
435
  except ModuleNotFoundError as exc:
335
436
  print(
336
437
  json.dumps(
@@ -344,14 +445,22 @@ def main() -> int:
344
445
  )
345
446
  )
346
447
  except KeyError as exc:
448
+ from . import helpers
449
+
450
+ variable = helpers.environment_variable_missing(exc)
451
+ advice = (
452
+ helpers.environment_read_advice(variable)
453
+ if variable is not None
454
+ else 'Read a key with os.environ.get("VARIABLE"), which yields None here.'
455
+ )
347
456
  print(
348
457
  json.dumps(
349
458
  {
350
459
  "error": (
351
460
  f"authored source import failed with KeyError: {exc}. The check runs without your "
352
461
  "environment because the worker has none. Do not read provider or MCP credentials at "
353
- "module scope. Name the model without its key and use cmpnd.mcp_tools(url) for a "
354
- "registered server.\n" + _concise_traceback()
462
+ f"module scope. {advice} Use cmpnd.mcp_tools(url) for a registered server.\n"
463
+ + _concise_traceback()
355
464
  )
356
465
  }
357
466
  )
@@ -1403,9 +1403,10 @@ class _LMMixin(_CallbackBase):
1403
1403
  if parent_span and getattr(parent_span, "span_type", None) == models.SpanType.RLM:
1404
1404
  span.attributes["rlm.subcall"] = "true"
1405
1405
 
1406
- # Capture prompt/messages if enabled
1406
+ # Capture prompt/messages if enabled. A credential passed to the call itself, as in
1407
+ # `lm(prompt, api_key=...)`, is stripped first: every member of the org reads a span.
1407
1408
  if self._capture_prompts:
1408
- cleaned_inputs = helpers.clean_inputs(inputs, dspy, self._max_input_field_size)
1409
+ cleaned_inputs = helpers.clean_inputs(helpers.without_credentials(inputs), dspy, self._max_input_field_size)
1409
1410
  self._reference_rendered_messages(parent_span, inputs, cleaned_inputs)
1410
1411
  span.set_inputs(cleaned_inputs)
1411
1412