cmpnd 0.33.0__tar.gz → 0.33.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. {cmpnd-0.33.0 → cmpnd-0.33.2}/PKG-INFO +1 -1
  2. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/execution.py +68 -4
  3. {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/sdk-instrumentation.md +20 -0
  4. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/README.md +18 -0
  5. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/exporter_http.py +28 -1
  6. {cmpnd-0.33.0 → cmpnd-0.33.2}/pyproject.toml +1 -1
  7. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/conftest.py +44 -15
  8. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_capture_prompts.py +25 -4
  9. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_edge_cases.py +18 -6
  10. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_parallel_export.py +33 -10
  11. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_project_attribution.py +61 -22
  12. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_save_load.py +76 -16
  13. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_trace_structure.py +83 -23
  14. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_real_provider_twins.py +57 -12
  15. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_execute.py +150 -0
  16. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_ingest_second_producer.py +53 -0
  17. cmpnd-0.33.2/tests/test_real_provider_twins_readiness.py +68 -0
  18. {cmpnd-0.33.0 → cmpnd-0.33.2}/uv.lock +1 -1
  19. {cmpnd-0.33.0 → cmpnd-0.33.2}/.gitignore +0 -0
  20. {cmpnd-0.33.0 → cmpnd-0.33.2}/AGENTS.md +0 -0
  21. {cmpnd-0.33.0 → cmpnd-0.33.2}/CLAUDE.md +0 -0
  22. {cmpnd-0.33.0 → cmpnd-0.33.2}/README.md +0 -0
  23. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/__init__.py +0 -0
  24. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_assessments.py +0 -0
  25. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_authored_import.py +0 -0
  26. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_deployments.py +0 -0
  27. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_exporter_pool.py +0 -0
  28. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_gepa_patch.py +0 -0
  29. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_http.py +0 -0
  30. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_program_patch.py +0 -0
  31. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_rlm_patch.py +0 -0
  32. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_runtime_tools.py +0 -0
  33. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_timespec.py +0 -0
  34. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_traces.py +0 -0
  35. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/_valsets.py +0 -0
  36. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/authored.py +0 -0
  37. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/callback.py +0 -0
  38. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/__init__.py +0 -0
  39. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/api_client.py +0 -0
  40. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/__init__.py +0 -0
  41. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/admin.py +0 -0
  42. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/assessments.py +0 -0
  43. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/auth.py +0 -0
  44. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/datasets.py +0 -0
  45. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/deployments.py +0 -0
  46. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/evals.py +0 -0
  47. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/login.py +0 -0
  48. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/optimizations.py +0 -0
  49. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/org.py +0 -0
  50. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/programs.py +0 -0
  51. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/provider_credentials.py +0 -0
  52. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/signatures.py +0 -0
  53. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/tool_servers.py +0 -0
  54. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/top.py +0 -0
  55. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/traces.py +0 -0
  56. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/commands/valsets.py +0 -0
  57. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/credentials.py +0 -0
  58. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/deprecation.py +0 -0
  59. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/pagination.py +0 -0
  60. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/shell.py +0 -0
  61. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/trace_render.py +0 -0
  62. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/tree_help.py +0 -0
  63. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/cli/verbs.py +0 -0
  64. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/client.py +0 -0
  65. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/configuration.py +0 -0
  66. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/context.py +0 -0
  67. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/dataset_sync.py +0 -0
  68. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/datasets.py +0 -0
  69. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/decorators.py +0 -0
  70. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/deployment.py +0 -0
  71. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/deployment_metadata.py +0 -0
  72. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/eval_handler.py +0 -0
  73. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/exporter.py +0 -0
  74. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/exporter_http.py +0 -0
  75. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/helpers.py +0 -0
  76. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/identity.py +0 -0
  77. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/imaging.py +0 -0
  78. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/__init__.py +0 -0
  79. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/decode.py +0 -0
  80. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/encode.py +0 -0
  81. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/hash.py +0 -0
  82. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/program.py +0 -0
  83. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/reflection.py +0 -0
  84. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/types.py +0 -0
  85. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/ir/xxh64.py +0 -0
  86. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/lm_configuration.py +0 -0
  87. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/mcp_servers.py +0 -0
  88. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/models.py +0 -0
  89. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimization.py +0 -0
  90. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimizers/__init__.py +0 -0
  91. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimizers/gepa.py +0 -0
  92. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimizers/gepa_callback.py +0 -0
  93. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimizers/resume.py +0 -0
  94. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/optimizers/tracker.py +0 -0
  95. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/packaging.py +0 -0
  96. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/references.py +0 -0
  97. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/retry.py +0 -0
  98. {cmpnd-0.33.0 → cmpnd-0.33.2}/cmpnd/traces.py +0 -0
  99. {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/2026-04-02-code-review.md +0 -0
  100. {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/cli.md +0 -0
  101. {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/client.md +0 -0
  102. {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/custom-proposer.md +0 -0
  103. {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  104. {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  105. {cmpnd-0.33.0 → cmpnd-0.33.2}/docs/reading-traces.md +0 -0
  106. {cmpnd-0.33.0 → cmpnd-0.33.2}/examples/generalizing_proposer.py +0 -0
  107. {cmpnd-0.33.0 → cmpnd-0.33.2}/examples/local_ollama.py +0 -0
  108. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/LINEAGE.md +0 -0
  109. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/__init__.py +0 -0
  110. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/bodies.py +0 -0
  111. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/protocols.py +0 -0
  112. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/py.typed +0 -0
  113. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/request_log.py +0 -0
  114. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/retry.py +0 -0
  115. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  116. {cmpnd-0.33.0 → cmpnd-0.33.2}/ingest/pyproject.toml +0 -0
  117. {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/audit-sdist.sh +0 -0
  118. {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/capture_review_run.py +0 -0
  119. {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/check_release_artifacts.py +0 -0
  120. {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/fixtures/review_run.json.gz +0 -0
  121. {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/gepa_resume_bug.py +0 -0
  122. {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/release-stress.sh +0 -0
  123. {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/require_version_bump.py +0 -0
  124. {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/seed_review_run.py +0 -0
  125. {cmpnd-0.33.0 → cmpnd-0.33.2}/scripts/test-in-linux.sh +0 -0
  126. {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/__init__.pyi +0 -0
  127. {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/primitives/__init__.pyi +0 -0
  128. {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/primitives/prediction.pyi +0 -0
  129. {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/utils/__init__.pyi +0 -0
  130. {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/utils/callback.pyi +0 -0
  131. {cmpnd-0.33.0 → cmpnd-0.33.2}/stubs/dspy/utils/dummies.pyi +0 -0
  132. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/__init__.py +0 -0
  133. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_deployment_signature_helpers.py +0 -0
  134. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_exporter_helpers.py +0 -0
  135. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_fake_clock.py +0 -0
  136. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_identity_helpers.py +0 -0
  137. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_resolve_refs.py +0 -0
  138. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/_traced_run.py +0 -0
  139. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/conftest.py +0 -0
  140. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/__init__.py +0 -0
  141. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/committee_task.py +0 -0
  142. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  143. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  144. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/fake_lm.py +0 -0
  145. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/helpers.py +0 -0
  146. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/target_capabilities.py +0 -0
  147. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_assessments_cli.py +0 -0
  148. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_composite_module.py +0 -0
  149. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_dataset_auto_creation.py +0 -0
  150. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_deploy_execute.py +0 -0
  151. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_deploy_programs.py +0 -0
  152. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_example_hash.py +0 -0
  153. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_gepa_optimization.py +0 -0
  154. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_interpreter_spans.py +0 -0
  155. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_lm_accuracy.py +0 -0
  156. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_module_tracing.py +0 -0
  157. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimization_lineage.py +0 -0
  158. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimization_metric_identity.py +0 -0
  159. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimize_failures.py +0 -0
  160. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimize_loop.py +0 -0
  161. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimize_review_committee.py +0 -0
  162. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  163. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_react_tool_spans.py +0 -0
  164. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_traces_read.py +0 -0
  165. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_user_attribution.py +0 -0
  166. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/e2e/test_valsets_cli.py +0 -0
  167. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/01_predict_qa.json +0 -0
  168. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  169. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  170. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  171. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  172. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  173. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/07_program_of_thought.json +0 -0
  174. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/08_opaque_module.json +0 -0
  175. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  176. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/10_react.json +0 -0
  177. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/11_refine.json +0 -0
  178. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/12_retrieve.json +0 -0
  179. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/13_knn.json +0 -0
  180. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/14_parallel.json +0 -0
  181. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/identity_vectors/README.md +0 -0
  182. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/ingest_conformance.py +0 -0
  183. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/__init__.py +0 -0
  184. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/authored_optimize/judge.py +0 -0
  185. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  186. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/conftest.py +0 -0
  187. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  188. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/hosted_optimize_probe.py +0 -0
  189. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/optimization_run_probe.py +0 -0
  190. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  191. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/test_logs_renders_traces.py +0 -0
  192. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/test_optimization_run.py +0 -0
  193. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/test_optimizing_progress.py +0 -0
  194. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/integration/test_unhappy_path.py +0 -0
  195. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/__init__.py +0 -0
  196. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/__main__.py +0 -0
  197. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/drain.py +0 -0
  198. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/ingest.py +0 -0
  199. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/latency_transport.py +0 -0
  200. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/rlm_shape.py +0 -0
  201. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/rlm_size_report.py +0 -0
  202. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/sweep.py +0 -0
  203. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/test_rlm_shape.py +0 -0
  204. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/load/test_rlm_size_report.py +0 -0
  205. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/__init__.py +0 -0
  206. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/conftest.py +0 -0
  207. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/seed_parity.py +0 -0
  208. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_auth.py +0 -0
  209. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_datasets.py +0 -0
  210. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_deployments.py +0 -0
  211. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_evals.py +0 -0
  212. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_health.py +0 -0
  213. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_modules.py +0 -0
  214. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_optimizations.py +0 -0
  215. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_programs.py +0 -0
  216. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_signatures.py +0 -0
  217. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_trace_streaming.py +0 -0
  218. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/parity/test_traces.py +0 -0
  219. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/span_reference_fixture.py +0 -0
  220. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/__init__.py +0 -0
  221. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/_provider.py +0 -0
  222. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/_readiness.py +0 -0
  223. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/_reliability.py +0 -0
  224. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/_result.py +0 -0
  225. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/mint_bedrock_token.py +0 -0
  226. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_deploy_programs.py +0 -0
  227. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_multi_client_stress.py +0 -0
  228. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_optimize_client_committee.py +0 -0
  229. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_optimize_loop.py +0 -0
  230. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_optimize_loop_wasm.py +0 -0
  231. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_optimize_stress.py +0 -0
  232. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_provider_roles.py +0 -0
  233. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_readiness.py +0 -0
  234. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_reliability.py +0 -0
  235. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_reliability_classify.py +0 -0
  236. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_rlm_wasm.py +0 -0
  237. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/stress/test_trace_search.py +0 -0
  238. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_adapter_reentrancy.py +0 -0
  239. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_assessments_api.py +0 -0
  240. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_authored.py +0 -0
  241. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_authored_bundle_collisions.py +0 -0
  242. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_authored_metric_contract.py +0 -0
  243. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_authored_named_models.py +0 -0
  244. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_authored_optimize.py +0 -0
  245. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_callback.py +0 -0
  246. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_candidate_lineage.py +0 -0
  247. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/__init__.py +0 -0
  248. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/conftest.py +0 -0
  249. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_admin.py +0 -0
  250. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_api_client.py +0 -0
  251. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_assessments.py +0 -0
  252. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_auth.py +0 -0
  253. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_credentials.py +0 -0
  254. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_datasets.py +0 -0
  255. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_deployments.py +0 -0
  256. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_evals.py +0 -0
  257. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_login.py +0 -0
  258. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_main.py +0 -0
  259. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_optimizations.py +0 -0
  260. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_org.py +0 -0
  261. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_pagination.py +0 -0
  262. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_parity.py +0 -0
  263. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_programs.py +0 -0
  264. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_provider_credentials.py +0 -0
  265. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_schema_sample.py +0 -0
  266. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_shell.py +0 -0
  267. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_signatures.py +0 -0
  268. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_tool_servers.py +0 -0
  269. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_top.py +0 -0
  270. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_traces.py +0 -0
  271. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_traces_search_wire.py +0 -0
  272. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_tree_help.py +0 -0
  273. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cli/test_valsets.py +0 -0
  274. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_client.py +0 -0
  275. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_clustering/__init__.py +0 -0
  276. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_clustering/conftest.py +0 -0
  277. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_clustering/test_edge_cases.py +0 -0
  278. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_clustering/test_hash_consistency.py +0 -0
  279. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_clustering/test_type_conversion.py +0 -0
  280. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_cmp551_grouping_identity.py +0 -0
  281. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_configuration.py +0 -0
  282. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_context.py +0 -0
  283. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_context_propagation.py +0 -0
  284. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_dataset_polling.py +0 -0
  285. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_dataset_sync.py +0 -0
  286. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_datasets.py +0 -0
  287. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_decorators.py +0 -0
  288. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_deploy.py +0 -0
  289. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_deploy_mcp_tool_references.py +0 -0
  290. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_deployment_metadata.py +0 -0
  291. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_deployments_read.py +0 -0
  292. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_eval.py +0 -0
  293. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_eval_example_attribution.py +0 -0
  294. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_eval_start.py +0 -0
  295. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_eval_trace_grouping.py +0 -0
  296. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_exception_paths.py +0 -0
  297. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_execute_run_identity.py +0 -0
  298. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_exporter.py +0 -0
  299. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_exporter_queue.py +0 -0
  300. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_exporter_real.py +0 -0
  301. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_fake_lm.py +0 -0
  302. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_gepa_callback.py +0 -0
  303. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_gepa_integration.py +0 -0
  304. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_gepa_output_normalization.py +0 -0
  305. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_gepa_resume.py +0 -0
  306. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_identity.py +0 -0
  307. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_imaging.py +0 -0
  308. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_import_weight.py +0 -0
  309. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_ingest_conformance.py +0 -0
  310. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_integration.py +0 -0
  311. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_interpreter_callback.py +0 -0
  312. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_ir_encoder_properties.py +0 -0
  313. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_ir_golden_vectors.py +0 -0
  314. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_ir_roundtrip.py +0 -0
  315. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_lm_configuration.py +0 -0
  316. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_lm_usage_shapes.py +0 -0
  317. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_mcp_tools.py +0 -0
  318. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_models.py +0 -0
  319. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimization_progress.py +0 -0
  320. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimization_start.py +0 -0
  321. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize.py +0 -0
  322. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_budget.py +0 -0
  323. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_lift.py +0 -0
  324. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_lms.py +0 -0
  325. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_proposer.py +0 -0
  326. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_reflection_lm.py +0 -0
  327. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_optimize_terminal_failure.py +0 -0
  328. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_packaging.py +0 -0
  329. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_packaging_callable_source.py +0 -0
  330. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_packaging_mcp_tools.py +0 -0
  331. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_packaging_program_source.py +0 -0
  332. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_poll_until.py +0 -0
  333. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_program_lineage_persist.py +0 -0
  334. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_project_payload.py +0 -0
  335. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_reflector_properties.py +0 -0
  336. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_reflector_smoke.py +0 -0
  337. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_release_stress_script.py +0 -0
  338. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_repro_reactv2_bugs.py +0 -0
  339. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_require_version_bump.py +0 -0
  340. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_retry.py +0 -0
  341. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_reversible_failures.py +0 -0
  342. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_rlm_patch.py +0 -0
  343. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_run_attribution_paths.py +0 -0
  344. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_runtime_tools.py +0 -0
  345. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_schemas.py +0 -0
  346. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_seed_review_run.py +0 -0
  347. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_span_references.py +0 -0
  348. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_span_sent_once.py +0 -0
  349. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_trace_fact_wire_contract.py +0 -0
  350. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_trace_facts.py +0 -0
  351. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_trace_render.py +0 -0
  352. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_traces_read.py +0 -0
  353. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_tracked_gepa.py +0 -0
  354. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_tracker_enrich.py +0 -0
  355. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_truncation.py +0 -0
  356. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_valsets_api.py +0 -0
  357. {cmpnd-0.33.0 → cmpnd-0.33.2}/tests/test_xxh64.py +0 -0
  358. {cmpnd-0.33.0 → cmpnd-0.33.2}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.33.0
3
+ Version: 0.33.2
4
4
  Summary: DSPy observability and deployment SDK for cmpnd
5
5
  Project-URL: Homepage, https://cmpnd.ai
6
6
  Author-email: cmpnd <hello@cmpnd.ai>
@@ -8,6 +8,8 @@ from __future__ import annotations
8
8
 
9
9
  import inspect
10
10
  import keyword
11
+ import math
12
+ import time
11
13
  from typing import TYPE_CHECKING, Any
12
14
 
13
15
  import httpx
@@ -20,6 +22,7 @@ from httpx import HTTPStatusError
20
22
 
21
23
  from . import _http, configuration
22
24
  from .deployment_metadata import DeploymentArtifactMetadata, DeploymentSignatureResponse, ProgramFieldType
25
+ from .retry import retry_after_seconds
23
26
 
24
27
  if TYPE_CHECKING:
25
28
  from dspy.primitives.prediction import Prediction
@@ -89,6 +92,64 @@ def _retryable_provider_failure(
89
92
  )
90
93
 
91
94
 
95
+ _CAPACITY_MAX_ATTEMPTS = 3
96
+ _CAPACITY_MAX_WAIT_SECONDS = 15.0
97
+ _capacity_now = time.monotonic
98
+ _capacity_sleep = time.sleep
99
+
100
+
101
+ def _pre_admission_capacity(resp: httpx.Response) -> bool:
102
+ """Only a worker's explicit pre-admission refusal proves no program ran."""
103
+ if resp.status_code != 503:
104
+ return False
105
+ try:
106
+ body = resp.json()
107
+ except ValueError:
108
+ return False
109
+ return (
110
+ isinstance(body, dict)
111
+ and body.get("retryable") is True
112
+ and isinstance(body.get("error"), dict)
113
+ and body["error"].get("type") == "Capacity"
114
+ )
115
+
116
+
117
+ def _post_with_capacity_retry(
118
+ client: httpx.Client, url: str, body: dict[str, Any], api_key: str, timeout: Any
119
+ ) -> httpx.Response:
120
+ # httpx's numeric timeout applies to each network phase, not the entire call.
121
+ # Also bound the waits and each later request by elapsed time so retries do
122
+ # not silently grant the call a fresh timeout.
123
+ deadline = (
124
+ _capacity_now() + timeout
125
+ if isinstance(timeout, (int, float)) and not isinstance(timeout, bool) and timeout > 0
126
+ else None
127
+ )
128
+ last_response: httpx.Response | None = None
129
+ for attempt in range(_CAPACITY_MAX_ATTEMPTS):
130
+ remaining = deadline - _capacity_now() if deadline is not None else None
131
+ if attempt and remaining is not None and remaining <= 0:
132
+ assert last_response is not None
133
+ return last_response
134
+ if remaining is None:
135
+ resp = client.post(url, json=body, headers={"X-API-Key": api_key})
136
+ else:
137
+ resp = client.post(url, json=body, headers={"X-API-Key": api_key}, timeout=remaining)
138
+ last_response = resp
139
+ if not _pre_admission_capacity(resp) or attempt == _CAPACITY_MAX_ATTEMPTS - 1:
140
+ return resp
141
+
142
+ asked = retry_after_seconds(resp)
143
+ if asked is not None and (not math.isfinite(asked) or asked > _CAPACITY_MAX_WAIT_SECONDS):
144
+ return resp
145
+ delay = max(0.5 * 2**attempt, asked or 0.0, 0.0)
146
+ if delay > _CAPACITY_MAX_WAIT_SECONDS or (deadline is not None and delay >= deadline - _capacity_now()):
147
+ return resp
148
+ _capacity_sleep(delay)
149
+ assert last_response is not None
150
+ return last_response
151
+
152
+
92
153
  class DeployedProgram:
93
154
  """A deployed DSPy program that can be called like a local module.
94
155
 
@@ -258,7 +319,8 @@ def execute(deployment_id, timeout=30, metadata=None, tags=None, **inputs):
258
319
  Args:
259
320
  deployment_id: The deployment ID returned by deploy(), or a
260
321
  DeployedProgram instance.
261
- timeout: HTTP request timeout in seconds (default 30).
322
+ timeout: HTTP request timeout in seconds (default 30). Capacity retries
323
+ share this budget when it is numeric.
262
324
  metadata: Optional ``dict[str, str]`` of decision-time correlation
263
325
  data merged onto the server-side root trace (e.g.
264
326
  ``{"discord.message_id": "123"}``). Forwarded to the execute
@@ -298,10 +360,12 @@ def execute(deployment_id, timeout=30, metadata=None, tags=None, **inputs):
298
360
  body["tags"] = _validate_str_map("tags", tags)
299
361
 
300
362
  with httpx.Client(timeout=timeout) as client:
301
- resp = client.post(
363
+ resp = _post_with_capacity_retry(
364
+ client,
302
365
  f"{config.endpoint}/api/v1/deployments/{dep_id}/execute",
303
- json=body,
304
- headers={"X-API-Key": config.api_key},
366
+ body,
367
+ config.api_key,
368
+ timeout,
305
369
  )
306
370
 
307
371
  if resp.status_code == 409:
@@ -241,6 +241,11 @@ All tunables live on `Config` in `python-sdk/cmpnd/config.py:12`:
241
241
 
242
242
  Override any of these by passing kwargs to `cmpnd.configure(...)`.
243
243
 
244
+ The HTTP transport also reads connect, read, write and connection pool timeouts from
245
+ environment variables when the exporter is constructed. Each defaults to 30 seconds. See
246
+ [`cmpnd-ingest` HTTP request timeouts](../ingest/README.md#http-request-timeouts) for
247
+ the variable names and their scope.
248
+
244
249
  ### Shape of the loop
245
250
 
246
251
  - `start()` at `exporter_http.py:131` creates the worker thread and the `ThreadPoolExecutor`, then registers `shutdown()` via `threading._register_atexit` (or `atexit.register` as a fallback). The registration ordering is deliberate — there's a comment at `exporter_http.py:139` explaining why it has to go after the executor init.
@@ -358,6 +363,21 @@ private prediction metadata, so a program can use `output` or
358
363
  `deployment_run_id` as an output field. The accessor also accepts an execute
359
364
  exception. Trace delivery can finish after the execute response arrives.
360
365
 
366
+ When the worker refuses an execute before admitting it, serve returns HTTP 503
367
+ with `error.type: "Capacity"`, `retryable: true`, and a `Retry-After` hint.
368
+ `cmpnd.execute()` retries only that exact response, up to three total attempts.
369
+ It waits at least as long as the hint, but never waits more than 15 seconds for
370
+ one retry. If the hint exceeds that bound, the SDK returns the Capacity error
371
+ without retrying early. Other failures, including provider errors and uncertain
372
+ transport failures, return immediately because the program may have run.
373
+
374
+ A numeric `timeout` still controls each HTTP request. The SDK also charges
375
+ Capacity retry waits and later requests against the same elapsed budget. The
376
+ default is 30 seconds, so callers running programs that may spend longer in
377
+ admission or execution must pass a larger timeout. Serve assigns a fresh run id
378
+ to each attempt. When serve responds, `cmpnd.deployment_run_id()` returns only
379
+ the final attempt's run id, whether execute succeeds or raises.
380
+
361
381
  ```python
362
382
  try:
363
383
  result = deployed(question="What changed?")
@@ -97,6 +97,24 @@ queue is `max_queue_size` deep and sheds past it rather than growing. Only the t
97
97
  run-record entry points block, because a run must exist before anything referring to it
98
98
  ships.
99
99
 
100
+ ## HTTP request timeouts
101
+
102
+ `BatchExporter` reads these environment variables when it is constructed. Each accepts a finite,
103
+ positive number of seconds and defaults to 30 seconds.
104
+
105
+ | Variable | What it bounds |
106
+ |---|---|
107
+ | `CMPND_INGEST_CONNECT_TIMEOUT_SECONDS` | Establishing a connection to serve |
108
+ | `CMPND_INGEST_READ_TIMEOUT_SECONDS` | Waiting for the next response chunk |
109
+ | `CMPND_INGEST_WRITE_TIMEOUT_SECONDS` | Waiting to send the next request chunk |
110
+ | `CMPND_INGEST_POOL_TIMEOUT_SECONDS` | Waiting for an available connection |
111
+
112
+ The same settings apply to blocking run writes and queued exports in both the SDK and
113
+ the hosted search process. HTTPX applies each timeout to a phase of network activity;
114
+ none sets a total request deadline. The body size caps do not bound upload time or
115
+ serve's processing time. Raise the write timeout for slow uploads or the read timeout
116
+ for slow responses. Export retry budgets remain separate.
117
+
100
118
  ## Proving conformance from another producer
101
119
 
102
120
  Set `CMPND_INGEST_REQUEST_LOG=<path>` and the transport appends one
@@ -20,6 +20,7 @@ import base64
20
20
  import gzip
21
21
  import json
22
22
  import logging
23
+ import math
23
24
  import os
24
25
  import queue
25
26
  import threading
@@ -112,6 +113,31 @@ class ExportRetriesExhausted(RuntimeError):
112
113
  # spending a round trip — and megabytes of gzip — on a 400 it could have predicted.
113
114
  CHECKPOINT_STATE_MAX_BYTES = 8 * 1024 * 1024
114
115
 
116
+ _DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0
117
+
118
+
119
+ def _http_timeout_seconds(phase: str) -> float:
120
+ name = f"CMPND_INGEST_{phase}_TIMEOUT_SECONDS"
121
+ value = os.getenv(name)
122
+ if value is None:
123
+ return _DEFAULT_HTTP_TIMEOUT_SECONDS
124
+ try:
125
+ seconds = float(value)
126
+ except ValueError as exc:
127
+ raise ValueError(f"{name} must be a finite, positive number of seconds") from exc
128
+ if not math.isfinite(seconds) or seconds <= 0:
129
+ raise ValueError(f"{name} must be a finite, positive number of seconds")
130
+ return seconds
131
+
132
+
133
+ def _http_timeout() -> httpx.Timeout:
134
+ return httpx.Timeout(
135
+ connect=_http_timeout_seconds("CONNECT"),
136
+ read=_http_timeout_seconds("READ"),
137
+ write=_http_timeout_seconds("WRITE"),
138
+ pool=_http_timeout_seconds("POOL"),
139
+ )
140
+
115
141
 
116
142
  class CheckpointStateTooLarge(ValueError):
117
143
  """A checkpoint's state is past the contract's ceiling, so it was never sent.
@@ -273,6 +299,7 @@ class BatchExporter:
273
299
  def __init__(self, config: ExporterConfig, transport: httpx.BaseTransport | None = None) -> None:
274
300
  self._config = config
275
301
  self._transport = transport
302
+ self._http_timeout = _http_timeout()
276
303
  # Two lanes, one bound each — see _SHEDDABLE_ITEM_TYPES for why the
277
304
  # split exists, and _next_item for why the drainer reads them in this
278
305
  # order. Both are bounded: an unbounded lifecycle lane would trade a
@@ -963,7 +990,7 @@ class BatchExporter:
963
990
  "X-API-Key": self._config.api_key,
964
991
  "Content-Type": "application/json",
965
992
  },
966
- "timeout": 30.0,
993
+ "timeout": self._http_timeout,
967
994
  }
968
995
  if self._transport:
969
996
  kwargs["transport"] = self._transport
@@ -5,7 +5,7 @@ name = "cmpnd"
5
5
  # it's new) — no git tags. Bump with `uv version --bump {minor,patch}` or by
6
6
  # hand. Runtime `cmpnd.__version__` reads installed metadata, so it tracks
7
7
  # this automatically. See docs/plans/2026-07-02-sdk-pypi-tag-free-publish.md.
8
- version = "0.33.0"
8
+ version = "0.33.2"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -17,6 +17,8 @@ from __future__ import annotations
17
17
 
18
18
  import os
19
19
  import time
20
+ from collections.abc import Callable
21
+ from typing import Any
20
22
 
21
23
  import httpx
22
24
  import pytest
@@ -118,12 +120,12 @@ def flush_and_wait(seconds: float = 3.0) -> None:
118
120
 
119
121
 
120
122
  # ---------------------------------------------------------------------------
121
- # ServerClient — thin wrapper around the Rails API
123
+ # ServerClient — thin wrapper around the serve API
122
124
  # ---------------------------------------------------------------------------
123
125
 
124
126
 
125
127
  class ServerClient:
126
- """HTTP client for querying the Rails API during tests."""
128
+ """HTTP client for querying the serve API during tests."""
127
129
 
128
130
  def __init__(self, endpoint: str, api_key: str) -> None:
129
131
  self._client = httpx.Client(
@@ -140,16 +142,44 @@ class ServerClient:
140
142
 
141
143
  # -- Traces -------------------------------------------------------------
142
144
 
143
- def get_trace(self, trace_id: str, retries: int = 3, delay: float = 2.0) -> dict:
144
- for attempt in range(retries):
145
- resp = self._client.get(f"/api/v1/traces/{trace_id}")
146
- if resp.status_code == 404 and attempt < retries - 1:
147
- time.sleep(delay)
148
- continue
149
- resp.raise_for_status()
150
- return resp.json()
151
- resp.raise_for_status()
152
- return resp.json()
145
+ def get_trace(
146
+ self,
147
+ trace_id: str,
148
+ *,
149
+ ready: Callable[[dict[str, Any]], bool] | None = None,
150
+ timeout: float = 60.0,
151
+ interval: float = 2.0,
152
+ ) -> dict[str, Any]:
153
+ """Wait for a selected trace and the fields its caller will assert."""
154
+ deadline = time.monotonic() + timeout
155
+ last_detail: dict[str, Any] | None = None
156
+ last_status: int | None = None
157
+ while (remaining := deadline - time.monotonic()) > 0:
158
+ try:
159
+ response = self._client.get(f"/api/v1/traces/{trace_id}", timeout=min(10.0, remaining))
160
+ except httpx.TimeoutException:
161
+ response = None
162
+ if response is not None:
163
+ last_status = response.status_code
164
+ if response.status_code == 200:
165
+ detail = response.json()
166
+ assert isinstance(detail, dict), f"trace {trace_id} returned a non-object detail"
167
+ last_detail = detail
168
+ if (ready is None or ready(detail)) and time.monotonic() <= deadline:
169
+ return detail
170
+ elif response.status_code != 404:
171
+ response.raise_for_status()
172
+ remaining = deadline - time.monotonic()
173
+ if remaining > 0:
174
+ time.sleep(min(interval, remaining))
175
+ if last_detail is None:
176
+ raise AssertionError(f"trace {trace_id} did not appear within {timeout}s; last HTTP status={last_status}")
177
+ trace = last_detail.get("trace") or last_detail
178
+ spans = last_detail.get("spans")
179
+ raise AssertionError(
180
+ f"trace {trace_id} did not reach the asserted state within {timeout}s; "
181
+ f"last trace status={trace.get('status')!r}, spans={len(spans) if isinstance(spans, list) else None}"
182
+ )
153
183
 
154
184
  def search_traces(self, **filters) -> dict:
155
185
  resp = self._client.post("/api/v1/traces/search", json=filters)
@@ -218,9 +248,8 @@ class ServerClient:
218
248
  def _pin_fake_lm() -> None:
219
249
  """Set dspy's active LM to the deterministic in-process fake (CMP-255).
220
250
 
221
- The dummy api_key/api_base ride into deploy's ensure_provider_key on both the
222
- Rails and Go tiers (see docs/plans/2026-06-11-deterministic-e2e-inference.md);
223
- the fake ignores them. Only touches ``dspy.settings.lm`` — the litellm
251
+ The dummy api_key/api_base satisfy DSPy's LM configuration; the fake
252
+ ignores them. Only touches ``dspy.settings.lm`` — the litellm
224
253
  provider map (and any test-specific responder) is left alone.
225
254
  """
226
255
  import dspy
@@ -12,6 +12,7 @@ from cmpnd.callback import CmpndCallback
12
12
  from tests.e2e.conftest import (
13
13
  CMPND_E2E_API_KEY,
14
14
  E2E_ENDPOINT,
15
+ ServerClient,
15
16
  flush_and_wait,
16
17
  )
17
18
 
@@ -24,7 +25,7 @@ def _is_empty(value) -> bool:
24
25
 
25
26
 
26
27
  class TestCapturePromptsDisabled:
27
- def test_lm_call_inputs_empty_when_disabled(self, server_client, trace_capture):
28
+ def test_lm_call_inputs_empty_when_disabled(self, server_client: ServerClient, trace_capture):
28
29
  """With capture_prompts=False: LM_CALL inputs should be empty."""
29
30
  from cmpnd.exporter import ensure_exporter_started, shutdown_exporter
30
31
 
@@ -55,7 +56,16 @@ class TestCapturePromptsDisabled:
55
56
 
56
57
  trace_id = trace_capture.last_trace_id
57
58
  assert trace_id is not None
58
- data = server_client.get_trace(trace_id, retries=5, delay=2.0)
59
+ data = server_client.get_trace(
60
+ trace_id,
61
+ ready=lambda detail: (
62
+ detail["trace"]["status"] == "ok"
63
+ and any(
64
+ span.get("span_type") == "lm_call" and not _is_empty(span.get("outputs"))
65
+ for span in detail["spans"]
66
+ )
67
+ ),
68
+ )
59
69
  spans = data["spans"]
60
70
 
61
71
  lm_spans = [s for s in spans if s.get("span_type") == "lm_call"]
@@ -74,7 +84,7 @@ class TestCapturePromptsDisabled:
74
84
 
75
85
 
76
86
  class TestCapturePromptsEnabled:
77
- def test_lm_call_inputs_present_when_enabled(self, server_client, trace_capture):
87
+ def test_lm_call_inputs_present_when_enabled(self, server_client: ServerClient, trace_capture):
78
88
  """With capture_prompts=True: LM_CALL inputs should contain the formatted prompt."""
79
89
  # Reconfigure with capture_prompts=True
80
90
  cmpnd.configure(
@@ -99,7 +109,18 @@ class TestCapturePromptsEnabled:
99
109
 
100
110
  trace_id = trace_capture.last_trace_id
101
111
  assert trace_id is not None
102
- data = server_client.get_trace(trace_id)
112
+ data = server_client.get_trace(
113
+ trace_id,
114
+ ready=lambda detail: (
115
+ detail["trace"]["status"] == "ok"
116
+ and any(
117
+ span.get("span_type") == "lm_call"
118
+ and not _is_empty(span.get("inputs"))
119
+ and not _is_empty(span.get("outputs"))
120
+ for span in detail["spans"]
121
+ )
122
+ ),
123
+ )
103
124
  spans = data["spans"]
104
125
 
105
126
  lm_spans = [s for s in spans if s.get("span_type") == "lm_call"]
@@ -9,13 +9,13 @@ from __future__ import annotations
9
9
  import dspy
10
10
  import pytest
11
11
 
12
- from tests.e2e.conftest import flush_and_wait
12
+ from tests.e2e.conftest import ServerClient, flush_and_wait
13
13
 
14
14
  pytestmark = pytest.mark.e2e
15
15
 
16
16
 
17
17
  class TestMultipleSequentialCalls:
18
- def test_sequential_calls_distinct_traces(self, server_client, trace_capture):
18
+ def test_sequential_calls_distinct_traces(self, server_client: ServerClient, trace_capture):
19
19
  predict = dspy.Predict("question -> answer")
20
20
 
21
21
  # First call
@@ -33,15 +33,15 @@ class TestMultipleSequentialCalls:
33
33
  flush_and_wait()
34
34
 
35
35
  # Both traces should be on the server
36
- first_data = server_client.get_trace(first_trace_id)
36
+ first_data = server_client.get_trace(first_trace_id, ready=lambda data: data["trace"]["status"] == "ok")
37
37
  assert first_data["trace"]["status"] == "ok"
38
38
 
39
- second_data = server_client.get_trace(second_trace_id)
39
+ second_data = server_client.get_trace(second_trace_id, ready=lambda data: data["trace"]["status"] == "ok")
40
40
  assert second_data["trace"]["status"] == "ok"
41
41
 
42
42
 
43
43
  class TestLargeInputTruncation:
44
- def test_large_input(self, server_client, trace_capture):
44
+ def test_large_input(self, server_client: ServerClient, trace_capture):
45
45
  predict = dspy.Predict("text -> summary")
46
46
  large_text = "The quick brown fox. " * 2000
47
47
  predict(text=large_text)
@@ -51,7 +51,19 @@ class TestLargeInputTruncation:
51
51
  trace_id = trace_capture.last_trace_id
52
52
  assert trace_id is not None
53
53
 
54
- data = server_client.get_trace(trace_id)
54
+ def ready(data: dict) -> bool:
55
+ predict_spans = [
56
+ span
57
+ for span in data["spans"]
58
+ if span.get("span_type") == "predict" or "predict" in span.get("name", "").lower()
59
+ ]
60
+ return (
61
+ data["trace"]["status"] == "ok"
62
+ and bool(predict_spans)
63
+ and "text" in (predict_spans[0].get("inputs") or {})
64
+ )
65
+
66
+ data = server_client.get_trace(trace_id, ready=ready)
55
67
  trace = data["trace"]
56
68
  spans = data["spans"]
57
69
 
@@ -7,11 +7,14 @@ across multiple endpoint types in a single flush cycle.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
10
13
  import dspy
11
14
  import pytest
12
15
 
13
16
  from cmpnd.exporter import get_exporter
14
- from tests.e2e.conftest import flush_and_wait
17
+ from tests.e2e.conftest import ServerClient, flush_and_wait
15
18
 
16
19
  pytestmark = pytest.mark.e2e
17
20
 
@@ -21,12 +24,12 @@ pytestmark = pytest.mark.e2e
21
24
  # ---------------------------------------------------------------------------
22
25
 
23
26
 
24
- def _fetch_trace(server_client, trace_capture):
27
+ def _fetch_trace(server_client: ServerClient, trace_capture, ready: Callable[[dict[str, Any]], bool]):
25
28
  """Flush, then fetch the last captured trace and its spans."""
26
29
  flush_and_wait()
27
30
  trace_id = trace_capture.last_trace_id
28
31
  assert trace_id is not None, "No trace was captured"
29
- data = server_client.get_trace(trace_id)
32
+ data = server_client.get_trace(trace_id, ready=ready)
30
33
  return data["trace"], data["spans"]
31
34
 
32
35
 
@@ -38,12 +41,21 @@ def _fetch_trace(server_client, trace_capture):
38
41
  class TestParallelExportTraceIntegrity:
39
42
  """Verify traces and spans arrive intact after parallel dispatch."""
40
43
 
41
- def test_trace_with_spans_arrives_intact(self, server_client, trace_capture):
44
+ def test_trace_with_spans_arrives_intact(self, server_client: ServerClient, trace_capture):
42
45
  """A standard DSPy call should produce a complete trace with all spans."""
43
46
  predict = dspy.Predict("question -> answer")
44
47
  predict(question="What is the speed of light?")
45
48
 
46
- trace, spans = _fetch_trace(server_client, trace_capture)
49
+ trace, spans = _fetch_trace(
50
+ server_client,
51
+ trace_capture,
52
+ lambda data: (
53
+ data["trace"]["program_name"] is not None
54
+ and data["trace"]["status"] in ("ok", "error")
55
+ and len(data["spans"]) >= 1
56
+ and all(span["span_type"] is not None and span["name"] is not None for span in data["spans"])
57
+ ),
58
+ )
47
59
 
48
60
  assert trace["program_name"] is not None
49
61
  assert trace["status"] in ("ok", "error")
@@ -54,18 +66,26 @@ class TestParallelExportTraceIntegrity:
54
66
  assert span["span_type"] is not None
55
67
  assert span["name"] is not None
56
68
 
57
- def test_token_counts_populated(self, server_client, trace_capture):
69
+ def test_token_counts_populated(self, server_client: ServerClient, trace_capture):
58
70
  """Token counts should survive parallel serialization."""
59
71
  predict = dspy.Predict("topic -> explanation")
60
72
  predict(topic="Gravity")
61
73
 
62
- trace, spans = _fetch_trace(server_client, trace_capture)
74
+ trace, spans = _fetch_trace(
75
+ server_client,
76
+ trace_capture,
77
+ lambda data: (
78
+ (data["trace"].get("total_tokens") or 0) > 0
79
+ and (data["trace"].get("prompt_tokens") or 0) > 0
80
+ and (data["trace"].get("completion_tokens") or 0) > 0
81
+ ),
82
+ )
63
83
 
64
84
  assert trace["total_tokens"] > 0
65
85
  assert trace["prompt_tokens"] > 0
66
86
  assert trace["completion_tokens"] > 0
67
87
 
68
- def test_no_data_loss_after_parallel_flush(self, server_client, trace_capture):
88
+ def test_no_data_loss_after_parallel_flush(self, server_client: ServerClient, trace_capture):
69
89
  """Exporter stats should show zero drops/errors for a clean call."""
70
90
  predict = dspy.Predict("animal -> habitat")
71
91
  predict(animal="Penguin")
@@ -85,7 +105,7 @@ class TestParallelExportTraceIntegrity:
85
105
  class TestParallelExportMixedWorkload:
86
106
  """Verify multiple export types dispatched in parallel all land on server."""
87
107
 
88
- def test_multiple_traces_in_quick_succession(self, server_client, trace_capture):
108
+ def test_multiple_traces_in_quick_succession(self, server_client: ServerClient, trace_capture):
89
109
  """Multiple DSPy calls before flush should all arrive via parallel dispatch."""
90
110
  predict = dspy.Predict("fruit -> color")
91
111
 
@@ -99,6 +119,9 @@ class TestParallelExportMixedWorkload:
99
119
 
100
120
  # Verify all traces are retrievable from the server
101
121
  for trace_id in trace_capture.trace_ids[-3:]:
102
- data = server_client.get_trace(trace_id)
122
+ data = server_client.get_trace(
123
+ trace_id,
124
+ ready=lambda detail: detail["trace"]["status"] in ("ok", "error") and len(detail["spans"]) >= 1,
125
+ )
103
126
  assert data["trace"]["status"] in ("ok", "error")
104
127
  assert len(data["spans"]) >= 1