argus-code-review 0.2.9__tar.gz → 0.2.11__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.
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/CHANGELOG.md +30 -1
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/PKG-INFO +1 -1
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/graph.py +134 -25
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_bench_config_guard.py +126 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_llm_models.py +21 -3
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_plan_review.py +205 -37
- argus_code_review-0.2.11/tests/test_structured_output_methods.py +102 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/.gitignore +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/LICENSE +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/README.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/__init__.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/bench.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/bench_default.toml +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/cli.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/config.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/coverage.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/dotenv_utils.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/gemini_cache.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/gemini_runner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/github_client.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/helpers.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/llm/models.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/llm/output_models.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/llm/pricing.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/llm/usage.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/models.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/openai_client.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/openai_runner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/pipeline_models.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/__init__.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/actions_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/engine.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/eslint_bundle/.gitignore +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/eslint_bundle/README.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/eslint_bundle/eslint.config.js +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/eslint_bundle/package-lock.json +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/eslint_bundle/package.json +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/js_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/migration_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/rules/README.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/sarif.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/scanner_utils.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/secrets_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/shadow.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/terraform_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/precheck/workflow_lint_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/__init__.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-blocking-validator.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-coverage-check.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-cross-cutting.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-feedback-verifier.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-lite.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-planner.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-preflight-router.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-prior-art.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-specialist-deployment.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-specialist-frontend.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-specialist-infra.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-specialist-llm-patterns.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-specialist-observability.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-specialist-orchestration.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-specialist-security.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-specialist-slackbot.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-specialist-sql.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-subagent.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-tests-and-docs.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts/pr-review-writer.md +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/prompts_runtime.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/repo_provision.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/review_tools.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/runners.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/storage/__init__.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/storage/http.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/storage/models.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/storage/precheck.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/storage/resolver.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/storage/session.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/storage/sql.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/argus/storage/sqlite.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/pyproject.toml +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/schema/008_add_code_reviews.sql +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/schema/009_add_reviewer_version.sql +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/schema/010_add_review_patterns.sql +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/schema/011_add_review_progress_columns.sql +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/schema/015_create_agent_runs.sql +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/schema/016_add_agent_runs_failure_reason.sql +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/schema/017_add_precheck_rules.sql +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/schema/018_widen_agent_runs_failure_reason.sql +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/__init__.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/conftest.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/golden/review_response.schema.json +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/storage/__init__.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/storage/test_backend_contract.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/storage/test_http.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/storage/test_models.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/storage/test_resolver.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/storage/test_session.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/storage/test_sql.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/storage/test_sqlite_backend.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_actions_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_argus_review_local.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_bench.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_catchup_gate.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_cli_args.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_cli_output_contract.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_cli_post_review.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_cli_preflight.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_cli_prompts.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_config.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_conftest.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_gemini_cache.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_gemini_runner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_github_client_checks_signal.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_github_client_write.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_graph_fetch_diff.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_graph_get_llm_temperature.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_graph_http_guards.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_graph_precheck.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_graph_preflight_image_bump.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_graph_progress.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_graph_storage_resolution.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_graph_timeout_surfacing.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_js_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_llm_models_override.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_llm_pricing.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_migration_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_models.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_multi_round.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_openai_client.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_openai_runner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_output_models.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_packaging.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_precheck_engine.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_precheck_engine_integration.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_precheck_sarif.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_precheck_shadow.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_precheck_shadow_integration.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_prompts.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_repo_provision.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_review_patterns_integration.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_review_tools.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_runners_context7.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_runners_context_usage.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_runners_helpers.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_runners_new.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_runners_turn_budget.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_scanner_utils.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_secrets_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_specialist_validation.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_stage_costs.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_storage_precheck.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_terraform_scanner.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_terraform_scanner_integration.py +0 -0
- {argus_code_review-0.2.9 → argus_code_review-0.2.11}/tests/test_workflow_lint_scanner.py +0 -0
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.11] - 2026-10-12
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- The bench-config-change guard (TECH-6282) no longer fails closed on a file whose
|
|
15
|
+
patch GitHub's compare API omits (diff too large) when the repo has explicitly
|
|
16
|
+
declared that file generated/non-diffable via `.gitattributes`
|
|
17
|
+
(`linguist-generated=true` / `-diff`) — the same signal other AI review tools
|
|
18
|
+
(e.g. Cubic) already honor to skip generated files. Previously, any oversized
|
|
19
|
+
file anywhere in a PR's diff (e.g. a large Drizzle-generated migration snapshot,
|
|
20
|
+
unrelated to Argus's own bench config) forced every review round to BLOCKING
|
|
21
|
+
with no way out, since the guard treated "missing patch" as suspicious
|
|
22
|
+
regardless of whether the file was remotely bench-related. The real
|
|
23
|
+
bench-config-path check (`_is_bench_config_path`) is unaffected: a genuine
|
|
24
|
+
bench config file can't hide behind a `.gitattributes` declaration. (TECH-7402)
|
|
25
|
+
|
|
26
|
+
## [0.2.10] - 2026-10-03
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Fixed structured output generation on Claude 5.5 (`claude-opus-5-5`, `claude-sonnet-5-5`) across the planner, preflight, and coverage stages (TECH-7156, #33):
|
|
31
|
+
- Migrated `plan_review`, `run_preflight_check`, and `check_coverage` to Anthropic native JSON-schema output (`output_config.format` with `type="json_schema"`) instead of unsupported forced tool choice (`tool_choice`).
|
|
32
|
+
- Preserved planner raw streaming, strict validation, repair fallback via OpenAI, truncation detection with transient retry (`PlannerTransientError`), and stage usage accounting.
|
|
33
|
+
- Added targeted regression coverage for streamed native structured output chunk shapes and non-streaming native JSON-schema stage bindings (`tests/test_plan_review.py`, `tests/test_structured_output_methods.py`).
|
|
34
|
+
- Updated architecture documentation (`docs/ARCHITECTURE.md`) to reflect native structured output for the planner.
|
|
35
|
+
- End-to-end evidence: verified via a full Argus branch review through the Redesign Health Anthropic proxy with model overrides unset (the RH proxy itself was unchanged).
|
|
36
|
+
|
|
10
37
|
## [0.2.9] - 2026-10-02
|
|
11
38
|
|
|
12
39
|
### Changed
|
|
@@ -380,7 +407,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
380
407
|
packaged set.
|
|
381
408
|
- `argus --version`, `argus prompts list`, and `argus prompts export`.
|
|
382
409
|
|
|
383
|
-
[Unreleased]: https://github.com/redesignhealth/argus-review/compare/v0.2.
|
|
410
|
+
[Unreleased]: https://github.com/redesignhealth/argus-review/compare/v0.2.11...HEAD
|
|
411
|
+
[0.2.11]: https://github.com/redesignhealth/argus-review/compare/v0.2.10...v0.2.11
|
|
412
|
+
[0.2.10]: https://github.com/redesignhealth/argus-review/compare/v0.2.9...v0.2.10
|
|
384
413
|
[0.2.9]: https://github.com/redesignhealth/argus-review/compare/v0.2.8...v0.2.9
|
|
385
414
|
[0.2.8]: https://github.com/redesignhealth/argus-review/compare/v0.2.7...v0.2.8
|
|
386
415
|
[0.2.7]: https://github.com/redesignhealth/argus-review/compare/v0.2.6...v0.2.7
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: argus-code-review
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.11
|
|
4
4
|
Summary: Self-orchestrated PR review agent using LangGraph + Claude Agent SDK
|
|
5
5
|
Project-URL: Repository, https://github.com/redesignhealth/argus-review
|
|
6
6
|
Project-URL: Issues, https://github.com/redesignhealth/argus-review/issues
|
|
@@ -27,6 +27,7 @@ parameters, making add_node() calls report spurious arg-type errors.
|
|
|
27
27
|
|
|
28
28
|
import asyncio
|
|
29
29
|
import base64
|
|
30
|
+
import contextlib
|
|
30
31
|
import json
|
|
31
32
|
import logging
|
|
32
33
|
import operator
|
|
@@ -54,7 +55,7 @@ from argus.llm.usage import (
|
|
|
54
55
|
)
|
|
55
56
|
|
|
56
57
|
from langchain.chat_models import init_chat_model
|
|
57
|
-
from anthropic import APIConnectionError, APITimeoutError
|
|
58
|
+
from anthropic import APIConnectionError, APITimeoutError, transform_schema
|
|
58
59
|
from langgraph.graph import END, START, StateGraph
|
|
59
60
|
from sqlalchemy.exc import ProgrammingError
|
|
60
61
|
from langgraph.types import RetryPolicy, Send
|
|
@@ -1080,9 +1081,9 @@ async def plan_review(diff: str, description: str) -> ReviewPlan:
|
|
|
1080
1081
|
|
|
1081
1082
|
Two-phase pattern mirroring the writer at ``write_review``:
|
|
1082
1083
|
|
|
1083
|
-
1. Phase 1 — Opus emits the plan via streamed
|
|
1084
|
-
we receive the raw JSON text
|
|
1085
|
-
than routing through ``with_structured_output`` (which strict-parses
|
|
1084
|
+
1. Phase 1 — Opus emits the plan via streamed native structured output
|
|
1085
|
+
(``output_config.format``) so we receive the raw JSON text directly,
|
|
1086
|
+
rather than routing through ``with_structured_output`` (which strict-parses
|
|
1086
1087
|
the wire body via the Anthropic SDK and dies on invalid escape
|
|
1087
1088
|
sequences Opus occasionally emits, e.g. ``\\s`` inside a string).
|
|
1088
1089
|
2. Phase 2 — attempt a strict Pydantic parse; if it fails (invalid
|
|
@@ -1093,29 +1094,73 @@ async def plan_review(diff: str, description: str) -> ReviewPlan:
|
|
|
1093
1094
|
"""
|
|
1094
1095
|
prompt = await fetch_prompt("pr-review-planner")
|
|
1095
1096
|
messages = _build_planner_messages(prompt, diff, description)
|
|
1097
|
+
# Anthropic native structured output config for ReviewPlan.
|
|
1098
|
+
# Note: langchain-anthropic's `with_structured_output` chains a non-streaming
|
|
1099
|
+
# PydanticOutputParser, preventing raw JSON stream capture needed for
|
|
1100
|
+
# truncation detection and OpenAI JSON-escape repair fallback. Rather than
|
|
1101
|
+
# importing langchain-anthropic's private `_convert_to_anthropic_output_config_format`,
|
|
1102
|
+
# we use the public `anthropic.transform_schema` to construct native `output_config.format`.
|
|
1096
1103
|
model = (
|
|
1097
1104
|
_get_llm(_PLANNER_MODEL, "planner")
|
|
1098
|
-
.
|
|
1105
|
+
.bind(
|
|
1106
|
+
output_config={
|
|
1107
|
+
"format": {
|
|
1108
|
+
"type": "json_schema",
|
|
1109
|
+
"schema": transform_schema(ReviewPlan),
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
)
|
|
1099
1113
|
.with_config(
|
|
1100
1114
|
run_name="planner-phase1-stream",
|
|
1101
|
-
tags=["planner", "
|
|
1115
|
+
tags=["planner", "native_structured_output", "json-escape-repair"],
|
|
1102
1116
|
)
|
|
1103
1117
|
)
|
|
1104
1118
|
|
|
1105
|
-
#
|
|
1106
|
-
#
|
|
1107
|
-
#
|
|
1119
|
+
# Native output streams JSON as text content. Collect string text and
|
|
1120
|
+
# text content blocks, while preserving legacy tool_call_chunks handling
|
|
1121
|
+
# for backwards/mock compatibility. If a chunk exposes text content,
|
|
1122
|
+
# tool_call_chunks are skipped for that chunk to avoid double-counting.
|
|
1108
1123
|
raw_json_parts: list[str] = []
|
|
1109
1124
|
dropped_nonzero_indices: set[int] = set()
|
|
1110
1125
|
async for chunk in model.astream(messages):
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1126
|
+
text_piece = ""
|
|
1127
|
+
content = getattr(chunk, "content", None)
|
|
1128
|
+
if isinstance(content, str):
|
|
1129
|
+
text_piece = content
|
|
1130
|
+
elif isinstance(content, list):
|
|
1131
|
+
pieces: list[str] = []
|
|
1132
|
+
for block in content:
|
|
1133
|
+
if isinstance(block, str):
|
|
1134
|
+
pieces.append(block)
|
|
1135
|
+
elif isinstance(block, dict):
|
|
1136
|
+
block_type = block.get("type")
|
|
1137
|
+
if (block_type is None or block_type == "text") and isinstance(
|
|
1138
|
+
block.get("text"), str
|
|
1139
|
+
):
|
|
1140
|
+
pieces.append(block["text"])
|
|
1141
|
+
elif hasattr(block, "text") and isinstance(getattr(block, "text"), str):
|
|
1142
|
+
block_type = getattr(block, "type", None)
|
|
1143
|
+
if block_type is None or block_type == "text":
|
|
1144
|
+
pieces.append(block.text)
|
|
1145
|
+
text_piece = "".join(pieces)
|
|
1146
|
+
elif hasattr(chunk, "text") and isinstance(getattr(chunk, "text"), str):
|
|
1147
|
+
text_piece = chunk.text
|
|
1148
|
+
|
|
1149
|
+
if text_piece:
|
|
1150
|
+
raw_json_parts.append(text_piece)
|
|
1151
|
+
else:
|
|
1152
|
+
for tc_chunk in getattr(chunk, "tool_call_chunks", []) or []:
|
|
1153
|
+
if isinstance(tc_chunk, dict):
|
|
1154
|
+
idx = tc_chunk.get("index", 0)
|
|
1155
|
+
args = tc_chunk.get("args")
|
|
1156
|
+
else:
|
|
1157
|
+
idx = getattr(tc_chunk, "index", 0)
|
|
1158
|
+
args = getattr(tc_chunk, "args", None)
|
|
1159
|
+
if idx != 0:
|
|
1160
|
+
dropped_nonzero_indices.add(idx)
|
|
1161
|
+
continue
|
|
1162
|
+
if args:
|
|
1163
|
+
raw_json_parts.append(args)
|
|
1119
1164
|
|
|
1120
1165
|
if dropped_nonzero_indices:
|
|
1121
1166
|
logger.warning(
|
|
@@ -1127,29 +1172,29 @@ async def plan_review(diff: str, description: str) -> ReviewPlan:
|
|
|
1127
1172
|
raw_json = "".join(raw_json_parts)
|
|
1128
1173
|
if not raw_json:
|
|
1129
1174
|
logger.error(
|
|
1130
|
-
"Planner produced no
|
|
1175
|
+
"Planner produced no structured output; model=%s schema=ReviewPlan",
|
|
1131
1176
|
_PLANNER_MODEL,
|
|
1132
1177
|
)
|
|
1133
1178
|
raise ValueError(
|
|
1134
|
-
"Planner produced no
|
|
1135
|
-
"
|
|
1179
|
+
"Planner produced no structured output — model returned no JSON "
|
|
1180
|
+
"content matching the ReviewPlan schema."
|
|
1136
1181
|
)
|
|
1137
1182
|
|
|
1138
1183
|
# Truncation guard: a mid-stream failure can leave raw_json syntactically
|
|
1139
1184
|
# parseable at the top level but missing tail content. A complete
|
|
1140
|
-
#
|
|
1185
|
+
# structured output JSON object must start with `{` and end with `}` after
|
|
1141
1186
|
# whitespace trim. If it doesn't, the stream was cut off — fail loud
|
|
1142
1187
|
# rather than feed partial JSON to the fallback repair step.
|
|
1143
1188
|
stripped = raw_json.strip()
|
|
1144
1189
|
if not (stripped.startswith("{") and stripped.endswith("}")):
|
|
1145
1190
|
logger.error(
|
|
1146
|
-
"Planner
|
|
1191
|
+
"Planner structured output appears truncated; length=%d starts_with=%r ends_with=%r",
|
|
1147
1192
|
len(raw_json),
|
|
1148
1193
|
stripped[:1],
|
|
1149
1194
|
stripped[-1:] if stripped else "",
|
|
1150
1195
|
)
|
|
1151
1196
|
raise PlannerTransientError(
|
|
1152
|
-
"Planner
|
|
1197
|
+
"Planner structured output appears truncated (does not start with "
|
|
1153
1198
|
"'{' and end with '}') — stream likely terminated mid-response."
|
|
1154
1199
|
)
|
|
1155
1200
|
|
|
@@ -1339,7 +1384,9 @@ async def check_coverage(
|
|
|
1339
1384
|
)
|
|
1340
1385
|
|
|
1341
1386
|
prompt = await fetch_prompt("pr-review-coverage-check")
|
|
1342
|
-
model = _get_llm(_COVERAGE_MODEL, "coverage").with_structured_output(
|
|
1387
|
+
model = _get_llm(_COVERAGE_MODEL, "coverage").with_structured_output(
|
|
1388
|
+
CoverageResult, method="json_schema"
|
|
1389
|
+
)
|
|
1343
1390
|
messages = [
|
|
1344
1391
|
{"role": "system", "content": prompt},
|
|
1345
1392
|
*_build_coverage_messages(plan, findings, sorted(uncovered)),
|
|
@@ -1445,7 +1492,7 @@ async def run_preflight_check(
|
|
|
1445
1492
|
prompt = await fetch_prompt("pr-review-preflight-router")
|
|
1446
1493
|
llm = _get_llm(
|
|
1447
1494
|
f"anthropic:{CLAUDE_DEFAULT}", "preflight", max_tokens=256, temperature=0
|
|
1448
|
-
).with_structured_output(PreflightResult)
|
|
1495
|
+
).with_structured_output(PreflightResult, method="json_schema")
|
|
1449
1496
|
prior_context = (
|
|
1450
1497
|
f"Prior round verdict: {prior_verdict}" if prior_verdict else "No prior review (round 1)"
|
|
1451
1498
|
)
|
|
@@ -2047,6 +2094,62 @@ def _detect_bench_config_routing_lines(diff: str) -> list[str]:
|
|
|
2047
2094
|
return matches
|
|
2048
2095
|
|
|
2049
2096
|
|
|
2097
|
+
async def _is_generated_nodiff_path(worktree_path: str | None, filename: str) -> bool:
|
|
2098
|
+
"""Return True if the repo itself declares ``filename`` generated/non-diffable.
|
|
2099
|
+
|
|
2100
|
+
A file whose patch GitHub's compare API omits (diff too large) is otherwise
|
|
2101
|
+
treated as bench-config-unconfirmed below regardless of whether the file is
|
|
2102
|
+
remotely bench-related -- this fires for any oversized file anywhere in the
|
|
2103
|
+
diff, e.g. a large auto-generated migration snapshot unrelated to Argus's own
|
|
2104
|
+
config. Repos already have a standard, explicit, auditable way to declare a
|
|
2105
|
+
file generated-and-not-meaningfully-diffable: ``.gitattributes``
|
|
2106
|
+
``linguist-generated=true`` / ``-diff``. Honor that signal here so a
|
|
2107
|
+
missing-patch file only stays unconfirmed (and forces BLOCKING) when the repo
|
|
2108
|
+
hasn't already declared it non-diffable. This does NOT weaken
|
|
2109
|
+
``_is_bench_config_path`` above, which matches on filename regardless of patch
|
|
2110
|
+
content or gitattributes -- a real bench config file can't hide behind this.
|
|
2111
|
+
"""
|
|
2112
|
+
if not worktree_path:
|
|
2113
|
+
return False
|
|
2114
|
+
try:
|
|
2115
|
+
proc = await asyncio.create_subprocess_exec(
|
|
2116
|
+
"git",
|
|
2117
|
+
"-C",
|
|
2118
|
+
worktree_path,
|
|
2119
|
+
"check-attr",
|
|
2120
|
+
"diff",
|
|
2121
|
+
"linguist-generated",
|
|
2122
|
+
"--",
|
|
2123
|
+
filename,
|
|
2124
|
+
stdout=asyncio.subprocess.PIPE,
|
|
2125
|
+
stderr=asyncio.subprocess.PIPE,
|
|
2126
|
+
)
|
|
2127
|
+
try:
|
|
2128
|
+
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
|
|
2129
|
+
except (asyncio.TimeoutError, TimeoutError):
|
|
2130
|
+
proc.kill()
|
|
2131
|
+
with contextlib.suppress(Exception):
|
|
2132
|
+
await proc.communicate()
|
|
2133
|
+
return False
|
|
2134
|
+
except Exception:
|
|
2135
|
+
return False
|
|
2136
|
+
if proc.returncode != 0:
|
|
2137
|
+
return False
|
|
2138
|
+
diff_unset = False
|
|
2139
|
+
linguist_generated = False
|
|
2140
|
+
for line in stdout.decode(errors="replace").splitlines():
|
|
2141
|
+
# Format: "<path>: <attr>: <value>"
|
|
2142
|
+
parts = line.split(": ", 2)
|
|
2143
|
+
if len(parts) != 3:
|
|
2144
|
+
continue
|
|
2145
|
+
_, attr, value = parts
|
|
2146
|
+
if attr == "diff" and value == "unset":
|
|
2147
|
+
diff_unset = True
|
|
2148
|
+
elif attr == "linguist-generated" and value == "true":
|
|
2149
|
+
linguist_generated = True
|
|
2150
|
+
return diff_unset or linguist_generated
|
|
2151
|
+
|
|
2152
|
+
|
|
2050
2153
|
def _is_high_blast_radius(diff: str) -> str | None:
|
|
2051
2154
|
r"""Return the first changed path that matches a high-blast-radius pattern, or None.
|
|
2052
2155
|
|
|
@@ -2321,6 +2424,12 @@ async def _node_fetch_diff(state: ReviewState, config: RunnableConfig) -> dict[s
|
|
|
2321
2424
|
bench_config_changes.append(
|
|
2322
2425
|
f"added line modifying bench routing in {filename}: {line}"
|
|
2323
2426
|
)
|
|
2427
|
+
elif await _is_generated_nodiff_path(worktree_path, filename):
|
|
2428
|
+
logger.info(
|
|
2429
|
+
"Missing patch content for '%s' ignored for bench-config verification: "
|
|
2430
|
+
"repo declares it generated/non-diffable via .gitattributes",
|
|
2431
|
+
filename,
|
|
2432
|
+
)
|
|
2324
2433
|
elif not unconfirmed_reason and filename not in bench_config_changes:
|
|
2325
2434
|
unconfirmed_reason = (
|
|
2326
2435
|
f"Diff patch content missing or empty for '{filename}' in GitHub API "
|
|
@@ -13,6 +13,7 @@ Covers:
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import subprocess
|
|
16
17
|
from typing import Any
|
|
17
18
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
18
19
|
|
|
@@ -21,6 +22,7 @@ import pytest
|
|
|
21
22
|
from argus.graph import (
|
|
22
23
|
_detect_bench_config_routing_lines,
|
|
23
24
|
_is_bench_config_path,
|
|
25
|
+
_is_generated_nodiff_path,
|
|
24
26
|
_node_fetch_diff,
|
|
25
27
|
run_review,
|
|
26
28
|
)
|
|
@@ -607,6 +609,130 @@ class TestMultiRoundBenchConfigGuardRegressions:
|
|
|
607
609
|
assert fired is False
|
|
608
610
|
assert response.verdict == Verdict.APPROVE
|
|
609
611
|
|
|
612
|
+
@pytest.mark.asyncio
|
|
613
|
+
async def test_missing_patch_on_gitattributes_generated_file_does_not_fail_closed(
|
|
614
|
+
self, tmp_path: Any
|
|
615
|
+
) -> None:
|
|
616
|
+
"""A file the repo declares generated/non-diffable via .gitattributes must NOT fail
|
|
617
|
+
closed just because GitHub omitted its patch (e.g. a large Drizzle/ORM migration
|
|
618
|
+
snapshot)."""
|
|
619
|
+
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
|
|
620
|
+
(tmp_path / ".gitattributes").write_text(
|
|
621
|
+
"migrations/meta/*.json linguist-generated=true -diff\n"
|
|
622
|
+
)
|
|
623
|
+
generated_path = tmp_path / "migrations" / "meta"
|
|
624
|
+
generated_path.mkdir(parents=True)
|
|
625
|
+
(generated_path / "0001_snapshot.json").write_text("{}")
|
|
626
|
+
|
|
627
|
+
req = ReviewRequest(repo="org/repo", pr_number=42)
|
|
628
|
+
diff = (
|
|
629
|
+
"diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json\n"
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
mock_gh = MagicMock()
|
|
633
|
+
mock_gh.get_compare_files.return_value = (
|
|
634
|
+
[
|
|
635
|
+
{
|
|
636
|
+
"filename": "migrations/meta/0001_snapshot.json",
|
|
637
|
+
"status": "modified",
|
|
638
|
+
"patch": None,
|
|
639
|
+
}
|
|
640
|
+
],
|
|
641
|
+
False,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
state = {"request": req.model_dump()}
|
|
645
|
+
config: dict[str, Any] = {"configurable": {"worktree_path": str(tmp_path)}}
|
|
646
|
+
|
|
647
|
+
with (
|
|
648
|
+
patch("argus.graph._fetch_prior_review", new_callable=AsyncMock, return_value=None),
|
|
649
|
+
patch("argus.graph._fetch_dismissed_findings", new_callable=AsyncMock, return_value=[]),
|
|
650
|
+
patch(
|
|
651
|
+
"argus.graph._fetch_pr_diff_and_description",
|
|
652
|
+
new_callable=AsyncMock,
|
|
653
|
+
return_value=(diff, "desc", "head12345678", "main"),
|
|
654
|
+
),
|
|
655
|
+
patch(_GH_CLIENT_CLASS, return_value=mock_gh),
|
|
656
|
+
):
|
|
657
|
+
node_result = await _node_fetch_diff(state, config)
|
|
658
|
+
|
|
659
|
+
assert node_result["bench_config_unconfirmed"] is None
|
|
660
|
+
assert node_result["bench_config_changes"] == []
|
|
661
|
+
|
|
662
|
+
response = _make_response(verdict=Verdict.APPROVE)
|
|
663
|
+
fired = apply_bench_config_change_gate(
|
|
664
|
+
response, node_result["bench_config_changes"], node_result["bench_config_unconfirmed"]
|
|
665
|
+
)
|
|
666
|
+
assert fired is False
|
|
667
|
+
assert response.verdict == Verdict.APPROVE
|
|
668
|
+
|
|
669
|
+
@pytest.mark.asyncio
|
|
670
|
+
async def test_missing_patch_without_worktree_still_fails_closed(self) -> None:
|
|
671
|
+
"""No worktree_path (e.g. an older call path) means the gitattributes exemption
|
|
672
|
+
can't be checked, so the existing fail-closed behavior must still apply."""
|
|
673
|
+
req = ReviewRequest(repo="org/repo", pr_number=42)
|
|
674
|
+
diff = (
|
|
675
|
+
"diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json\n"
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
mock_gh = MagicMock()
|
|
679
|
+
mock_gh.get_compare_files.return_value = (
|
|
680
|
+
[
|
|
681
|
+
{
|
|
682
|
+
"filename": "migrations/meta/0001_snapshot.json",
|
|
683
|
+
"status": "modified",
|
|
684
|
+
"patch": None,
|
|
685
|
+
}
|
|
686
|
+
],
|
|
687
|
+
False,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
state = {"request": req.model_dump()}
|
|
691
|
+
config: dict[str, Any] = {"configurable": {}}
|
|
692
|
+
|
|
693
|
+
with (
|
|
694
|
+
patch("argus.graph._fetch_prior_review", new_callable=AsyncMock, return_value=None),
|
|
695
|
+
patch("argus.graph._fetch_dismissed_findings", new_callable=AsyncMock, return_value=[]),
|
|
696
|
+
patch(
|
|
697
|
+
"argus.graph._fetch_pr_diff_and_description",
|
|
698
|
+
new_callable=AsyncMock,
|
|
699
|
+
return_value=(diff, "desc", "head12345678", "main"),
|
|
700
|
+
),
|
|
701
|
+
patch(_GH_CLIENT_CLASS, return_value=mock_gh),
|
|
702
|
+
):
|
|
703
|
+
node_result = await _node_fetch_diff(state, config)
|
|
704
|
+
|
|
705
|
+
assert node_result["bench_config_unconfirmed"] is not None
|
|
706
|
+
assert "Diff patch content missing or empty" in node_result["bench_config_unconfirmed"]
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class TestIsGeneratedNodiffPath:
|
|
710
|
+
"""Unit tests for _is_generated_nodiff_path."""
|
|
711
|
+
|
|
712
|
+
@pytest.mark.asyncio
|
|
713
|
+
async def test_no_worktree_path_returns_false(self) -> None:
|
|
714
|
+
assert await _is_generated_nodiff_path(None, "some/file.json") is False
|
|
715
|
+
|
|
716
|
+
@pytest.mark.asyncio
|
|
717
|
+
async def test_linguist_generated_true_returns_true(self, tmp_path: Any) -> None:
|
|
718
|
+
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
|
|
719
|
+
(tmp_path / ".gitattributes").write_text("big.json linguist-generated=true -diff\n")
|
|
720
|
+
(tmp_path / "big.json").write_text("{}")
|
|
721
|
+
|
|
722
|
+
assert await _is_generated_nodiff_path(str(tmp_path), "big.json") is True
|
|
723
|
+
|
|
724
|
+
@pytest.mark.asyncio
|
|
725
|
+
async def test_unrelated_file_returns_false(self, tmp_path: Any) -> None:
|
|
726
|
+
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
|
|
727
|
+
(tmp_path / ".gitattributes").write_text("big.json linguist-generated=true -diff\n")
|
|
728
|
+
(tmp_path / "other.py").write_text("x = 1\n")
|
|
729
|
+
|
|
730
|
+
assert await _is_generated_nodiff_path(str(tmp_path), "other.py") is False
|
|
731
|
+
|
|
732
|
+
@pytest.mark.asyncio
|
|
733
|
+
async def test_non_git_directory_returns_false(self, tmp_path: Any) -> None:
|
|
734
|
+
assert await _is_generated_nodiff_path(str(tmp_path), "whatever.json") is False
|
|
735
|
+
|
|
610
736
|
|
|
611
737
|
class TestRunReviewBenchGuardIntegration:
|
|
612
738
|
"""Integration test for run_review executing the bench config change gate."""
|
|
@@ -7,6 +7,7 @@ cost-estimation surface.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import litellm
|
|
10
11
|
import pytest
|
|
11
12
|
|
|
12
13
|
from argus.llm.models import (
|
|
@@ -49,12 +50,13 @@ class TestPricingLookup:
|
|
|
49
50
|
|
|
50
51
|
def test_claude_default_pricing_rates(self) -> None:
|
|
51
52
|
"""Regression guard: claude-sonnet-5-5 rates in litellm must match
|
|
52
|
-
expected rates ($2/$10/$0.
|
|
53
|
+
expected rates ($2/$10/$0.10 per Mtok). Anthropic lowered the cache-read
|
|
54
|
+
rate from $0.20 to $0.10 on 2026-10-07 (litellm#45113)."""
|
|
53
55
|
cost = get_token_cost(ALIAS_MAP["claude-default"])
|
|
54
56
|
assert cost is not None
|
|
55
57
|
assert cost.input_cost_per_token == pytest.approx(2e-6)
|
|
56
58
|
assert cost.output_cost_per_token == pytest.approx(10e-6)
|
|
57
|
-
assert cost.cache_read_cost_per_token == pytest.approx(0.
|
|
59
|
+
assert cost.cache_read_cost_per_token == pytest.approx(0.1e-6)
|
|
58
60
|
|
|
59
61
|
def test_claude_opus_5_5_pricing_rates(self) -> None:
|
|
60
62
|
"""Regression guard: claude-opus-5-5 rates in litellm must match
|
|
@@ -85,13 +87,28 @@ class TestPricingLookup:
|
|
|
85
87
|
assert cost.cache_read_cost_per_token < cost.input_cost_per_token
|
|
86
88
|
|
|
87
89
|
|
|
90
|
+
@pytest.fixture
|
|
91
|
+
def fixed_claude_default_rates(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
92
|
+
"""litellm.model_cost is fetched live at import, so pin the row used by the
|
|
93
|
+
arithmetic tests; only the TestPricingLookup guards read live rates."""
|
|
94
|
+
row = {
|
|
95
|
+
"input_cost_per_token": 2e-6,
|
|
96
|
+
"output_cost_per_token": 10e-6,
|
|
97
|
+
"cache_read_input_token_cost": 0.1e-6,
|
|
98
|
+
"cache_creation_input_token_cost": 2.5e-6,
|
|
99
|
+
}
|
|
100
|
+
monkeypatch.setitem(litellm.model_cost, ALIAS_MAP["claude-default"], row)
|
|
101
|
+
|
|
102
|
+
|
|
88
103
|
class TestEstimateCostUsd:
|
|
104
|
+
@pytest.mark.usefixtures("fixed_claude_default_rates")
|
|
89
105
|
def test_computes_expected_cost_for_claude_default(self) -> None:
|
|
90
106
|
cost = estimate_cost_usd(
|
|
91
107
|
ALIAS_MAP["claude-default"], input_tokens=1_000_000, output_tokens=1_000_000
|
|
92
108
|
)
|
|
93
109
|
assert cost == pytest.approx(2.00 + 10.00)
|
|
94
110
|
|
|
111
|
+
@pytest.mark.usefixtures("fixed_claude_default_rates")
|
|
95
112
|
def test_cached_input_tokens_billed_separately_at_cache_read_rate(self) -> None:
|
|
96
113
|
"""cached_input_tokens is additive (billed at the cache-read rate),
|
|
97
114
|
not a subset subtracted from input_tokens."""
|
|
@@ -101,8 +118,9 @@ class TestEstimateCostUsd:
|
|
|
101
118
|
output_tokens=0,
|
|
102
119
|
cached_input_tokens=1_000_000,
|
|
103
120
|
)
|
|
104
|
-
assert cost == pytest.approx(2.00 + 0.
|
|
121
|
+
assert cost == pytest.approx(2.00 + 0.10)
|
|
105
122
|
|
|
123
|
+
@pytest.mark.usefixtures("fixed_claude_default_rates")
|
|
106
124
|
def test_cache_creation_tokens_billed(self) -> None:
|
|
107
125
|
"""cache_creation_tokens is billed at the cache-creation rate."""
|
|
108
126
|
cost = estimate_cost_usd(
|