coderouter-cli 2.7.7__tar.gz → 2.7.8__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.
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/CHANGELOG.md +88 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/PKG-INFO +1 -1
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/agent_cli.py +284 -95
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/config/schemas.py +44 -8
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/backend_health.py +63 -1
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/logging.py +60 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/fallback.py +37 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/README.en.md +1 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/README.md +1 -0
- coderouter_cli-2.7.8/docs/backends/external-agents.en.md +233 -0
- coderouter_cli-2.7.8/docs/backends/external-agents.md +233 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/designs/external-agents-adapter.md +20 -3
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers-agent-cli.yaml +71 -44
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/pyproject.toml +1 -1
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_agent_cli.py +357 -18
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_backend_health.py +182 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mE_config_validation.py +41 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/.gitignore +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/LICENSE +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/README.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/README.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/cli.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/cost.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/errors.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/launcher_routes.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/language_tax.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/launcher_speculative.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/output_filters.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/gguf_dl.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/hf-ollama-models.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/launcher-quickstart.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/launcher.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/launcher.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/architecture.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/context-budget.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/continuous-probing.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/drift-detection.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/partial-stitch.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/language-tax.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/language-tax.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/remote-access.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/remote-access.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/security.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/low-memory-integration.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/.env.example +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/README.md +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers-multiagent.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.context-budget-test.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.llamacpp-vllm.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.ollama-auto-custom.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.ollama-auto.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.ollama-free-chain.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/examples/providers.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/__init__.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/conftest.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_auto_router_cjk.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_budget.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_capability.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_capability_tool_choice.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_cli.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_cli_bind_warning.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_config.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_config_shim_actions.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_errors.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_cache_strip.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_empty_response.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fallback_tool_choice_emulate.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h1_prometheus_drift.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h2_h6_translation.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h3_client_reuse.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h4_h5_guards.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h7_probe_restore.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_h8_launcher_auth.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mA_routing.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mB_adapters.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mC_convert.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mD_logging_metrics.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_fix_mF_ingress.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_ingress_count_tokens.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_language_tax.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_language_tax_integration.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_launcher_mtp.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_launcher_mtp_fallback.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_launcher_provider_sync.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_models_passthrough.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_output_filters.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_repair_byte_fallback.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_role_normalization.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_tokens_saved_metric.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_tool_repair_lenient.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_tool_repair_r4.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_toolrepair_bench.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.7.7 → coderouter_cli-2.7.8}/tests/test_translation_reverse.py +0 -0
|
@@ -9,6 +9,94 @@ are kept verbatim where the Japanese text itself is the subject).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [v2.7.8] — 2026-07-10 (agent_cli Phase 1d: grok)
|
|
13
|
+
|
|
14
|
+
Adds the second target to the `agent_cli` adapter: `agent: grok` (the xAI
|
|
15
|
+
Grok CLI) is now implemented alongside `claude` from Phase 1a. The
|
|
16
|
+
implementation was verified against the real CLI (grok v0.2.93, [stable]
|
|
17
|
+
channel, 2026-07-10), and several design-time assumptions from the 2026-07
|
|
18
|
+
research did not survive contact with it: the CLI now supports OAuth
|
|
19
|
+
subscription login (the design assumed grok was API-key-only with no
|
|
20
|
+
subscription path), `--sandbox` takes a profile value
|
|
21
|
+
(`off|workspace|read-only|strict`) rather than being a bare flag, a
|
|
22
|
+
Claude-Code-compatible `--permission-mode` exists, and `-p`/`--single` only
|
|
23
|
+
accepts the prompt as an argv value (stdin is not accepted as the prompt) —
|
|
24
|
+
so the adapter delivers the prompt via a temp file instead. The grok CLI is
|
|
25
|
+
still early beta: version pinning is recommended, and the JSON output is
|
|
26
|
+
parsed defensively so schema churn degrades into retryable fallback rather
|
|
27
|
+
than hard failure. `codex` / `gemini` remain schema-declared but rejected
|
|
28
|
+
(Phase 1b/1c pending).
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`agent: grok` builder/parser in `AgentCliAdapter`**
|
|
33
|
+
(`coderouter/adapters/agent_cli.py`) — invokes the Grok CLI one-shot as
|
|
34
|
+
`grok --prompt-file <workdir>/.coderouter-prompt-<uuid>.txt
|
|
35
|
+
--output-format json -m <model> --cwd <workdir> --max-turns <N>
|
|
36
|
+
--no-memory --sandbox read-only --permission-mode plan` (read_only
|
|
37
|
+
default). Auth is subscription-first, same as claude: the CLI's OAuth
|
|
38
|
+
login (`grok login`, SuperGrok / X Premium+) stores credentials at
|
|
39
|
+
`~/.grok/auth.json` (7-day expiry, auto-refresh; `GROK_HOME` overrides),
|
|
40
|
+
which the adapter's `HOME` inheritance reaches with
|
|
41
|
+
`passthrough_env: []`. For CI/API-key use the CLI reads
|
|
42
|
+
`GROK_CODE_XAI_API_KEY` (not `XAI_API_KEY`) — list it in
|
|
43
|
+
`passthrough_env` only if needed; it takes precedence over OAuth.
|
|
44
|
+
- **`sandbox_mode` → grok flag mapping** — `read_only` →
|
|
45
|
+
`--sandbox read-only --permission-mode plan`; `edit` →
|
|
46
|
+
`--sandbox workspace --permission-mode acceptEdits`; `full_auto` →
|
|
47
|
+
`--sandbox workspace --always-approve`. As with claude, the effective
|
|
48
|
+
mode is clamped to read-only whenever `allow_file_writes=false`,
|
|
49
|
+
regardless of `sandbox_mode` (defense in depth). Unlike claude,
|
|
50
|
+
`full_auto` maps distinctly from `edit`.
|
|
51
|
+
- **Prompt delivery via `--prompt-file`** — grok's `-p`/`--single` requires
|
|
52
|
+
the prompt as an argv VALUE (verified on the real CLI: stdin is not
|
|
53
|
+
accepted as the prompt), and putting huge prompts on argv would hit
|
|
54
|
+
Linux's `MAX_ARG_STRLEN` (~128KiB) and expose the full prompt text to
|
|
55
|
+
`ps`. The adapter instead writes the prompt to a mode-`0600` temp file
|
|
56
|
+
inside the isolated workdir and passes `--prompt-file`; the file is
|
|
57
|
+
always deleted afterwards, including on the timeout and error paths.
|
|
58
|
+
- **`--no-memory` always passed** — grok has a cross-session memory
|
|
59
|
+
feature; disabling it unconditionally preserves the
|
|
60
|
+
"one request = one stateless transformation" ethos (a previous call's
|
|
61
|
+
memory can never leak into the next response). Not configurable.
|
|
62
|
+
- **grok JSON parsing + zero-usage reporting** — `--output-format json`
|
|
63
|
+
emits a single JSON object `{"text", "stopReason", "sessionId",
|
|
64
|
+
"requestId", "thought"?}` (verified on grok v0.2.93). `text` becomes the
|
|
65
|
+
final answer; `sessionId` surfaces as the `coderouter_session_id`
|
|
66
|
+
response metadata. There are NO token-usage or cost fields, so usage is
|
|
67
|
+
reported as zeros and `coderouter_cost_usd` stays 0 unless the operator
|
|
68
|
+
sets unit prices in `ProviderConfig.cost` (in contrast to claude's
|
|
69
|
+
direct `total_cost_usd`). Parsing is defensive: anything malformed
|
|
70
|
+
raises a retryable `AdapterError` and the fallback chain advances.
|
|
71
|
+
Runtime errors (exit 1 with the error text on stderr) surface a stderr
|
|
72
|
+
tail in the `AdapterError` message.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- **Unsupported-agent error message** — constructing the adapter with a
|
|
77
|
+
not-yet-implemented agent now lists what IS implemented (e.g.
|
|
78
|
+
`agent 'codex' is not implemented yet (implemented: claude, grok)`)
|
|
79
|
+
instead of the Phase-1a-specific "claude only" wording. Still
|
|
80
|
+
`retryable=False` — a configuration error, not a fallback candidate.
|
|
81
|
+
- **Schema docstrings** (`coderouter/config/schemas.py`) — `AgentCliConfig`
|
|
82
|
+
field descriptions updated for the two-agent reality; the design-time
|
|
83
|
+
"grok requires `XAI_API_KEY` in `passthrough_env`" validation note was
|
|
84
|
+
dropped as obsolete (grok now supports subscription OAuth).
|
|
85
|
+
- **Docs & examples** — `docs/backends/external-agents.md`/`.en.md` gain a
|
|
86
|
+
full grok section (config example, adapter argv, sandbox mapping table,
|
|
87
|
+
prompt-file rationale, `--no-memory` rationale, JSON schema and
|
|
88
|
+
zero-usage note, OAuth setup steps, early-beta/version-pin caveat) and
|
|
89
|
+
updated implementation-status wording. The design doc
|
|
90
|
+
`docs/designs/external-agents-adapter.md` gets a new appendix §11.3
|
|
91
|
+
recording the verified deltas between the 2026-07 research and grok
|
|
92
|
+
v0.2.93 (body text left as the historical record).
|
|
93
|
+
`examples/providers-agent-cli.yaml` promotes grok out of the
|
|
94
|
+
not-implemented preview into a documented (still commented-out, so
|
|
95
|
+
copying the file doesn't require grok to be installed) block with the
|
|
96
|
+
corrected model (`grok-4.5` — `grok-code-fast-1` was retired
|
|
97
|
+
2026-05-15), `paid: false` for subscription OAuth, and
|
|
98
|
+
`passthrough_env: []`.
|
|
99
|
+
|
|
12
100
|
## [v2.7.7] — 2026-07-10 (External coding agents: agent_cli adapter, Phase 1a claude)
|
|
13
101
|
|
|
14
102
|
**PR #62 / #63**. Adds a new adapter kind, `agent_cli`, that lets CodeRouter front an external
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.8
|
|
4
4
|
Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zephel01/CodeRouter
|
|
6
6
|
Project-URL: Repository, https://github.com/zephel01/CodeRouter
|
|
@@ -18,24 +18,36 @@ CodeRouter merely performs the one conversion. Following the
|
|
|
18
18
|
``openai_compat`` precedent, a *single* adapter class fronts *multiple*
|
|
19
19
|
target agents, dispatched on the ``agent`` field.
|
|
20
20
|
|
|
21
|
-
Phase 1a
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
``
|
|
21
|
+
Implemented agents (Phase 1a + 1d)
|
|
22
|
+
==================================
|
|
23
|
+
|
|
24
|
+
Two targets are implemented here:
|
|
25
|
+
|
|
26
|
+
* ``claude`` (Claude Code CLI, Phase 1a) — the most stable CLI, the most
|
|
27
|
+
fine-grained safety controls, and the only one that emits
|
|
28
|
+
``total_cost_usd`` directly (making it the reference implementation for
|
|
29
|
+
the parser / cost path).
|
|
30
|
+
* ``grok`` (grok CLI, Phase 1d) — headless one-shot via ``--prompt-file`` +
|
|
31
|
+
``--output-format json``. The CLI emits no token/cost figures, so usage
|
|
32
|
+
is reported as zeros (cost stays 0 unless the operator sets
|
|
33
|
+
``ProviderConfig.cost``, design §5.1.6). ``--no-memory`` is always passed
|
|
34
|
+
so a user-level cross-session memory setting cannot leak state between
|
|
35
|
+
requests.
|
|
36
|
+
|
|
37
|
+
The remaining agents (codex / gemini) are declared in the config schema so
|
|
38
|
+
providers.yaml is forward-compatible, but constructing an adapter for them
|
|
39
|
+
raises a clear ``AdapterError`` until their phase (1b / 1c) lands.
|
|
31
40
|
|
|
32
41
|
Security (design §6, non-negotiable)
|
|
33
42
|
====================================
|
|
34
43
|
|
|
35
44
|
* **allowlist argv only** — the child is launched with
|
|
36
45
|
:func:`asyncio.create_subprocess_exec` and a list argv. ``shell=True`` is
|
|
37
|
-
never used; the prompt is
|
|
38
|
-
|
|
46
|
+
never used; the prompt is never subject to shell interpretation (claude
|
|
47
|
+
reads it from stdin; grok reads it from a private ``0600`` prompt file
|
|
48
|
+
inside the resolved workdir — its ``-p`` requires the prompt as an argv
|
|
49
|
+
value, and argv would both hit Linux's ~128KiB ``MAX_ARG_STRLEN`` on huge
|
|
50
|
+
prompts and leak the text into ``ps`` output).
|
|
39
51
|
* **default read-only** — ``allow_file_writes=False`` /
|
|
40
52
|
``sandbox_mode="read_only"`` are the defaults, mapped to claude's
|
|
41
53
|
``--permission-mode plan``. Writes require explicit opt-in and the sandbox
|
|
@@ -104,6 +116,17 @@ _CLAUDE_PERMISSION_MODE = {
|
|
|
104
116
|
"full_auto": "acceptEdits",
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
# sandbox_mode → grok sandbox/approval flags (design §5.4, grok CLI v0.2.93).
|
|
120
|
+
# grok's ``--sandbox`` takes a built-in profile VALUE (off|workspace|
|
|
121
|
+
# read-only|strict) and its ``--permission-mode`` shares Claude Code's value
|
|
122
|
+
# set; ``full_auto`` swaps the permission mode for ``--always-approve``
|
|
123
|
+
# (auto-approve all tool executions).
|
|
124
|
+
_GROK_SANDBOX_ARGS = {
|
|
125
|
+
"read_only": ["--sandbox", "read-only", "--permission-mode", "plan"],
|
|
126
|
+
"edit": ["--sandbox", "workspace", "--permission-mode", "acceptEdits"],
|
|
127
|
+
"full_auto": ["--sandbox", "workspace", "--always-approve"],
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
|
|
108
131
|
def _chunk_text(text: str, size: int = _STREAM_CHUNK_CHARS) -> Iterator[str]:
|
|
109
132
|
"""Split ``text`` into ``size``-char pieces for the pseudo-stream."""
|
|
@@ -112,7 +135,7 @@ def _chunk_text(text: str, size: int = _STREAM_CHUNK_CHARS) -> Iterator[str]:
|
|
|
112
135
|
|
|
113
136
|
|
|
114
137
|
class AgentCliAdapter(BaseAdapter):
|
|
115
|
-
"""Invoke an external coding-agent CLI one-shot (
|
|
138
|
+
"""Invoke an external coding-agent CLI one-shot (claude + grok).
|
|
116
139
|
|
|
117
140
|
The ``agent`` field selects the argv builder / output parser via the
|
|
118
141
|
dispatch tables built in :meth:`__init__`, mirroring how
|
|
@@ -122,9 +145,10 @@ class AgentCliAdapter(BaseAdapter):
|
|
|
122
145
|
def __init__(self, config: ProviderConfig) -> None:
|
|
123
146
|
"""Bind to a ``ProviderConfig`` and reject unsupported agents.
|
|
124
147
|
|
|
125
|
-
Constructing an adapter for an agent other than ``claude``
|
|
126
|
-
non-retryable :class:`AdapterError` — the other targets are
|
|
127
|
-
in the schema but not implemented until their phase
|
|
148
|
+
Constructing an adapter for an agent other than ``claude`` / ``grok``
|
|
149
|
+
raises a non-retryable :class:`AdapterError` — the other targets are
|
|
150
|
+
declared in the schema but not implemented until their phase
|
|
151
|
+
(design §9).
|
|
128
152
|
"""
|
|
129
153
|
super().__init__(config)
|
|
130
154
|
if config.agent_cli is None: # pragma: no cover - schema enforces this
|
|
@@ -134,21 +158,30 @@ class AgentCliAdapter(BaseAdapter):
|
|
|
134
158
|
retryable=False,
|
|
135
159
|
)
|
|
136
160
|
self.acfg: AgentCliConfig = config.agent_cli
|
|
137
|
-
if self.acfg.agent
|
|
161
|
+
if self.acfg.agent not in ("claude", "grok"):
|
|
138
162
|
raise AdapterError(
|
|
139
|
-
f"agent {self.acfg.agent!r} is not implemented
|
|
140
|
-
f"(claude
|
|
141
|
-
f"agent's phase.",
|
|
163
|
+
f"agent {self.acfg.agent!r} is not implemented yet "
|
|
164
|
+
f"(implemented: claude, grok). Wait for Phase 1b/1c.",
|
|
142
165
|
provider=config.name,
|
|
143
166
|
retryable=False,
|
|
144
167
|
)
|
|
145
|
-
# agent → argv builder / output parser dispatch tables.
|
|
146
|
-
#
|
|
147
|
-
self._builders = {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
self.
|
|
168
|
+
# agent → argv builder / output parser dispatch tables. claude landed
|
|
169
|
+
# in Phase 1a, grok in Phase 1d; Phase 1b/1c add codex / gemini.
|
|
170
|
+
self._builders = {
|
|
171
|
+
"claude": self._build_claude_argv,
|
|
172
|
+
"grok": self._build_grok_argv,
|
|
173
|
+
}
|
|
174
|
+
self._parsers = {
|
|
175
|
+
"claude": self._parse_claude,
|
|
176
|
+
"grok": self._parse_grok,
|
|
177
|
+
}
|
|
178
|
+
# Prompt delivery is per-agent: claude reads its print-mode prompt
|
|
179
|
+
# from stdin (10MB cap), which keeps argv free of the (potentially
|
|
180
|
+
# huge) prompt text. grok's ``-p`` REQUIRES the prompt as its argv
|
|
181
|
+
# value (piped stdin is only appended as extra context, verified on
|
|
182
|
+
# v0.2.93), so grok gets the prompt via ``--prompt-file`` instead —
|
|
183
|
+
# see ``_write_prompt_file`` for the rationale.
|
|
184
|
+
self._uses_stdin = self.acfg.agent == "claude"
|
|
152
185
|
|
|
153
186
|
# ------------------------------------------------------------------
|
|
154
187
|
# BaseAdapter contract
|
|
@@ -177,8 +210,7 @@ class AgentCliAdapter(BaseAdapter):
|
|
|
177
210
|
depth = self._current_depth()
|
|
178
211
|
if depth >= self.acfg.agent_depth_limit:
|
|
179
212
|
raise AdapterError(
|
|
180
|
-
f"agent recursion depth {depth} >= limit "
|
|
181
|
-
f"{self.acfg.agent_depth_limit}",
|
|
213
|
+
f"agent recursion depth {depth} >= limit {self.acfg.agent_depth_limit}",
|
|
182
214
|
provider=self.name,
|
|
183
215
|
retryable=False,
|
|
184
216
|
)
|
|
@@ -195,68 +227,82 @@ class AgentCliAdapter(BaseAdapter):
|
|
|
195
227
|
|
|
196
228
|
prompt = self._render_prompt(request, overrides)
|
|
197
229
|
workdir = self._resolve_workdir()
|
|
198
|
-
argv = self._builders[self.acfg.agent](workdir)
|
|
199
|
-
# Resolve the executable to an absolute path so argv[0] is a concrete
|
|
200
|
-
# binary independent of the child's minimal PATH (design §6 allowlist).
|
|
201
|
-
resolved = shutil.which(argv[0])
|
|
202
|
-
if resolved is not None:
|
|
203
|
-
argv = [resolved, *argv[1:]]
|
|
204
|
-
env = self._build_child_env()
|
|
205
|
-
|
|
206
|
-
logger.info(
|
|
207
|
-
"agent-cli-exec",
|
|
208
|
-
extra={
|
|
209
|
-
"provider": self.name,
|
|
210
|
-
"agent": self.acfg.agent,
|
|
211
|
-
"argv0": argv[0],
|
|
212
|
-
"timeout_s": timeout,
|
|
213
|
-
},
|
|
214
|
-
)
|
|
215
230
|
|
|
231
|
+
# Agents that cannot take the prompt on stdin (grok) get it through a
|
|
232
|
+
# private temp file inside the workdir; it is ALWAYS removed in the
|
|
233
|
+
# ``finally`` below, including on timeout / exception paths.
|
|
234
|
+
prompt_file = None if self._uses_stdin else self._write_prompt_file(prompt, workdir)
|
|
216
235
|
try:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
236
|
+
argv = self._builders[self.acfg.agent](workdir, prompt_file)
|
|
237
|
+
# Resolve the executable to an absolute path so argv[0] is a
|
|
238
|
+
# concrete binary independent of the child's minimal PATH
|
|
239
|
+
# (design §6 allowlist).
|
|
240
|
+
resolved = shutil.which(argv[0])
|
|
241
|
+
if resolved is not None:
|
|
242
|
+
argv = [resolved, *argv[1:]]
|
|
243
|
+
env = self._build_child_env()
|
|
244
|
+
|
|
245
|
+
logger.info(
|
|
246
|
+
"agent-cli-exec",
|
|
247
|
+
extra={
|
|
248
|
+
"provider": self.name,
|
|
249
|
+
"agent": self.acfg.agent,
|
|
250
|
+
"argv0": argv[0],
|
|
251
|
+
"timeout_s": timeout,
|
|
252
|
+
},
|
|
227
253
|
)
|
|
228
|
-
except (FileNotFoundError, OSError) as exc:
|
|
229
|
-
raise AdapterError(
|
|
230
|
-
f"failed to launch {self.acfg.command!r}: {exc}",
|
|
231
|
-
provider=self.name,
|
|
232
|
-
retryable=False,
|
|
233
|
-
) from exc
|
|
234
254
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
255
|
+
try:
|
|
256
|
+
proc = await asyncio.create_subprocess_exec(
|
|
257
|
+
*argv,
|
|
258
|
+
stdin=asyncio.subprocess.PIPE,
|
|
259
|
+
stdout=asyncio.subprocess.PIPE,
|
|
260
|
+
stderr=asyncio.subprocess.PIPE,
|
|
261
|
+
cwd=workdir,
|
|
262
|
+
env=env,
|
|
263
|
+
# New session/process group so a timeout can SIGKILL the
|
|
264
|
+
# whole group (the CLI hangs its real LLM call off a
|
|
265
|
+
# child).
|
|
266
|
+
start_new_session=True,
|
|
267
|
+
)
|
|
268
|
+
except (FileNotFoundError, OSError) as exc:
|
|
269
|
+
raise AdapterError(
|
|
270
|
+
f"failed to launch {self.acfg.command!r}: {exc}",
|
|
271
|
+
provider=self.name,
|
|
272
|
+
retryable=False,
|
|
273
|
+
) from exc
|
|
249
274
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
275
|
+
stdin_bytes = prompt.encode("utf-8") if self._uses_stdin else None
|
|
276
|
+
try:
|
|
277
|
+
stdout, stderr = await asyncio.wait_for(
|
|
278
|
+
proc.communicate(input=stdin_bytes), timeout=timeout
|
|
279
|
+
)
|
|
280
|
+
except TimeoutError as exc:
|
|
281
|
+
self._kill_process_group(proc)
|
|
282
|
+
with contextlib.suppress(Exception):
|
|
283
|
+
await proc.wait()
|
|
284
|
+
raise AdapterError(
|
|
285
|
+
f"{self.acfg.agent} exec timed out after {timeout}s",
|
|
286
|
+
provider=self.name,
|
|
287
|
+
retryable=True,
|
|
288
|
+
) from exc
|
|
258
289
|
|
|
259
|
-
|
|
290
|
+
if proc.returncode != 0:
|
|
291
|
+
detail = self._error_detail(stdout, stderr)
|
|
292
|
+
raise AdapterError(
|
|
293
|
+
f"{self.acfg.agent} exited {proc.returncode}: {detail}",
|
|
294
|
+
provider=self.name,
|
|
295
|
+
status_code=None,
|
|
296
|
+
retryable=self._is_retryable_exit(proc.returncode),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
final_text, usage, meta = self._parsers[self.acfg.agent](stdout, stderr)
|
|
300
|
+
finally:
|
|
301
|
+
if prompt_file is not None:
|
|
302
|
+
# Best-effort cleanup — the child may already have exited and
|
|
303
|
+
# a vanished file is not an error worth surfacing.
|
|
304
|
+
with contextlib.suppress(OSError):
|
|
305
|
+
os.unlink(prompt_file)
|
|
260
306
|
return self._to_chat_response(final_text, usage, meta)
|
|
261
307
|
|
|
262
308
|
async def stream(
|
|
@@ -281,9 +327,7 @@ class AgentCliAdapter(BaseAdapter):
|
|
|
281
327
|
id=resp.id,
|
|
282
328
|
created=resp.created,
|
|
283
329
|
model=resp.model,
|
|
284
|
-
choices=[
|
|
285
|
-
{"index": 0, "delta": {"content": piece}, "finish_reason": None}
|
|
286
|
-
],
|
|
330
|
+
choices=[{"index": 0, "delta": {"content": piece}, "finish_reason": None}],
|
|
287
331
|
)
|
|
288
332
|
yield StreamChunk(
|
|
289
333
|
id=resp.id,
|
|
@@ -297,7 +341,7 @@ class AgentCliAdapter(BaseAdapter):
|
|
|
297
341
|
# claude argv builder + output parser
|
|
298
342
|
# ------------------------------------------------------------------
|
|
299
343
|
|
|
300
|
-
def _build_claude_argv(self, workdir: str) -> list[str]:
|
|
344
|
+
def _build_claude_argv(self, workdir: str, prompt_file: str | None = None) -> list[str]:
|
|
301
345
|
"""Assemble the ``claude -p`` argv (design §5.1.5 / §5.4).
|
|
302
346
|
|
|
303
347
|
Shape::
|
|
@@ -305,10 +349,13 @@ class AgentCliAdapter(BaseAdapter):
|
|
|
305
349
|
claude -p --output-format json --model <m> --max-turns <n>
|
|
306
350
|
--permission-mode <plan|acceptEdits> --add-dir <workdir>
|
|
307
351
|
|
|
308
|
-
The prompt is fed on stdin (not argv), so it never appears here
|
|
309
|
-
|
|
310
|
-
|
|
352
|
+
The prompt is fed on stdin (not argv), so it never appears here and
|
|
353
|
+
``prompt_file`` is ignored (it exists only to keep the builder
|
|
354
|
+
signature uniform across agents). ``--bare`` is deliberately NOT
|
|
355
|
+
added — it would skip OAuth/keychain reads and break subscription
|
|
356
|
+
auth (design §5.3.4).
|
|
311
357
|
"""
|
|
358
|
+
del prompt_file # claude takes the prompt on stdin, not from a file.
|
|
312
359
|
model = self.acfg.model or self.config.model
|
|
313
360
|
argv = [self.acfg.command, "-p", "--output-format", "json", "--model", model]
|
|
314
361
|
if self.acfg.max_turns is not None:
|
|
@@ -418,10 +465,154 @@ class AgentCliAdapter(BaseAdapter):
|
|
|
418
465
|
usage["duration_ms"] = data["duration_ms"]
|
|
419
466
|
return usage
|
|
420
467
|
|
|
468
|
+
# ------------------------------------------------------------------
|
|
469
|
+
# grok argv builder + output parser (Phase 1d)
|
|
470
|
+
# ------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
def _build_grok_argv(self, workdir: str, prompt_file: str | None = None) -> list[str]:
|
|
473
|
+
"""Assemble the grok headless argv (design §5, grok CLI v0.2.93).
|
|
474
|
+
|
|
475
|
+
Shape::
|
|
476
|
+
|
|
477
|
+
grok --prompt-file <f> --output-format json -m <m> --cwd <w>
|
|
478
|
+
--max-turns <n> --no-memory --sandbox <profile>
|
|
479
|
+
[--permission-mode <mode> | --always-approve]
|
|
480
|
+
|
|
481
|
+
The prompt travels via ``--prompt-file`` (never argv / stdin): grok's
|
|
482
|
+
``-p`` requires the prompt as its argv value, and putting it there
|
|
483
|
+
would hit Linux's ~128KiB ``MAX_ARG_STRLEN`` on large prompts and
|
|
484
|
+
leak the text into ``ps`` output. ``--no-memory`` is deliberate: it
|
|
485
|
+
enforces the one-request-one-transformation statelessness even if
|
|
486
|
+
the user's grok config enables cross-session memory.
|
|
487
|
+
"""
|
|
488
|
+
if prompt_file is None: # pragma: no cover - generate() always supplies it
|
|
489
|
+
raise AdapterError(
|
|
490
|
+
"grok argv requires a prompt file",
|
|
491
|
+
provider=self.name,
|
|
492
|
+
retryable=False,
|
|
493
|
+
)
|
|
494
|
+
model = self.acfg.model or self.config.model
|
|
495
|
+
argv = [
|
|
496
|
+
self.acfg.command,
|
|
497
|
+
"--prompt-file",
|
|
498
|
+
prompt_file,
|
|
499
|
+
"--output-format",
|
|
500
|
+
"json",
|
|
501
|
+
"-m",
|
|
502
|
+
model,
|
|
503
|
+
"--cwd",
|
|
504
|
+
workdir,
|
|
505
|
+
]
|
|
506
|
+
if self.acfg.max_turns is not None:
|
|
507
|
+
argv += ["--max-turns", str(self.acfg.max_turns)]
|
|
508
|
+
argv += ["--no-memory"]
|
|
509
|
+
argv += self._grok_sandbox_args()
|
|
510
|
+
return argv
|
|
511
|
+
|
|
512
|
+
def _grok_sandbox_args(self) -> list[str]:
|
|
513
|
+
"""Map ``sandbox_mode`` → grok sandbox/approval flags, clamped.
|
|
514
|
+
|
|
515
|
+
Same clamp as claude (design §5.4): when ``allow_file_writes`` is
|
|
516
|
+
False the effective mode is forced to ``read_only`` regardless of
|
|
517
|
+
``sandbox_mode``, so writes always require the explicit opt-in.
|
|
518
|
+
"""
|
|
519
|
+
mode = self.acfg.sandbox_mode if self.acfg.allow_file_writes else "read_only"
|
|
520
|
+
return list(_GROK_SANDBOX_ARGS[mode])
|
|
521
|
+
|
|
522
|
+
def _parse_grok(
|
|
523
|
+
self, stdout: bytes, stderr: bytes
|
|
524
|
+
) -> tuple[str, dict[str, Any], dict[str, Any]]:
|
|
525
|
+
"""Parse grok ``--output-format json`` output (verified v0.2.93).
|
|
526
|
+
|
|
527
|
+
Real-run shape::
|
|
528
|
+
|
|
529
|
+
{"text": "...", "stopReason": "EndTurn", "sessionId": "<uuid>",
|
|
530
|
+
"requestId": "<uuid>", "thought": "..."}
|
|
531
|
+
|
|
532
|
+
The CLI is early beta, so parsing is deliberately defensive: empty /
|
|
533
|
+
non-JSON / non-object stdout and a missing ``text`` field all raise
|
|
534
|
+
a retryable :class:`AdapterError` so the chain can fall through.
|
|
535
|
+
``thought`` is ignored — only the final ``text`` is the answer.
|
|
536
|
+
"""
|
|
537
|
+
text = stdout.decode("utf-8", "replace").strip()
|
|
538
|
+
if not text:
|
|
539
|
+
raise AdapterError(
|
|
540
|
+
"grok produced no stdout to parse",
|
|
541
|
+
provider=self.name,
|
|
542
|
+
retryable=True,
|
|
543
|
+
)
|
|
544
|
+
try:
|
|
545
|
+
data = json.loads(text)
|
|
546
|
+
except json.JSONDecodeError as exc:
|
|
547
|
+
raise AdapterError(
|
|
548
|
+
f"grok emitted non-JSON output: {exc}",
|
|
549
|
+
provider=self.name,
|
|
550
|
+
retryable=True,
|
|
551
|
+
) from exc
|
|
552
|
+
if not isinstance(data, dict):
|
|
553
|
+
raise AdapterError(
|
|
554
|
+
"grok JSON output was not an object",
|
|
555
|
+
provider=self.name,
|
|
556
|
+
retryable=True,
|
|
557
|
+
)
|
|
558
|
+
result = data.get("text")
|
|
559
|
+
if not isinstance(result, str):
|
|
560
|
+
raise AdapterError(
|
|
561
|
+
"grok JSON output missing string 'text' field",
|
|
562
|
+
provider=self.name,
|
|
563
|
+
retryable=True,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# grok emits NO token usage / cost fields (verified), so usage is
|
|
567
|
+
# all-zeros; the cost dashboard shows 0 for this provider unless the
|
|
568
|
+
# operator sets ``ProviderConfig.cost`` rates (design §5.1.6).
|
|
569
|
+
usage: dict[str, Any] = {
|
|
570
|
+
"prompt_tokens": 0,
|
|
571
|
+
"completion_tokens": 0,
|
|
572
|
+
"total_tokens": 0,
|
|
573
|
+
}
|
|
574
|
+
meta: dict[str, Any] = {}
|
|
575
|
+
session_id = data.get("sessionId")
|
|
576
|
+
if isinstance(session_id, str):
|
|
577
|
+
meta["coderouter_session_id"] = session_id
|
|
578
|
+
return result, usage, meta
|
|
579
|
+
|
|
421
580
|
# ------------------------------------------------------------------
|
|
422
581
|
# helpers: prompt rendering, response shaping, env, workdir, kill
|
|
423
582
|
# ------------------------------------------------------------------
|
|
424
583
|
|
|
584
|
+
def _write_prompt_file(self, prompt: str, workdir: str) -> str:
|
|
585
|
+
"""Write the prompt to a private ``0600`` temp file in ``workdir``.
|
|
586
|
+
|
|
587
|
+
Rationale: grok's ``-p`` requires the prompt as an argv value, but a
|
|
588
|
+
huge prompt would hit Linux's ~128KiB ``MAX_ARG_STRLEN`` and argv
|
|
589
|
+
leaks into ``ps`` output; file delivery keeps argv small and private,
|
|
590
|
+
and ``0600`` + the isolated workdir bounds exposure. ``O_EXCL`` with
|
|
591
|
+
a uuid4 name makes creation race-free; the caller (``generate``)
|
|
592
|
+
deletes the file in a ``finally`` block on every path.
|
|
593
|
+
"""
|
|
594
|
+
path = os.path.join(workdir, f".coderouter-prompt-{uuid.uuid4().hex}.txt")
|
|
595
|
+
try:
|
|
596
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
597
|
+
except OSError as exc:
|
|
598
|
+
raise AdapterError(
|
|
599
|
+
f"failed to create prompt file in {workdir}: {exc}",
|
|
600
|
+
provider=self.name,
|
|
601
|
+
retryable=False,
|
|
602
|
+
) from exc
|
|
603
|
+
try:
|
|
604
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
605
|
+
handle.write(prompt)
|
|
606
|
+
except OSError as exc:
|
|
607
|
+
with contextlib.suppress(OSError):
|
|
608
|
+
os.unlink(path)
|
|
609
|
+
raise AdapterError(
|
|
610
|
+
f"failed to write prompt file {path}: {exc}",
|
|
611
|
+
provider=self.name,
|
|
612
|
+
retryable=False,
|
|
613
|
+
) from exc
|
|
614
|
+
return path
|
|
615
|
+
|
|
425
616
|
def _to_chat_response(
|
|
426
617
|
self, final_text: str, usage: dict[str, Any], meta: dict[str, Any]
|
|
427
618
|
) -> ChatResponse:
|
|
@@ -442,9 +633,7 @@ class AgentCliAdapter(BaseAdapter):
|
|
|
442
633
|
**meta,
|
|
443
634
|
)
|
|
444
635
|
|
|
445
|
-
def _render_prompt(
|
|
446
|
-
self, request: ChatRequest, overrides: ProviderCallOverrides | None
|
|
447
|
-
) -> str:
|
|
636
|
+
def _render_prompt(self, request: ChatRequest, overrides: ProviderCallOverrides | None) -> str:
|
|
448
637
|
"""Flatten the chat messages into a single role-tagged prompt string.
|
|
449
638
|
|
|
450
639
|
The profile-level ``append_system_prompt`` (if any) is prepended as a
|