claude-smart 0.2.41 → 0.2.43
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.
- package/.claude-plugin/marketplace.json +17 -0
- package/README.md +1 -1
- package/bin/claude-smart.js +86 -48
- package/package.json +10 -3
- package/plugin/.claude-plugin/plugin.json +9 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +2 -2
- package/plugin/dashboard/next.config.ts +9 -1
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +91 -0
- package/plugin/scripts/backend-service.sh +46 -15
- package/plugin/scripts/cli.sh +29 -1
- package/plugin/scripts/codex-hook.js +72 -4
- package/plugin/scripts/dashboard-build.sh +1 -0
- package/plugin/scripts/dashboard-service.sh +1 -0
- package/plugin/scripts/ensure-plugin-root.sh +7 -14
- package/plugin/scripts/hook_entry.sh +1 -0
- package/plugin/scripts/smart-install.sh +18 -2
- package/plugin/src/claude_smart/cli.py +72 -38
- package/plugin/src/claude_smart/context_format.py +11 -12
- package/plugin/src/claude_smart/cs_cite.py +26 -12
- package/plugin/src/claude_smart/ids.py +13 -5
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +53 -0
- package/plugin/vendor/reflexio/LICENSE +201 -0
- package/plugin/vendor/reflexio/README.md +338 -0
- package/plugin/vendor/reflexio/pyproject.toml +271 -0
- package/plugin/vendor/reflexio/reflexio/README.md +184 -0
- package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
- package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
- package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
- package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +266 -0
- package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +961 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
- package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
- package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
- package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
- package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
- package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
- package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
- package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
- package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
- package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
- package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
- package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
- package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
- package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
- package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
- package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
- package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
- package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1103 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +766 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +280 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
- package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
- package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +616 -0
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +2714 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +540 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
- package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +255 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +279 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +127 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +340 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1626 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +351 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +468 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +777 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +960 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +741 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +734 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +800 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +70 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1253 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +372 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +568 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
- package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
- package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
- package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
- package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
|
@@ -0,0 +1,1122 @@
|
|
|
1
|
+
"""Claude Code CLI as a LiteLLM custom provider.
|
|
2
|
+
|
|
3
|
+
Routes ``litellm.completion(model="claude-code/...", ...)`` through the
|
|
4
|
+
user's locally-installed ``claude`` CLI (the Claude Code binary), so
|
|
5
|
+
reflexio's extractors can run with no external LLM API key — they reuse
|
|
6
|
+
whatever auth the user already has for Claude Code.
|
|
7
|
+
|
|
8
|
+
Activation is opt-in via ``CLAUDE_SMART_USE_LOCAL_CLI=1``. Without it,
|
|
9
|
+
the provider does not register and reflexio falls back to its normal
|
|
10
|
+
OpenAI/Anthropic/etc. provider priority.
|
|
11
|
+
|
|
12
|
+
Structured output: when callers pass a Pydantic ``response_format``,
|
|
13
|
+
the JSON schema is appended to the system prompt instructing the CLI
|
|
14
|
+
to reply with matching JSON. The CLI's text reply is returned as
|
|
15
|
+
``message.content``; ``LiteLLMClient._maybe_parse_structured_output``
|
|
16
|
+
then parses it into the Pydantic instance via the existing pipeline.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import inspect
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess # noqa: S404 — subprocess is the integration point; inputs are sanitised.
|
|
28
|
+
import tempfile
|
|
29
|
+
import time
|
|
30
|
+
from contextlib import suppress
|
|
31
|
+
from datetime import UTC, datetime
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
import litellm
|
|
36
|
+
from litellm.llms.custom_llm import CustomLLM
|
|
37
|
+
from litellm.types.utils import (
|
|
38
|
+
ChatCompletionMessageToolCall,
|
|
39
|
+
Choices,
|
|
40
|
+
Function,
|
|
41
|
+
Message,
|
|
42
|
+
ModelResponse,
|
|
43
|
+
Usage,
|
|
44
|
+
)
|
|
45
|
+
from pydantic import BaseModel
|
|
46
|
+
|
|
47
|
+
from reflexio.server.llm.providers.claude_code_stream_parser import (
|
|
48
|
+
ParseResult,
|
|
49
|
+
classify_stall,
|
|
50
|
+
parse_reset_estimate,
|
|
51
|
+
parse_stream_json,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_LOGGER = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
PROVIDER_KEY = "claude-code"
|
|
57
|
+
ENV_ENABLE = "CLAUDE_SMART_USE_LOCAL_CLI"
|
|
58
|
+
_ENV_CLI_PATH = "CLAUDE_SMART_CLI_PATH"
|
|
59
|
+
_ENV_HOST = "CLAUDE_SMART_HOST"
|
|
60
|
+
_ENV_CODEX_PATH = "CLAUDE_SMART_CODEX_PATH"
|
|
61
|
+
_ENV_TIMEOUT = "CLAUDE_SMART_CLI_TIMEOUT"
|
|
62
|
+
_ENV_MODEL = "CLAUDE_SMART_CLI_MODEL"
|
|
63
|
+
_HOST_CODEX = "codex"
|
|
64
|
+
_HOST_CLAUDE_CODE = "claude-code"
|
|
65
|
+
_CODEX_COMPAT_SCRIPT_NAMES = (
|
|
66
|
+
("codex-claude-compat.cmd", "codex-claude-compat")
|
|
67
|
+
if os.name == "nt"
|
|
68
|
+
else ("codex-claude-compat", "codex-claude-compat.cmd")
|
|
69
|
+
)
|
|
70
|
+
_CODEX_COMPAT_SCRIPT_NAME_SET = set(_CODEX_COMPAT_SCRIPT_NAMES)
|
|
71
|
+
_DEFAULT_TIMEOUT_SECONDS = 120
|
|
72
|
+
_DEFAULT_CLI_MODEL = "claude-sonnet-4-6"
|
|
73
|
+
|
|
74
|
+
_TRUTHY_ENV_VALUES = {"1", "true", "yes"}
|
|
75
|
+
_UNSUPPORTED_PARAMS_WARNED: set[str] = set()
|
|
76
|
+
_IMAGE_WARNED = False
|
|
77
|
+
_MULTITURN_WARNED = False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ClaudeCodeCLIError(RuntimeError):
|
|
81
|
+
"""Raised when the claude CLI subprocess fails in a way we cannot recover from."""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _env_enabled() -> bool:
|
|
85
|
+
"""Return True when ``CLAUDE_SMART_USE_LOCAL_CLI`` is set to a truthy value.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
bool: True if the opt-in env var is set, False otherwise.
|
|
89
|
+
"""
|
|
90
|
+
raw = os.environ.get(ENV_ENABLE)
|
|
91
|
+
return bool(raw) and raw.lower() in _TRUTHY_ENV_VALUES
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _host() -> str:
|
|
95
|
+
"""Return the host that owns this backend process."""
|
|
96
|
+
return (
|
|
97
|
+
_HOST_CODEX if os.environ.get(_ENV_HOST) == _HOST_CODEX else _HOST_CLAUDE_CODE
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _cli_name() -> str:
|
|
102
|
+
"""Return the expected local CLI binary for the active host."""
|
|
103
|
+
return "codex" if _host() == _HOST_CODEX else "claude"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _candidate_codex_compat_path() -> Path | None:
|
|
107
|
+
"""Return the Codex compatibility wrapper from plugin roots, if present."""
|
|
108
|
+
for env_var in ("PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"):
|
|
109
|
+
root = os.environ.get(env_var)
|
|
110
|
+
if not root:
|
|
111
|
+
continue
|
|
112
|
+
for name in _CODEX_COMPAT_SCRIPT_NAMES:
|
|
113
|
+
candidate = Path(root) / "scripts" / name
|
|
114
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
115
|
+
return candidate
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _resolve_cli_path() -> str | None:
|
|
120
|
+
"""Return the path to the active host CLI, or None if unavailable.
|
|
121
|
+
|
|
122
|
+
Honours the ``CLAUDE_SMART_CLI_PATH`` override before falling back to
|
|
123
|
+
host-specific defaults. Claude Code uses ``claude``. Codex prefers the
|
|
124
|
+
compatibility wrapper shipped with the plugin, then falls back to
|
|
125
|
+
``codex`` directly.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
str | None: Absolute path to the CLI, or None if not found.
|
|
129
|
+
"""
|
|
130
|
+
override = os.environ.get(_ENV_CLI_PATH)
|
|
131
|
+
if override:
|
|
132
|
+
candidate = Path(override)
|
|
133
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
134
|
+
return str(candidate)
|
|
135
|
+
_LOGGER.warning(
|
|
136
|
+
"%s=%s is not an executable file; falling back to PATH",
|
|
137
|
+
_ENV_CLI_PATH,
|
|
138
|
+
override,
|
|
139
|
+
)
|
|
140
|
+
if _host() == _HOST_CODEX:
|
|
141
|
+
compat = _candidate_codex_compat_path()
|
|
142
|
+
if compat is not None:
|
|
143
|
+
return str(compat)
|
|
144
|
+
return os.environ.get(_ENV_CODEX_PATH) or shutil.which("codex")
|
|
145
|
+
return shutil.which("claude")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_claude_code_available() -> bool:
|
|
149
|
+
"""Return True when the local CLI provider is usable right now.
|
|
150
|
+
|
|
151
|
+
Both the opt-in env var *and* a resolvable CLI path are required, so
|
|
152
|
+
an unrelated env var can't silently redirect extraction traffic.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
bool: True iff ``CLAUDE_SMART_USE_LOCAL_CLI`` is truthy AND a
|
|
156
|
+
host CLI is resolvable.
|
|
157
|
+
"""
|
|
158
|
+
return _env_enabled() and _resolve_cli_path() is not None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _flatten_content(content: Any) -> str:
|
|
162
|
+
"""Collapse LiteLLM content (string or content-block list) to plain text.
|
|
163
|
+
|
|
164
|
+
Image blocks are silently skipped with a one-time WARN log (see
|
|
165
|
+
``_warn_image_dropped_once``). cache_control markers are ignored
|
|
166
|
+
since the CLI does not accept them.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
content: LiteLLM content — string, list of content blocks, or None.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
str: Plain-text content; empty string if no text survives.
|
|
173
|
+
"""
|
|
174
|
+
if content is None:
|
|
175
|
+
return ""
|
|
176
|
+
if isinstance(content, str):
|
|
177
|
+
return content
|
|
178
|
+
if isinstance(content, list):
|
|
179
|
+
parts: list[str] = []
|
|
180
|
+
for block in content:
|
|
181
|
+
if isinstance(block, dict):
|
|
182
|
+
block_type = block.get("type")
|
|
183
|
+
if block_type in {"image", "image_url"}:
|
|
184
|
+
_warn_image_dropped_once()
|
|
185
|
+
continue
|
|
186
|
+
text = block.get("text") or block.get("content")
|
|
187
|
+
if isinstance(text, str):
|
|
188
|
+
parts.append(text)
|
|
189
|
+
elif isinstance(block, str):
|
|
190
|
+
parts.append(block)
|
|
191
|
+
return "\n".join(parts)
|
|
192
|
+
return str(content)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _warn_image_dropped_once() -> None:
|
|
196
|
+
"""Emit a single WARN when image content is dropped by the CLI bridge.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
None
|
|
200
|
+
"""
|
|
201
|
+
global _IMAGE_WARNED
|
|
202
|
+
if not _IMAGE_WARNED:
|
|
203
|
+
_LOGGER.warning(
|
|
204
|
+
"claude-code provider: image content blocks are dropped — "
|
|
205
|
+
"the CLI bridge accepts text only. Install an image-capable "
|
|
206
|
+
"LLM provider for screenshot analysis."
|
|
207
|
+
)
|
|
208
|
+
_IMAGE_WARNED = True
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _warn_multiturn_once() -> None:
|
|
212
|
+
"""Emit a single WARN when multi-turn context gets flattened to text.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
None
|
|
216
|
+
"""
|
|
217
|
+
global _MULTITURN_WARNED
|
|
218
|
+
if not _MULTITURN_WARNED:
|
|
219
|
+
_LOGGER.warning(
|
|
220
|
+
"claude-code provider: multi-turn messages are flattened into a "
|
|
221
|
+
"single 'User:/Assistant:' transcript. Quality may differ from "
|
|
222
|
+
"the Anthropic messages API."
|
|
223
|
+
)
|
|
224
|
+
_MULTITURN_WARNED = True
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _warn_unsupported_param_once(name: str) -> None:
|
|
228
|
+
"""Emit a single WARN when a LiteLLM param has no CLI equivalent.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
name: Parameter name that was ignored (e.g. ``"temperature"``).
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
None
|
|
235
|
+
"""
|
|
236
|
+
if name in _UNSUPPORTED_PARAMS_WARNED:
|
|
237
|
+
return
|
|
238
|
+
_UNSUPPORTED_PARAMS_WARNED.add(name)
|
|
239
|
+
_LOGGER.warning(
|
|
240
|
+
"claude-code provider: ignoring unsupported parameter %r — "
|
|
241
|
+
"the CLI does not expose this control.",
|
|
242
|
+
name,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _schema_instruction(response_format: Any) -> str | None:
|
|
247
|
+
"""Build a schema instruction to append to the system prompt.
|
|
248
|
+
|
|
249
|
+
Accepts either a Pydantic model class, a LiteLLM ``json_schema``
|
|
250
|
+
response_format dict, or a plain JSON-schema dict. Returns None
|
|
251
|
+
when nothing usable is found — callers fall through to unstructured
|
|
252
|
+
completion.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
response_format: The response_format value from LiteLLM kwargs
|
|
256
|
+
or ``optional_params`` — a Pydantic class or a dict.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
str | None: Instruction text to append to the system prompt,
|
|
260
|
+
or None if no schema could be extracted.
|
|
261
|
+
"""
|
|
262
|
+
schema = _extract_json_schema(response_format)
|
|
263
|
+
if not schema:
|
|
264
|
+
return None
|
|
265
|
+
return (
|
|
266
|
+
"You MUST respond with a single JSON object that strictly matches "
|
|
267
|
+
"the schema below. Output JSON only — no markdown fences, no prose, "
|
|
268
|
+
"no explanation.\n\n"
|
|
269
|
+
f"Schema:\n{json.dumps(schema, indent=2)}"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _extract_json_schema(response_format: Any) -> dict[str, Any] | None:
|
|
274
|
+
"""Extract a JSON schema from LiteLLM's response_format values.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
response_format: Pydantic class, LiteLLM dict
|
|
278
|
+
(``{"type": "json_schema", "json_schema": {"schema": ...}}``),
|
|
279
|
+
or a raw JSON-schema dict.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
dict | None: The JSON schema, or None if one cannot be extracted.
|
|
283
|
+
"""
|
|
284
|
+
if response_format is None:
|
|
285
|
+
return None
|
|
286
|
+
if inspect.isclass(response_format) and issubclass(response_format, BaseModel):
|
|
287
|
+
return response_format.model_json_schema()
|
|
288
|
+
if isinstance(response_format, dict):
|
|
289
|
+
if response_format.get("type") == "json_schema":
|
|
290
|
+
inner = response_format.get("json_schema") or {}
|
|
291
|
+
if isinstance(inner, dict):
|
|
292
|
+
schema = inner.get("schema") or inner
|
|
293
|
+
if isinstance(schema, dict):
|
|
294
|
+
return schema
|
|
295
|
+
if "properties" in response_format or "$ref" in response_format:
|
|
296
|
+
return response_format
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _split_system_and_dialogue(
|
|
301
|
+
messages: list[dict[str, Any]],
|
|
302
|
+
) -> tuple[str, str]:
|
|
303
|
+
"""Split chat messages into (system_prompt, dialogue) for the CLI.
|
|
304
|
+
|
|
305
|
+
The ``claude -p`` CLI takes one stdin prompt and an optional
|
|
306
|
+
``--append-system-prompt``. Multi-turn context is flattened into a
|
|
307
|
+
single textual dialogue prefixed with role labels, since the CLI
|
|
308
|
+
does not accept a messages array.
|
|
309
|
+
|
|
310
|
+
System messages are merged (joined with blank lines) and returned
|
|
311
|
+
separately for the ``--append-system-prompt`` flag. ``tool`` role
|
|
312
|
+
messages are folded in as ``Tool:`` lines.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
messages: LiteLLM-style chat messages.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
tuple[str, str]: ``(system_prompt, dialogue)``. Either may be empty.
|
|
319
|
+
"""
|
|
320
|
+
systems: list[str] = []
|
|
321
|
+
turns: list[str] = []
|
|
322
|
+
non_system_roles = 0
|
|
323
|
+
for msg in messages:
|
|
324
|
+
role = msg.get("role", "user")
|
|
325
|
+
content = _flatten_content(msg.get("content"))
|
|
326
|
+
tool_calls = msg.get("tool_calls") if role == "assistant" else None
|
|
327
|
+
if not content and not tool_calls:
|
|
328
|
+
continue
|
|
329
|
+
if role == "system":
|
|
330
|
+
systems.append(content)
|
|
331
|
+
continue
|
|
332
|
+
non_system_roles += 1
|
|
333
|
+
if role == "assistant":
|
|
334
|
+
# When the assistant message carries tool_calls (content is
|
|
335
|
+
# typically None), serialise them as breadcrumbs so the CLI's
|
|
336
|
+
# next-turn context shows which tools were invoked. This is
|
|
337
|
+
# required for multi-turn tool loops to converge.
|
|
338
|
+
if tool_calls:
|
|
339
|
+
breadcrumbs = []
|
|
340
|
+
for tc in tool_calls:
|
|
341
|
+
name = _tool_call_attr(tc, "name") or "?"
|
|
342
|
+
args = _tool_call_attr(tc, "arguments") or "{}"
|
|
343
|
+
breadcrumbs.append(f"called {name} with {args}")
|
|
344
|
+
prefix = "; ".join(breadcrumbs)
|
|
345
|
+
if content:
|
|
346
|
+
turns.append(f"Assistant: {content}\n[tools: {prefix}]")
|
|
347
|
+
else:
|
|
348
|
+
turns.append(f"Assistant: [tools: {prefix}]")
|
|
349
|
+
else:
|
|
350
|
+
turns.append(f"Assistant: {content}")
|
|
351
|
+
elif role == "tool":
|
|
352
|
+
tcid = msg.get("tool_call_id") or "?"
|
|
353
|
+
turns.append(f"Tool[{tcid}]: {content}")
|
|
354
|
+
else:
|
|
355
|
+
turns.append(f"User: {content}")
|
|
356
|
+
if non_system_roles > 1:
|
|
357
|
+
_warn_multiturn_once()
|
|
358
|
+
return "\n\n".join(systems), "\n\n".join(turns)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _tool_call_attr(tc: Any, attr: str) -> str | None:
|
|
362
|
+
"""Read a tool-call field from either a LiteLLM object or a plain dict.
|
|
363
|
+
|
|
364
|
+
``tool_calls`` entries may be ``ChatCompletionMessageToolCall`` objects
|
|
365
|
+
(each carrying a ``.function`` with ``.name`` / ``.arguments``) or
|
|
366
|
+
raw dicts (``{"function": {"name": ..., "arguments": ...}}``). Walk
|
|
367
|
+
both shapes.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
tc: A single ``tool_calls`` entry (object or dict).
|
|
371
|
+
attr: The attribute to read (``"name"`` or ``"arguments"``).
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
str | None: The attribute's string value, or ``None`` if absent.
|
|
375
|
+
"""
|
|
376
|
+
if isinstance(tc, dict):
|
|
377
|
+
fn = tc.get("function") or {}
|
|
378
|
+
value = fn.get(attr) if isinstance(fn, dict) else getattr(fn, attr, None)
|
|
379
|
+
else:
|
|
380
|
+
fn = getattr(tc, "function", None)
|
|
381
|
+
value = getattr(fn, attr, None) if fn is not None else None
|
|
382
|
+
if value is None:
|
|
383
|
+
return None
|
|
384
|
+
return value if isinstance(value, str) else json.dumps(value)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _run_cli_stream(
|
|
388
|
+
cli_path: str,
|
|
389
|
+
system_prompt: str,
|
|
390
|
+
dialogue: str,
|
|
391
|
+
timeout_seconds: int,
|
|
392
|
+
) -> ParseResult:
|
|
393
|
+
"""Invoke the active host CLI and return a ParseResult.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
cli_path (str): Path to the host executable or compatibility wrapper.
|
|
397
|
+
system_prompt (str): Combined system prompt to append (may be empty).
|
|
398
|
+
dialogue (str): Flattened user/assistant dialogue sent on stdin.
|
|
399
|
+
timeout_seconds (int): Subprocess timeout.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
ParseResult: Aggregated state of the stream — success flag, terminal
|
|
403
|
+
text, retry errors observed, and stderr.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
ClaudeCodeCLIError: On timeout or missing binary.
|
|
407
|
+
"""
|
|
408
|
+
if (
|
|
409
|
+
_host() == _HOST_CODEX
|
|
410
|
+
and Path(cli_path).name not in _CODEX_COMPAT_SCRIPT_NAME_SET
|
|
411
|
+
):
|
|
412
|
+
return _run_codex_stream(
|
|
413
|
+
codex_path=cli_path,
|
|
414
|
+
system_prompt=system_prompt,
|
|
415
|
+
dialogue=dialogue,
|
|
416
|
+
timeout_seconds=timeout_seconds,
|
|
417
|
+
)
|
|
418
|
+
return _run_claude_stream(
|
|
419
|
+
cli_path=cli_path,
|
|
420
|
+
system_prompt=system_prompt,
|
|
421
|
+
dialogue=dialogue,
|
|
422
|
+
timeout_seconds=timeout_seconds,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _run_claude_stream(
|
|
427
|
+
*,
|
|
428
|
+
cli_path: str,
|
|
429
|
+
system_prompt: str,
|
|
430
|
+
dialogue: str,
|
|
431
|
+
timeout_seconds: int,
|
|
432
|
+
) -> ParseResult:
|
|
433
|
+
"""Invoke ``claude -p --output-format stream-json`` and return a ParseResult."""
|
|
434
|
+
model = os.environ.get(_ENV_MODEL) or _DEFAULT_CLI_MODEL
|
|
435
|
+
cmd = [
|
|
436
|
+
cli_path,
|
|
437
|
+
"-p",
|
|
438
|
+
"--output-format",
|
|
439
|
+
"stream-json",
|
|
440
|
+
"--verbose",
|
|
441
|
+
"--include-partial-messages",
|
|
442
|
+
"--model",
|
|
443
|
+
model,
|
|
444
|
+
]
|
|
445
|
+
if system_prompt:
|
|
446
|
+
cmd.extend(["--append-system-prompt", system_prompt])
|
|
447
|
+
|
|
448
|
+
# Tag the child process so any hooks it fires (e.g. claude-smart's
|
|
449
|
+
# Stop hook) can detect that this is a reflexio-internal invocation
|
|
450
|
+
# and skip publishing — otherwise extractor system prompts get
|
|
451
|
+
# re-published as user interactions and contaminate the corpus.
|
|
452
|
+
#
|
|
453
|
+
# CLAUDE_CODE_MAX_RETRIES=3 keeps short infrastructure blips tolerated
|
|
454
|
+
# while bounding the worst-case stall to a few seconds before we
|
|
455
|
+
# surface the failure to reflexio's stall_state table.
|
|
456
|
+
env = os.environ.copy()
|
|
457
|
+
env["CLAUDE_SMART_INTERNAL"] = "1"
|
|
458
|
+
env["CLAUDE_CODE_MAX_RETRIES"] = "3"
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
proc = subprocess.run( # noqa: S603 — cmd is constructed from validated parts.
|
|
462
|
+
cmd,
|
|
463
|
+
input=dialogue,
|
|
464
|
+
capture_output=True,
|
|
465
|
+
text=True,
|
|
466
|
+
timeout=timeout_seconds,
|
|
467
|
+
check=False,
|
|
468
|
+
env=env,
|
|
469
|
+
)
|
|
470
|
+
except subprocess.TimeoutExpired as exc:
|
|
471
|
+
raise ClaudeCodeCLIError(
|
|
472
|
+
f"claude CLI timed out after {timeout_seconds}s"
|
|
473
|
+
) from exc
|
|
474
|
+
except FileNotFoundError as exc:
|
|
475
|
+
raise ClaudeCodeCLIError(f"claude CLI not found at {cli_path}") from exc
|
|
476
|
+
|
|
477
|
+
return parse_stream_json(
|
|
478
|
+
proc.stdout, exit_code=proc.returncode, stderr_text=proc.stderr
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _run_codex_stream(
|
|
483
|
+
*,
|
|
484
|
+
codex_path: str,
|
|
485
|
+
system_prompt: str,
|
|
486
|
+
dialogue: str,
|
|
487
|
+
timeout_seconds: int,
|
|
488
|
+
) -> ParseResult:
|
|
489
|
+
"""Invoke ``codex exec`` and shape its output like a terminal stream result."""
|
|
490
|
+
output_path = _temporary_output_path()
|
|
491
|
+
cmd = [
|
|
492
|
+
codex_path,
|
|
493
|
+
"exec",
|
|
494
|
+
"--sandbox",
|
|
495
|
+
"read-only",
|
|
496
|
+
"--skip-git-repo-check",
|
|
497
|
+
"--ephemeral",
|
|
498
|
+
"--ignore-rules",
|
|
499
|
+
"--output-last-message",
|
|
500
|
+
str(output_path),
|
|
501
|
+
"-",
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
env = os.environ.copy()
|
|
505
|
+
env[_ENV_HOST] = _HOST_CODEX
|
|
506
|
+
env["CLAUDE_SMART_INTERNAL"] = "1"
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
proc = subprocess.run( # noqa: S603 — cmd is constructed from validated parts.
|
|
510
|
+
cmd,
|
|
511
|
+
input=_codex_prompt(prompt=dialogue, system_prompt=system_prompt),
|
|
512
|
+
capture_output=True,
|
|
513
|
+
text=True,
|
|
514
|
+
timeout=timeout_seconds,
|
|
515
|
+
check=False,
|
|
516
|
+
env=env,
|
|
517
|
+
)
|
|
518
|
+
try:
|
|
519
|
+
terminal_text = output_path.read_text(encoding="utf-8").strip()
|
|
520
|
+
except OSError:
|
|
521
|
+
terminal_text = ""
|
|
522
|
+
except subprocess.TimeoutExpired as exc:
|
|
523
|
+
raise ClaudeCodeCLIError(
|
|
524
|
+
f"codex CLI timed out after {timeout_seconds}s"
|
|
525
|
+
) from exc
|
|
526
|
+
except FileNotFoundError as exc:
|
|
527
|
+
raise ClaudeCodeCLIError(f"codex CLI not found at {codex_path}") from exc
|
|
528
|
+
finally:
|
|
529
|
+
with suppress(OSError):
|
|
530
|
+
output_path.unlink()
|
|
531
|
+
|
|
532
|
+
return ParseResult(
|
|
533
|
+
success=proc.returncode == 0 and bool(terminal_text),
|
|
534
|
+
terminal_text=terminal_text,
|
|
535
|
+
stderr_text=proc.stderr,
|
|
536
|
+
raw_lines_parsed=1 if terminal_text else 0,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _temporary_output_path() -> Path:
|
|
541
|
+
with tempfile.NamedTemporaryFile(
|
|
542
|
+
prefix="claude-smart-codex-", delete=False
|
|
543
|
+
) as handle:
|
|
544
|
+
return Path(handle.name)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _codex_prompt(*, prompt: str, system_prompt: str) -> str:
|
|
548
|
+
if not system_prompt:
|
|
549
|
+
return prompt
|
|
550
|
+
return f"{system_prompt}\n\n## Task\n{prompt}"
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _build_model_response(
|
|
554
|
+
model: str,
|
|
555
|
+
terminal_text: str,
|
|
556
|
+
elapsed_seconds: float,
|
|
557
|
+
) -> ModelResponse:
|
|
558
|
+
"""Wrap the CLI's terminal text in a LiteLLM ``ModelResponse``.
|
|
559
|
+
|
|
560
|
+
The stream-json transport does not surface usage tokens at the terminal
|
|
561
|
+
event, so prompt/completion counts are reported as zero. Downstream
|
|
562
|
+
LiteLLM callers tolerate this (usage is informational, not load-bearing).
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
model (str): The model string originally requested
|
|
566
|
+
(e.g. ``claude-code/default``).
|
|
567
|
+
terminal_text (str): The terminal ``result`` text from the CLI.
|
|
568
|
+
elapsed_seconds (float): Wall time the subprocess took — for logging only.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
ModelResponse: Shaped to match what callers of ``litellm.completion`` expect.
|
|
572
|
+
"""
|
|
573
|
+
message = Message(role="assistant", content=terminal_text)
|
|
574
|
+
choice = Choices(index=0, message=message, finish_reason="stop")
|
|
575
|
+
usage = Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0)
|
|
576
|
+
response = ModelResponse(
|
|
577
|
+
id=f"claude-code-{int(time.time())}",
|
|
578
|
+
choices=[choice],
|
|
579
|
+
created=int(time.time()),
|
|
580
|
+
model=model,
|
|
581
|
+
object="chat.completion",
|
|
582
|
+
usage=usage,
|
|
583
|
+
)
|
|
584
|
+
_LOGGER.debug(
|
|
585
|
+
"claude-code provider: model=%s elapsed=%.2fs",
|
|
586
|
+
model,
|
|
587
|
+
elapsed_seconds,
|
|
588
|
+
)
|
|
589
|
+
return response
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
_TOOL_USE_INSTRUCTION_TEMPLATE = (
|
|
593
|
+
"## EXTERNAL TOOL-CALLING MODE\n"
|
|
594
|
+
"\n"
|
|
595
|
+
"You are running as a non-interactive subprocess driven by an external "
|
|
596
|
+
"orchestrator. The orchestrator will execute tools on your behalf. You "
|
|
597
|
+
"MUST NOT use Read, Edit, Write, Bash, Glob, Grep, TodoWrite, Task, or "
|
|
598
|
+
"ANY of your built-in Claude Code tools. The tools listed below are "
|
|
599
|
+
"NOT real callable functions in this session — they are the external "
|
|
600
|
+
"tools the ORCHESTRATOR exposes, which YOU describe by emitting a "
|
|
601
|
+
"structured JSON request as your final text response.\n"
|
|
602
|
+
"\n"
|
|
603
|
+
"Your response MUST be EXACTLY one JSON object on a single line, with "
|
|
604
|
+
"this shape:\n"
|
|
605
|
+
'{{"tool": "<tool_name>", "args": {{...}} }}\n'
|
|
606
|
+
"\n"
|
|
607
|
+
"Hard rules — failing any of these will break the orchestrator:\n"
|
|
608
|
+
"1. Output ONLY the JSON object as plain text. No prose before or after.\n"
|
|
609
|
+
"2. No markdown. No ```json``` code fences.\n"
|
|
610
|
+
"3. Do NOT invoke any built-in tool. Do NOT search the codebase. Just "
|
|
611
|
+
" emit the JSON.\n"
|
|
612
|
+
"4. ``tool`` must be exactly one of the names listed below.\n"
|
|
613
|
+
"5. ``args`` must be a JSON object matching that tool's parameters.\n"
|
|
614
|
+
"6. To terminate the orchestrator's loop, call the tool named "
|
|
615
|
+
"``{finish}`` with the appropriate args.\n"
|
|
616
|
+
"\n"
|
|
617
|
+
"Available orchestrator tools (each described by name + params schema):\n"
|
|
618
|
+
"{tool_specs}\n"
|
|
619
|
+
"\n"
|
|
620
|
+
"Now decide which tool to call and output ONLY the JSON object.\n"
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _render_tools_instruction(tools: list[Any], finish_tool: str = "finish") -> str:
|
|
625
|
+
"""Render a LiteLLM tools spec into a system-prompt addendum.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
tools: LiteLLM-style tools list. Each entry is a dict with
|
|
629
|
+
``{"type": "function", "function": {"name", "description", "parameters"}}``.
|
|
630
|
+
finish_tool: The conventional finish tool name (echoed in the
|
|
631
|
+
instructions so the model has a recognised termination signal).
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
str: The system-prompt block to append.
|
|
635
|
+
"""
|
|
636
|
+
lines: list[str] = []
|
|
637
|
+
for tool in tools or []:
|
|
638
|
+
if not isinstance(tool, dict):
|
|
639
|
+
continue
|
|
640
|
+
raw_fn = tool.get("function")
|
|
641
|
+
fn: dict[str, Any] = raw_fn if isinstance(raw_fn, dict) else {}
|
|
642
|
+
name = fn.get("name") or "?"
|
|
643
|
+
desc = fn.get("description") or ""
|
|
644
|
+
params = fn.get("parameters") or {}
|
|
645
|
+
lines.append(
|
|
646
|
+
f"- {name}: {desc}\n parameters: {json.dumps(params, separators=(',', ':'))}"
|
|
647
|
+
)
|
|
648
|
+
tool_specs = "\n".join(lines) if lines else "(no tools)"
|
|
649
|
+
return _TOOL_USE_INSTRUCTION_TEMPLATE.format(
|
|
650
|
+
finish=finish_tool, tool_specs=tool_specs
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _maybe_append_tools_instruction(system_prompt: str, tools: list[Any] | None) -> str:
|
|
655
|
+
"""Append the tool-use instructions when tools are present.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
system_prompt: Existing system prompt (possibly empty).
|
|
659
|
+
tools: LiteLLM-style tools list, or None/empty.
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
str: The (possibly-augmented) system prompt.
|
|
663
|
+
"""
|
|
664
|
+
if not tools:
|
|
665
|
+
return system_prompt
|
|
666
|
+
instruction = _render_tools_instruction(tools)
|
|
667
|
+
if system_prompt:
|
|
668
|
+
return f"{system_prompt}\n\n{instruction}"
|
|
669
|
+
return instruction
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _parse_tool_use(text: str, tool_names: set[str]) -> dict[str, Any] | None:
|
|
673
|
+
"""Try to parse a ``{"tool": ..., "args": ...}`` block from claude's output.
|
|
674
|
+
|
|
675
|
+
Walks candidate JSON substrings (plain, code-fenced, first balanced
|
|
676
|
+
``{...}``) in order, returning the first that parses to a dict with a
|
|
677
|
+
recognised ``tool`` name and a dict ``args``.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
text: Raw text from claude's ``-p --output-format json`` ``result``.
|
|
681
|
+
tool_names: Set of tool names registered by the caller. The returned
|
|
682
|
+
``tool`` must be in this set.
|
|
683
|
+
|
|
684
|
+
Returns:
|
|
685
|
+
dict | None: ``{"name": str, "args": dict}`` on success; ``None`` if
|
|
686
|
+
no valid tool-use JSON could be located.
|
|
687
|
+
"""
|
|
688
|
+
stripped = (text or "").strip()
|
|
689
|
+
if not stripped:
|
|
690
|
+
return None
|
|
691
|
+
candidates: list[str] = [stripped]
|
|
692
|
+
fence = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", stripped, re.DOTALL)
|
|
693
|
+
if fence:
|
|
694
|
+
candidates.append(fence.group(1))
|
|
695
|
+
# Find the first balanced ``{...}`` object via JSONDecoder.raw_decode
|
|
696
|
+
# instead of the greedy ``r"\{.*\}"`` regex. The greedy form swallows
|
|
697
|
+
# any trailing ``{...}`` on the same line and silently misses a valid
|
|
698
|
+
# tool-call when claude appends explanatory text after the JSON.
|
|
699
|
+
decoder = json.JSONDecoder()
|
|
700
|
+
for idx, ch in enumerate(stripped):
|
|
701
|
+
if ch != "{":
|
|
702
|
+
continue
|
|
703
|
+
try:
|
|
704
|
+
obj, end = decoder.raw_decode(stripped[idx:])
|
|
705
|
+
except json.JSONDecodeError:
|
|
706
|
+
continue
|
|
707
|
+
if isinstance(obj, dict):
|
|
708
|
+
candidates.append(stripped[idx : idx + end])
|
|
709
|
+
break
|
|
710
|
+
for candidate in candidates:
|
|
711
|
+
try:
|
|
712
|
+
parsed = json.loads(candidate)
|
|
713
|
+
except json.JSONDecodeError:
|
|
714
|
+
continue
|
|
715
|
+
if not isinstance(parsed, dict):
|
|
716
|
+
continue
|
|
717
|
+
name = parsed.get("tool")
|
|
718
|
+
args = parsed.get("args")
|
|
719
|
+
if name in tool_names and isinstance(args, dict):
|
|
720
|
+
return {"name": name, "args": args}
|
|
721
|
+
return None
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _build_model_response_with_tool_call(
|
|
725
|
+
*,
|
|
726
|
+
model: str,
|
|
727
|
+
terminal_text: str,
|
|
728
|
+
elapsed_seconds: float,
|
|
729
|
+
tool_use: dict[str, Any],
|
|
730
|
+
) -> ModelResponse:
|
|
731
|
+
"""Wrap the CLI terminal text as a ``ModelResponse`` carrying one ``tool_calls`` entry.
|
|
732
|
+
|
|
733
|
+
The stream-json transport does not surface usage tokens at the terminal
|
|
734
|
+
event, so prompt/completion counts are reported as zero (same convention
|
|
735
|
+
as :func:`_build_model_response`). Downstream LiteLLM callers tolerate
|
|
736
|
+
this — usage is informational, not load-bearing.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
model: Model string passed in by LiteLLM.
|
|
740
|
+
terminal_text: The terminal ``result`` text from the CLI (retained
|
|
741
|
+
for signature parity with the plain-text branch; surfaced via
|
|
742
|
+
logging only).
|
|
743
|
+
elapsed_seconds: Subprocess wall time, for logging only.
|
|
744
|
+
tool_use: Parsed ``{"name": str, "args": dict}`` from the model output.
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
ModelResponse: A LiteLLM response with ``choices[0].message.tool_calls`` set
|
|
748
|
+
and ``content`` set to ``None`` — matches OpenAI/Anthropic tool-call shape.
|
|
749
|
+
"""
|
|
750
|
+
del terminal_text # retained for signature parity only
|
|
751
|
+
call_id = f"call_{int(time.time() * 1000)}"
|
|
752
|
+
tool_call = ChatCompletionMessageToolCall(
|
|
753
|
+
id=call_id,
|
|
754
|
+
type="function",
|
|
755
|
+
function=Function(
|
|
756
|
+
name=tool_use["name"],
|
|
757
|
+
arguments=json.dumps(tool_use["args"]),
|
|
758
|
+
),
|
|
759
|
+
)
|
|
760
|
+
message = Message(role="assistant", content=None, tool_calls=[tool_call])
|
|
761
|
+
choice = Choices(index=0, message=message, finish_reason="tool_calls")
|
|
762
|
+
usage = Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0)
|
|
763
|
+
response = ModelResponse(
|
|
764
|
+
id=f"claude-code-{int(time.time())}",
|
|
765
|
+
choices=[choice],
|
|
766
|
+
created=int(time.time()),
|
|
767
|
+
model=model,
|
|
768
|
+
object="chat.completion",
|
|
769
|
+
usage=usage,
|
|
770
|
+
)
|
|
771
|
+
_LOGGER.debug(
|
|
772
|
+
"claude-code provider: tool_call name=%s elapsed=%.2fs",
|
|
773
|
+
tool_use["name"],
|
|
774
|
+
elapsed_seconds,
|
|
775
|
+
)
|
|
776
|
+
return response
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _maybe_append_schema(system_prompt: str, response_format: Any) -> str:
|
|
780
|
+
"""Return *system_prompt* extended with a JSON-schema instruction when applicable.
|
|
781
|
+
|
|
782
|
+
Args:
|
|
783
|
+
system_prompt: Existing system prompt (possibly empty).
|
|
784
|
+
response_format: The LiteLLM response_format value, if any.
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
str: The (possibly-augmented) system prompt.
|
|
788
|
+
"""
|
|
789
|
+
instruction = _schema_instruction(response_format)
|
|
790
|
+
if not instruction:
|
|
791
|
+
return system_prompt
|
|
792
|
+
if system_prompt:
|
|
793
|
+
return f"{system_prompt}\n\n{instruction}"
|
|
794
|
+
return instruction
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
_IGNORED_PARAMS: tuple[str, ...] = (
|
|
798
|
+
"temperature",
|
|
799
|
+
"max_tokens",
|
|
800
|
+
"top_p",
|
|
801
|
+
"stop",
|
|
802
|
+
"seed",
|
|
803
|
+
"frequency_penalty",
|
|
804
|
+
"presence_penalty",
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _warn_on_ignored_params(*sources: Any) -> None:
|
|
809
|
+
"""Emit one-time warnings for LiteLLM params the CLI cannot honour.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
*sources: Any number of dict-like sources (kwargs, ``optional_params``).
|
|
813
|
+
|
|
814
|
+
Returns:
|
|
815
|
+
None
|
|
816
|
+
"""
|
|
817
|
+
for source in sources:
|
|
818
|
+
if not isinstance(source, dict):
|
|
819
|
+
continue
|
|
820
|
+
for name in _IGNORED_PARAMS:
|
|
821
|
+
if source.get(name) is not None:
|
|
822
|
+
_warn_unsupported_param_once(name)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
class ClaudeCodeLLM(CustomLLM):
|
|
826
|
+
"""LiteLLM custom handler routing completions through the ``claude`` CLI."""
|
|
827
|
+
|
|
828
|
+
def __init__(
|
|
829
|
+
self,
|
|
830
|
+
cli_path: str | None = None,
|
|
831
|
+
timeout_seconds: int | None = None,
|
|
832
|
+
storage: Any | None = None,
|
|
833
|
+
):
|
|
834
|
+
"""Initialise the handler.
|
|
835
|
+
|
|
836
|
+
Args:
|
|
837
|
+
cli_path (str | None): Override for the ``claude`` binary path.
|
|
838
|
+
timeout_seconds (int | None): Override for subprocess timeout.
|
|
839
|
+
storage (Any | None): A BaseStorage-shaped object used to persist
|
|
840
|
+
stall_state on credit/auth failures. Typed as ``Any`` to avoid
|
|
841
|
+
a circular import with ``server.services.storage``. When None,
|
|
842
|
+
stall state is not recorded (back-compat).
|
|
843
|
+
"""
|
|
844
|
+
super().__init__()
|
|
845
|
+
self._explicit_cli_path = cli_path
|
|
846
|
+
self._explicit_timeout = timeout_seconds
|
|
847
|
+
self._storage = storage
|
|
848
|
+
|
|
849
|
+
def _cli_path(self) -> str:
|
|
850
|
+
"""Resolve the CLI path, raising when unavailable.
|
|
851
|
+
|
|
852
|
+
Returns:
|
|
853
|
+
str: Absolute path to the ``claude`` executable.
|
|
854
|
+
|
|
855
|
+
Raises:
|
|
856
|
+
ClaudeCodeCLIError: If the CLI cannot be located.
|
|
857
|
+
"""
|
|
858
|
+
path = self._explicit_cli_path or _resolve_cli_path()
|
|
859
|
+
if not path:
|
|
860
|
+
raise ClaudeCodeCLIError(
|
|
861
|
+
f"{_cli_name()} CLI not found for {_host()}. Install the host "
|
|
862
|
+
f"CLI or set {_ENV_CLI_PATH} to an executable path."
|
|
863
|
+
)
|
|
864
|
+
return path
|
|
865
|
+
|
|
866
|
+
def _timeout(self) -> int:
|
|
867
|
+
"""Resolve the subprocess timeout.
|
|
868
|
+
|
|
869
|
+
Returns:
|
|
870
|
+
int: Timeout in seconds.
|
|
871
|
+
"""
|
|
872
|
+
if self._explicit_timeout is not None:
|
|
873
|
+
return self._explicit_timeout
|
|
874
|
+
raw = os.environ.get(_ENV_TIMEOUT)
|
|
875
|
+
if raw:
|
|
876
|
+
try:
|
|
877
|
+
return max(1, int(raw))
|
|
878
|
+
except ValueError:
|
|
879
|
+
_LOGGER.warning("Ignoring non-integer %s=%r", _ENV_TIMEOUT, raw)
|
|
880
|
+
return _DEFAULT_TIMEOUT_SECONDS
|
|
881
|
+
|
|
882
|
+
def completion( # type: ignore[override]
|
|
883
|
+
self,
|
|
884
|
+
*args: Any,
|
|
885
|
+
model: str = "claude-code/default",
|
|
886
|
+
messages: list[dict[str, Any]] | None = None,
|
|
887
|
+
optional_params: dict[str, Any] | None = None,
|
|
888
|
+
**kwargs: Any,
|
|
889
|
+
) -> ModelResponse:
|
|
890
|
+
"""Execute a completion via the ``claude`` CLI.
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
*args: Ignored; LiteLLM may pass ``model_response`` positionally.
|
|
894
|
+
model: The requested model slug (e.g. ``claude-code/default``).
|
|
895
|
+
messages: LiteLLM-style chat messages.
|
|
896
|
+
optional_params: LiteLLM's bag of generation params — checked for
|
|
897
|
+
``response_format`` and logged when unsupported knobs are set.
|
|
898
|
+
**kwargs: Other LiteLLM arguments (api_key, logging_obj, etc.);
|
|
899
|
+
ignored by this handler.
|
|
900
|
+
|
|
901
|
+
Returns:
|
|
902
|
+
ModelResponse: Shaped to match what ``litellm.completion`` callers expect.
|
|
903
|
+
|
|
904
|
+
Raises:
|
|
905
|
+
ClaudeCodeCLIError: On CLI failure or missing binary.
|
|
906
|
+
"""
|
|
907
|
+
del args
|
|
908
|
+
messages = messages or []
|
|
909
|
+
optional_params = optional_params or {}
|
|
910
|
+
|
|
911
|
+
# Tools may arrive in either ``optional_params["tools"]`` (LiteLLM's
|
|
912
|
+
# standard plumbing for custom providers) or as a top-level ``tools``
|
|
913
|
+
# kwarg. Read both before discarding kwargs.
|
|
914
|
+
tools = optional_params.get("tools") or kwargs.get("tools")
|
|
915
|
+
del kwargs
|
|
916
|
+
|
|
917
|
+
_warn_on_ignored_params(optional_params)
|
|
918
|
+
|
|
919
|
+
response_format = optional_params.get("response_format")
|
|
920
|
+
system_prompt, dialogue = _split_system_and_dialogue(messages)
|
|
921
|
+
# When ``tools`` is present, render the tools spec into the system
|
|
922
|
+
# prompt and ignore response_format (mutually exclusive with our
|
|
923
|
+
# tool_use JSON output contract).
|
|
924
|
+
if tools:
|
|
925
|
+
system_prompt = _maybe_append_tools_instruction(system_prompt, tools)
|
|
926
|
+
else:
|
|
927
|
+
system_prompt = _maybe_append_schema(system_prompt, response_format)
|
|
928
|
+
|
|
929
|
+
started = time.perf_counter()
|
|
930
|
+
result = _run_cli_stream(
|
|
931
|
+
cli_path=self._cli_path(),
|
|
932
|
+
system_prompt=system_prompt,
|
|
933
|
+
dialogue=dialogue,
|
|
934
|
+
timeout_seconds=self._timeout(),
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
if result.success:
|
|
938
|
+
self._clear_stall_safely()
|
|
939
|
+
elapsed = time.perf_counter() - started
|
|
940
|
+
|
|
941
|
+
# When ``tools`` are provided, attempt to parse the model's
|
|
942
|
+
# terminal text as a ``{"tool": ..., "args": ...}`` JSON object.
|
|
943
|
+
# If parsing succeeds, return a tool-call ModelResponse; otherwise
|
|
944
|
+
# warn (so the silent fall-through is observable) and return a
|
|
945
|
+
# plain-text response, which the caller treats as "no tool_calls"
|
|
946
|
+
# and uses to terminate the tool loop.
|
|
947
|
+
if tools:
|
|
948
|
+
tool_names: set[str] = {
|
|
949
|
+
name
|
|
950
|
+
for tool in tools
|
|
951
|
+
if isinstance(tool, dict)
|
|
952
|
+
and isinstance(
|
|
953
|
+
name := (tool.get("function") or {}).get("name"), str
|
|
954
|
+
)
|
|
955
|
+
}
|
|
956
|
+
tool_use = _parse_tool_use(result.terminal_text, tool_names)
|
|
957
|
+
if tool_use is not None:
|
|
958
|
+
return _build_model_response_with_tool_call(
|
|
959
|
+
model=model,
|
|
960
|
+
terminal_text=result.terminal_text,
|
|
961
|
+
elapsed_seconds=elapsed,
|
|
962
|
+
tool_use=tool_use,
|
|
963
|
+
)
|
|
964
|
+
# Log a metadata-only warning (no raw payload) — the model
|
|
965
|
+
# output can carry user content / source code; deferring the
|
|
966
|
+
# body to a DEBUG fingerprint avoids turning a recoverable
|
|
967
|
+
# parse miss into a log-retention concern.
|
|
968
|
+
_LOGGER.warning(
|
|
969
|
+
"claude-code provider: tools=%s were provided but no valid "
|
|
970
|
+
"tool_use JSON was parsed from model output; the tool loop "
|
|
971
|
+
"will terminate without a finish_tool call.",
|
|
972
|
+
sorted(tool_names),
|
|
973
|
+
)
|
|
974
|
+
_LOGGER.debug(
|
|
975
|
+
"claude-code provider: unparsable tool-use payload length=%d",
|
|
976
|
+
len(result.terminal_text),
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
return _build_model_response(
|
|
980
|
+
model=model,
|
|
981
|
+
terminal_text=result.terminal_text,
|
|
982
|
+
elapsed_seconds=elapsed,
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
self._record_stall_safely(result)
|
|
986
|
+
raise ClaudeCodeCLIError(
|
|
987
|
+
f"claude -p stream failed; retry_errors={result.retry_errors}; "
|
|
988
|
+
f"stderr={result.stderr_text[:200]!r}"
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
def _record_stall_safely(self, result: ParseResult) -> None:
|
|
992
|
+
"""Persist a stall_state row on credit/auth failure. Never raises.
|
|
993
|
+
|
|
994
|
+
Args:
|
|
995
|
+
result (ParseResult): Output of :func:`_run_cli_stream`.
|
|
996
|
+
|
|
997
|
+
Returns:
|
|
998
|
+
None
|
|
999
|
+
"""
|
|
1000
|
+
reason = classify_stall(result)
|
|
1001
|
+
if reason is None or self._storage is None:
|
|
1002
|
+
return
|
|
1003
|
+
try:
|
|
1004
|
+
self._storage.upsert_stall_state(
|
|
1005
|
+
reason=reason,
|
|
1006
|
+
stalled_at=datetime.now(UTC),
|
|
1007
|
+
reset_estimate=parse_reset_estimate(
|
|
1008
|
+
f"{result.stderr_text} {result.terminal_text}"
|
|
1009
|
+
),
|
|
1010
|
+
error_message=(result.stderr_text or result.terminal_text)[:1000],
|
|
1011
|
+
)
|
|
1012
|
+
except Exception as exc: # noqa: BLE001 — never crash the provider over telemetry.
|
|
1013
|
+
_LOGGER.warning("Failed to record stall_state: %s", exc)
|
|
1014
|
+
|
|
1015
|
+
def _clear_stall_safely(self) -> None:
|
|
1016
|
+
"""Clear any prior stall_state row after a successful run. Never raises.
|
|
1017
|
+
|
|
1018
|
+
Returns:
|
|
1019
|
+
None
|
|
1020
|
+
"""
|
|
1021
|
+
if self._storage is None:
|
|
1022
|
+
return
|
|
1023
|
+
try:
|
|
1024
|
+
self._storage.clear_stall_state()
|
|
1025
|
+
except Exception as exc: # noqa: BLE001 — never crash the provider over telemetry.
|
|
1026
|
+
_LOGGER.warning("Failed to clear stall_state: %s", exc)
|
|
1027
|
+
|
|
1028
|
+
async def acompletion( # type: ignore[override]
|
|
1029
|
+
self, *args: Any, **kwargs: Any
|
|
1030
|
+
) -> ModelResponse:
|
|
1031
|
+
"""Async entry point — delegates to the sync CLI call via ``to_thread``.
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
*args: Forwarded to :meth:`completion`.
|
|
1035
|
+
**kwargs: Forwarded to :meth:`completion`.
|
|
1036
|
+
|
|
1037
|
+
Returns:
|
|
1038
|
+
ModelResponse: The CLI-backed completion result.
|
|
1039
|
+
"""
|
|
1040
|
+
import asyncio
|
|
1041
|
+
|
|
1042
|
+
return await asyncio.to_thread(self.completion, *args, **kwargs)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
_REGISTERED = False
|
|
1046
|
+
_HANDLER: ClaudeCodeLLM | None = None
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def register_if_enabled(storage: Any | None = None) -> bool:
|
|
1050
|
+
"""Register the ``claude-code`` provider with LiteLLM if enabled and available.
|
|
1051
|
+
|
|
1052
|
+
Idempotent — safe to call more than once per process. Opt-in via
|
|
1053
|
+
``CLAUDE_SMART_USE_LOCAL_CLI=1``. Skips registration (with a warning)
|
|
1054
|
+
when the env var is set but the CLI is not on PATH.
|
|
1055
|
+
|
|
1056
|
+
Args:
|
|
1057
|
+
storage (Any | None): Optional BaseStorage-shaped handle used by the
|
|
1058
|
+
provider to persist stall_state on credit/auth failures. The
|
|
1059
|
+
caller (``LiteLLMClient`` import-time wiring) typically has no
|
|
1060
|
+
storage available, so use :func:`set_storage` to late-bind it
|
|
1061
|
+
once a request context exists.
|
|
1062
|
+
|
|
1063
|
+
Returns:
|
|
1064
|
+
bool: True if the provider is registered after this call.
|
|
1065
|
+
"""
|
|
1066
|
+
global _REGISTERED, _HANDLER
|
|
1067
|
+
if _REGISTERED:
|
|
1068
|
+
if storage is not None and _HANDLER is not None:
|
|
1069
|
+
_HANDLER._storage = storage
|
|
1070
|
+
return True
|
|
1071
|
+
if not _env_enabled():
|
|
1072
|
+
return False
|
|
1073
|
+
cli_path = _resolve_cli_path()
|
|
1074
|
+
if not cli_path:
|
|
1075
|
+
_LOGGER.warning(
|
|
1076
|
+
"%s=1 is set but the %s CLI is not available for %s. "
|
|
1077
|
+
"Install the host CLI or set %s; skipping provider registration.",
|
|
1078
|
+
ENV_ENABLE,
|
|
1079
|
+
_cli_name(),
|
|
1080
|
+
_host(),
|
|
1081
|
+
_ENV_CLI_PATH,
|
|
1082
|
+
)
|
|
1083
|
+
return False
|
|
1084
|
+
|
|
1085
|
+
existing = list(getattr(litellm, "custom_provider_map", None) or [])
|
|
1086
|
+
if any(entry.get("provider") == PROVIDER_KEY for entry in existing):
|
|
1087
|
+
_REGISTERED = True
|
|
1088
|
+
return True
|
|
1089
|
+
_HANDLER = ClaudeCodeLLM(storage=storage)
|
|
1090
|
+
existing.append({"provider": PROVIDER_KEY, "custom_handler": _HANDLER})
|
|
1091
|
+
litellm.custom_provider_map = existing
|
|
1092
|
+
_REGISTERED = True
|
|
1093
|
+
_LOGGER.info("Registered %s LiteLLM provider (cli=%s)", PROVIDER_KEY, cli_path)
|
|
1094
|
+
return True
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def set_storage(storage: Any) -> None:
|
|
1098
|
+
"""Bind storage onto the registered handler after registration.
|
|
1099
|
+
|
|
1100
|
+
The provider is registered at LiteLLM-import time, before any
|
|
1101
|
+
request-scoped storage exists. Once a storage instance is available,
|
|
1102
|
+
call this to enable stall_state persistence on the live handler.
|
|
1103
|
+
|
|
1104
|
+
Args:
|
|
1105
|
+
storage (Any): BaseStorage-shaped instance.
|
|
1106
|
+
|
|
1107
|
+
Returns:
|
|
1108
|
+
None
|
|
1109
|
+
"""
|
|
1110
|
+
if _HANDLER is not None:
|
|
1111
|
+
_HANDLER._storage = storage
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
__all__ = [
|
|
1115
|
+
"ENV_ENABLE",
|
|
1116
|
+
"PROVIDER_KEY",
|
|
1117
|
+
"ClaudeCodeCLIError",
|
|
1118
|
+
"ClaudeCodeLLM",
|
|
1119
|
+
"is_claude_code_available",
|
|
1120
|
+
"register_if_enabled",
|
|
1121
|
+
"set_storage",
|
|
1122
|
+
]
|