cmpnd 0.44.0__tar.gz → 0.46.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 (379) hide show
  1. {cmpnd-0.44.0 → cmpnd-0.46.0}/PKG-INFO +1 -1
  2. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/__init__.py +6 -1
  3. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_authored_import.py +2 -4
  4. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_http.py +45 -1
  5. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/callback.py +6 -1
  6. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/dataset_sync.py +2 -4
  7. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/datasets.py +2 -4
  8. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/deployment.py +20 -8
  9. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/deployment_runs.py +1 -1
  10. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/lm_configuration.py +68 -4
  11. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/mcp_servers.py +2 -3
  12. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/optimization.py +35 -21
  13. {cmpnd-0.44.0 → cmpnd-0.46.0}/docs/cli.md +10 -0
  14. {cmpnd-0.44.0 → cmpnd-0.46.0}/docs/sdk-instrumentation.md +30 -0
  15. cmpnd-0.46.0/ingest/cmpnd_ingest/client_floor.py +101 -0
  16. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/exporter_http.py +4 -3
  17. {cmpnd-0.44.0 → cmpnd-0.46.0}/pyproject.toml +1 -1
  18. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/conftest.py +13 -12
  19. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/hosted_optimize_probe.py +58 -0
  20. cmpnd-0.46.0/tests/test_client_floor.py +181 -0
  21. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_dataset_sync.py +5 -5
  22. cmpnd-0.46.0/tests/test_decision_client_spans.py +144 -0
  23. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_deploy.py +48 -4
  24. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_optimize.py +93 -5
  25. cmpnd-0.46.0/tests/test_system_one.py +106 -0
  26. cmpnd-0.46.0/tests/test_upload_auth_headers.py +59 -0
  27. {cmpnd-0.44.0 → cmpnd-0.46.0}/uv.lock +1 -1
  28. {cmpnd-0.44.0 → cmpnd-0.46.0}/.gitignore +0 -0
  29. {cmpnd-0.44.0 → cmpnd-0.46.0}/AGENTS.md +0 -0
  30. {cmpnd-0.44.0 → cmpnd-0.46.0}/CLAUDE.md +0 -0
  31. {cmpnd-0.44.0 → cmpnd-0.46.0}/README.md +0 -0
  32. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_assessments.py +0 -0
  33. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_deployments.py +0 -0
  34. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_exporter_pool.py +0 -0
  35. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_gepa_patch.py +0 -0
  36. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_program_patch.py +0 -0
  37. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_rlm_patch.py +0 -0
  38. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_runtime_tools.py +0 -0
  39. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_timespec.py +0 -0
  40. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_traces.py +0 -0
  41. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/_valsets.py +0 -0
  42. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/authored.py +0 -0
  43. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/__init__.py +0 -0
  44. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/api_client.py +0 -0
  45. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/__init__.py +0 -0
  46. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/admin.py +0 -0
  47. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/assessments.py +0 -0
  48. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/auth.py +0 -0
  49. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/datasets.py +0 -0
  50. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/deployments.py +0 -0
  51. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/evals.py +0 -0
  52. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/login.py +0 -0
  53. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/optimizations.py +0 -0
  54. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/org.py +0 -0
  55. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/programs.py +0 -0
  56. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  57. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/signatures.py +0 -0
  58. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  59. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/top.py +0 -0
  60. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/traces.py +0 -0
  61. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/commands/valsets.py +0 -0
  62. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/credentials.py +0 -0
  63. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/deprecation.py +0 -0
  64. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/pagination.py +0 -0
  65. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/shell.py +0 -0
  66. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/trace_render.py +0 -0
  67. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/tree_help.py +0 -0
  68. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/cli/verbs.py +0 -0
  69. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/client.py +0 -0
  70. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/configuration.py +0 -0
  71. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/context.py +0 -0
  72. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/decorators.py +0 -0
  73. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/deployment_metadata.py +0 -0
  74. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/eval_handler.py +0 -0
  75. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/execution.py +0 -0
  76. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/exporter.py +0 -0
  77. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/exporter_http.py +0 -0
  78. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/helpers.py +0 -0
  79. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/identity.py +0 -0
  80. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/imaging.py +0 -0
  81. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/ir/__init__.py +0 -0
  82. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/ir/decode.py +0 -0
  83. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/ir/encode.py +0 -0
  84. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/ir/hash.py +0 -0
  85. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/ir/program.py +0 -0
  86. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/ir/reflection.py +0 -0
  87. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/ir/types.py +0 -0
  88. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/ir/xxh64.py +0 -0
  89. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/models.py +0 -0
  90. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/named_programs.py +0 -0
  91. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/optimizers/__init__.py +0 -0
  92. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/optimizers/gepa.py +0 -0
  93. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  94. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/optimizers/resume.py +0 -0
  95. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/optimizers/tracker.py +0 -0
  96. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/packaging.py +0 -0
  97. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/programs.py +0 -0
  98. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/references.py +0 -0
  99. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/retry.py +0 -0
  100. {cmpnd-0.44.0 → cmpnd-0.46.0}/cmpnd/traces.py +0 -0
  101. {cmpnd-0.44.0 → cmpnd-0.46.0}/docs/2026-04-02-code-review.md +0 -0
  102. {cmpnd-0.44.0 → cmpnd-0.46.0}/docs/client.md +0 -0
  103. {cmpnd-0.44.0 → cmpnd-0.46.0}/docs/custom-proposer.md +0 -0
  104. {cmpnd-0.44.0 → cmpnd-0.46.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  105. {cmpnd-0.44.0 → cmpnd-0.46.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  106. {cmpnd-0.44.0 → cmpnd-0.46.0}/docs/reading-traces.md +0 -0
  107. {cmpnd-0.44.0 → cmpnd-0.46.0}/examples/generalizing_proposer.py +0 -0
  108. {cmpnd-0.44.0 → cmpnd-0.46.0}/examples/local_ollama.py +0 -0
  109. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/LINEAGE.md +0 -0
  110. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/README.md +0 -0
  111. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  112. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  113. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  114. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  115. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/py.typed +0 -0
  116. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  117. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/retry.py +0 -0
  118. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  119. {cmpnd-0.44.0 → cmpnd-0.46.0}/ingest/pyproject.toml +0 -0
  120. {cmpnd-0.44.0 → cmpnd-0.46.0}/scripts/audit-sdist.sh +0 -0
  121. {cmpnd-0.44.0 → cmpnd-0.46.0}/scripts/capture_review_run.py +0 -0
  122. {cmpnd-0.44.0 → cmpnd-0.46.0}/scripts/check-pip-install.sh +0 -0
  123. {cmpnd-0.44.0 → cmpnd-0.46.0}/scripts/check_release_artifacts.py +0 -0
  124. {cmpnd-0.44.0 → cmpnd-0.46.0}/scripts/fixtures/review_run.json.gz +0 -0
  125. {cmpnd-0.44.0 → cmpnd-0.46.0}/scripts/gepa_resume_bug.py +0 -0
  126. {cmpnd-0.44.0 → cmpnd-0.46.0}/scripts/release-stress.sh +0 -0
  127. {cmpnd-0.44.0 → cmpnd-0.46.0}/scripts/require_version_bump.py +0 -0
  128. {cmpnd-0.44.0 → cmpnd-0.46.0}/scripts/seed_review_run.py +0 -0
  129. {cmpnd-0.44.0 → cmpnd-0.46.0}/scripts/test-in-linux.sh +0 -0
  130. {cmpnd-0.44.0 → cmpnd-0.46.0}/stubs/dspy/__init__.pyi +0 -0
  131. {cmpnd-0.44.0 → cmpnd-0.46.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  132. {cmpnd-0.44.0 → cmpnd-0.46.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  133. {cmpnd-0.44.0 → cmpnd-0.46.0}/stubs/dspy/utils/__init__.pyi +0 -0
  134. {cmpnd-0.44.0 → cmpnd-0.46.0}/stubs/dspy/utils/callback.pyi +0 -0
  135. {cmpnd-0.44.0 → cmpnd-0.46.0}/stubs/dspy/utils/dummies.pyi +0 -0
  136. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/__init__.py +0 -0
  137. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/_deployment_signature_helpers.py +0 -0
  138. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/_exporter_helpers.py +0 -0
  139. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/_fake_clock.py +0 -0
  140. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/_identity_helpers.py +0 -0
  141. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/_resolve_refs.py +0 -0
  142. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/_traced_run.py +0 -0
  143. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/conftest.py +0 -0
  144. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/__init__.py +0 -0
  145. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/committee_task.py +0 -0
  146. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/conftest.py +0 -0
  147. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  148. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  149. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/fake_lm.py +0 -0
  150. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/helpers.py +0 -0
  151. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/target_capabilities.py +0 -0
  152. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_assessments_cli.py +0 -0
  153. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_capture_prompts.py +0 -0
  154. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_composite_module.py +0 -0
  155. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  156. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_deploy_execute.py +0 -0
  157. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_deploy_pandas.py +0 -0
  158. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_deploy_programs.py +0 -0
  159. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_edge_cases.py +0 -0
  160. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_example_hash.py +0 -0
  161. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_gepa_optimization.py +0 -0
  162. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_interpreter_spans.py +0 -0
  163. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_lm_accuracy.py +0 -0
  164. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_module_tracing.py +0 -0
  165. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_optimization_lineage.py +0 -0
  166. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  167. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_optimize_failures.py +0 -0
  168. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_optimize_loop.py +0 -0
  169. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  170. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  171. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_parallel_export.py +0 -0
  172. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_project_attribution.py +0 -0
  173. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_react_tool_spans.py +0 -0
  174. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_save_load.py +0 -0
  175. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_trace_structure.py +0 -0
  176. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_traces_read.py +0 -0
  177. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_user_attribution.py +0 -0
  178. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/e2e/test_valsets_cli.py +0 -0
  179. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  180. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  181. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  182. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  183. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  184. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  185. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  186. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  187. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  188. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/10_react.json +0 -0
  189. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/11_refine.json +0 -0
  190. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/12_retrieve.json +0 -0
  191. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/13_knn.json +0 -0
  192. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/14_parallel.json +0 -0
  193. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/identity_vectors/README.md +0 -0
  194. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/ingest_conformance.py +0 -0
  195. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/__init__.py +0 -0
  196. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/authored_optimize/judge.py +0 -0
  197. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  198. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  199. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/optimization_run_probe.py +0 -0
  200. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  201. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/test_logs_renders_traces.py +0 -0
  202. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/test_optimization_run.py +0 -0
  203. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/test_optimizing_progress.py +0 -0
  204. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/integration/test_unhappy_path.py +0 -0
  205. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/load/__init__.py +0 -0
  206. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/load/__main__.py +0 -0
  207. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/load/drain.py +0 -0
  208. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/load/ingest.py +0 -0
  209. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/load/latency_transport.py +0 -0
  210. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/load/rlm_shape.py +0 -0
  211. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/load/rlm_size_report.py +0 -0
  212. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/load/sweep.py +0 -0
  213. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/load/test_rlm_shape.py +0 -0
  214. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/load/test_rlm_size_report.py +0 -0
  215. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/__init__.py +0 -0
  216. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/conftest.py +0 -0
  217. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/seed_parity.py +0 -0
  218. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_auth.py +0 -0
  219. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_datasets.py +0 -0
  220. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_deployments.py +0 -0
  221. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_evals.py +0 -0
  222. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_health.py +0 -0
  223. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_modules.py +0 -0
  224. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_optimizations.py +0 -0
  225. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_programs.py +0 -0
  226. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_signatures.py +0 -0
  227. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_trace_streaming.py +0 -0
  228. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/parity/test_traces.py +0 -0
  229. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/span_reference_fixture.py +0 -0
  230. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/__init__.py +0 -0
  231. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/_provider.py +0 -0
  232. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/_readiness.py +0 -0
  233. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/_reliability.py +0 -0
  234. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/_result.py +0 -0
  235. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/mint_bedrock_token.py +0 -0
  236. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_deploy_programs.py +0 -0
  237. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_multi_client_stress.py +0 -0
  238. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_optimize_client_committee.py +0 -0
  239. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_optimize_loop.py +0 -0
  240. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  241. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_optimize_stress.py +0 -0
  242. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_provider_roles.py +0 -0
  243. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_readiness.py +0 -0
  244. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_real_provider_twins.py +0 -0
  245. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_reliability.py +0 -0
  246. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_reliability_classify.py +0 -0
  247. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_rlm_wasm.py +0 -0
  248. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/stress/test_trace_search.py +0 -0
  249. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_adapter_reentrancy.py +0 -0
  250. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_assessments_api.py +0 -0
  251. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_authored.py +0 -0
  252. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_authored_bundle_collisions.py +0 -0
  253. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_authored_metric_contract.py +0 -0
  254. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_authored_named_models.py +0 -0
  255. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_authored_optimize.py +0 -0
  256. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_callback.py +0 -0
  257. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_candidate_lineage.py +0 -0
  258. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/__init__.py +0 -0
  259. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/conftest.py +0 -0
  260. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_admin.py +0 -0
  261. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_api_client.py +0 -0
  262. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_assessments.py +0 -0
  263. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_auth.py +0 -0
  264. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_credentials.py +0 -0
  265. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_datasets.py +0 -0
  266. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_deployment_runs.py +0 -0
  267. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_deployments.py +0 -0
  268. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_evals.py +0 -0
  269. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_login.py +0 -0
  270. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_main.py +0 -0
  271. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_optimizations.py +0 -0
  272. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_org.py +0 -0
  273. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_pagination.py +0 -0
  274. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_parity.py +0 -0
  275. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_programs.py +0 -0
  276. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_provider_credentials.py +0 -0
  277. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_schema_sample.py +0 -0
  278. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_shell.py +0 -0
  279. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_signatures.py +0 -0
  280. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_tool_servers.py +0 -0
  281. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_top.py +0 -0
  282. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_traces.py +0 -0
  283. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  284. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_tree_help.py +0 -0
  285. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cli/test_valsets.py +0 -0
  286. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_client.py +0 -0
  287. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_client_program_names.py +0 -0
  288. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_clustering/__init__.py +0 -0
  289. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_clustering/conftest.py +0 -0
  290. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_clustering/test_edge_cases.py +0 -0
  291. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  292. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_clustering/test_type_conversion.py +0 -0
  293. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_cmp551_grouping_identity.py +0 -0
  294. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_configuration.py +0 -0
  295. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_context.py +0 -0
  296. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_context_propagation.py +0 -0
  297. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_dataset_polling.py +0 -0
  298. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_datasets.py +0 -0
  299. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_decorators.py +0 -0
  300. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  301. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_deploy_program_id.py +0 -0
  302. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_deployment_metadata.py +0 -0
  303. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_deployment_runs.py +0 -0
  304. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_deployments_read.py +0 -0
  305. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_eval.py +0 -0
  306. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_eval_example_attribution.py +0 -0
  307. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_eval_start.py +0 -0
  308. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_eval_trace_grouping.py +0 -0
  309. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_exception_paths.py +0 -0
  310. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_execute.py +0 -0
  311. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_execute_run_identity.py +0 -0
  312. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_exporter.py +0 -0
  313. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_exporter_program_names.py +0 -0
  314. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_exporter_queue.py +0 -0
  315. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_exporter_real.py +0 -0
  316. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_fake_lm.py +0 -0
  317. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_gepa_callback.py +0 -0
  318. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_gepa_integration.py +0 -0
  319. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_gepa_output_normalization.py +0 -0
  320. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_gepa_resume.py +0 -0
  321. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_identity.py +0 -0
  322. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_imaging.py +0 -0
  323. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_import_weight.py +0 -0
  324. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_ingest_conformance.py +0 -0
  325. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_ingest_second_producer.py +0 -0
  326. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_integration.py +0 -0
  327. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_interpreter_callback.py +0 -0
  328. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_ir_encoder_properties.py +0 -0
  329. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_ir_golden_vectors.py +0 -0
  330. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_ir_roundtrip.py +0 -0
  331. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_lm15_native_calls.py +0 -0
  332. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_lm_configuration.py +0 -0
  333. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_lm_usage_shapes.py +0 -0
  334. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_mcp_tools.py +0 -0
  335. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_models.py +0 -0
  336. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_multi_client_stress_readiness.py +0 -0
  337. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_optimization_progress.py +0 -0
  338. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_optimization_start.py +0 -0
  339. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_optimize_budget.py +0 -0
  340. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_optimize_lift.py +0 -0
  341. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_optimize_lms.py +0 -0
  342. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_optimize_proposer.py +0 -0
  343. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_optimize_reflection_lm.py +0 -0
  344. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_optimize_terminal_failure.py +0 -0
  345. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_packaging.py +0 -0
  346. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_packaging_callable_source.py +0 -0
  347. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_packaging_mcp_tools.py +0 -0
  348. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_packaging_program_source.py +0 -0
  349. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_poll_until.py +0 -0
  350. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_program_identity.py +0 -0
  351. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_program_lineage_persist.py +0 -0
  352. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_programs_create.py +0 -0
  353. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_project_payload.py +0 -0
  354. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_real_provider_twins_readiness.py +0 -0
  355. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_reflector_properties.py +0 -0
  356. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_reflector_smoke.py +0 -0
  357. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_release_stress_script.py +0 -0
  358. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_repro_reactv2_bugs.py +0 -0
  359. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_require_version_bump.py +0 -0
  360. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_retry.py +0 -0
  361. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_reversible_failures.py +0 -0
  362. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_rlm_patch.py +0 -0
  363. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_run_attribution_paths.py +0 -0
  364. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_runtime_tools.py +0 -0
  365. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_schemas.py +0 -0
  366. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_seed_review_run.py +0 -0
  367. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_signature_inputs.py +0 -0
  368. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_span_references.py +0 -0
  369. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_span_sent_once.py +0 -0
  370. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_trace_context_concurrency.py +0 -0
  371. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_trace_fact_wire_contract.py +0 -0
  372. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_trace_facts.py +0 -0
  373. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_trace_render.py +0 -0
  374. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_traces_read.py +0 -0
  375. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_tracked_gepa.py +0 -0
  376. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_tracker_enrich.py +0 -0
  377. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_truncation.py +0 -0
  378. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_valsets_api.py +0 -0
  379. {cmpnd-0.44.0 → cmpnd-0.46.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.44.0
3
+ Version: 0.46.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>
@@ -15,6 +15,8 @@ Usage:
15
15
  import importlib.metadata
16
16
  from typing import TYPE_CHECKING
17
17
 
18
+ from cmpnd_ingest.client_floor import UpgradeRequiredWarning
19
+
18
20
  from . import authored, programs, traces
19
21
  from ._http import ApiError
20
22
  from .callback import CmpndCallback, trace_id
@@ -46,7 +48,7 @@ from .deployment_runs import (
46
48
  )
47
49
  from .execution import DeployedProgram, RetryableProviderError, deployment_run_id, execute
48
50
  from .exporter import flush_exporter, shutdown_exporter
49
- from .lm_configuration import configure_lm
51
+ from .lm_configuration import SystemOne, configure_lm
50
52
  from .mcp_servers import (
51
53
  Bearer,
52
54
  McpToolReference,
@@ -80,6 +82,7 @@ __all__ = [
80
82
  # Configuration
81
83
  "configure",
82
84
  "configure_lm",
85
+ "SystemOne",
83
86
  "get_config",
84
87
  "require_config",
85
88
  # Main callback
@@ -110,6 +113,8 @@ __all__ = [
110
113
  "program",
111
114
  "authored",
112
115
  "ApiError",
116
+ # Raised once when the server no longer supports this release
117
+ "UpgradeRequiredWarning",
113
118
  # Types
114
119
  "SpanType",
115
120
  "TraceStatus",
@@ -118,16 +118,14 @@ def _program_facts(
118
118
  ) -> dict[str, Any]:
119
119
  """Everything the deployment create call reads off the imported program."""
120
120
 
121
- from . import configuration, deployment, identity, mcp_servers, packaging
121
+ from . import _http, configuration, deployment, identity, mcp_servers, packaging
122
122
  from .authored import ProgramFacts
123
123
 
124
124
  references = deployment._mcp_tool_references(program)
125
125
  pinned: dict[tuple[str, str], Any] = {}
126
126
  if references:
127
- import httpx
128
-
129
127
  config = configuration.require_config()
130
- with httpx.Client(timeout=30) as client:
128
+ with _http.new_client(timeout=30) as client:
131
129
  pinned = deployment._resolve_mcp_tool_references(client, config, references)
132
130
  tool_references = [
133
131
  {
@@ -13,8 +13,10 @@ from __future__ import annotations
13
13
  import os
14
14
  import re
15
15
  from typing import Any
16
+ from urllib.parse import urlsplit
16
17
 
17
18
  import httpx
19
+ from cmpnd_ingest import client_floor
18
20
 
19
21
 
20
22
  class ApiError(Exception):
@@ -76,6 +78,48 @@ def server_detail(response: httpx.Response) -> str:
76
78
  return (response.text or "").strip()[:2000]
77
79
 
78
80
 
81
+ def new_client(**kwargs: Any) -> httpx.Client:
82
+ """An ``httpx.Client`` that warns when the server no longer supports this
83
+ release (:mod:`cmpnd_ingest.client_floor`).
84
+
85
+ Every client the SDK builds comes from here, so every answer the platform
86
+ sends gets the check. A caller's own ``event_hooks`` run after it.
87
+ """
88
+ hooks = client_floor.event_hooks()
89
+ for event, callers in (kwargs.pop("event_hooks", None) or {}).items():
90
+ hooks.setdefault(event, []).extend(callers)
91
+ return httpx.Client(event_hooks=hooks, **kwargs)
92
+
93
+
94
+ _DEFAULT_PORTS = {"http": 80, "https": 443}
95
+
96
+
97
+ def _origin(url: str) -> tuple[str, str, int] | None:
98
+ """A URL's (scheme, host, port), or None when it cannot be read as one."""
99
+ try:
100
+ parts = urlsplit(url)
101
+ port = parts.port
102
+ except ValueError:
103
+ return None
104
+ scheme = parts.scheme.lower()
105
+ if scheme not in _DEFAULT_PORTS or not parts.hostname:
106
+ return None
107
+ return scheme, parts.hostname.lower(), port if port is not None else _DEFAULT_PORTS[scheme]
108
+
109
+
110
+ def upload_auth_headers(upload_url: str, endpoint: str, api_key: str) -> dict[str, str]:
111
+ """The authentication header for a PUT to an ``upload_url`` serve advertised.
112
+
113
+ Serve authenticates its upload routes with the API key. The SDK sends the URL
114
+ as given, so the key goes only to a URL on the configured endpoint's origin:
115
+ the same scheme, host and port. Any other origin gets no key.
116
+ """
117
+ own = _origin(endpoint)
118
+ if not api_key or own is None or _origin(upload_url) != own:
119
+ return {}
120
+ return {"X-API-Key": api_key}
121
+
122
+
79
123
  class ApiClient:
80
124
  """Thin HTTP client for the cmpnd backend API."""
81
125
 
@@ -150,7 +194,7 @@ class ApiClient:
150
194
  url = f"{self._endpoint}{path}"
151
195
  timeout = self.configured_timeout()
152
196
  try:
153
- with httpx.Client(
197
+ with new_client(
154
198
  headers={"X-API-Key": self._api_key, "Content-Type": "application/json"},
155
199
  timeout=timeout,
156
200
  ) as client:
@@ -383,6 +383,10 @@ def _lm15_cache_hit(entry: Any, response: Any) -> bool | None:
383
383
 
384
384
  def _usage_dialect(instance: Any, usage: Any) -> str | None:
385
385
  """Name the response surface only when its shape establishes one."""
386
+ # A decision client (DSPy's `TypeSafe`) speaks System One. Its usage names chat's token
387
+ # fields, so the shape alone would misname it. DSPy dispatches on the same flag.
388
+ if getattr(instance, "supports_decision_requests", False) is True:
389
+ return "system_one"
386
390
  model_type = _text_field(instance, "model_type")
387
391
  if model_type == "responses":
388
392
  return "responses"
@@ -1489,7 +1493,8 @@ class _LMMixin(_CallbackBase):
1489
1493
  observation.usage_dialect = _usage_dialect(instance, usage)
1490
1494
  if observation.usage_dialect in {"chat_completions", "responses"}:
1491
1495
  observation.prompt_inclusion = "includes_cache_reads"
1492
- elif observation.usage_dialect == "converse":
1496
+ elif observation.usage_dialect in {"converse", "system_one"}:
1497
+ # System One serves nothing from a cache, so its prompt is all fresh input.
1493
1498
  observation.prompt_inclusion = "excludes_cache_reads"
1494
1499
 
1495
1500
  if usage_reported and usage is not None:
@@ -10,9 +10,7 @@ from __future__ import annotations
10
10
  import logging
11
11
  from typing import Any
12
12
 
13
- import httpx
14
-
15
- from . import configuration, identity, models
13
+ from . import _http, configuration, identity, models
16
14
 
17
15
  logger = logging.getLogger(__name__)
18
16
 
@@ -37,7 +35,7 @@ def check_dataset_exists(dataset_hash: str) -> bool:
37
35
  url = f"{config.endpoint}/api/v1/datasets/check/{dataset_hash}"
38
36
 
39
37
  try:
40
- with httpx.Client(timeout=5.0) as client:
38
+ with _http.new_client(timeout=5.0) as client:
41
39
  response = client.get(
42
40
  url,
43
41
  headers={"Authorization": f"Bearer {config.api_key}"},
@@ -8,9 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from typing import Any
10
10
 
11
- import httpx
12
-
13
- from . import configuration
11
+ from . import _http, configuration
14
12
 
15
13
 
16
14
  class DatasetNotFoundError(Exception):
@@ -114,7 +112,7 @@ def _fetch_dataset(
114
112
  params["version"] = str(version)
115
113
 
116
114
  # Make request to backend
117
- with httpx.Client(timeout=30.0) as client:
115
+ with _http.new_client(timeout=30.0) as client:
118
116
  response = client.get(
119
117
  url,
120
118
  params=params,
@@ -17,7 +17,18 @@ from typing import Any
17
17
 
18
18
  import httpx
19
19
 
20
- from . import authored, configuration, deployment_metadata, execution, identity, mcp_servers, models, packaging, retry
20
+ from . import (
21
+ _http,
22
+ authored,
23
+ configuration,
24
+ deployment_metadata,
25
+ execution,
26
+ identity,
27
+ mcp_servers,
28
+ models,
29
+ packaging,
30
+ retry,
31
+ )
21
32
 
22
33
  logger = logging.getLogger(__name__)
23
34
 
@@ -427,7 +438,7 @@ def deploy(module, metric=None, timeout=300, metadata=None, *, program_id=None):
427
438
  references = _mcp_tool_references(module)
428
439
  pinned_mcp_tools: dict[tuple[str, str], Any] = {}
429
440
  if references:
430
- with httpx.Client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
441
+ with _http.new_client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
431
442
  pinned_mcp_tools = _resolve_mcp_tool_references(client, config, references)
432
443
 
433
444
  # Package the module
@@ -539,7 +550,7 @@ def _deploy_prepared(
539
550
  """Create, upload, and poll one fully prepared source bundle."""
540
551
  headers = {"X-API-Key": config.api_key, "Content-Type": "application/json"}
541
552
 
542
- with httpx.Client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
553
+ with _http.new_client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
543
554
  created = _send_retrying(
544
555
  lambda: client.post(
545
556
  f"{config.endpoint}/api/v1/deployments",
@@ -568,12 +579,13 @@ def _deploy_prepared(
568
579
  )
569
580
  for warning in answer.get("warnings") or []:
570
581
  logger.warning("deploy: %s", warning)
582
+ upload_url = answer["upload_url"]
583
+ upload_headers = {
584
+ "Content-Type": "application/zip",
585
+ **_http.upload_auth_headers(upload_url, config.endpoint, config.api_key),
586
+ }
571
587
  uploaded = _send_retrying(
572
- lambda: client.put(
573
- answer["upload_url"],
574
- content=zip_bytes,
575
- headers={"Content-Type": "application/zip"},
576
- ),
588
+ lambda: client.put(upload_url, content=zip_bytes, headers=upload_headers),
577
589
  "upload deployment ZIP",
578
590
  may_resend_with_no_answer=lambda _: True,
579
591
  may_resend_after_answer=_repeatable_answer,
@@ -327,7 +327,7 @@ class _RunClient:
327
327
  self.retry_after = 0.0
328
328
 
329
329
  def __enter__(self) -> _RunClient:
330
- self.client = httpx.Client(headers={"X-API-Key": self.api_key})
330
+ self.client = _http.new_client(headers={"X-API-Key": self.api_key})
331
331
  return self
332
332
 
333
333
  def __exit__(self, *_: object) -> None:
@@ -9,11 +9,25 @@ class LMConfigurationError(ValueError):
9
9
  """A hosted binding declaration cannot be resolved consistently."""
10
10
 
11
11
 
12
- class LMCapabilities(TypedDict):
13
- """Explicit adapter hints for a hosted model, independent of its provider."""
12
+ class LMCapabilities(TypedDict, total=False):
13
+ """Explicit adapter hints for a hosted model, independent of its provider.
14
+
15
+ ``dialect`` is present only for a System One binding, which :class:`SystemOne`
16
+ writes. Absent, the binding is a chat model.
17
+ """
14
18
 
15
19
  supported_params: list[str]
16
20
  supports_response_schema: bool
21
+ dialect: str
22
+
23
+
24
+ # The wire protocol a binding's model speaks, named for the protocol and never for a
25
+ # provider or a client library: a chat model answers chat completions, and a System One
26
+ # model (TypeSafe's System One protocol, which other providers serve too) answers a set
27
+ # of questions about a state.
28
+ CHAT_COMPLETIONS = "chat_completions"
29
+ SYSTEM_ONE = "system_one"
30
+ _DIALECTS = frozenset({CHAT_COMPLETIONS, SYSTEM_ONE})
17
31
 
18
32
 
19
33
  def _validate_lm_capabilities(value: object) -> dict[str, LMCapabilities]:
@@ -27,7 +41,7 @@ def _validate_lm_capabilities(value: object) -> dict[str, LMCapabilities]:
27
41
  )
28
42
  if not isinstance(declaration, dict):
29
43
  raise LMConfigurationError(f"lm_capabilities[{binding!r}] must be a capability declaration")
30
- unknown = declaration.keys() - {"supported_params", "supports_response_schema"}
44
+ unknown = declaration.keys() - {"supported_params", "supports_response_schema", "dialect"}
31
45
  if unknown:
32
46
  raise LMConfigurationError(f"lm_capabilities[{binding!r}] has unknown fields: {sorted(unknown, key=str)}")
33
47
  params = declaration.get("supported_params", [])
@@ -38,11 +52,24 @@ def _validate_lm_capabilities(value: object) -> dict[str, LMCapabilities]:
38
52
  schema = declaration.get("supports_response_schema", False)
39
53
  if not isinstance(schema, bool):
40
54
  raise LMConfigurationError(f"lm_capabilities[{binding!r}].supports_response_schema must be a boolean")
55
+ dialect = declaration.get("dialect", CHAT_COMPLETIONS)
56
+ if dialect not in _DIALECTS:
57
+ raise LMConfigurationError(
58
+ f"lm_capabilities[{binding!r}].dialect must be one of {sorted(_DIALECTS)} (got {dialect!r})"
59
+ )
60
+ if dialect == SYSTEM_ONE and (params or schema):
61
+ raise LMConfigurationError(
62
+ f"lm_capabilities[{binding!r}] is a System One binding, which takes no sampling "
63
+ "parameters and no response schema"
64
+ )
41
65
  if schema and "response_format" not in params:
42
66
  raise LMConfigurationError(
43
67
  f"lm_capabilities[{binding!r}] requires response_format in supported_params for schema output"
44
68
  )
45
- result[binding] = {"supported_params": list(dict.fromkeys(params)), "supports_response_schema": schema}
69
+ hints: LMCapabilities = {"supported_params": list(dict.fromkeys(params)), "supports_response_schema": schema}
70
+ if dialect == SYSTEM_ONE:
71
+ hints["dialect"] = SYSTEM_ONE
72
+ result[binding] = hints
46
73
  return result
47
74
 
48
75
 
@@ -122,6 +149,11 @@ def configure_lm(
122
149
  )
123
150
  _merge(_names, _name(lm), hints)
124
151
  return
152
+ _declare(lm, hints)
153
+
154
+
155
+ def _declare(lm: Any, hints: dict[str, Any]) -> None:
156
+ """Attach a declaration to one LM object, shared by every binding made from it."""
125
157
  model = _model(lm)
126
158
  declared = _object_declaration(lm)
127
159
  if declared is not None and declared != (model, hints):
@@ -131,6 +163,38 @@ def configure_lm(
131
163
  _objects.append(lm)
132
164
 
133
165
 
166
+ class SystemOne:
167
+ """A System One decision model, for a run's ``lms``.
168
+
169
+ A System One model answers a set of questions about a state instead of continuing a
170
+ conversation: TypeSafe's Jev, or another endpoint serving the same schema. A hosted
171
+ judge built on ``dspy.experimental.TypeSafe`` runs against the run's System One
172
+ binding without naming a key or a URL::
173
+
174
+ cmpnd.optimize(..., lms={"judge": cmpnd.SystemOne("typesafe/jev-latest")})
175
+
176
+ ``model`` is a platform model id: the prefix names the organization's sealed
177
+ credential, and the rest is the model at that credential's endpoint. Writing the
178
+ binding this way is the only thing that makes it a System One binding; a model name
179
+ on its own never does.
180
+ """
181
+
182
+ def __init__(self, model: object):
183
+ if not isinstance(model, str) or not model.strip():
184
+ raise ValueError(f"cmpnd.SystemOne needs a model id such as 'typesafe/jev-latest' (got {model!r})")
185
+ credential, _, name = model.strip().partition("/")
186
+ if not credential or not name:
187
+ raise ValueError(
188
+ f"cmpnd.SystemOne({model!r}) names no credential: write the model as "
189
+ "'<credential>/<model>', for example 'typesafe/jev-latest'"
190
+ )
191
+ self.model = model.strip()
192
+ _declare(self, {"supported_params": [], "supports_response_schema": False, "dialect": SYSTEM_ONE})
193
+
194
+ def __repr__(self) -> str:
195
+ return f"cmpnd.SystemOne({self.model!r})"
196
+
197
+
134
198
  def restore(declarations: object) -> None:
135
199
  """Load model-pinned declarations from a packaged artifact."""
136
200
  if not isinstance(declarations, dict):
@@ -669,15 +669,14 @@ def _registered_tools(url: str, timeout_s: float) -> list[PinnedTool] | None | o
669
669
  the registry route, so notebook use keeps its existing direct connection.
670
670
  ``None`` means the configured registry answered and does not know this server.
671
671
  """
672
- from . import configuration
672
+ from . import _http, configuration
673
673
 
674
674
  config = configuration.get_config()
675
675
  if config is None:
676
676
  return _NO_PLATFORM_REGISTRY
677
- import httpx
678
677
 
679
678
  headers = {"X-API-Key": config.api_key}
680
- with httpx.Client(timeout=timeout_s) as client:
679
+ with _http.new_client(timeout=timeout_s) as client:
681
680
  listing = client.get(f"{config.endpoint}/api/v1/org/tool-servers", headers=headers)
682
681
  if listing.status_code == 404:
683
682
  return _NO_PLATFORM_REGISTRY
@@ -462,6 +462,23 @@ def _lms_config(config: dict[str, Any], lms: Any) -> None:
462
462
  if isinstance(lm, str):
463
463
  declared[name] = {"model": lm}
464
464
  continue
465
+ from .lm_configuration import SystemOne
466
+
467
+ if isinstance(lm, SystemOne):
468
+ # A System One model takes no sampling, and its declaration (which says it
469
+ # is one) travels in the bundle rather than on the wire.
470
+ declared[name] = {"model": lm.model}
471
+ continue
472
+ if getattr(lm, "supports_decision_requests", False):
473
+ # dspy.experimental.TypeSafe, or a client like it: it carries a key and a URL
474
+ # rather than the name of a sealed credential, so the platform has nothing to
475
+ # resolve. Duck-typed so this module never imports dspy at scope.
476
+ raise ValueError(
477
+ f"{subject} is a {type(lm).__name__} client, which names a key and a URL rather "
478
+ f"than a credential. Bind the model with cmpnd.SystemOne, for example "
479
+ f"lms={{{name!r}: cmpnd.SystemOne('typesafe/jev-latest')}}; a judge that builds "
480
+ f"its own {type(lm).__name__} runs against that binding when hosted"
481
+ )
465
482
  model = _lm_model(lm, subject)
466
483
  _refuse_credential_on(lm, model, subject)
467
484
  entry: dict[str, Any] = {"model": model}
@@ -991,7 +1008,7 @@ def _run_steps(
991
1008
  current_deployment_id = deployment_id
992
1009
  non_improving = 0
993
1010
 
994
- with httpx.Client(timeout=_HTTP_TIMEOUT_SECONDS) as client:
1011
+ with _http.new_client(timeout=_HTTP_TIMEOUT_SECONDS) as client:
995
1012
  for step in range(max_steps):
996
1013
  data = "upload" if step == 0 else "reuse"
997
1014
  step_config = dict(config)
@@ -1014,7 +1031,7 @@ def _run_steps(
1014
1031
  optimization_id = initiated["optimization_id"]
1015
1032
 
1016
1033
  if data == "upload":
1017
- _upload_bundle(client, initiated, bundle)
1034
+ _upload_bundle(client, initiated, bundle, endpoint=cfg.endpoint, api_key=cfg.api_key)
1018
1035
 
1019
1036
  status = _poll_until_terminal(
1020
1037
  client,
@@ -1226,7 +1243,7 @@ def _refuse_conflicting_members(
1226
1243
  headers = {"X-API-Key": cfg.api_key}
1227
1244
  conflicting: list[str] = []
1228
1245
  try:
1229
- with httpx.Client(timeout=_HTTP_TIMEOUT_SECONDS) as client:
1246
+ with _http.new_client(timeout=_HTTP_TIMEOUT_SECONDS) as client:
1230
1247
  listing = client.get(f"{cfg.endpoint}/api/v1/deployments/{deployment_id}/source", headers=headers)
1231
1248
  if not listing.is_success:
1232
1249
  return
@@ -1397,26 +1414,21 @@ def _initiate_step(
1397
1414
  )
1398
1415
 
1399
1416
 
1400
- def _upload_bundle(client: httpx.Client, initiated: dict[str, Any], bundle: bytes) -> None:
1417
+ def _upload_bundle(
1418
+ client: httpx.Client, initiated: dict[str, Any], bundle: bytes, *, endpoint: str, api_key: str
1419
+ ) -> None:
1401
1420
  """Upload the optimization bundle to the slot the initiate step returned.
1402
1421
 
1403
- Two postures, one path — keyed on ``upload_fields`` exactly as the deploy
1404
- upload is:
1405
-
1406
- - **Presigned POST** (the deploy Lambda stack): ``upload_fields`` is a
1407
- non-empty form the destination key is pinned by; the bundle rides as the
1408
- ``file`` part of a multipart POST.
1409
- - **PUT** (the on-prem worker-sandbox): no form fields — the URL *is* the
1410
- slot. The bundle is the raw PUT body.
1411
-
1412
- serve normalizes the awaiting_data response so both keys are always present
1413
- (``upload_method`` defaulting to PUT, ``upload_fields`` to ``{}``), so the
1414
- branch here is just "are there presigned fields to post."
1422
+ Serve answers an ``awaiting_data`` initiate with ``upload_url``, ``upload_method:
1423
+ "PUT"`` and empty ``upload_fields``, and the bundle goes out as the raw PUT body.
1424
+ Serve authenticates that route with the API key, which rides the PUT only when
1425
+ ``upload_url`` is on ``endpoint``'s origin (``_http.upload_auth_headers``). A
1426
+ non-empty ``upload_fields`` is a presigned form, which no current server sends.
1427
+ The bundle then rides as the ``file`` part of a multipart POST.
1415
1428
 
1416
- Both postures write to a slot the destination key is already pinned to, so
1417
- re-sending the same bytes overwrites the same object — this call is
1418
- idempotent by construction, and rides through a transient answer *and* a
1419
- transport blip alike.
1429
+ The slot pins its destination, so re-sending the same bytes writes the same
1430
+ object. That makes this call idempotent, so it rides through a transient answer
1431
+ and a transport blip alike.
1420
1432
  """
1421
1433
  upload_url = initiated.get("upload_url")
1422
1434
  if not upload_url:
@@ -1424,6 +1436,7 @@ def _upload_bundle(client: httpx.Client, initiated: dict[str, Any], bundle: byte
1424
1436
  upload_fields = initiated.get("upload_fields")
1425
1437
  method = (initiated.get("upload_method") or ("POST" if upload_fields else "PUT")).upper()
1426
1438
  what = "upload optimization bundle"
1439
+ auth = _http.upload_auth_headers(upload_url, endpoint, api_key)
1427
1440
 
1428
1441
  if upload_fields:
1429
1442
  resp = _request(
@@ -1431,6 +1444,7 @@ def _upload_bundle(client: httpx.Client, initiated: dict[str, Any], bundle: byte
1431
1444
  upload_url,
1432
1445
  data=dict(upload_fields),
1433
1446
  files={"file": ("data.zip", bundle, "application/zip")},
1447
+ headers=auth,
1434
1448
  ),
1435
1449
  what,
1436
1450
  )
@@ -1440,7 +1454,7 @@ def _upload_bundle(client: httpx.Client, initiated: dict[str, Any], bundle: byte
1440
1454
  method,
1441
1455
  upload_url,
1442
1456
  content=bundle,
1443
- headers={"Content-Type": "application/zip"},
1457
+ headers={"Content-Type": "application/zip", **auth},
1444
1458
  ),
1445
1459
  what,
1446
1460
  )
@@ -32,6 +32,16 @@ retries, because a second send would create a second program.
32
32
  `cmpnd.programs.create(name=...)` makes the same call from Python and returns
33
33
  the row.
34
34
 
35
+ ## When the server no longer supports your release
36
+
37
+ Every answer from the platform names the oldest `cmpnd` release it supports.
38
+ When yours is older, the CLI and the SDK warn once per process and name the
39
+ upgrade, `pip install -U cmpnd`. The server still answers, so an older release
40
+ keeps working wherever nothing it relies on has changed. To stay on an older
41
+ release without the warning, set `CMPND_NO_UPGRADE_WARNING=1`. In code,
42
+ `warnings.filterwarnings("ignore", category=cmpnd.UpgradeRequiredWarning)`
43
+ does the same.
44
+
35
45
  ## Deploy flow (Docker-style)
36
46
 
37
47
  These commands print human-readable output by default. Each takes `--json` for
@@ -73,6 +73,27 @@ endpoint accept JSON schemas. Undeclared bindings keep prompt-only JSON. These
73
73
  hints do not add a format the provider cannot accept or change local DSPy's
74
74
  capability lookup.
75
75
 
76
+ ### System One judges
77
+
78
+ A System One model answers a set of questions about a state instead of continuing
79
+ a conversation. TypeSafe's Jev is one, and other endpoints serve the same schema.
80
+ Declare one for a run with `cmpnd.SystemOne`:
81
+
82
+ ```python
83
+ cmpnd.optimize(..., lms={"judge": cmpnd.SystemOne("typesafe/jev-latest")})
84
+ ```
85
+
86
+ The prefix names the organization's sealed credential and the rest names the
87
+ model at that credential's endpoint. Writing the binding this way is the only
88
+ thing that makes it a System One binding; a model name never does. The binding
89
+ carries no sampling, and its declaration travels in the bundle like any other.
90
+
91
+ Hosted, a judge built on `dspy.experimental.TypeSafe` runs unchanged. The kernel
92
+ replaces `TypeSafe` with a client that spends the run's System One binding, so
93
+ the judge's own model, key and base URL are not used. With several System One
94
+ bindings, the judge picks one through `lms[name]`. A `TypeSafe` object passed in
95
+ `lms` is refused, because it names a key and a URL rather than a credential.
96
+
76
97
  ## The callback
77
98
 
78
99
  `python-sdk/cmpnd/callback.py` implements `CmpndCallback`, which satisfies `dspy.utils.callback.BaseCallback` without inheriting it — `callback.py` must import on the standard library alone (`tests/test_import_weight.py`), so it cannot import dspy to get a base class. DSPy calls its hook methods synchronously on whatever thread is executing the module. The callback owns no long-lived state beyond a per-call span builder map; everything trace/span related flows through context vars (see next section).
@@ -285,6 +306,10 @@ Two callers implement the waiting:
285
306
  - **`exporter_http._send_json_retrying`** waits on the shutdown event rather than sleeping, so a backoff can't hold the process open at exit (and the post-shutdown drain gets no retries at all, deliberately).
286
307
  - **`optimization._request`** clips every wait by the caller's `timeout` deadline, which stays the outer bound of `optimize()`. See [the optimize loop](#the-optimize-loop-and-transient-tolerance) below.
287
308
 
309
+ ### The server says when this release is too old
310
+
311
+ Every API answer names the oldest cmpnd release the server supports, in the `X-Cmpnd-Minimum-Client-Version` header. `cmpnd_ingest/client_floor.py` reads it in an httpx response hook. When the installed `cmpnd` is older, it raises an `UpgradeRequiredWarning` once per process that names both versions and the command `pip install -U cmpnd`. The server refuses nothing for a client's version, so a call below the floor still goes through, and the warning appears beside whatever it does. Every client the SDK builds comes from `cmpnd._http.new_client`, which installs the hook, and the exporter installs it in `_client_kwargs`. `tests/test_client_floor.py` fails on an `httpx.Client` built any other way in `cmpnd/`. An install of `cmpnd_ingest` without `cmpnd` has no version to compare and says nothing. `CMPND_NO_UPGRADE_WARNING=1` silences the warning for a user who stays on an older release on purpose, and the warning names that switch. `cmpnd.UpgradeRequiredWarning` is public, so a `warnings` filter silences it in code.
312
+
288
313
  ### The optimize loop and transient tolerance
289
314
 
290
315
  `optimization._poll_until_terminal` is what a caller blocks on for the length of a run, so one transient answer or one reset connection must not destroy a healthy run. It rides through transient answers on the shared budget, resetting after each successful poll, and never extends past the caller's `timeout` (default 600s per step) — a backoff wait is clipped by the deadline rather than overshooting it.
@@ -333,6 +358,11 @@ done.
333
358
  | `PUT upload_url` (the ZIP) | retried — the slot pins the key and the bytes are fixed, so a re-send overwrites the same object | retried, for the same reason |
334
359
  | `GET /api/v1/deployments/{id}` (poll) | retried on the next tick — a read is repeatable | retried on the next tick |
335
360
 
361
+ Serve authenticates its upload routes with the API key. The deploy upload and
362
+ the optimization bundle upload send `X-API-Key` when `upload_url` has the
363
+ configured endpoint's scheme, host and port (`_http.upload_auth_headers`). A URL
364
+ on any other origin gets no key.
365
+
336
366
  After the poll reports ready, the SDK reads
337
367
  `GET /api/v1/deployments/{id}/signature`. This bounded read returns persisted
338
368
  artifact metadata and never asks a worker or analytics store. A failed metadata