cmpnd 0.3.0__tar.gz → 0.4.1__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.
- {cmpnd-0.3.0 → cmpnd-0.4.1}/CLAUDE.md +1 -1
- {cmpnd-0.3.0 → cmpnd-0.4.1}/PKG-INFO +1 -1
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/__init__.py +7 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/_gepa_patch.py +3 -1
- cmpnd-0.4.1/cmpnd/_program_patch.py +112 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/callback.py +70 -10
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/api_client.py +10 -0
- cmpnd-0.4.1/cmpnd/cli/commands/admin.py +274 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/org.py +44 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/deployment.py +8 -8
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/optimizers/gepa.py +3 -1
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/optimizers/gepa_callback.py +11 -3
- cmpnd-0.4.1/cmpnd/optimizers/resume.py +91 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/optimizers/tracker.py +73 -17
- {cmpnd-0.3.0 → cmpnd-0.4.1}/pyproject.toml +1 -1
- cmpnd-0.4.1/scripts/capture_review_run.py +162 -0
- cmpnd-0.4.1/scripts/fixtures/review_run.json.gz +0 -0
- cmpnd-0.4.1/scripts/gepa_resume_bug.py +187 -0
- cmpnd-0.4.1/scripts/seed_review_run.py +504 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/committee_task.py +4 -2
- cmpnd-0.4.1/tests/e2e/data/committee_train.ndjson.gz +0 -0
- cmpnd-0.4.1/tests/e2e/data/committee_val.ndjson.gz +0 -0
- cmpnd-0.4.1/tests/e2e/test_optimization_lineage.py +96 -0
- cmpnd-0.4.1/tests/e2e/test_optimize_review_committee.py +161 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_optimize_server_owned_completion.py +1 -1
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/conftest.py +22 -35
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/seed_parity.py +15 -50
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_auth.py +14 -11
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_datasets.py +20 -18
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_deployments.py +11 -8
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_evals.py +10 -6
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_health.py +8 -6
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_modules.py +7 -5
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_optimizations.py +13 -10
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_programs.py +9 -6
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_signatures.py +9 -6
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_trace_streaming.py +32 -35
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/test_traces.py +11 -7
- cmpnd-0.4.1/tests/stress/_reliability.py +81 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/stress/test_deploy_programs.py +39 -5
- cmpnd-0.4.1/tests/stress/test_optimize_client_committee.py +83 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/stress/test_optimize_loop.py +4 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/stress/test_optimize_stress.py +4 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/stress/test_real_provider_twins.py +22 -8
- cmpnd-0.4.1/tests/stress/test_reliability.py +55 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_callback.py +123 -51
- cmpnd-0.4.1/tests/test_cli/test_admin.py +201 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_api_client.py +26 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_org.py +25 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_gepa_callback.py +3 -0
- cmpnd-0.4.1/tests/test_gepa_resume.py +145 -0
- cmpnd-0.4.1/tests/test_optimize_terminal_failure.py +110 -0
- cmpnd-0.4.1/tests/test_program_lineage_persist.py +122 -0
- cmpnd-0.4.1/tests/test_seed_review_run.py +120 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/uv.lock +116 -0
- cmpnd-0.3.0/cmpnd/cli/commands/admin.py +0 -59
- cmpnd-0.3.0/tests/e2e/data/committee_train.ndjson +0 -8
- cmpnd-0.3.0/tests/e2e/data/committee_val.ndjson +0 -5
- cmpnd-0.3.0/tests/test_cli/test_admin.py +0 -33
- {cmpnd-0.3.0 → cmpnd-0.4.1}/.gitignore +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/README.md +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/configuration.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/context.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/datasets.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/decorators.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/execution.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/exporter.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/helpers.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/identity.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/models.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/optimization.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/cmpnd/packaging.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/docs/sdk-instrumentation.md +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/examples/local_ollama.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/integration/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/integration/conftest.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/parity/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/stress/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_configuration.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_context.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_datasets.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_decorators.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_deploy.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_eval.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_eval_start.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_execute.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_exporter.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_identity.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_integration.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_models.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_optimize.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_packaging.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_project_payload.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_schemas.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_stats_export.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/tests/test_truncation.py +0 -0
- {cmpnd-0.3.0 → cmpnd-0.4.1}/todo.md +0 -0
|
@@ -35,7 +35,7 @@ uv run ruff format --check . # format check
|
|
|
35
35
|
- `cmpnd/callback.py` - DSPy instrumentation, signature extraction
|
|
36
36
|
- `cmpnd/optimizers/tracker.py` - Optimization tracking
|
|
37
37
|
- `cmpnd/packaging.py` - Source ZIP generation for deploy (ported from deploy/local_cmpnd.py)
|
|
38
|
-
- `cmpnd/deployment.py` - `deploy()` function: SDK ->
|
|
38
|
+
- `cmpnd/deployment.py` - `deploy()` function: SDK -> serve -> AWS deploy stack
|
|
39
39
|
|
|
40
40
|
## Reflector ↔ DSPy compatibility
|
|
41
41
|
|
|
@@ -129,4 +129,11 @@ def auto_instrument() -> CmpndCallback:
|
|
|
129
129
|
|
|
130
130
|
_gepa_patch.patch_gepa()
|
|
131
131
|
|
|
132
|
+
# Persist the compiled-from lineage tag across state-only save/load so a
|
|
133
|
+
# continued optimization still stamps compiled_from_run_id after a disk
|
|
134
|
+
# reload / process restart (CMP-444).
|
|
135
|
+
from . import _program_patch
|
|
136
|
+
|
|
137
|
+
_program_patch.patch_program_state()
|
|
138
|
+
|
|
132
139
|
return callback
|
|
@@ -144,7 +144,9 @@ def patch_gepa() -> None:
|
|
|
144
144
|
|
|
145
145
|
except Exception as e:
|
|
146
146
|
if callback is not None: # pyright: ignore[reportUnnecessaryComparison]
|
|
147
|
-
|
|
147
|
+
# Flush a terminal 'failed', not just in-memory state — else the
|
|
148
|
+
# running row start() sent lingers forever (CMP-414).
|
|
149
|
+
callback._tracker.finalize_error(e)
|
|
148
150
|
raise
|
|
149
151
|
|
|
150
152
|
finally:
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Persist the compiled-from lineage tag across DSPy's state-only save/load.
|
|
2
|
+
|
|
3
|
+
GEPA tags a compiled program with ``result._optimization_run_id`` (see
|
|
4
|
+
``cmpnd/optimizers/gepa.py`` and ``cmpnd/_gepa_patch.py``); a subsequent
|
|
5
|
+
``compile()`` starting from that program reads the tag back in
|
|
6
|
+
``tracker.enrich_from_program`` and sends it as ``compiled_from_run_id``, which
|
|
7
|
+
is how the cross-run lineage chain (Go breadcrumb bar) is built.
|
|
8
|
+
|
|
9
|
+
That tag is a plain instance attribute. DSPy's state-only save path
|
|
10
|
+
(``module.save("p.json")`` / ``.pkl`` with ``save_program=False``) serializes
|
|
11
|
+
**only named parameters** — ``BaseModule.dump_state`` returns
|
|
12
|
+
``{name: param.dump_state()}`` and ``load_state`` iterates only parameter names
|
|
13
|
+
— so the tag is dropped on both ends of a disk round-trip and the lineage
|
|
14
|
+
silently vanishes across a save/load or process restart. (``save_program=True``
|
|
15
|
+
cloudpickles the whole instance and already preserves it; the gap is exclusively
|
|
16
|
+
the state-only path, which is the one DSPy steers users toward.)
|
|
17
|
+
|
|
18
|
+
DSPy exposes no extension point for out-of-band instance state (no
|
|
19
|
+
``get_extra_state`` / attribute allowlist; ``metadata`` is DSPy-owned). Rather
|
|
20
|
+
than change DSPy upstream (CMP-444 decision), ``patch_program_state()`` wraps
|
|
21
|
+
``dump_state`` / ``load_state`` on the two classes that define them — the
|
|
22
|
+
composite ``Module`` path and the bare ``Predict`` path — to round-trip the tag
|
|
23
|
+
under a namespaced carrier key. Installed lazily from
|
|
24
|
+
``cmpnd.auto_instrument()`` alongside ``patch_gepa()``.
|
|
25
|
+
|
|
26
|
+
The carrier is a single reserved top-level key (the same shape DSPy itself uses
|
|
27
|
+
for its sibling ``metadata`` key). Stock ``Module.load_state`` ignores it (it
|
|
28
|
+
reads only parameter names); ``Predict.load_state`` ``setattr``s every key, so
|
|
29
|
+
the ``Predict`` wrapper strips the carrier after restoring the attribute so it
|
|
30
|
+
doesn't leak onto the instance.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
# Reserved top-level key carrying cmpnd's out-of-band program state. Namespaced
|
|
38
|
+
# so it can't collide with a DSPy named-parameter path.
|
|
39
|
+
_LINEAGE_KEY = "_cmpnd"
|
|
40
|
+
_RUN_ID_FIELD = "optimization_run_id"
|
|
41
|
+
|
|
42
|
+
_patched = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _inject(state: Any, module: Any) -> Any:
|
|
46
|
+
"""Add the lineage carrier to a dumped state dict when the module is tagged."""
|
|
47
|
+
run_id = getattr(module, "_optimization_run_id", None)
|
|
48
|
+
if run_id is not None and isinstance(state, dict):
|
|
49
|
+
state[_LINEAGE_KEY] = {_RUN_ID_FIELD: str(run_id)}
|
|
50
|
+
return state
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _restore(state: Any, module: Any) -> None:
|
|
54
|
+
"""Re-attach ``_optimization_run_id`` from a loaded state dict's carrier."""
|
|
55
|
+
if not isinstance(state, dict):
|
|
56
|
+
return
|
|
57
|
+
carrier = state.get(_LINEAGE_KEY)
|
|
58
|
+
if isinstance(carrier, dict) and carrier.get(_RUN_ID_FIELD):
|
|
59
|
+
module._optimization_run_id = str(carrier[_RUN_ID_FIELD])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# The wrappers forward *args/**kwargs verbatim rather than naming dump_state's
|
|
63
|
+
# json_mode / load_state's allow_unsafe_lm_state: those signatures differ across
|
|
64
|
+
# DSPy versions (allow_unsafe_lm_state was added after 3.1.x), and the patch is
|
|
65
|
+
# installed process-wide, so it must be transparent to every caller and version.
|
|
66
|
+
def _wrap_dump(orig):
|
|
67
|
+
def dump_state(self, *args, **kwargs):
|
|
68
|
+
return _inject(orig(self, *args, **kwargs), self)
|
|
69
|
+
|
|
70
|
+
return dump_state
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _wrap_load(orig, *, strip: bool):
|
|
74
|
+
def load_state(self, state, *args, **kwargs):
|
|
75
|
+
_restore(state, self)
|
|
76
|
+
# Predict.load_state setattrs every non-excluded key; drop the carrier
|
|
77
|
+
# so it doesn't land as a stray `_cmpnd` attribute. Module.load_state
|
|
78
|
+
# reads only parameter names, so it needs no strip.
|
|
79
|
+
if strip and isinstance(state, dict) and _LINEAGE_KEY in state:
|
|
80
|
+
state = {k: v for k, v in state.items() if k != _LINEAGE_KEY}
|
|
81
|
+
return orig(self, state, *args, **kwargs)
|
|
82
|
+
|
|
83
|
+
return load_state
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def patch_program_state() -> None:
|
|
87
|
+
"""Wrap dump_state/load_state so the lineage tag survives a disk reload.
|
|
88
|
+
|
|
89
|
+
Idempotent (a module-level flag guards against double-patching); a no-op
|
|
90
|
+
when dspy isn't installed.
|
|
91
|
+
"""
|
|
92
|
+
global _patched
|
|
93
|
+
if _patched:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
import dspy
|
|
98
|
+
except ImportError:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# Composite programs (dspy.Module subclasses, ChainOfThought, …) inherit
|
|
102
|
+
# dump_state/load_state from BaseModule; patching on dspy.Module covers every
|
|
103
|
+
# composite. A bare Predict program overrides both, so patch that path too.
|
|
104
|
+
# The methods are swapped via getattr/setattr rather than direct attribute
|
|
105
|
+
# assignment because dspy.*.load_state types as Unknown to basedpyright (its
|
|
106
|
+
# body imports the untyped dspy.predict.predict) — reflection keeps the swap
|
|
107
|
+
# off the type checker's attribute-access path across every DSPy version.
|
|
108
|
+
for cls, strip in ((dspy.Module, False), (dspy.Predict, True)):
|
|
109
|
+
setattr(cls, "dump_state", _wrap_dump(getattr(cls, "dump_state")))
|
|
110
|
+
setattr(cls, "load_state", _wrap_load(getattr(cls, "load_state"), strip=strip))
|
|
111
|
+
|
|
112
|
+
_patched = True
|
|
@@ -747,18 +747,20 @@ class _LMMixin(_CallbackBase):
|
|
|
747
747
|
|
|
748
748
|
context.pop_span()
|
|
749
749
|
|
|
750
|
-
#
|
|
751
|
-
#
|
|
752
|
-
#
|
|
750
|
+
# An LM call with no active trace context, made while an optimization is
|
|
751
|
+
# running, is GEPA optimizer machinery — an LM call outside any program
|
|
752
|
+
# rollout. Two phases produce these: reflection (the instructor proposing
|
|
753
|
+
# new instructions, between evaluations) and metric (an LLM-as-judge /
|
|
754
|
+
# semantic metric scoring outputs, during evaluation). Rather than drop
|
|
755
|
+
# them, wrap each in its own trace linked to the optimization run so it
|
|
756
|
+
# shows up alongside the student traces (CMP-485). serve's
|
|
757
|
+
# TracedTokensForRun then accounts for their cost from the span, so these
|
|
758
|
+
# must NOT also be added to _orphaned_lm_usage / untraced_lm_usage — that
|
|
759
|
+
# would double-count them in the optimization total.
|
|
753
760
|
current_trace = context.get_current_trace()
|
|
754
761
|
if current_trace is None and self._active_optimization_run_id is not None:
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
self._orphaned_lm_usage[model_name] = {"prompt_tokens": 0, "completion_tokens": 0}
|
|
758
|
-
self._orphaned_lm_usage[model_name]["prompt_tokens"] += span.prompt_tokens or 0
|
|
759
|
-
self._orphaned_lm_usage[model_name]["completion_tokens"] += span.completion_tokens or 0
|
|
760
|
-
logger.debug(f"Orphaned LM usage ({model_name}): +{span.prompt_tokens}p/{span.completion_tokens}c tokens")
|
|
761
|
-
return # Don't export orphaned spans (no trace to attach to)
|
|
762
|
+
self._emit_optimization_lm_trace(span)
|
|
763
|
+
return # handled as a standalone GEPA-process trace
|
|
762
764
|
|
|
763
765
|
# Track span by trace_id for metric aggregation (LM spans have token info!)
|
|
764
766
|
if span.trace_id:
|
|
@@ -772,6 +774,64 @@ class _LMMixin(_CallbackBase):
|
|
|
772
774
|
if exp:
|
|
773
775
|
exp.export_span(span)
|
|
774
776
|
|
|
777
|
+
def _emit_optimization_lm_trace(self, span: models.SpanBuilder) -> None:
|
|
778
|
+
"""Wrap an LM call the GEPA optimizer made outside a program rollout in
|
|
779
|
+
its own trace, linked to the active optimization run.
|
|
780
|
+
|
|
781
|
+
Two phases produce such calls, on different models:
|
|
782
|
+
- "reflection": GEPA's instruction proposer (the reflection LM), run in
|
|
783
|
+
the optimize loop between evaluations.
|
|
784
|
+
- "metric": an LLM-as-judge / semantic metric (the student LM or a
|
|
785
|
+
separate judge LM), run during candidate evaluation after the program
|
|
786
|
+
trace has already closed.
|
|
787
|
+
|
|
788
|
+
We distinguish them by `_active_eval_run` — set only while DSPy Evaluate
|
|
789
|
+
is running — instead of assuming every untraced call is reflection (that
|
|
790
|
+
would mislabel judge calls). The trace is named with the umbrella "GEPA";
|
|
791
|
+
the specific model stays on the span's model_name, and `gepa.phase`
|
|
792
|
+
carries the distinction (CMP-485). Each becomes a standalone one-span
|
|
793
|
+
trace so the prompt, response, tokens and cost are visible alongside the
|
|
794
|
+
student traces instead of being reduced to an aggregate token count.
|
|
795
|
+
"""
|
|
796
|
+
run_id = self._active_optimization_run_id
|
|
797
|
+
if not run_id:
|
|
798
|
+
return
|
|
799
|
+
|
|
800
|
+
phase = "metric" if self._active_eval_run is not None else "reflection"
|
|
801
|
+
span.attributes["gepa.phase"] = phase
|
|
802
|
+
|
|
803
|
+
trace = models.TraceBuilder(
|
|
804
|
+
trace_id=span.trace_id or uuid4(),
|
|
805
|
+
start_time=span.start_time,
|
|
806
|
+
end_time=span.end_time,
|
|
807
|
+
program_name="GEPA",
|
|
808
|
+
)
|
|
809
|
+
# A metric-phase call can start while DSPy's Evaluate span is still on the
|
|
810
|
+
# context stack, so on_lm_start stamped this span with a parent_span_id for
|
|
811
|
+
# a span that isn't in this standalone one-span trace. This span IS the
|
|
812
|
+
# trace's root, so force it parentless and pin it to this trace so tree/root
|
|
813
|
+
# reconstruction works (CMP-485, Greptile P1).
|
|
814
|
+
span.parent_span_id = None
|
|
815
|
+
span.trace_id = trace.trace_id
|
|
816
|
+
trace.signature_name = span.model_name or phase
|
|
817
|
+
trace.status = span.status
|
|
818
|
+
trace.optimization_run_id = run_id
|
|
819
|
+
trace.tags["optimization_phase"] = phase
|
|
820
|
+
|
|
821
|
+
# Single-span trace: roll the span's usage up onto the trace so the
|
|
822
|
+
# traces list shows tokens/cost without a separate aggregation pass.
|
|
823
|
+
trace.prompt_tokens = span.prompt_tokens or 0
|
|
824
|
+
trace.completion_tokens = span.completion_tokens or 0
|
|
825
|
+
trace.total_tokens = span.total_tokens or 0
|
|
826
|
+
trace.total_cost = span.cost or 0.0
|
|
827
|
+
trace.span_count = 1
|
|
828
|
+
|
|
829
|
+
exp = exporter.get_exporter()
|
|
830
|
+
if exp:
|
|
831
|
+
exp.export_trace_stats(trace) # stats tier (never dropped)
|
|
832
|
+
trace._batch_spans = [span]
|
|
833
|
+
exp.export_trace(trace)
|
|
834
|
+
|
|
775
835
|
|
|
776
836
|
class _AdapterMixin(_CallbackBase):
|
|
777
837
|
"""DSPy adapter callbacks (format/parse start/end)."""
|
|
@@ -49,6 +49,12 @@ class ApiClient:
|
|
|
49
49
|
def post(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
50
50
|
return self._request("POST", path, json=json)
|
|
51
51
|
|
|
52
|
+
def patch(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
53
|
+
return self._request("PATCH", path, json=json)
|
|
54
|
+
|
|
55
|
+
def put(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
56
|
+
return self._request("PUT", path, json=json)
|
|
57
|
+
|
|
52
58
|
def delete(self, path: str) -> Any:
|
|
53
59
|
return self._request("DELETE", path)
|
|
54
60
|
|
|
@@ -77,6 +83,10 @@ class ApiClient:
|
|
|
77
83
|
response = client.get(url, params=params)
|
|
78
84
|
elif method == "DELETE":
|
|
79
85
|
response = client.delete(url)
|
|
86
|
+
elif method == "PATCH":
|
|
87
|
+
response = client.patch(url, json=json)
|
|
88
|
+
elif method == "PUT":
|
|
89
|
+
response = client.put(url, json=json)
|
|
80
90
|
else:
|
|
81
91
|
response = client.post(url, json=json)
|
|
82
92
|
except httpx.ConnectError:
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Platform-operator commands, hidden behind CMPND_CLI_ADMIN=1.
|
|
2
|
+
|
|
3
|
+
`cmpnd admin org create|set-owner|list|get|set-limit|set-github-login|elect-owner`
|
|
4
|
+
is the closed-beta org bootstrap and steward surface (CMP-126/CMP-278/CMP-472):
|
|
5
|
+
stand up a shared org with its GitHub auto-join key, anoint its first owner,
|
|
6
|
+
adjust its member cap or GitHub link, and grant ownership from the election
|
|
7
|
+
queue. `cmpnd admin user …` is the operator's user-lifecycle surface (list,
|
|
8
|
+
inspect, create, approve/reject the pending-signup queue, promote/demote admin,
|
|
9
|
+
disable/enable, reset a password, and manage a user's API-key scopes). `cmpnd
|
|
10
|
+
admin feature-flags …` reads and toggles the server's runtime flags. These hit
|
|
11
|
+
/api/v1/admin/* and require an API key carrying the admin:* scopes — the server
|
|
12
|
+
is the real authority. The CMPND_CLI_ADMIN env gate (mirroring CMPND_CLI_DEV)
|
|
13
|
+
only keeps these out of customers' --help; it grants nothing.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from argparse import ArgumentParser, _SubParsersAction
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .. import api_client
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
25
|
+
parser = subparsers.add_parser("admin", help="Platform-operator commands (CMPND_CLI_ADMIN)")
|
|
26
|
+
sub = parser.add_subparsers(dest="action")
|
|
27
|
+
|
|
28
|
+
_register_org(sub)
|
|
29
|
+
_register_user(sub)
|
|
30
|
+
_register_feature_flags(sub)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _register_org(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
34
|
+
org = sub.add_parser("org", help="Organization bootstrap")
|
|
35
|
+
osub = org.add_subparsers(dest="org_action")
|
|
36
|
+
|
|
37
|
+
create = osub.add_parser("create", help="Create an organization")
|
|
38
|
+
create.add_argument("name", help="Organization name")
|
|
39
|
+
create.add_argument(
|
|
40
|
+
"--github-login",
|
|
41
|
+
dest="github_login",
|
|
42
|
+
default="",
|
|
43
|
+
help="GitHub org login for OAuth auto-join (optional)",
|
|
44
|
+
)
|
|
45
|
+
create.set_defaults(func=handle_org_create)
|
|
46
|
+
|
|
47
|
+
set_owner = osub.add_parser("set-owner", help="Make an existing user the org's owner")
|
|
48
|
+
set_owner.add_argument("org_id", help="Organization id")
|
|
49
|
+
set_owner.add_argument("email", help="Owner email (must already be a provisioned user)")
|
|
50
|
+
set_owner.set_defaults(func=handle_org_set_owner)
|
|
51
|
+
|
|
52
|
+
ls = osub.add_parser("list", help="List organizations")
|
|
53
|
+
ls.set_defaults(func=handle_org_list)
|
|
54
|
+
|
|
55
|
+
get = osub.add_parser("get", help="Show one organization")
|
|
56
|
+
get.add_argument("org_id", help="Organization id")
|
|
57
|
+
get.set_defaults(func=handle_org_get)
|
|
58
|
+
|
|
59
|
+
set_limit = osub.add_parser("set-limit", help="Set an org's member cap")
|
|
60
|
+
set_limit.add_argument("org_id", help="Organization id")
|
|
61
|
+
set_limit.add_argument("max", type=int, help="Maximum members")
|
|
62
|
+
set_limit.set_defaults(func=handle_org_set_limit)
|
|
63
|
+
|
|
64
|
+
set_github_login = osub.add_parser("set-github-login", help="Set (or clear) an org's GitHub login")
|
|
65
|
+
set_github_login.add_argument("org_id", help="Organization id")
|
|
66
|
+
set_github_login.add_argument("login", help='GitHub org login (pass "" to unlink)')
|
|
67
|
+
set_github_login.set_defaults(func=handle_org_set_github_login)
|
|
68
|
+
|
|
69
|
+
elect_owner = osub.add_parser("elect-owner", help="Grant ownership from the election queue")
|
|
70
|
+
elect_owner.add_argument("github_login", help="The org's GitHub login")
|
|
71
|
+
elect_owner.add_argument("user_id", help="User id to make owner")
|
|
72
|
+
elect_owner.set_defaults(func=handle_org_elect_owner)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _register_user(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
76
|
+
user = sub.add_parser("user", help="User lifecycle management")
|
|
77
|
+
usub = user.add_subparsers(dest="user_action")
|
|
78
|
+
|
|
79
|
+
ls = usub.add_parser("list", help="List users")
|
|
80
|
+
ls.set_defaults(func=handle_user_list)
|
|
81
|
+
|
|
82
|
+
get = usub.add_parser("get", help="Show one user")
|
|
83
|
+
get.add_argument("id", help="User id")
|
|
84
|
+
get.set_defaults(func=handle_user_get)
|
|
85
|
+
|
|
86
|
+
create = usub.add_parser("create", help="Provision a user")
|
|
87
|
+
create.add_argument("email", help="User email")
|
|
88
|
+
create.add_argument("--name", default="", help="Display name (optional)")
|
|
89
|
+
create.add_argument("--org-id", dest="org_id", default="", help="Org to place the user in (optional)")
|
|
90
|
+
create.add_argument("--password", default="", help="Initial password (optional)")
|
|
91
|
+
create.add_argument("--admin", dest="admin", action="store_true", help="Grant platform-admin")
|
|
92
|
+
create.set_defaults(func=handle_user_create)
|
|
93
|
+
|
|
94
|
+
approve = usub.add_parser("approve", help="Approve a pending signup")
|
|
95
|
+
approve.add_argument("id", help="User id")
|
|
96
|
+
approve.set_defaults(func=handle_user_approve)
|
|
97
|
+
|
|
98
|
+
promote = usub.add_parser("promote", help="Grant platform-admin")
|
|
99
|
+
promote.add_argument("id", help="User id")
|
|
100
|
+
promote.set_defaults(func=handle_user_promote)
|
|
101
|
+
|
|
102
|
+
demote = usub.add_parser("demote", help="Revoke platform-admin")
|
|
103
|
+
demote.add_argument("id", help="User id")
|
|
104
|
+
demote.set_defaults(func=handle_user_demote)
|
|
105
|
+
|
|
106
|
+
reject = usub.add_parser("reject", help="Decline a pending signup")
|
|
107
|
+
reject.add_argument("id", help="User id")
|
|
108
|
+
reject.add_argument("--org-id", dest="org_id", required=True, help="Org the pending signup belongs to")
|
|
109
|
+
reject.set_defaults(func=handle_user_reject)
|
|
110
|
+
|
|
111
|
+
disable = usub.add_parser("disable", help="Disable a user")
|
|
112
|
+
disable.add_argument("id", help="User id")
|
|
113
|
+
disable.set_defaults(func=handle_user_disable)
|
|
114
|
+
|
|
115
|
+
enable = usub.add_parser("enable", help="Re-enable a user")
|
|
116
|
+
enable.add_argument("id", help="User id")
|
|
117
|
+
enable.set_defaults(func=handle_user_enable)
|
|
118
|
+
|
|
119
|
+
reset_password = usub.add_parser("reset-password", help="Set a user's password")
|
|
120
|
+
reset_password.add_argument("id", help="User id")
|
|
121
|
+
reset_password.add_argument("--password", required=True, help="New password")
|
|
122
|
+
reset_password.set_defaults(func=handle_user_reset_password)
|
|
123
|
+
|
|
124
|
+
keys = usub.add_parser("keys", help="List a user's API keys")
|
|
125
|
+
keys.add_argument("id", help="User id")
|
|
126
|
+
keys.set_defaults(func=handle_user_keys)
|
|
127
|
+
|
|
128
|
+
set_scopes = usub.add_parser("set-scopes", help="Replace an API key's scopes")
|
|
129
|
+
set_scopes.add_argument("id", help="User id")
|
|
130
|
+
set_scopes.add_argument("key_id", help="API key id")
|
|
131
|
+
set_scopes.add_argument(
|
|
132
|
+
"--scope",
|
|
133
|
+
dest="scopes",
|
|
134
|
+
action="append",
|
|
135
|
+
required=True,
|
|
136
|
+
help="A scope to grant (repeat for multiple)",
|
|
137
|
+
)
|
|
138
|
+
set_scopes.set_defaults(func=handle_user_set_scopes)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _register_feature_flags(sub: _SubParsersAction[ArgumentParser]) -> None:
|
|
142
|
+
ff = sub.add_parser("feature-flags", help="Runtime feature flags")
|
|
143
|
+
ffsub = ff.add_subparsers(dest="ff_action")
|
|
144
|
+
|
|
145
|
+
ls = ffsub.add_parser("list", help="List feature flags")
|
|
146
|
+
ls.set_defaults(func=handle_feature_flags_list)
|
|
147
|
+
|
|
148
|
+
set_flag = ffsub.add_parser("set", help="Enable (or, with --disabled, disable) a flag")
|
|
149
|
+
set_flag.add_argument("name", help="Flag name")
|
|
150
|
+
set_flag.add_argument("--disabled", action="store_true", help="Disable the flag (default is enable)")
|
|
151
|
+
set_flag.set_defaults(func=handle_feature_flags_set)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# --- org handlers ---------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def handle_org_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
158
|
+
return client.post(
|
|
159
|
+
"/api/v1/admin/organizations",
|
|
160
|
+
json={"name": args.name, "github_login": args.github_login},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def handle_org_set_owner(client: api_client.ApiClient, args: Any) -> Any:
|
|
165
|
+
# 204 No Content → post() returns None; synthesize a confirmation.
|
|
166
|
+
client.post(f"/api/v1/admin/organizations/{args.org_id}/owner", json={"email": args.email})
|
|
167
|
+
return {"org_id": args.org_id, "owner": args.email}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def handle_org_list(client: api_client.ApiClient, _args: Any) -> Any:
|
|
171
|
+
return client.get("/api/v1/admin/organizations")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def handle_org_get(client: api_client.ApiClient, args: Any) -> Any:
|
|
175
|
+
return client.get(f"/api/v1/admin/organizations/{args.org_id}")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def handle_org_set_limit(client: api_client.ApiClient, args: Any) -> Any:
|
|
179
|
+
return client.patch(f"/api/v1/admin/organizations/{args.org_id}", json={"max_members": args.max})
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def handle_org_set_github_login(client: api_client.ApiClient, args: Any) -> Any:
|
|
183
|
+
# An empty login unlinks the org from GitHub auto-join; pass it through
|
|
184
|
+
# verbatim so the server can distinguish "clear" from "unchanged".
|
|
185
|
+
return client.patch(f"/api/v1/admin/organizations/{args.org_id}", json={"github_login": args.login})
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def handle_org_elect_owner(client: api_client.ApiClient, args: Any) -> Any:
|
|
189
|
+
# 204 No Content → post() returns None; synthesize a confirmation.
|
|
190
|
+
client.post(
|
|
191
|
+
"/api/v1/admin/organizations/elect-owner",
|
|
192
|
+
json={"github_login": args.github_login, "user_id": args.user_id},
|
|
193
|
+
)
|
|
194
|
+
return {"elected": args.user_id, "github_login": args.github_login}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# --- user handlers --------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def handle_user_list(client: api_client.ApiClient, _args: Any) -> Any:
|
|
201
|
+
return client.get("/api/v1/admin/users")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def handle_user_get(client: api_client.ApiClient, args: Any) -> Any:
|
|
205
|
+
return client.get(f"/api/v1/admin/users/{args.id}")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def handle_user_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
209
|
+
# email/name/org_id/is_admin are always sent; password is only included
|
|
210
|
+
# when supplied so the server keeps its own default (e.g. no local login).
|
|
211
|
+
body = {
|
|
212
|
+
"email": args.email,
|
|
213
|
+
"name": args.name,
|
|
214
|
+
"org_id": args.org_id,
|
|
215
|
+
"is_admin": args.admin,
|
|
216
|
+
}
|
|
217
|
+
if args.password:
|
|
218
|
+
body["password"] = args.password
|
|
219
|
+
return client.post("/api/v1/admin/users", json=body)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def handle_user_approve(client: api_client.ApiClient, args: Any) -> Any:
|
|
223
|
+
return client.patch(f"/api/v1/admin/users/{args.id}", json={"is_approved": True})
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def handle_user_promote(client: api_client.ApiClient, args: Any) -> Any:
|
|
227
|
+
return client.patch(f"/api/v1/admin/users/{args.id}", json={"is_admin": True})
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def handle_user_demote(client: api_client.ApiClient, args: Any) -> Any:
|
|
231
|
+
return client.patch(f"/api/v1/admin/users/{args.id}", json={"is_admin": False})
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def handle_user_reject(client: api_client.ApiClient, args: Any) -> Any:
|
|
235
|
+
# 204 No Content → post() returns None; synthesize a confirmation.
|
|
236
|
+
client.post(f"/api/v1/admin/users/{args.id}/reject", json={"org_id": args.org_id})
|
|
237
|
+
return {"rejected": args.id}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def handle_user_disable(client: api_client.ApiClient, args: Any) -> Any:
|
|
241
|
+
client.post(f"/api/v1/admin/users/{args.id}/disable")
|
|
242
|
+
return {"disabled": args.id}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def handle_user_enable(client: api_client.ApiClient, args: Any) -> Any:
|
|
246
|
+
client.post(f"/api/v1/admin/users/{args.id}/enable")
|
|
247
|
+
return {"enabled": args.id}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def handle_user_reset_password(client: api_client.ApiClient, args: Any) -> Any:
|
|
251
|
+
client.post(f"/api/v1/admin/users/{args.id}/reset-password", json={"password": args.password})
|
|
252
|
+
return {"password_reset": args.id}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def handle_user_keys(client: api_client.ApiClient, args: Any) -> Any:
|
|
256
|
+
return client.get(f"/api/v1/admin/users/{args.id}/keys")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def handle_user_set_scopes(client: api_client.ApiClient, args: Any) -> Any:
|
|
260
|
+
return client.put(
|
|
261
|
+
f"/api/v1/admin/users/{args.id}/keys/{args.key_id}/scopes",
|
|
262
|
+
json={"scopes": args.scopes},
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# --- feature-flag handlers ------------------------------------------------
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def handle_feature_flags_list(client: api_client.ApiClient, _args: Any) -> Any:
|
|
270
|
+
return client.get("/api/v1/admin/feature-flags")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def handle_feature_flags_set(client: api_client.ApiClient, args: Any) -> Any:
|
|
274
|
+
return client.put(f"/api/v1/admin/feature-flags/{args.name}", json={"enabled": not args.disabled})
|
|
@@ -15,6 +15,13 @@ hit /api/v1/org/ownership-requests and are self-scoped to the calling key's
|
|
|
15
15
|
user — no owner check (a platform admin grants ownership from the election
|
|
16
16
|
queue; the request is just recorded intent).
|
|
17
17
|
|
|
18
|
+
`cmpnd org keys list|create|revoke` is your own API-key surface (CMP-472):
|
|
19
|
+
list the keys you've minted, mint a new scoped key (the response carries the
|
|
20
|
+
one-time token — the server stores only its hash), and revoke one. These hit
|
|
21
|
+
/api/v1/org/keys and are self-scoped to the calling key's user — you only ever
|
|
22
|
+
see and manage your own keys, bound to your active org (the non-interactive
|
|
23
|
+
alternative to the `cmpnd login` loopback).
|
|
24
|
+
|
|
18
25
|
Always registered (no env gate); the server is the authority.
|
|
19
26
|
"""
|
|
20
27
|
|
|
@@ -66,6 +73,27 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
66
73
|
withdraw.add_argument("github_login", help="The org's GitHub login (e.g. acme)")
|
|
67
74
|
withdraw.set_defaults(func=handle_withdraw_ownership)
|
|
68
75
|
|
|
76
|
+
keys = sub.add_parser("keys", help="Manage your organization's API keys")
|
|
77
|
+
ksub = keys.add_subparsers(dest="keys_action")
|
|
78
|
+
|
|
79
|
+
keys_list = ksub.add_parser("list", help="List the org's API keys")
|
|
80
|
+
keys_list.set_defaults(func=handle_keys_list)
|
|
81
|
+
|
|
82
|
+
keys_create = ksub.add_parser("create", help="Mint a new scoped API key")
|
|
83
|
+
keys_create.add_argument("name", help="A label for the key")
|
|
84
|
+
keys_create.add_argument(
|
|
85
|
+
"--scope",
|
|
86
|
+
dest="scopes",
|
|
87
|
+
action="append",
|
|
88
|
+
required=True,
|
|
89
|
+
help="A scope to grant (repeat for multiple)",
|
|
90
|
+
)
|
|
91
|
+
keys_create.set_defaults(func=handle_keys_create)
|
|
92
|
+
|
|
93
|
+
keys_revoke = ksub.add_parser("revoke", help="Revoke an API key")
|
|
94
|
+
keys_revoke.add_argument("key_id", help="API key id (from `org keys list`)")
|
|
95
|
+
keys_revoke.set_defaults(func=handle_keys_revoke)
|
|
96
|
+
|
|
69
97
|
|
|
70
98
|
def handle_members_pending(client: api_client.ApiClient, _args: Any) -> Any:
|
|
71
99
|
return client.get("/api/v1/org/members/pending")
|
|
@@ -113,3 +141,19 @@ def handle_ownership_requests(client: api_client.ApiClient, _args: Any) -> Any:
|
|
|
113
141
|
def handle_withdraw_ownership(client: api_client.ApiClient, args: Any) -> Any:
|
|
114
142
|
client.post(f"/api/v1/org/ownership-requests/{args.github_login}/withdraw")
|
|
115
143
|
return {"withdrawn": args.github_login}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def handle_keys_list(client: api_client.ApiClient, _args: Any) -> Any:
|
|
147
|
+
return client.get("/api/v1/org/keys")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def handle_keys_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
151
|
+
# The response carries the one-time key token — the server stores only its
|
|
152
|
+
# hash, so this is the single moment it's shown. Pass it straight through.
|
|
153
|
+
return client.post("/api/v1/org/keys", json={"name": args.name, "scopes": args.scopes})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def handle_keys_revoke(client: api_client.ApiClient, args: Any) -> Any:
|
|
157
|
+
# 204 No Content → delete() returns None; synthesize a confirmation.
|
|
158
|
+
client.delete(f"/api/v1/org/keys/{args.key_id}")
|
|
159
|
+
return {"revoked": args.key_id}
|
|
@@ -72,7 +72,7 @@ def _raise_http_error(response: httpx.Response, action: str) -> None:
|
|
|
72
72
|
class DeployFailed(RuntimeError):
|
|
73
73
|
"""Raised when a deployment transitions to ``status=failed``.
|
|
74
74
|
|
|
75
|
-
Carries the structured failure detail
|
|
75
|
+
Carries the structured failure detail the server persists when the parser
|
|
76
76
|
writes a failure marker. ``failure_phase`` is one of a closed set of
|
|
77
77
|
pipeline-stage names (e.g. ``unpack_zip``, ``signature_extraction``,
|
|
78
78
|
``lm_load``); ``failure_reason`` is a constant template synthesized
|
|
@@ -86,7 +86,7 @@ class DeployFailed(RuntimeError):
|
|
|
86
86
|
parser Lambda's CloudWatch group.
|
|
87
87
|
|
|
88
88
|
Both fields are ``None`` when the server didn't return structured
|
|
89
|
-
detail (older deployments or older
|
|
89
|
+
detail (older deployments or older server versions).
|
|
90
90
|
|
|
91
91
|
Subclasses ``RuntimeError`` so existing ``except RuntimeError`` callers
|
|
92
92
|
keep working.
|
|
@@ -117,7 +117,7 @@ class DeployFailed(RuntimeError):
|
|
|
117
117
|
def deploy(module, metric=None, timeout=300, metadata=None):
|
|
118
118
|
"""Deploy a DSPy module via the Cmpnd platform.
|
|
119
119
|
|
|
120
|
-
Packages the module into a source ZIP, uploads it through
|
|
120
|
+
Packages the module into a source ZIP, uploads it through the platform API to the
|
|
121
121
|
deploy stack, and polls until the program is parsed and ready.
|
|
122
122
|
|
|
123
123
|
Args:
|
|
@@ -192,18 +192,18 @@ def deploy(module, metric=None, timeout=300, metadata=None):
|
|
|
192
192
|
if lm_kwargs.get("api_base"):
|
|
193
193
|
deploy_body["lm_api_host"] = lm_kwargs["api_base"]
|
|
194
194
|
|
|
195
|
-
# Client for
|
|
196
|
-
|
|
195
|
+
# Client for platform API calls (JSON)
|
|
196
|
+
api_headers = {
|
|
197
197
|
"X-API-Key": config.api_key,
|
|
198
198
|
"Content-Type": "application/json",
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
with httpx.Client(timeout=30.0) as client:
|
|
202
|
-
# Step 1: Create deployment via
|
|
202
|
+
# Step 1: Create deployment via the platform API
|
|
203
203
|
create_resp = client.post(
|
|
204
204
|
f"{config.endpoint}/api/v1/deployments",
|
|
205
205
|
json=deploy_body,
|
|
206
|
-
headers=
|
|
206
|
+
headers=api_headers,
|
|
207
207
|
)
|
|
208
208
|
_raise_http_error(create_resp, "create deployment")
|
|
209
209
|
data = create_resp.json()
|
|
@@ -241,7 +241,7 @@ def deploy(module, metric=None, timeout=300, metadata=None):
|
|
|
241
241
|
|
|
242
242
|
poll_resp = client.get(
|
|
243
243
|
f"{config.endpoint}/api/v1/deployments/{deployment_id}",
|
|
244
|
-
headers=
|
|
244
|
+
headers=api_headers,
|
|
245
245
|
)
|
|
246
246
|
_raise_http_error(poll_resp, "poll deployment status")
|
|
247
247
|
status_data = poll_resp.json()
|