claude-code-generator 0.4.12__tar.gz → 0.4.14__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.
- {claude_code_generator-0.4.12/src/claude_code_generator.egg-info → claude_code_generator-0.4.14}/PKG-INFO +1 -1
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/pyproject.toml +1 -1
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14/src/claude_code_generator.egg-info}/PKG-INFO +1 -1
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/__init__.py +1 -1
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/env.py +30 -10
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/_client_lifecycle.py +15 -4
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/cycle_loop.py +6 -2
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/ollama_budget.py +72 -49
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-6-test.md +13 -6
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/subprocess_runner.py +11 -1
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_ollama_budget.py +77 -28
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/LICENSE +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/README.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/setup.cfg +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/claude_code_generator.egg-info/SOURCES.txt +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/claude_code_generator.egg-info/dependency_links.txt +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/claude_code_generator.egg-info/entry_points.txt +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/claude_code_generator.egg-info/requires.txt +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/claude_code_generator.egg-info/top_level.txt +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/agents.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/cli.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/__init__.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_bench_io.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_crash_recovery.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_detect.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_dispatch.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_resume.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_validators.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/bench.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/bench_compare.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/bench_export.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/generate.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/init.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/optimize.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/review.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/status.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/effort.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/gh/__init__.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/gh/core.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/gh/issues.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/gh/labels.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/gh/milestones.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/git_ops.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/logging_setup.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/memory.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/__init__.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/_comments.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/_memory_writers.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/_phase5_precommit.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/cycle_prompts.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase0_complexity.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase1_plan.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase2_review.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase3_4_implement.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase5_closure.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase6_test.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase7_commit.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/preflight.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/__init__.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/hashes.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-cycle-specializer.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-optimize-requirements.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-0-complexity.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-1-planning.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-2-batch-review.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-2-issue-review.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-3-implementation.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-5-final-review.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-7-commit.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-review.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/repo_info.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/repomap.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/requirements_structure.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/__init__.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/_telemetry.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/batch.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/fake_runner.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/mcp.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/message_parsing.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/options.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/protocol.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/rate_limit.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/retry.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/sdk_runner.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/soft_reset.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/state_guard.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/types.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/utils.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/state.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/state_retention.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/__init__.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/angular.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/base.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/fastapi.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/finance.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/fullstack.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/nestjs.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/python-cli.md +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_agents.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_bench.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_bench_compare.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_bench_export.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_bench_fixture.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_bench_regression.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_changelog.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_claude_md.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_client_lifecycle.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_comments.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_commit_message.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_crash_recovery.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_cycle_loop.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_cycle_loop_multicycle.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_cycle_ollama_model.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_cycle_prompts.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_delta_planning.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_dependencies.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_detect.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_docs_no_default_max_turns.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_docs_ollama_model_guide.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_docs_ollama_pro.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_effective_model_routing.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_effort.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_env.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_generate.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_generate_ollama.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_generate_resume.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_gh.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_gh_labels.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_gh_milestones.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_gh_repo_threading.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_gh_submodules.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_git_ops.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_init.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_logging_setup.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_max_turns_cli_flag.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_mcp.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_memory.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_memory_writers.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_message_parsing.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_no_max_turns_in_call_sites.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_no_max_turns_literal.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_non_goals_grep_guard.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_ollama_rate_limit.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_optimize.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_options.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase0.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase1.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase2.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase2_batch.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase3_4.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase5.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase5_precommit.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase6.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase7.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase_mcp_regression.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase_token_logging.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_preflight.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_preflight_ollama.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_prompt_drift.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_prompt_prefix_snapshots.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_prompt_prefix_stability.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_prompts.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_rate_limit.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_repo_info.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_repomap.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_requirements_structure.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_retry.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_review.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_runner_protocol.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_runner_protocol_annotations.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_runner_types.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_runner_utils.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_sdk_runner.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_sdk_runner_shared.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_session_mode.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_state.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_state_guard.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_state_retention.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_status.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_subprocess_runner.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_telemetry.py +0 -0
- {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_version.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "claude-code-generator"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.14"
|
|
8
8
|
description = "Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -48,7 +48,17 @@ def strip_dangerous_env() -> None:
|
|
|
48
48
|
(``sdk_runner.run``, ``run_with_shared_client``).
|
|
49
49
|
"""
|
|
50
50
|
ollama_mode = _is_localhost_base_url(os.environ.get("ANTHROPIC_BASE_URL"))
|
|
51
|
-
|
|
51
|
+
# On the Ollama codepath, ANTHROPIC_AUTH_TOKEN carries the daemon token
|
|
52
|
+
# and must survive stripping. ANTHROPIC_API_KEY is also protected when it
|
|
53
|
+
# has been set to "" by assert_safe_environment_ollama() — the empty-string
|
|
54
|
+
# sentinel tells the SDK to accept non-Claude model tags. A non-empty
|
|
55
|
+
# ANTHROPIC_API_KEY (e.g. "sk-evil") is still stripped because it would
|
|
56
|
+
# route traffic to Anthropic's real API, defeating the localhost redirect.
|
|
57
|
+
protected: set[str] = set()
|
|
58
|
+
if ollama_mode:
|
|
59
|
+
protected.add("ANTHROPIC_AUTH_TOKEN")
|
|
60
|
+
if os.environ.get("ANTHROPIC_API_KEY") == "":
|
|
61
|
+
protected.add("ANTHROPIC_API_KEY")
|
|
52
62
|
for var in DANGEROUS_VARS:
|
|
53
63
|
if var in protected:
|
|
54
64
|
continue
|
|
@@ -116,7 +126,7 @@ def _require_ollama_preconditions() -> str:
|
|
|
116
126
|
|
|
117
127
|
Refuses the bypass when:
|
|
118
128
|
* ``OLLAMA_API_KEY`` is unset or empty.
|
|
119
|
-
* ``ANTHROPIC_BASE_URL`` is pre-set to
|
|
129
|
+
* ``ANTHROPIC_BASE_URL`` is pre-set to a non-localhost value.
|
|
120
130
|
|
|
121
131
|
A non-empty ``ANTHROPIC_API_KEY`` in the parent env is **not** a refusal:
|
|
122
132
|
``_build_ollama_env`` strips every ``ANTHROPIC_*`` var from the returned
|
|
@@ -132,10 +142,10 @@ def _require_ollama_preconditions() -> str:
|
|
|
132
142
|
"before running with provider='ollama'."
|
|
133
143
|
)
|
|
134
144
|
preset_base = os.environ.get("ANTHROPIC_BASE_URL")
|
|
135
|
-
if preset_base and preset_base
|
|
145
|
+
if preset_base and not _is_localhost_base_url(preset_base):
|
|
136
146
|
raise RuntimeError(
|
|
137
|
-
f"ANTHROPIC_BASE_URL={preset_base!r} does not
|
|
138
|
-
f"
|
|
147
|
+
f"ANTHROPIC_BASE_URL={preset_base!r} does not point at a local "
|
|
148
|
+
f"daemon. Unset it or point it at localhost/127.0.0.1."
|
|
139
149
|
)
|
|
140
150
|
return token
|
|
141
151
|
|
|
@@ -222,14 +232,24 @@ def assert_single_workspace(
|
|
|
222
232
|
)
|
|
223
233
|
|
|
224
234
|
|
|
235
|
+
_LOCALHOST_PREFIXES: tuple[str, ...] = (
|
|
236
|
+
"http://localhost:",
|
|
237
|
+
"http://127.0.0.1:",
|
|
238
|
+
"http://[::1]:",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
225
242
|
def _is_localhost_base_url(base_url: str | None) -> bool:
|
|
226
243
|
"""Return True when the ANTHROPIC_BASE_URL points at any localhost port.
|
|
227
244
|
|
|
228
|
-
Deliberately lenient —
|
|
229
|
-
|
|
230
|
-
Ollama preflight refusal
|
|
245
|
+
Deliberately lenient — ``http://localhost:*``, ``http://127.0.0.1:*``,
|
|
246
|
+
and ``http://[::1]:*`` all match — because the workspace invariant is
|
|
247
|
+
\"local endpoint\", not the strict :11434 the Ollama preflight refusal
|
|
248
|
+
gate enforces.
|
|
231
249
|
"""
|
|
232
|
-
|
|
250
|
+
if base_url is None:
|
|
251
|
+
return False
|
|
252
|
+
return any(base_url.startswith(prefix) for prefix in _LOCALHOST_PREFIXES)
|
|
233
253
|
|
|
234
254
|
|
|
235
255
|
def _log_localhost_short_circuit_once(state: dict[str, object]) -> None:
|
|
@@ -302,7 +322,7 @@ def assert_safe_environment_ollama() -> None:
|
|
|
302
322
|
preset_base = os.environ.get("ANTHROPIC_BASE_URL")
|
|
303
323
|
if not token:
|
|
304
324
|
return
|
|
305
|
-
if preset_base and preset_base
|
|
325
|
+
if preset_base and not _is_localhost_base_url(preset_base):
|
|
306
326
|
return
|
|
307
327
|
|
|
308
328
|
os.environ["ANTHROPIC_AUTH_TOKEN"] = token
|
|
@@ -92,12 +92,15 @@ def _open_cycle_client(options_template: Any) -> Any:
|
|
|
92
92
|
return ClaudeSDKClient(options=options_template)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
def _build_cycle_options(project_dir: Path) -> Any:
|
|
95
|
+
def _build_cycle_options(project_dir: Path, effective_model: str | None = None) -> Any:
|
|
96
96
|
"""Return cycle-scoped options for the shared ``ClaudeSDKClient``.
|
|
97
97
|
|
|
98
98
|
``ClaudeSDKClient`` locks options at open-time — per-query changes do not
|
|
99
99
|
apply. The shared client therefore opens with:
|
|
100
100
|
|
|
101
|
+
- ``model``: the effective Ollama model tag (when on the Ollama codepath),
|
|
102
|
+
or ``None`` to let the SDK default (Anthropic Max path). Must be set at
|
|
103
|
+
open-time because per-query overrides are silently ignored.
|
|
101
104
|
- ``system_prompt``: canonical ``claude_code`` preset with
|
|
102
105
|
``exclude_dynamic_sections=True`` (§2 cache-safe prefix).
|
|
103
106
|
- ``extra_headers``: default betas (extended-cache-ttl + token-efficient-tools).
|
|
@@ -114,6 +117,9 @@ def _build_cycle_options(project_dir: Path) -> Any:
|
|
|
114
117
|
|
|
115
118
|
Args:
|
|
116
119
|
project_dir: Project root; used to resolve MCP servers.
|
|
120
|
+
effective_model: Ollama model tag to lock into the shared client
|
|
121
|
+
(threaded from cycle_loop). When ``None``, the SDK default
|
|
122
|
+
(Anthropic Max path) is used.
|
|
117
123
|
|
|
118
124
|
Returns:
|
|
119
125
|
A ``ClaudeAgentOptions`` instance (or duck-typed fallback) suitable
|
|
@@ -121,15 +127,18 @@ def _build_cycle_options(project_dir: Path) -> Any:
|
|
|
121
127
|
"""
|
|
122
128
|
mcp_servers = build_mcp_servers(project_dir)
|
|
123
129
|
allowed_tools: list[str] = list(_SHARED_CLIENT_ALLOWED_TOOLS) + mcp_tool_wildcards(mcp_servers)
|
|
124
|
-
|
|
130
|
+
kwargs: dict[str, Any] = dict(
|
|
125
131
|
allowed_tools=allowed_tools,
|
|
126
132
|
permission_mode="bypassPermissions",
|
|
127
133
|
mcp_servers=mcp_servers,
|
|
128
134
|
)
|
|
135
|
+
if effective_model is not None:
|
|
136
|
+
kwargs["model"] = effective_model
|
|
137
|
+
return make_agent_options(**kwargs)
|
|
129
138
|
|
|
130
139
|
|
|
131
140
|
@contextlib.asynccontextmanager
|
|
132
|
-
async def managed_shared_client(state: State, state_path: Path): # type: ignore[return]
|
|
141
|
+
async def managed_shared_client(state: State, state_path: Path, effective_model: str | None = None): # type: ignore[return]
|
|
133
142
|
"""Async context manager owning the full shared-client lifecycle.
|
|
134
143
|
|
|
135
144
|
On entry: strips dangerous env vars (non-negotiable #1), opens one
|
|
@@ -151,6 +160,8 @@ async def managed_shared_client(state: State, state_path: Path): # type: ignore
|
|
|
151
160
|
Args:
|
|
152
161
|
state: Root state (mutated by mark/clear helpers).
|
|
153
162
|
state_path: Path to ``state.json`` for atomic persistence.
|
|
163
|
+
effective_model: Ollama model tag to lock into the shared client.
|
|
164
|
+
When ``None``, the SDK default (Anthropic Max path) is used.
|
|
154
165
|
|
|
155
166
|
Yields:
|
|
156
167
|
The open ``ClaudeSDKClient`` instance.
|
|
@@ -158,7 +169,7 @@ async def managed_shared_client(state: State, state_path: Path): # type: ignore
|
|
|
158
169
|
_env.strip_dangerous_env()
|
|
159
170
|
# state.json lives at {project_dir}/.code-generator/state.json.
|
|
160
171
|
project_dir = state_path.parent.parent
|
|
161
|
-
options = _build_cycle_options(project_dir)
|
|
172
|
+
options = _build_cycle_options(project_dir, effective_model=effective_model)
|
|
162
173
|
async with _open_cycle_client(options) as client:
|
|
163
174
|
_state.mark_client_alive(state, state_path)
|
|
164
175
|
try:
|
|
@@ -435,7 +435,9 @@ async def run_single_mode(
|
|
|
435
435
|
state_path = project_dir / ".code-generator" / "state.json"
|
|
436
436
|
|
|
437
437
|
if state.session_mode in ("shared", "batch"):
|
|
438
|
-
async with managed_shared_client(
|
|
438
|
+
async with managed_shared_client(
|
|
439
|
+
state, state_path, effective_model=effective_model
|
|
440
|
+
) as client:
|
|
439
441
|
await _run_phases(
|
|
440
442
|
state,
|
|
441
443
|
None,
|
|
@@ -665,7 +667,9 @@ async def _run_cycle_phases(
|
|
|
665
667
|
log_prefix = f"cycle{cycle.id}_"
|
|
666
668
|
|
|
667
669
|
if state.session_mode in ("shared", "batch"):
|
|
668
|
-
async with managed_shared_client(
|
|
670
|
+
async with managed_shared_client(
|
|
671
|
+
state, state_path, effective_model=effective_model
|
|
672
|
+
) as client:
|
|
669
673
|
await _run_phases(
|
|
670
674
|
state,
|
|
671
675
|
cycle,
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
"""Per-cycle safety backstop for the Ollama codepath.
|
|
2
2
|
|
|
3
|
-
The pre-0.4.11 design
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
already lives in two places and triggers on real malfunctions, not on an
|
|
7
|
-
arbitrary counter:
|
|
3
|
+
The pre-0.4.11 design aborted cycles on two arbitrary counters — 200 turns
|
|
4
|
+
and 1 h wall-clock. That's the wrong layer: fault-detection already lives
|
|
5
|
+
in two places that trigger on real malfunctions, not on counters:
|
|
8
6
|
|
|
9
7
|
* :class:`~code_generator.runner.retry.CircuitBreaker` — trips after ``N``
|
|
10
8
|
consecutive failures on a single phase call. Already wrapped around
|
|
@@ -13,29 +11,27 @@ arbitrary counter:
|
|
|
13
11
|
* :func:`~code_generator.runner.rate_limit.handle_ollama_429` —
|
|
14
12
|
wait-and-resume on 429s returned by the Ollama daemon.
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
Starting with 0.4.13 both thresholds are **soft warnings** — the module
|
|
15
|
+
never aborts the pipeline. Weak open models are slow AND chatty; letting
|
|
16
|
+
them run is the right call. The operator can always Ctrl-C a runaway.
|
|
17
17
|
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* The wall-clock cap remains a **hard abort**, but the default is raised
|
|
23
|
-
to 4 hours. It exists purely to catch a stuck daemon or a model trapped
|
|
24
|
-
in a pathological loop the CircuitBreaker cannot see (e.g. infinite
|
|
25
|
-
``end_turn``→``continue`` cycle producing no tool calls).
|
|
18
|
+
* ``OLLAMA_SOFT_TURN_WARN`` (int, positive; default 500). Emitted once
|
|
19
|
+
per cycle when cumulative ``num_turns`` crosses the threshold.
|
|
20
|
+
* ``OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS`` (int, positive; default 14400).
|
|
21
|
+
Emitted once per cycle when elapsed wall-clock crosses the threshold.
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
Backwards-compatible shims: the legacy env names ``OLLAMA_TURN_BUDGET`` and
|
|
24
|
+
``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` remain honoured and map onto the new
|
|
25
|
+
soft-warn thresholds. Scripts that previously relied on the abort now see
|
|
26
|
+
a WARNING instead; the 0.4.11 changelog entry called this out for the turn
|
|
27
|
+
budget, and 0.4.13 extends the same semantics to wall-clock.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Backwards-compatible shim: the old ``OLLAMA_TURN_BUDGET`` env variable is
|
|
33
|
-
still honoured and maps onto the soft-warn threshold, so operators with
|
|
34
|
-
existing scripts see no behaviour change beyond the abort becoming a
|
|
35
|
-
warning.
|
|
29
|
+
:class:`OllamaBudgetExceeded` is retained only as a concrete exception
|
|
30
|
+
type for backwards compatibility with callers that ``except`` it — it is
|
|
31
|
+
no longer raised by this module.
|
|
36
32
|
|
|
37
33
|
Nothing is persisted in ``state.json`` — the tracker is per-run and
|
|
38
|
-
discarded on
|
|
34
|
+
discarded on clean completion.
|
|
39
35
|
"""
|
|
40
36
|
|
|
41
37
|
from __future__ import annotations
|
|
@@ -57,7 +53,7 @@ _logger = logging.getLogger(__name__)
|
|
|
57
53
|
# ---------------------------------------------------------------------------
|
|
58
54
|
|
|
59
55
|
_DEFAULT_SOFT_TURN_WARN = 500
|
|
60
|
-
|
|
56
|
+
_DEFAULT_WALLCLOCK_SOFT_WARN_SECONDS = 14400 # 4 h
|
|
61
57
|
|
|
62
58
|
|
|
63
59
|
def _read_int_env(name: str, default: int) -> int:
|
|
@@ -100,17 +96,31 @@ aborts.
|
|
|
100
96
|
OLLAMA_TURN_BUDGET = OLLAMA_SOFT_TURN_WARN
|
|
101
97
|
"""Backwards-compatible alias for :data:`OLLAMA_SOFT_TURN_WARN`."""
|
|
102
98
|
|
|
103
|
-
|
|
104
|
-
"OLLAMA_WALLCLOCK_BUDGET_SECONDS"
|
|
105
|
-
)
|
|
106
|
-
|
|
99
|
+
def _resolve_wallclock_soft_warn() -> int:
|
|
100
|
+
"""Honour legacy ``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` env var for backwards compat."""
|
|
101
|
+
legacy = _read_int_env("OLLAMA_WALLCLOCK_BUDGET_SECONDS", 0)
|
|
102
|
+
if legacy:
|
|
103
|
+
return legacy
|
|
104
|
+
return _read_int_env(
|
|
105
|
+
"OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS", _DEFAULT_WALLCLOCK_SOFT_WARN_SECONDS
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS = _resolve_wallclock_soft_warn()
|
|
110
|
+
"""Soft warning threshold on per-cycle wall-clock elapsed (seconds).
|
|
107
111
|
|
|
108
|
-
Defaults to 14400 (4 h)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
Defaults to 14400 (4 h). Override via ``OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS``
|
|
113
|
+
(new name) or the legacy ``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` (preserved for
|
|
114
|
+
backwards compatibility). The value is **non-blocking**: the pipeline only
|
|
115
|
+
logs a WARNING once per cycle when elapsed first crosses this threshold.
|
|
116
|
+
It never aborts.
|
|
112
117
|
"""
|
|
113
118
|
|
|
119
|
+
# Kept as a module-level alias so existing importers (tests, scripts) keep
|
|
120
|
+
# working. The semantics are now "soft warning threshold", not "abort".
|
|
121
|
+
OLLAMA_WALLCLOCK_BUDGET_SECONDS = OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS
|
|
122
|
+
"""Backwards-compatible alias for :data:`OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS`."""
|
|
123
|
+
|
|
114
124
|
|
|
115
125
|
# ---------------------------------------------------------------------------
|
|
116
126
|
# Exception
|
|
@@ -118,11 +128,12 @@ stuck daemon or a pathological loop the per-phase
|
|
|
118
128
|
|
|
119
129
|
|
|
120
130
|
class OllamaBudgetExceeded(RuntimeError):
|
|
121
|
-
"""
|
|
131
|
+
"""Retained only for backwards compatibility with ``except`` clauses.
|
|
122
132
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
in
|
|
133
|
+
As of 0.4.13 this module never raises ``OllamaBudgetExceeded``. Both the
|
|
134
|
+
turn counter and the wall-clock are non-blocking soft WARNINGs. Real
|
|
135
|
+
per-call failures are handled by the ``CircuitBreaker`` in
|
|
136
|
+
:mod:`code_generator.runner.retry`.
|
|
126
137
|
|
|
127
138
|
Subclasses ``RuntimeError`` to match the existing safety-abort hierarchy
|
|
128
139
|
(e.g. :class:`~code_generator.runner.types.OverageAbort`).
|
|
@@ -137,9 +148,8 @@ class OllamaBudgetExceeded(RuntimeError):
|
|
|
137
148
|
class OllamaBudgetTracker:
|
|
138
149
|
"""Adaptive per-cycle safety backstop; a no-op on the Anthropic Max path.
|
|
139
150
|
|
|
140
|
-
Emits one WARNING
|
|
141
|
-
|
|
142
|
-
turn count — real failures are the responsibility of
|
|
151
|
+
Emits at most one WARNING per threshold (turn count, wall-clock). **Never
|
|
152
|
+
aborts**. Real failures are the responsibility of
|
|
143
153
|
:class:`~code_generator.runner.retry.CircuitBreaker` and the rate-limit
|
|
144
154
|
handlers in :mod:`code_generator.runner.rate_limit`.
|
|
145
155
|
"""
|
|
@@ -162,6 +172,7 @@ class OllamaBudgetTracker:
|
|
|
162
172
|
self._clock = clock or time.monotonic
|
|
163
173
|
self._start_time: float | None = None
|
|
164
174
|
self._turn_warning_emitted = False
|
|
175
|
+
self._wallclock_warning_emitted = False
|
|
165
176
|
|
|
166
177
|
def start(self) -> None:
|
|
167
178
|
"""Record the cycle start time. Idempotent; only the first call matters."""
|
|
@@ -169,11 +180,11 @@ class OllamaBudgetTracker:
|
|
|
169
180
|
self._start_time = self._clock()
|
|
170
181
|
|
|
171
182
|
def check(self, state: State, cycle: CycleState | None) -> None:
|
|
172
|
-
"""Warn on
|
|
183
|
+
"""Warn on either threshold; never raise."""
|
|
173
184
|
if not self._active:
|
|
174
185
|
return
|
|
175
186
|
self._check_turn_soft_warn(state, cycle)
|
|
176
|
-
self.
|
|
187
|
+
self._check_wallclock_soft_warn()
|
|
177
188
|
|
|
178
189
|
def _check_turn_soft_warn(self, state: State, cycle: CycleState | None) -> None:
|
|
179
190
|
"""Emit one WARNING the first time the cycle crosses the soft threshold.
|
|
@@ -196,17 +207,29 @@ class OllamaBudgetTracker:
|
|
|
196
207
|
)
|
|
197
208
|
self._turn_warning_emitted = True
|
|
198
209
|
|
|
199
|
-
def
|
|
200
|
-
|
|
210
|
+
def _check_wallclock_soft_warn(self) -> None:
|
|
211
|
+
"""Emit one WARNING the first time the cycle crosses the wall-clock threshold.
|
|
212
|
+
|
|
213
|
+
Never raises. As of 0.4.13 the wall-clock ceiling is advisory: a
|
|
214
|
+
genuinely stuck daemon will be caught by the per-phase
|
|
215
|
+
CircuitBreaker or by the operator's Ctrl-C; aborting a live session
|
|
216
|
+
on a counter wastes hours of work.
|
|
217
|
+
"""
|
|
218
|
+
if self._wallclock_warning_emitted or self._start_time is None:
|
|
201
219
|
return
|
|
202
220
|
elapsed = self._clock() - self._start_time
|
|
203
|
-
if elapsed >
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"
|
|
208
|
-
"
|
|
221
|
+
if elapsed > OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS:
|
|
222
|
+
_logger.warning(
|
|
223
|
+
"Ollama cycle has been running for %.0fs (soft threshold: "
|
|
224
|
+
"%ds). Letting it run — real stalls are caught by the "
|
|
225
|
+
"per-phase CircuitBreaker in runner/retry.py. Raise the "
|
|
226
|
+
"threshold via OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS or the "
|
|
227
|
+
"legacy OLLAMA_WALLCLOCK_BUDGET_SECONDS env var to silence "
|
|
228
|
+
"this warning. Use Ctrl-C to stop a truly runaway cycle.",
|
|
229
|
+
elapsed,
|
|
230
|
+
OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS,
|
|
209
231
|
)
|
|
232
|
+
self._wallclock_warning_emitted = True
|
|
210
233
|
|
|
211
234
|
|
|
212
235
|
def _sum_num_turns(state: State, cycle: CycleState | None) -> int:
|
|
@@ -18,16 +18,22 @@ You are a senior engineer specialized in testing. Your task is to run the projec
|
|
|
18
18
|
- `Cargo.toml` → Rust/cargo test
|
|
19
19
|
- `go.mod` → Go test
|
|
20
20
|
|
|
21
|
-
2. **Run the full test suite
|
|
21
|
+
2. **Run the full test suite with concise output** (see Constraints):
|
|
22
22
|
```bash
|
|
23
|
-
# Adapt to the detected framework
|
|
24
|
-
pytest -
|
|
25
|
-
npm test -- --run
|
|
23
|
+
# Adapt to the detected framework.
|
|
24
|
+
pytest -q --tb=line # Python — quiet, one-line tracebacks
|
|
25
|
+
npm test -- --run --reporter=default # Vitest
|
|
26
26
|
npm test # Jest/Angular
|
|
27
|
-
cargo test --all
|
|
28
|
-
go test ./...
|
|
27
|
+
cargo test --all --quiet # Rust
|
|
28
|
+
go test ./... 2>&1 | tail -200 # Go — only the last 200 lines
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
**Do not use `-v`, `-vv`, `-s`, or any other verbose/stream flag.** A
|
|
32
|
+
tool result returned to the model cannot exceed roughly 1 MB — verbose
|
|
33
|
+
pytest output on a medium-sized project blows past that limit and
|
|
34
|
+
crashes the SDK stream reader mid-cycle. Start quiet; escalate only the
|
|
35
|
+
individual failing test with `-v` after you have the list of failures.
|
|
36
|
+
|
|
31
37
|
3. **If all tests pass on the first attempt:**
|
|
32
38
|
- Also run any available linters/type checkers (`mypy`, `ruff`, `eslint`, `tsc --noEmit`)
|
|
33
39
|
- Collect test coverage if the framework supports it
|
|
@@ -98,6 +104,7 @@ You are a senior engineer specialized in testing. Your task is to run the projec
|
|
|
98
104
|
- **Do not ask the user for confirmation**: act autonomously in YOLO mode.
|
|
99
105
|
- **If you find flakiness** (a test that passes/fails non-deterministically), do not ignore it: document the flaky behavior in the bug issue.
|
|
100
106
|
- **Environment variables already available globally**: `GITHUB_TOKEN`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`, `OLLAMA_API_KEY`, `OLLAMA_BASE_URL`. If a test fails because "an API key is missing" among these, the cause is **not** the missing key — check the variable name, the `.env` loading, or an explicit override in the test. Do not add dummy keys as a fix. For tests that make real calls and are slow/expensive, use mocking/VCR cassettes instead of disabling them.
|
|
107
|
+
- **Tool-result size ceiling.** Bash tool-results larger than ~1 MB crash the SDK stream reader. Always prefer `-q`/`--tb=line`/`--quiet` over `-v`/`-vv`/`-s`. If a command genuinely produces more than 1 MB of output, tee it to a file (`cmd > /tmp/out.log 2>&1 || true`) and then `Read` or `Grep` the file — never let the full output flow back through a single tool result.
|
|
101
108
|
|
|
102
109
|
---
|
|
103
110
|
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
import asyncio
|
|
14
14
|
import contextlib
|
|
15
15
|
import json
|
|
16
|
+
import os
|
|
16
17
|
import subprocess
|
|
17
18
|
import time
|
|
18
19
|
from typing import TYPE_CHECKING, Any
|
|
@@ -313,7 +314,16 @@ async def run(
|
|
|
313
314
|
"CLI session starting (model=%s effort=%s max_turns=%s).", model, effort_display, max_turns
|
|
314
315
|
)
|
|
315
316
|
|
|
316
|
-
|
|
317
|
+
# Detect Ollama mode so build_agent_env() preserves the scoped localhost
|
|
318
|
+
# routing (ANTHROPIC_API_KEY="" + pinned BASE_URL). Without provider="ollama"
|
|
319
|
+
# the default "anthropic-max" path omits ANTHROPIC_API_KEY, which causes
|
|
320
|
+
# the CLI to reject non-Claude model tags or fall back to real credentials.
|
|
321
|
+
_provider: _env.Provider = (
|
|
322
|
+
"ollama"
|
|
323
|
+
if _env._is_localhost_base_url(os.environ.get("ANTHROPIC_BASE_URL"))
|
|
324
|
+
else "anthropic-max"
|
|
325
|
+
)
|
|
326
|
+
safe_env = _env.build_agent_env(provider=_provider)
|
|
317
327
|
|
|
318
328
|
text_parts: list[str] = []
|
|
319
329
|
result_holder: dict[str, Any] = {
|
|
@@ -1,30 +1,35 @@
|
|
|
1
|
-
"""Tests for the Ollama per-cycle adaptive safety backstop (
|
|
1
|
+
"""Tests for the Ollama per-cycle adaptive safety backstop (0.4.13).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
pipeline is never aborted on
|
|
5
|
-
the job of :class:`~code_generator.runner.retry.CircuitBreaker`
|
|
6
|
-
rate-limit handlers.
|
|
3
|
+
Both the turn counter and the wall-clock are **non-blocking soft warnings**
|
|
4
|
+
— the pipeline is never aborted on either threshold alone. Real per-call
|
|
5
|
+
failures are the job of :class:`~code_generator.runner.retry.CircuitBreaker`
|
|
6
|
+
and the rate-limit handlers.
|
|
7
7
|
|
|
8
|
-
Backwards compatibility: ``OLLAMA_TURN_BUDGET``
|
|
9
|
-
|
|
8
|
+
Backwards compatibility: ``OLLAMA_TURN_BUDGET`` and
|
|
9
|
+
``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` are preserved as aliases for
|
|
10
|
+
:data:`OLLAMA_SOFT_TURN_WARN` and :data:`OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS`
|
|
11
|
+
respectively, so existing scripts keep importing.
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
from __future__ import annotations
|
|
13
15
|
|
|
14
16
|
import logging
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
17
18
|
|
|
18
19
|
from code_generator import state as _state
|
|
19
20
|
from code_generator.orchestrator.ollama_budget import (
|
|
20
21
|
OLLAMA_SOFT_TURN_WARN,
|
|
21
22
|
OLLAMA_TURN_BUDGET,
|
|
22
23
|
OLLAMA_WALLCLOCK_BUDGET_SECONDS,
|
|
24
|
+
OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS,
|
|
23
25
|
OllamaBudgetExceeded,
|
|
24
26
|
OllamaBudgetTracker,
|
|
25
27
|
)
|
|
26
28
|
from code_generator.runner.types import TokenUsage
|
|
27
29
|
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
import pytest
|
|
32
|
+
|
|
28
33
|
# ---------------------------------------------------------------------------
|
|
29
34
|
# Constants
|
|
30
35
|
# ---------------------------------------------------------------------------
|
|
@@ -38,8 +43,12 @@ class TestThresholdConstants:
|
|
|
38
43
|
"""Legacy ``OLLAMA_TURN_BUDGET`` must alias the new soft-warn constant."""
|
|
39
44
|
assert OLLAMA_TURN_BUDGET == OLLAMA_SOFT_TURN_WARN
|
|
40
45
|
|
|
41
|
-
def
|
|
42
|
-
assert
|
|
46
|
+
def test_wallclock_soft_warn_default_is_4_hours(self) -> None:
|
|
47
|
+
assert OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS == 14400
|
|
48
|
+
|
|
49
|
+
def test_wallclock_budget_alias_matches_soft_warn(self) -> None:
|
|
50
|
+
"""Legacy ``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` must alias the soft-warn constant."""
|
|
51
|
+
assert OLLAMA_WALLCLOCK_BUDGET_SECONDS == OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS
|
|
43
52
|
|
|
44
53
|
|
|
45
54
|
# ---------------------------------------------------------------------------
|
|
@@ -149,9 +158,9 @@ class TestTurnSoftWarning:
|
|
|
149
158
|
# ---------------------------------------------------------------------------
|
|
150
159
|
|
|
151
160
|
|
|
152
|
-
class
|
|
153
|
-
def
|
|
154
|
-
"""Elapsed <
|
|
161
|
+
class TestWallclockSoftWarning:
|
|
162
|
+
def test_under_threshold_does_not_warn(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
163
|
+
"""Elapsed < threshold → no WARNING emitted, no raise."""
|
|
155
164
|
st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
|
|
156
165
|
now = 1_000_000.0
|
|
157
166
|
tracker = OllamaBudgetTracker(
|
|
@@ -159,12 +168,18 @@ class TestWallclockBudget:
|
|
|
159
168
|
clock=lambda: now,
|
|
160
169
|
)
|
|
161
170
|
tracker.start()
|
|
162
|
-
now +=
|
|
171
|
+
now += OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS - 10
|
|
163
172
|
|
|
164
|
-
|
|
173
|
+
with caplog.at_level(logging.WARNING):
|
|
174
|
+
tracker.check(st, cycle)
|
|
175
|
+
|
|
176
|
+
wallclock_warnings = [r for r in caplog.records if "running for" in r.message.lower()]
|
|
177
|
+
assert wallclock_warnings == []
|
|
165
178
|
|
|
166
|
-
def
|
|
167
|
-
|
|
179
|
+
def test_over_threshold_warns_without_raising(
|
|
180
|
+
self, caplog: pytest.LogCaptureFixture
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Crossing the threshold logs a WARNING; the pipeline continues."""
|
|
168
183
|
st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
|
|
169
184
|
t = [1_000_000.0]
|
|
170
185
|
|
|
@@ -173,14 +188,32 @@ class TestWallclockBudget:
|
|
|
173
188
|
|
|
174
189
|
tracker = OllamaBudgetTracker(provider_is_ollama=True, clock=_clock)
|
|
175
190
|
tracker.start()
|
|
176
|
-
t[0] +=
|
|
191
|
+
t[0] += OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS + 1
|
|
192
|
+
|
|
193
|
+
with caplog.at_level(logging.WARNING):
|
|
194
|
+
tracker.check(st, cycle) # must not raise
|
|
195
|
+
|
|
196
|
+
wallclock_warnings = [r for r in caplog.records if "running for" in r.message.lower()]
|
|
197
|
+
assert len(wallclock_warnings) == 1
|
|
198
|
+
assert str(OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS) in wallclock_warnings[0].message
|
|
199
|
+
|
|
200
|
+
def test_warning_is_emitted_only_once_per_cycle(
|
|
201
|
+
self, caplog: pytest.LogCaptureFixture
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Subsequent checks after the first wall-clock warning must stay silent."""
|
|
204
|
+
st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
|
|
205
|
+
t = [1_000_000.0]
|
|
206
|
+
tracker = OllamaBudgetTracker(provider_is_ollama=True, clock=lambda: t[0])
|
|
207
|
+
tracker.start()
|
|
208
|
+
t[0] += OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS + 1
|
|
177
209
|
|
|
178
|
-
with
|
|
210
|
+
with caplog.at_level(logging.WARNING):
|
|
211
|
+
tracker.check(st, cycle)
|
|
212
|
+
tracker.check(st, cycle)
|
|
179
213
|
tracker.check(st, cycle)
|
|
180
214
|
|
|
181
|
-
|
|
182
|
-
assert
|
|
183
|
-
assert str(OLLAMA_WALLCLOCK_BUDGET_SECONDS) in msg
|
|
215
|
+
wallclock_warnings = [r for r in caplog.records if "running for" in r.message.lower()]
|
|
216
|
+
assert len(wallclock_warnings) == 1
|
|
184
217
|
|
|
185
218
|
def test_start_is_required_before_check(self) -> None:
|
|
186
219
|
"""check() without start() returns immediately on wall-clock (None start)."""
|
|
@@ -190,7 +223,7 @@ class TestWallclockBudget:
|
|
|
190
223
|
tracker.check(st, cycle) # must not raise
|
|
191
224
|
|
|
192
225
|
def test_extreme_turn_count_does_not_raise(self) -> None:
|
|
193
|
-
"""
|
|
226
|
+
"""Neither counter should raise on its own — both are soft warnings now."""
|
|
194
227
|
st, cycle = _make_state_with_usage(
|
|
195
228
|
{}, cycle_turns={"phase3_4": OLLAMA_SOFT_TURN_WARN * 100}
|
|
196
229
|
)
|
|
@@ -198,6 +231,16 @@ class TestWallclockBudget:
|
|
|
198
231
|
|
|
199
232
|
tracker.check(st, cycle) # must not raise
|
|
200
233
|
|
|
234
|
+
def test_extreme_wallclock_does_not_raise(self) -> None:
|
|
235
|
+
"""Wall-clock far past threshold must not raise either (0.4.13)."""
|
|
236
|
+
st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
|
|
237
|
+
t = [1_000_000.0]
|
|
238
|
+
tracker = OllamaBudgetTracker(provider_is_ollama=True, clock=lambda: t[0])
|
|
239
|
+
tracker.start()
|
|
240
|
+
t[0] += OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS * 10 # 40 h
|
|
241
|
+
|
|
242
|
+
tracker.check(st, cycle) # must not raise
|
|
243
|
+
|
|
201
244
|
|
|
202
245
|
# ---------------------------------------------------------------------------
|
|
203
246
|
# Anthropic Max path — thresholds do not fire
|
|
@@ -218,15 +261,21 @@ class TestAnthropicMaxUntouched:
|
|
|
218
261
|
turn_warnings = [r for r in caplog.records if "consumed" in r.message.lower()]
|
|
219
262
|
assert turn_warnings == []
|
|
220
263
|
|
|
221
|
-
def
|
|
222
|
-
|
|
264
|
+
def test_anthropic_max_mode_skips_wallclock_warning(
|
|
265
|
+
self, caplog: pytest.LogCaptureFixture
|
|
266
|
+
) -> None:
|
|
267
|
+
"""provider_is_ollama=False → no WARNING, no raise."""
|
|
223
268
|
st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
|
|
224
269
|
t = [1_000_000.0]
|
|
225
270
|
tracker = OllamaBudgetTracker(provider_is_ollama=False, clock=lambda: t[0])
|
|
226
271
|
tracker.start()
|
|
227
|
-
t[0] +=
|
|
272
|
+
t[0] += OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS + 100
|
|
228
273
|
|
|
229
|
-
|
|
274
|
+
with caplog.at_level(logging.WARNING):
|
|
275
|
+
tracker.check(st, cycle)
|
|
276
|
+
|
|
277
|
+
wallclock_warnings = [r for r in caplog.records if "running for" in r.message.lower()]
|
|
278
|
+
assert wallclock_warnings == []
|
|
230
279
|
|
|
231
280
|
|
|
232
281
|
# ---------------------------------------------------------------------------
|
|
File without changes
|