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,1595 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LiteLLM-based unified LLM client.
|
|
3
|
+
|
|
4
|
+
This module provides a unified interface to multiple LLM providers (OpenAI, Claude, Azure OpenAI)
|
|
5
|
+
using LiteLLM. It maintains the same interface as the existing LLMClient for easy replacement.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import litellm
|
|
19
|
+
import tiktoken
|
|
20
|
+
from pydantic import BaseModel
|
|
21
|
+
|
|
22
|
+
from reflexio.models.config_schema import APIKeyConfig
|
|
23
|
+
from reflexio.server.llm.image_utils import (
|
|
24
|
+
SUPPORTED_IMAGE_MIME_TYPES,
|
|
25
|
+
ImageEncodingError,
|
|
26
|
+
)
|
|
27
|
+
from reflexio.server.llm.image_utils import (
|
|
28
|
+
encode_image_to_base64 as _encode_image_to_base64,
|
|
29
|
+
)
|
|
30
|
+
from reflexio.server.llm.llm_utils import (
|
|
31
|
+
is_pydantic_model,
|
|
32
|
+
strict_response_format_for_model,
|
|
33
|
+
)
|
|
34
|
+
from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
|
|
35
|
+
from reflexio.server.llm.providers.claude_code_provider import (
|
|
36
|
+
register_if_enabled as _register_claude_code,
|
|
37
|
+
)
|
|
38
|
+
from reflexio.server.llm.providers.embedding_service_provider import (
|
|
39
|
+
EmbeddingUnavailableError,
|
|
40
|
+
embedding_provider_mode,
|
|
41
|
+
get_service_embeddings,
|
|
42
|
+
should_use_embedding_service,
|
|
43
|
+
)
|
|
44
|
+
from reflexio.server.llm.providers.local_embedding_provider import (
|
|
45
|
+
LocalEmbedder,
|
|
46
|
+
)
|
|
47
|
+
from reflexio.server.llm.providers.local_embedding_provider import (
|
|
48
|
+
is_chromadb_importable as _is_chromadb_importable,
|
|
49
|
+
)
|
|
50
|
+
from reflexio.server.llm.providers.local_embedding_provider import (
|
|
51
|
+
register_if_chromadb_available as _register_local_embedder,
|
|
52
|
+
)
|
|
53
|
+
from reflexio.server.llm.providers.nomic_embedding_provider import (
|
|
54
|
+
NomicEmbedder,
|
|
55
|
+
)
|
|
56
|
+
from reflexio.server.llm.providers.nomic_embedding_provider import (
|
|
57
|
+
is_enabled as _nomic_embedder_enabled,
|
|
58
|
+
)
|
|
59
|
+
from reflexio.server.llm.providers.nomic_embedding_provider import (
|
|
60
|
+
is_nomic_model as _is_nomic_model,
|
|
61
|
+
)
|
|
62
|
+
from reflexio.server.llm.providers.nomic_embedding_provider import (
|
|
63
|
+
register_if_enabled as _register_nomic_embedder,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Suppress LiteLLM's verbose logging
|
|
67
|
+
litellm.suppress_debug_info = True
|
|
68
|
+
|
|
69
|
+
# Opt-in registration of claude-smart's local providers. All no-ops
|
|
70
|
+
# unless the matching env var is set. Safe to call at import.
|
|
71
|
+
_register_claude_code()
|
|
72
|
+
_register_local_embedder()
|
|
73
|
+
_register_nomic_embedder()
|
|
74
|
+
|
|
75
|
+
_LOGGER = logging.getLogger(__name__)
|
|
76
|
+
|
|
77
|
+
# OpenAI's documented max input length for text-embedding-3-* and ada-002 is
|
|
78
|
+
# 8191 tokens. Used as the fallback limit only when a model's name looks
|
|
79
|
+
# OpenAI-family but litellm's registry has no entry for it.
|
|
80
|
+
_OPENAI_EMBEDDING_FALLBACK_MAX_TOKENS = 8191
|
|
81
|
+
|
|
82
|
+
# Models whose truncation warning has already been emitted this process. Keeps
|
|
83
|
+
# batch backfills of millions of long docs from flooding logs — the first hit
|
|
84
|
+
# per model goes to WARNING, everything after to DEBUG.
|
|
85
|
+
_TRUNCATION_WARNED_MODELS: set[str] = set()
|
|
86
|
+
|
|
87
|
+
# Model-name prefixes that route through OpenAI's embedding API (and therefore
|
|
88
|
+
# share the 8191-token cap). Anything that does not start with one of these is
|
|
89
|
+
# treated as "unknown provider" when litellm has no registry entry.
|
|
90
|
+
_OPENAI_EMBEDDING_FAMILY_PREFIXES = ("text-embedding-", "openai/", "azure/")
|
|
91
|
+
|
|
92
|
+
# Python-to-JSON keyword replacements used by _sanitize_json_string.
|
|
93
|
+
_PYTHON_TO_JSON_REPLACEMENTS = {"True": "true", "False": "false", "None": "null"}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@lru_cache(maxsize=32)
|
|
97
|
+
def _get_embedding_limit(model: str) -> int | None:
|
|
98
|
+
"""
|
|
99
|
+
Resolve the maximum input token count for an embedding model.
|
|
100
|
+
|
|
101
|
+
Consults ``litellm.get_model_info`` first so provider-specific caps are
|
|
102
|
+
respected (OpenAI ~8191, Cohere 512, Voyage 32000, etc.). When litellm has
|
|
103
|
+
no entry for the model, falls back to the OpenAI 8191 cap only when the
|
|
104
|
+
model name looks OpenAI-family; otherwise returns ``None`` to disable
|
|
105
|
+
truncation for unknown providers (safer than over-truncating their input).
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
model (str): Embedding model name (e.g. 'text-embedding-3-small',
|
|
109
|
+
'cohere/embed-english-v3.0').
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
int | None: Maximum input tokens, or ``None`` when the limit is unknown
|
|
113
|
+
and no safe fallback applies.
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
info = litellm.get_model_info(model)
|
|
117
|
+
except Exception:
|
|
118
|
+
info = None
|
|
119
|
+
if info and info.get("mode") == "embedding":
|
|
120
|
+
max_tokens = info.get("max_input_tokens")
|
|
121
|
+
if isinstance(max_tokens, int) and max_tokens > 0:
|
|
122
|
+
return max_tokens
|
|
123
|
+
if model.startswith(_OPENAI_EMBEDDING_FAMILY_PREFIXES):
|
|
124
|
+
return _OPENAI_EMBEDDING_FALLBACK_MAX_TOKENS
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@lru_cache(maxsize=16)
|
|
129
|
+
def _get_embedding_encoding(model: str) -> tiktoken.Encoding:
|
|
130
|
+
"""
|
|
131
|
+
Return the tiktoken encoding for an embedding model, falling back to cl100k_base.
|
|
132
|
+
|
|
133
|
+
For non-OpenAI providers tiktoken does not know the real tokenizer, so the
|
|
134
|
+
cl100k_base fallback is an approximate proxy for token counting. That is
|
|
135
|
+
acceptable here because we truncate toward the provider's cap with the
|
|
136
|
+
proxy, which tends to over-truncate by a small fraction rather than under-
|
|
137
|
+
truncate and cause upstream 400s.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
model (str): Embedding model name (e.g. 'text-embedding-3-small').
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
tiktoken.Encoding: Encoder to use for token counting and truncation.
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
return tiktoken.encoding_for_model(model)
|
|
147
|
+
except KeyError:
|
|
148
|
+
return tiktoken.get_encoding("cl100k_base")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _truncate_for_embedding(
|
|
152
|
+
text: str, model: str, max_tokens: int | None = None
|
|
153
|
+
) -> str:
|
|
154
|
+
"""
|
|
155
|
+
Truncate a string so its token count fits within an embedding model's input limit.
|
|
156
|
+
|
|
157
|
+
The token budget is auto-resolved from ``_get_embedding_limit`` by default.
|
|
158
|
+
When the model has no known limit (unknown provider not in litellm's
|
|
159
|
+
registry and not OpenAI-family), returns the text unchanged — over-
|
|
160
|
+
truncating an unknown provider's input is worse than passing it through
|
|
161
|
+
and letting the provider's own error surface.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
text (str): Raw input text.
|
|
165
|
+
model (str): Embedding model name, used to pick the tokenizer and the
|
|
166
|
+
per-provider token cap.
|
|
167
|
+
max_tokens (int | None): Override for the resolved budget. Primarily
|
|
168
|
+
used by tests to exercise the truncation path on short strings;
|
|
169
|
+
leave as ``None`` in production callers.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
str: Original text if it already fits (or the model has no known
|
|
173
|
+
limit), otherwise a token-bounded prefix.
|
|
174
|
+
"""
|
|
175
|
+
if not text:
|
|
176
|
+
return text
|
|
177
|
+
if max_tokens is None:
|
|
178
|
+
max_tokens = _get_embedding_limit(model)
|
|
179
|
+
if max_tokens is None:
|
|
180
|
+
return text
|
|
181
|
+
encoding = _get_embedding_encoding(model)
|
|
182
|
+
tokens = encoding.encode(text, disallowed_special=())
|
|
183
|
+
if len(tokens) <= max_tokens:
|
|
184
|
+
return text
|
|
185
|
+
if model in _TRUNCATION_WARNED_MODELS:
|
|
186
|
+
_LOGGER.debug(
|
|
187
|
+
"Truncating embedding input from %d to %d tokens for model %s",
|
|
188
|
+
len(tokens),
|
|
189
|
+
max_tokens,
|
|
190
|
+
model,
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
_TRUNCATION_WARNED_MODELS.add(model)
|
|
194
|
+
_LOGGER.warning(
|
|
195
|
+
"Truncating embedding input from %d to %d tokens for model %s "
|
|
196
|
+
"(further occurrences will be logged at DEBUG)",
|
|
197
|
+
len(tokens),
|
|
198
|
+
max_tokens,
|
|
199
|
+
model,
|
|
200
|
+
)
|
|
201
|
+
return encoding.decode(tokens[:max_tokens])
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class LiteLLMConfig:
|
|
206
|
+
"""
|
|
207
|
+
Configuration for LiteLLM client.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
model: Model name to use (e.g., 'gpt-4o', 'claude-3-5-sonnet-20241022', 'azure/gpt-4')
|
|
211
|
+
temperature: Temperature for response generation (0.0 to 2.0)
|
|
212
|
+
max_tokens: Maximum tokens to generate
|
|
213
|
+
timeout: Request timeout in seconds
|
|
214
|
+
max_retries: Maximum number of retry attempts
|
|
215
|
+
retry_delay: Initial delay between retries in seconds (exponential backoff)
|
|
216
|
+
top_p: Top-p sampling parameter
|
|
217
|
+
api_key_config: Optional API key configuration from Config (overrides env vars)
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
model: str
|
|
221
|
+
temperature: float = 0.7
|
|
222
|
+
max_tokens: int | None = None
|
|
223
|
+
timeout: int = 120
|
|
224
|
+
max_retries: int = 1
|
|
225
|
+
retry_delay: float = 1.0
|
|
226
|
+
top_p: float = 1.0
|
|
227
|
+
api_key_config: APIKeyConfig | None = None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass
|
|
231
|
+
class ToolCallingChatResponse:
|
|
232
|
+
"""Response from a chat call that was routed in tool-calling mode.
|
|
233
|
+
|
|
234
|
+
Returned instead of ``str | BaseModel`` whenever the caller passes
|
|
235
|
+
``tools=...`` to ``generate_chat_response``. Callers inspect
|
|
236
|
+
``tool_calls`` to drive a tool loop; ``content`` is set on the
|
|
237
|
+
terminal (non-tool) turn.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
content: Text content from the model, or None when the model emitted tool calls.
|
|
241
|
+
tool_calls: List of tool call objects from the model, or None on the terminal turn.
|
|
242
|
+
finish_reason: The stop reason reported by the provider (e.g. "tool_calls", "stop").
|
|
243
|
+
usage: Raw usage object from the LLM response (provider-dependent shape), or None.
|
|
244
|
+
cost_usd: Estimated cost in USD for this call via litellm price table, or None when
|
|
245
|
+
the provider is not in the table (local ONNX, claude-code CLI, etc.).
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
content: str | None
|
|
249
|
+
tool_calls: list[Any] | None
|
|
250
|
+
finish_reason: str | None
|
|
251
|
+
usage: Any | None = None
|
|
252
|
+
cost_usd: float | None = None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class LiteLLMClientError(Exception):
|
|
256
|
+
"""Custom exception for LiteLLM client errors."""
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class StructuredOutputParseError(Exception):
|
|
260
|
+
"""Raised when a structured-output LLM call returns content that cannot be parsed.
|
|
261
|
+
|
|
262
|
+
Caught by the retry loop in ``_make_request`` so a malformed response
|
|
263
|
+
burns a retry attempt rather than silently returning unparsed content.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class LiteLLMClient:
|
|
268
|
+
"""
|
|
269
|
+
Unified LLM client using LiteLLM for multi-provider support.
|
|
270
|
+
|
|
271
|
+
Supports OpenAI, Claude, and Azure OpenAI models through a consistent interface.
|
|
272
|
+
Provides structured output support, multi-modal (image) input, and embeddings.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
SUPPORTED_IMAGE_FORMATS: set[str] = set(SUPPORTED_IMAGE_MIME_TYPES.keys())
|
|
276
|
+
|
|
277
|
+
# Providers that use a simple "prefix/" -> api_key mapping
|
|
278
|
+
_SIMPLE_PROVIDER_PREFIXES: dict[str, str] = {
|
|
279
|
+
"gemini/": "gemini",
|
|
280
|
+
"openrouter/": "openrouter",
|
|
281
|
+
"minimax/": "minimax",
|
|
282
|
+
"deepseek/": "deepseek",
|
|
283
|
+
"zai/": "zai",
|
|
284
|
+
"moonshot/": "moonshot",
|
|
285
|
+
"xai/": "xai",
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# Non-retryable error patterns
|
|
289
|
+
NON_RETRYABLE_ERRORS = [
|
|
290
|
+
"invalid_api_key",
|
|
291
|
+
"unauthorized",
|
|
292
|
+
"permission_denied",
|
|
293
|
+
"quota_exceeded",
|
|
294
|
+
"billing",
|
|
295
|
+
"invalid_request",
|
|
296
|
+
"authentication",
|
|
297
|
+
"forbidden",
|
|
298
|
+
"rate_limit", # Rate limits are handled by LiteLLM internally
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
# Models that only support temperature=1.0 (custom values cause errors or degraded performance)
|
|
302
|
+
TEMPERATURE_RESTRICTED_MODELS = {
|
|
303
|
+
"gpt-5",
|
|
304
|
+
"gpt-5-mini",
|
|
305
|
+
"gpt-5-nano",
|
|
306
|
+
"gpt-5-codex",
|
|
307
|
+
"gemini-3-flash-preview",
|
|
308
|
+
"gemini-3-pro-preview",
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
def __init__(self, config: LiteLLMConfig):
|
|
312
|
+
"""
|
|
313
|
+
Initialize the LiteLLM client.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
config: LiteLLM configuration containing model and provider settings.
|
|
317
|
+
|
|
318
|
+
Raises:
|
|
319
|
+
LiteLLMClientError: If initialization fails.
|
|
320
|
+
"""
|
|
321
|
+
self.config = config
|
|
322
|
+
self.logger = logging.getLogger(__name__)
|
|
323
|
+
self.logger.info("LiteLLM client initialized with model: %s", config.model)
|
|
324
|
+
|
|
325
|
+
# Pre-resolve API key configuration for the main model
|
|
326
|
+
self._api_key, self._api_base, self._api_version = self._resolve_api_key()
|
|
327
|
+
|
|
328
|
+
# Lazily-resolved default embedding model. Populated on first call to
|
|
329
|
+
# _resolve_default_embedding_model so a client built with no embedding
|
|
330
|
+
# use case never pays the auto-detection cost.
|
|
331
|
+
self._default_embedding_model: str | None = None
|
|
332
|
+
|
|
333
|
+
# Enable Braintrust observability when API key is configured
|
|
334
|
+
if os.environ.get("BRAINTRUST_API_KEY") and "braintrust" not in (
|
|
335
|
+
litellm.callbacks or []
|
|
336
|
+
):
|
|
337
|
+
litellm.callbacks = litellm.callbacks or []
|
|
338
|
+
litellm.callbacks.append("braintrust")
|
|
339
|
+
self.logger.info("Braintrust observability enabled")
|
|
340
|
+
|
|
341
|
+
def _resolve_api_key(
|
|
342
|
+
self, model: str | None = None, for_embedding: bool = False
|
|
343
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
344
|
+
"""
|
|
345
|
+
Resolve API key, base URL, and version from api_key_config based on model name.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
model: Optional model name to resolve keys for. Defaults to self.config.model.
|
|
349
|
+
for_embedding: If True, skip custom endpoint override (embeddings use their own provider).
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
tuple[Optional[str], Optional[str], Optional[str]]: (api_key, api_base, api_version)
|
|
353
|
+
"""
|
|
354
|
+
if not self.config.api_key_config:
|
|
355
|
+
return None, None, None
|
|
356
|
+
|
|
357
|
+
# Custom endpoint takes priority for non-embedding calls
|
|
358
|
+
if not for_embedding:
|
|
359
|
+
ce = self.config.api_key_config.custom_endpoint
|
|
360
|
+
if ce and ce.api_key and ce.api_base:
|
|
361
|
+
return ce.api_key, str(ce.api_base), None
|
|
362
|
+
|
|
363
|
+
model_to_check = model or self.config.model
|
|
364
|
+
model_lower = model_to_check.lower()
|
|
365
|
+
|
|
366
|
+
return self._resolve_by_prefix(model_lower)
|
|
367
|
+
|
|
368
|
+
def _resolve_by_prefix(
|
|
369
|
+
self, model_lower: str
|
|
370
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
371
|
+
"""Resolve API credentials by matching the model prefix to a provider.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
model_lower: Lowercased model name string.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
tuple[Optional[str], Optional[str], Optional[str]]: (api_key, api_base, api_version)
|
|
378
|
+
"""
|
|
379
|
+
akc = self.config.api_key_config
|
|
380
|
+
if not akc:
|
|
381
|
+
return None, None, None
|
|
382
|
+
|
|
383
|
+
# claude-code/* routes through the Claude Code CLI (custom provider);
|
|
384
|
+
# it has no API key config — auth comes from the CLI itself.
|
|
385
|
+
if model_lower.startswith("claude-code/"):
|
|
386
|
+
return None, None, None
|
|
387
|
+
|
|
388
|
+
for prefix, attr in self._SIMPLE_PROVIDER_PREFIXES.items():
|
|
389
|
+
if model_lower.startswith(prefix):
|
|
390
|
+
provider_cfg = getattr(akc, attr, None)
|
|
391
|
+
if provider_cfg:
|
|
392
|
+
return provider_cfg.api_key, None, None
|
|
393
|
+
return None, None, None
|
|
394
|
+
|
|
395
|
+
# DashScope (Qwen) — has an optional api_base
|
|
396
|
+
if model_lower.startswith("dashscope/"):
|
|
397
|
+
if akc.dashscope:
|
|
398
|
+
return akc.dashscope.api_key, akc.dashscope.api_base, None
|
|
399
|
+
return None, None, None
|
|
400
|
+
|
|
401
|
+
# Azure OpenAI
|
|
402
|
+
if model_lower.startswith("azure/"):
|
|
403
|
+
if akc.openai and akc.openai.azure_config:
|
|
404
|
+
azure = akc.openai.azure_config
|
|
405
|
+
return azure.api_key, str(azure.endpoint), azure.api_version
|
|
406
|
+
return None, None, None
|
|
407
|
+
|
|
408
|
+
# Anthropic/Claude models
|
|
409
|
+
if "claude" in model_lower or "anthropic" in model_lower:
|
|
410
|
+
if akc.anthropic:
|
|
411
|
+
return akc.anthropic.api_key, None, None
|
|
412
|
+
return None, None, None
|
|
413
|
+
|
|
414
|
+
# OpenAI models (default fallback)
|
|
415
|
+
if akc.openai and akc.openai.api_key:
|
|
416
|
+
return akc.openai.api_key, None, None
|
|
417
|
+
|
|
418
|
+
return None, None, None
|
|
419
|
+
|
|
420
|
+
def generate_response(
|
|
421
|
+
self,
|
|
422
|
+
prompt: str,
|
|
423
|
+
system_message: str | None = None,
|
|
424
|
+
images: list[str | bytes | dict] | None = None,
|
|
425
|
+
image_media_type: str | None = None,
|
|
426
|
+
**kwargs: Any,
|
|
427
|
+
) -> str | BaseModel | ToolCallingChatResponse:
|
|
428
|
+
"""
|
|
429
|
+
Generate a response using the configured LLM.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
prompt: The user prompt/message.
|
|
433
|
+
system_message: Optional system message to set context.
|
|
434
|
+
images: Optional list of images (file paths, bytes, or pre-formatted content blocks).
|
|
435
|
+
image_media_type: Media type for images if passing bytes (e.g., 'image/png').
|
|
436
|
+
**kwargs: Additional parameters including:
|
|
437
|
+
- response_format: Pydantic BaseModel class for structured output
|
|
438
|
+
- parse_structured_output: Whether to parse structured output (default True)
|
|
439
|
+
- temperature: Override config temperature
|
|
440
|
+
- max_tokens: Override config max_tokens
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Generated response content. Returns string for text responses,
|
|
444
|
+
or BaseModel instance for Pydantic model responses.
|
|
445
|
+
|
|
446
|
+
Raises:
|
|
447
|
+
LiteLLMClientError: If the API call fails after all retries,
|
|
448
|
+
or if response_format is not a Pydantic BaseModel class.
|
|
449
|
+
"""
|
|
450
|
+
# Validate response_format if provided
|
|
451
|
+
response_format = kwargs.get("response_format")
|
|
452
|
+
if response_format is not None and not is_pydantic_model(response_format):
|
|
453
|
+
raise LiteLLMClientError(
|
|
454
|
+
"response_format must be a Pydantic BaseModel class, "
|
|
455
|
+
f"got {type(response_format).__name__}"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Build user message content
|
|
459
|
+
user_content = self._build_user_content(prompt, images, image_media_type)
|
|
460
|
+
|
|
461
|
+
# Build messages list
|
|
462
|
+
messages = []
|
|
463
|
+
if system_message:
|
|
464
|
+
messages.append({"role": "system", "content": system_message})
|
|
465
|
+
messages.append({"role": "user", "content": user_content})
|
|
466
|
+
|
|
467
|
+
return self._make_request(messages, **kwargs)
|
|
468
|
+
|
|
469
|
+
def generate_chat_response(
|
|
470
|
+
self,
|
|
471
|
+
messages: list[dict[str, Any]],
|
|
472
|
+
system_message: str | None = None,
|
|
473
|
+
*,
|
|
474
|
+
tools: list[Any] | None = None,
|
|
475
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
476
|
+
model_role: ModelRole | None = None,
|
|
477
|
+
**kwargs: Any,
|
|
478
|
+
) -> str | BaseModel | ToolCallingChatResponse:
|
|
479
|
+
"""
|
|
480
|
+
Generate a response from a list of chat messages.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
messages: List of messages in chat format [{"role": "...", "content": "..."}].
|
|
484
|
+
system_message: Optional system message to prepend.
|
|
485
|
+
tools: Optional list of tool definitions for tool-calling mode.
|
|
486
|
+
When provided, the return type is ``ToolCallingChatResponse``.
|
|
487
|
+
tool_choice: Optional tool choice control ("auto", "none", "required",
|
|
488
|
+
or a dict specifying a particular tool). Forwarded to the provider.
|
|
489
|
+
model_role: Optional ``ModelRole`` to override the model selected for
|
|
490
|
+
this request. The role is resolved via ``resolve_model_name`` using
|
|
491
|
+
the client's ``api_key_config``.
|
|
492
|
+
**kwargs: Additional parameters including:
|
|
493
|
+
- response_format: Pydantic BaseModel class for structured output
|
|
494
|
+
- parse_structured_output: Whether to parse structured output (default True)
|
|
495
|
+
- temperature: Override config temperature
|
|
496
|
+
- max_tokens: Override config max_tokens
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Generated response content. Returns string for text responses,
|
|
500
|
+
``BaseModel`` instance for Pydantic model responses, or
|
|
501
|
+
``ToolCallingChatResponse`` when ``tools`` is provided.
|
|
502
|
+
|
|
503
|
+
Raises:
|
|
504
|
+
LiteLLMClientError: If the API call fails after all retries,
|
|
505
|
+
or if response_format is not a Pydantic BaseModel class.
|
|
506
|
+
"""
|
|
507
|
+
# Validate response_format if provided
|
|
508
|
+
response_format = kwargs.get("response_format")
|
|
509
|
+
if response_format is not None and not is_pydantic_model(response_format):
|
|
510
|
+
raise LiteLLMClientError(
|
|
511
|
+
"response_format must be a Pydantic BaseModel class, "
|
|
512
|
+
f"got {type(response_format).__name__}"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Prepend system message if provided
|
|
516
|
+
final_messages = list(messages)
|
|
517
|
+
if system_message:
|
|
518
|
+
# Check if first message is already a system message
|
|
519
|
+
if final_messages and final_messages[0].get("role") == "system":
|
|
520
|
+
# Merge with existing system message
|
|
521
|
+
final_messages[0]["content"] = (
|
|
522
|
+
f"{system_message}\n\n{final_messages[0]['content']}"
|
|
523
|
+
)
|
|
524
|
+
else:
|
|
525
|
+
final_messages.insert(0, {"role": "system", "content": system_message})
|
|
526
|
+
|
|
527
|
+
# Forward tool-calling and model-role kwargs into _make_request
|
|
528
|
+
if tools is not None:
|
|
529
|
+
kwargs["tools"] = tools
|
|
530
|
+
if tool_choice is not None:
|
|
531
|
+
kwargs["tool_choice"] = tool_choice
|
|
532
|
+
if model_role is not None:
|
|
533
|
+
kwargs["model_role"] = model_role
|
|
534
|
+
|
|
535
|
+
return self._make_request(final_messages, **kwargs)
|
|
536
|
+
|
|
537
|
+
def _resolve_default_embedding_model(self) -> str:
|
|
538
|
+
"""
|
|
539
|
+
Resolve the embedding model to use when callers do not specify one.
|
|
540
|
+
|
|
541
|
+
Routes through the same auto-detection chain as the rest of reflexio
|
|
542
|
+
(``resolve_model_name`` for ``ModelRole.EMBEDDING``) so a session that
|
|
543
|
+
has the local ONNX embedder enabled — or any non-OpenAI provider —
|
|
544
|
+
does not silently fall back to ``text-embedding-3-small`` and produce
|
|
545
|
+
OpenAI 401s. Higher-precedence org config and site-var overrides are
|
|
546
|
+
the caller's responsibility to resolve and pass via ``model=``; this
|
|
547
|
+
helper handles only the auto-detect tier.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
str: The auto-detected embedding model name (cached after first call).
|
|
551
|
+
|
|
552
|
+
Raises:
|
|
553
|
+
RuntimeError: Propagated from ``resolve_model_name`` when no
|
|
554
|
+
embedding-capable provider is available.
|
|
555
|
+
"""
|
|
556
|
+
if self._default_embedding_model is None:
|
|
557
|
+
self._default_embedding_model = resolve_model_name(
|
|
558
|
+
ModelRole.EMBEDDING,
|
|
559
|
+
api_key_config=self.config.api_key_config,
|
|
560
|
+
)
|
|
561
|
+
return self._default_embedding_model
|
|
562
|
+
|
|
563
|
+
def get_embedding(
|
|
564
|
+
self, text: str, model: str | None = None, dimensions: int | None = None
|
|
565
|
+
) -> list[float]:
|
|
566
|
+
"""
|
|
567
|
+
Get embedding vector for the given text.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
text: The text to get embedding for.
|
|
571
|
+
model: Optional embedding model. When omitted, the model is
|
|
572
|
+
auto-detected via ``resolve_model_name(ModelRole.EMBEDDING)``
|
|
573
|
+
so callers inherit the local-embedder gate and any non-OpenAI
|
|
574
|
+
provider configured for this client.
|
|
575
|
+
dimensions: Optional number of dimensions for the embedding vector.
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
List of floats representing the embedding vector.
|
|
579
|
+
|
|
580
|
+
Raises:
|
|
581
|
+
LiteLLMClientError: If embedding generation fails.
|
|
582
|
+
"""
|
|
583
|
+
embedding_model = model or self._resolve_default_embedding_model()
|
|
584
|
+
mode = embedding_provider_mode(embedding_model)
|
|
585
|
+
if mode == "off":
|
|
586
|
+
raise EmbeddingUnavailableError("Embedding provider is disabled")
|
|
587
|
+
if should_use_embedding_service(embedding_model):
|
|
588
|
+
return get_service_embeddings(
|
|
589
|
+
[text], model=embedding_model, dimensions=dimensions
|
|
590
|
+
)[0]
|
|
591
|
+
|
|
592
|
+
# local/nomic-embed-* routes to the sentence-transformers Nomic
|
|
593
|
+
# provider (137M params, 768d Matryoshka-truncated to 512). Higher
|
|
594
|
+
# quality than the chromadb MiniLM fallback below; preferred when
|
|
595
|
+
# the dep is installed.
|
|
596
|
+
if _is_nomic_model(embedding_model) and _nomic_embedder_enabled():
|
|
597
|
+
try:
|
|
598
|
+
return NomicEmbedder.get().embed([text])[0]
|
|
599
|
+
except Exception as e:
|
|
600
|
+
raise LiteLLMClientError(
|
|
601
|
+
f"Nomic embedding generation failed: {str(e)}"
|
|
602
|
+
) from e
|
|
603
|
+
|
|
604
|
+
# local/* models route through the in-process ONNX embedder — no
|
|
605
|
+
# network call, no litellm API, no tiktoken truncation (the embedder
|
|
606
|
+
# applies its own token cap). The dispatch is gated solely on
|
|
607
|
+
# ``chromadb`` being importable; the env-var opt-in (claude-smart's
|
|
608
|
+
# ``CLAUDE_SMART_USE_LOCAL_EMBEDDING``) is enforced earlier in the
|
|
609
|
+
# auto-detection layer (see ``model_defaults._auto_detect_model``).
|
|
610
|
+
if embedding_model.startswith("local/"):
|
|
611
|
+
if mode == "cloud":
|
|
612
|
+
raise EmbeddingUnavailableError(
|
|
613
|
+
f"Local embedding model {embedding_model!r} cannot use cloud mode"
|
|
614
|
+
)
|
|
615
|
+
if not _is_chromadb_importable():
|
|
616
|
+
raise LiteLLMClientError(
|
|
617
|
+
f"Embedding model {embedding_model!r} requires chromadb. "
|
|
618
|
+
"Run `pip install chromadb`."
|
|
619
|
+
)
|
|
620
|
+
try:
|
|
621
|
+
return LocalEmbedder.get().embed([text])[0]
|
|
622
|
+
except Exception as e:
|
|
623
|
+
raise LiteLLMClientError(
|
|
624
|
+
f"Local embedding generation failed: {str(e)}"
|
|
625
|
+
) from e
|
|
626
|
+
|
|
627
|
+
text = _truncate_for_embedding(text, embedding_model)
|
|
628
|
+
|
|
629
|
+
try:
|
|
630
|
+
params = {"model": embedding_model, "input": [text]}
|
|
631
|
+
if dimensions:
|
|
632
|
+
params["dimensions"] = dimensions
|
|
633
|
+
|
|
634
|
+
# Resolve and add API key configuration if provided (overrides env vars)
|
|
635
|
+
api_key, api_base, api_version = self._resolve_api_key(
|
|
636
|
+
embedding_model, for_embedding=True
|
|
637
|
+
)
|
|
638
|
+
if api_key:
|
|
639
|
+
params["api_key"] = api_key
|
|
640
|
+
if api_base:
|
|
641
|
+
params["api_base"] = api_base
|
|
642
|
+
if api_version:
|
|
643
|
+
params["api_version"] = api_version
|
|
644
|
+
|
|
645
|
+
response = litellm.embedding(**params, timeout=self.config.timeout)
|
|
646
|
+
return response.data[0]["embedding"]
|
|
647
|
+
except Exception as e:
|
|
648
|
+
raise LiteLLMClientError(f"Embedding generation failed: {str(e)}") from e
|
|
649
|
+
|
|
650
|
+
def get_embeddings(
|
|
651
|
+
self,
|
|
652
|
+
texts: list[str],
|
|
653
|
+
model: str | None = None,
|
|
654
|
+
dimensions: int | None = None,
|
|
655
|
+
) -> list[list[float]]:
|
|
656
|
+
"""
|
|
657
|
+
Get embedding vectors for multiple texts in a single API call.
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
texts: List of texts to get embeddings for.
|
|
661
|
+
model: Optional embedding model. When omitted, the model is
|
|
662
|
+
auto-detected via ``resolve_model_name(ModelRole.EMBEDDING)``
|
|
663
|
+
so callers inherit the local-embedder gate and any non-OpenAI
|
|
664
|
+
provider configured for this client.
|
|
665
|
+
dimensions: Optional number of dimensions for the embedding vectors.
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
List of embedding vectors, one per input text, in the same order as input.
|
|
669
|
+
|
|
670
|
+
Raises:
|
|
671
|
+
LiteLLMClientError: If embedding generation fails.
|
|
672
|
+
"""
|
|
673
|
+
if not texts:
|
|
674
|
+
return []
|
|
675
|
+
|
|
676
|
+
embedding_model = model or self._resolve_default_embedding_model()
|
|
677
|
+
mode = embedding_provider_mode(embedding_model)
|
|
678
|
+
if mode == "off":
|
|
679
|
+
raise EmbeddingUnavailableError("Embedding provider is disabled")
|
|
680
|
+
if should_use_embedding_service(embedding_model):
|
|
681
|
+
return get_service_embeddings(
|
|
682
|
+
list(texts), model=embedding_model, dimensions=dimensions
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# See matching short-circuits in get_embedding above.
|
|
686
|
+
if _is_nomic_model(embedding_model) and _nomic_embedder_enabled():
|
|
687
|
+
try:
|
|
688
|
+
return NomicEmbedder.get().embed(list(texts))
|
|
689
|
+
except Exception as e:
|
|
690
|
+
raise LiteLLMClientError(
|
|
691
|
+
f"Nomic batch embedding generation failed: {str(e)}"
|
|
692
|
+
) from e
|
|
693
|
+
|
|
694
|
+
if embedding_model.startswith("local/"):
|
|
695
|
+
if mode == "cloud":
|
|
696
|
+
raise EmbeddingUnavailableError(
|
|
697
|
+
f"Local embedding model {embedding_model!r} cannot use cloud mode"
|
|
698
|
+
)
|
|
699
|
+
if not _is_chromadb_importable():
|
|
700
|
+
raise LiteLLMClientError(
|
|
701
|
+
f"Embedding model {embedding_model!r} requires chromadb. "
|
|
702
|
+
"Run `pip install chromadb`."
|
|
703
|
+
)
|
|
704
|
+
try:
|
|
705
|
+
return LocalEmbedder.get().embed(list(texts))
|
|
706
|
+
except Exception as e:
|
|
707
|
+
raise LiteLLMClientError(
|
|
708
|
+
f"Local batch embedding generation failed: {str(e)}"
|
|
709
|
+
) from e
|
|
710
|
+
|
|
711
|
+
texts = [_truncate_for_embedding(t, embedding_model) for t in texts]
|
|
712
|
+
|
|
713
|
+
try:
|
|
714
|
+
params = {"model": embedding_model, "input": texts}
|
|
715
|
+
if dimensions:
|
|
716
|
+
params["dimensions"] = dimensions
|
|
717
|
+
|
|
718
|
+
# Resolve and add API key configuration if provided (overrides env vars)
|
|
719
|
+
api_key, api_base, api_version = self._resolve_api_key(
|
|
720
|
+
embedding_model, for_embedding=True
|
|
721
|
+
)
|
|
722
|
+
if api_key:
|
|
723
|
+
params["api_key"] = api_key
|
|
724
|
+
if api_base:
|
|
725
|
+
params["api_base"] = api_base
|
|
726
|
+
if api_version:
|
|
727
|
+
params["api_version"] = api_version
|
|
728
|
+
|
|
729
|
+
response = litellm.embedding(**params, timeout=self.config.timeout)
|
|
730
|
+
# Response data may not be in order, sort by index to ensure correct ordering
|
|
731
|
+
sorted_data = sorted(response.data, key=lambda x: x["index"])
|
|
732
|
+
return [item["embedding"] for item in sorted_data]
|
|
733
|
+
except Exception as e:
|
|
734
|
+
raise LiteLLMClientError(
|
|
735
|
+
f"Batch embedding generation failed: {str(e)}"
|
|
736
|
+
) from e
|
|
737
|
+
|
|
738
|
+
def _build_completion_params(
|
|
739
|
+
self, messages: list[dict[str, Any]], **kwargs: Any
|
|
740
|
+
) -> tuple[dict[str, Any], Any, bool, int]:
|
|
741
|
+
"""Build completion request parameters from messages and kwargs.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
messages: List of messages to send
|
|
745
|
+
**kwargs: Additional parameters (response_format, max_retries, model, etc.)
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
Tuple of (params dict, response_format, parse_structured_output, max_retries)
|
|
749
|
+
"""
|
|
750
|
+
response_format = kwargs.pop("response_format", None)
|
|
751
|
+
strict_response_format = kwargs.pop("strict_response_format", True)
|
|
752
|
+
parse_structured_output = kwargs.pop("parse_structured_output", True)
|
|
753
|
+
max_retries_arg = kwargs.pop("max_retries", self.config.max_retries)
|
|
754
|
+
try:
|
|
755
|
+
max_retries = max(1, int(max_retries_arg))
|
|
756
|
+
except (TypeError, ValueError):
|
|
757
|
+
max_retries = max(1, int(self.config.max_retries))
|
|
758
|
+
|
|
759
|
+
# Pop tool-calling kwargs before the final params.update(kwargs) so they
|
|
760
|
+
# don't leak into the params dict twice.
|
|
761
|
+
tools = kwargs.pop("tools", None)
|
|
762
|
+
tool_choice = kwargs.pop("tool_choice", None)
|
|
763
|
+
model_role: ModelRole | None = kwargs.pop("model_role", None)
|
|
764
|
+
|
|
765
|
+
actual_model = kwargs.pop("model", self.config.model)
|
|
766
|
+
|
|
767
|
+
# model_role takes priority over the default model but falls through
|
|
768
|
+
# to the custom_endpoint override below (highest priority).
|
|
769
|
+
if model_role is not None:
|
|
770
|
+
actual_model = resolve_model_name(
|
|
771
|
+
role=model_role,
|
|
772
|
+
site_var_value=None,
|
|
773
|
+
config_override=None,
|
|
774
|
+
api_key_config=self.config.api_key_config,
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
ce = (
|
|
778
|
+
self.config.api_key_config.custom_endpoint
|
|
779
|
+
if self.config.api_key_config
|
|
780
|
+
else None
|
|
781
|
+
)
|
|
782
|
+
if ce and ce.api_key and ce.api_base:
|
|
783
|
+
actual_model = ce.model
|
|
784
|
+
|
|
785
|
+
params: dict[str, Any] = {
|
|
786
|
+
"model": actual_model,
|
|
787
|
+
"messages": messages,
|
|
788
|
+
"timeout": kwargs.pop("timeout", self.config.timeout),
|
|
789
|
+
"num_retries": 0,
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
temperature = kwargs.pop("temperature", self.config.temperature)
|
|
793
|
+
if self._is_temperature_restricted_model(actual_model):
|
|
794
|
+
params["temperature"] = 1.0
|
|
795
|
+
else:
|
|
796
|
+
params["temperature"] = temperature
|
|
797
|
+
|
|
798
|
+
# Determinism knob: `seed` is always injected (defaulting to 42) on
|
|
799
|
+
# providers that honor it, since seed alone is cheap and harmless.
|
|
800
|
+
# The companion temperature=0 override is opt-in via an explicit
|
|
801
|
+
# REFLEXIO_LLM_SEED env var so that caller-configured temperature
|
|
802
|
+
# flows through by default — silently clobbering a user's configured
|
|
803
|
+
# temperature was surprising. Current-gen reasoning models (gpt-5-*)
|
|
804
|
+
# ignore both knobs; the seed is best-effort.
|
|
805
|
+
default_seed = 42
|
|
806
|
+
seed_explicit = "REFLEXIO_LLM_SEED" in os.environ
|
|
807
|
+
seed_raw = os.environ.get("REFLEXIO_LLM_SEED", str(default_seed))
|
|
808
|
+
try:
|
|
809
|
+
params["seed"] = int(seed_raw)
|
|
810
|
+
except ValueError:
|
|
811
|
+
self.logger.warning(
|
|
812
|
+
"REFLEXIO_LLM_SEED=%r is not an int; falling back to default seed=%d",
|
|
813
|
+
seed_raw,
|
|
814
|
+
default_seed,
|
|
815
|
+
)
|
|
816
|
+
params["seed"] = default_seed
|
|
817
|
+
# Keep seed best-effort without mutating LiteLLM's process-wide
|
|
818
|
+
# drop_params setting. Providers that do not support seed can ignore it.
|
|
819
|
+
params["drop_params"] = True
|
|
820
|
+
if seed_explicit and not self._is_temperature_restricted_model(actual_model):
|
|
821
|
+
params["temperature"] = 0.0
|
|
822
|
+
|
|
823
|
+
max_tokens = kwargs.pop("max_tokens", self.config.max_tokens)
|
|
824
|
+
if max_tokens:
|
|
825
|
+
params["max_tokens"] = max_tokens
|
|
826
|
+
if self.config.top_p != 1.0:
|
|
827
|
+
params["top_p"] = self.config.top_p
|
|
828
|
+
if response_format:
|
|
829
|
+
params["response_format"] = self._provider_response_format(
|
|
830
|
+
response_format=response_format,
|
|
831
|
+
model=actual_model,
|
|
832
|
+
strict_response_format=strict_response_format,
|
|
833
|
+
)
|
|
834
|
+
if tools is not None:
|
|
835
|
+
params["tools"] = tools
|
|
836
|
+
if tool_choice is not None:
|
|
837
|
+
params["tool_choice"] = tool_choice
|
|
838
|
+
|
|
839
|
+
if actual_model != self.config.model:
|
|
840
|
+
api_key, api_base, api_version = self._resolve_api_key(actual_model)
|
|
841
|
+
else:
|
|
842
|
+
api_key, api_base, api_version = (
|
|
843
|
+
self._api_key,
|
|
844
|
+
self._api_base,
|
|
845
|
+
self._api_version,
|
|
846
|
+
)
|
|
847
|
+
if api_key:
|
|
848
|
+
params["api_key"] = api_key
|
|
849
|
+
if api_base:
|
|
850
|
+
params["api_base"] = api_base
|
|
851
|
+
if api_version:
|
|
852
|
+
params["api_version"] = api_version
|
|
853
|
+
|
|
854
|
+
params.update(kwargs)
|
|
855
|
+
|
|
856
|
+
# Braintrust metadata for observability (no-op if callback not registered)
|
|
857
|
+
if os.environ.get("BRAINTRUST_API_KEY"):
|
|
858
|
+
params["metadata"] = {
|
|
859
|
+
**params.get("metadata", {}),
|
|
860
|
+
"project_name": os.environ.get("BRAINTRUST_PROJECT_NAME", "reflexio"),
|
|
861
|
+
}
|
|
862
|
+
params["messages"] = self._apply_prompt_caching(
|
|
863
|
+
params["messages"], params["model"]
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
return params, response_format, parse_structured_output, max_retries
|
|
867
|
+
|
|
868
|
+
@staticmethod
|
|
869
|
+
@lru_cache(maxsize=256)
|
|
870
|
+
def _supports_response_schema(model: str) -> bool:
|
|
871
|
+
try:
|
|
872
|
+
return bool(litellm.supports_response_schema(model=model))
|
|
873
|
+
except Exception:
|
|
874
|
+
return False
|
|
875
|
+
|
|
876
|
+
def _provider_response_format(
|
|
877
|
+
self,
|
|
878
|
+
*,
|
|
879
|
+
response_format: Any,
|
|
880
|
+
model: str,
|
|
881
|
+
strict_response_format: bool,
|
|
882
|
+
) -> Any:
|
|
883
|
+
"""Return the provider-facing response_format while preserving parser schema.
|
|
884
|
+
|
|
885
|
+
Callers pass a Pydantic model so local parsing stays type-safe. When
|
|
886
|
+
LiteLLM says the target model supports JSON Schema response formats, we
|
|
887
|
+
send an explicit strict schema to constrain generation. Unsupported
|
|
888
|
+
providers keep the existing Pydantic response_format behavior.
|
|
889
|
+
"""
|
|
890
|
+
|
|
891
|
+
if (
|
|
892
|
+
strict_response_format
|
|
893
|
+
and is_pydantic_model(response_format)
|
|
894
|
+
and self._supports_response_schema(model)
|
|
895
|
+
):
|
|
896
|
+
return strict_response_format_for_model(response_format)
|
|
897
|
+
return response_format
|
|
898
|
+
|
|
899
|
+
def _compute_cost_usd(self, response: Any, model: str | None) -> float | None:
|
|
900
|
+
"""Compute call cost in USD via the litellm price table.
|
|
901
|
+
|
|
902
|
+
Falls back to None when the provider is not mapped (local ONNX,
|
|
903
|
+
claude-code CLI, etc.) rather than failing the request.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
response: Raw LLM response object.
|
|
907
|
+
model: Fully-qualified model name used for the call.
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
float | None: Cost in USD, or None when unavailable.
|
|
911
|
+
"""
|
|
912
|
+
try:
|
|
913
|
+
import litellm
|
|
914
|
+
|
|
915
|
+
cost = litellm.completion_cost(completion_response=response, model=model)
|
|
916
|
+
return float(cost) if cost else None
|
|
917
|
+
except Exception:
|
|
918
|
+
return None
|
|
919
|
+
|
|
920
|
+
def _log_token_usage(self, params: dict[str, Any], response: Any) -> None:
|
|
921
|
+
"""Log token usage with cache statistics and cost from an LLM response.
|
|
922
|
+
|
|
923
|
+
Args:
|
|
924
|
+
params: Request parameters (for model name)
|
|
925
|
+
response: LLM response object
|
|
926
|
+
"""
|
|
927
|
+
usage = getattr(response, "usage", None)
|
|
928
|
+
if not usage:
|
|
929
|
+
return
|
|
930
|
+
|
|
931
|
+
cache_info = ""
|
|
932
|
+
details = getattr(usage, "prompt_tokens_details", None)
|
|
933
|
+
if details:
|
|
934
|
+
cached = getattr(details, "cached_tokens", 0)
|
|
935
|
+
if cached:
|
|
936
|
+
cache_info = f", cached: {cached}"
|
|
937
|
+
cache_creation = getattr(usage, "cache_creation_input_tokens", None)
|
|
938
|
+
cache_read = getattr(usage, "cache_read_input_tokens", None)
|
|
939
|
+
if cache_creation or cache_read:
|
|
940
|
+
cache_info = (
|
|
941
|
+
f", cache_write: {cache_creation or 0}, cache_read: {cache_read or 0}"
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
cost = self._compute_cost_usd(response, params.get("model"))
|
|
945
|
+
cost_suffix = f", cost: ${cost:.6f}" if cost is not None else ""
|
|
946
|
+
|
|
947
|
+
self.logger.info(
|
|
948
|
+
"Token usage - model: %s, input: %s, output: %s, total: %s%s%s",
|
|
949
|
+
params.get("model"),
|
|
950
|
+
usage.prompt_tokens,
|
|
951
|
+
usage.completion_tokens,
|
|
952
|
+
usage.total_tokens,
|
|
953
|
+
cache_info,
|
|
954
|
+
cost_suffix,
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
def _handle_retry_or_raise(
|
|
958
|
+
self,
|
|
959
|
+
error: Exception,
|
|
960
|
+
params: dict[str, Any],
|
|
961
|
+
attempt: int,
|
|
962
|
+
max_retries: int,
|
|
963
|
+
response_format: Any,
|
|
964
|
+
elapsed_seconds: float,
|
|
965
|
+
) -> None:
|
|
966
|
+
"""Handle retry logic or raise on non-retryable/final errors.
|
|
967
|
+
|
|
968
|
+
Args:
|
|
969
|
+
error: The exception that occurred
|
|
970
|
+
params: Request parameters (for logging)
|
|
971
|
+
attempt: Current attempt index (0-based)
|
|
972
|
+
max_retries: Maximum number of retries
|
|
973
|
+
response_format: Response format (for logging)
|
|
974
|
+
elapsed_seconds: Time elapsed for this attempt
|
|
975
|
+
|
|
976
|
+
Raises:
|
|
977
|
+
LiteLLMClientError: If the error is non-retryable or this was the last attempt
|
|
978
|
+
"""
|
|
979
|
+
error_str = str(error).lower()
|
|
980
|
+
|
|
981
|
+
self.logger.error(
|
|
982
|
+
"event=llm_request_end model=%s timeout=%s has_response_format=%s attempt=%d/%d elapsed_seconds=%.3f success=%s error_type=%s error=%s",
|
|
983
|
+
params.get("model"),
|
|
984
|
+
params.get("timeout"),
|
|
985
|
+
response_format is not None,
|
|
986
|
+
attempt + 1,
|
|
987
|
+
max_retries,
|
|
988
|
+
elapsed_seconds,
|
|
989
|
+
False,
|
|
990
|
+
type(error).__name__,
|
|
991
|
+
str(error),
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
if self._is_non_retryable_error(error_str):
|
|
995
|
+
self.logger.error("Non-retryable error: %s", error)
|
|
996
|
+
raise LiteLLMClientError(f"API call failed: {str(error)}") from error
|
|
997
|
+
|
|
998
|
+
if attempt < max_retries - 1:
|
|
999
|
+
delay = self.config.retry_delay * (2**attempt)
|
|
1000
|
+
self.logger.warning(
|
|
1001
|
+
"Request failed (attempt %s/%s): %s. Retrying in %ss...",
|
|
1002
|
+
attempt + 1,
|
|
1003
|
+
max_retries,
|
|
1004
|
+
error,
|
|
1005
|
+
delay,
|
|
1006
|
+
)
|
|
1007
|
+
time.sleep(delay)
|
|
1008
|
+
else:
|
|
1009
|
+
self.logger.error(
|
|
1010
|
+
"LLM request failed (model=%s, has_response_format=%s): %s",
|
|
1011
|
+
params.get("model"),
|
|
1012
|
+
response_format is not None,
|
|
1013
|
+
error,
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
def _make_request(
|
|
1017
|
+
self, messages: list[dict[str, Any]], **kwargs: Any
|
|
1018
|
+
) -> str | BaseModel | ToolCallingChatResponse:
|
|
1019
|
+
"""
|
|
1020
|
+
Make a request to the LLM with retry logic.
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
messages: List of messages to send.
|
|
1024
|
+
**kwargs: Additional parameters.
|
|
1025
|
+
|
|
1026
|
+
Returns:
|
|
1027
|
+
Response content as string, BaseModel instance, or
|
|
1028
|
+
ToolCallingChatResponse when the request was in tool-calling mode.
|
|
1029
|
+
|
|
1030
|
+
Raises:
|
|
1031
|
+
LiteLLMClientError: If the request fails after all retries.
|
|
1032
|
+
"""
|
|
1033
|
+
params, response_format, parse_structured_output, max_retries = (
|
|
1034
|
+
self._build_completion_params(messages, **kwargs)
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
last_error: Exception | None = None
|
|
1038
|
+
# A StructuredOutputParseError is typically a transient malformed or
|
|
1039
|
+
# truncated response that succeeds on a fresh generation, so grant it
|
|
1040
|
+
# one extra attempt beyond the configured budget (once per request).
|
|
1041
|
+
effective_max_retries = max_retries
|
|
1042
|
+
structured_parse_retry_granted = False
|
|
1043
|
+
attempt = 0
|
|
1044
|
+
while attempt < effective_max_retries:
|
|
1045
|
+
request_start = time.perf_counter()
|
|
1046
|
+
self.logger.info(
|
|
1047
|
+
"event=llm_request_start model=%s timeout=%s has_response_format=%s attempt=%d/%d",
|
|
1048
|
+
params.get("model"),
|
|
1049
|
+
params.get("timeout"),
|
|
1050
|
+
response_format is not None,
|
|
1051
|
+
attempt + 1,
|
|
1052
|
+
effective_max_retries,
|
|
1053
|
+
)
|
|
1054
|
+
try:
|
|
1055
|
+
response = litellm.completion(**params)
|
|
1056
|
+
message = response.choices[0].message # type: ignore[reportAttributeAccessIssue]
|
|
1057
|
+
content = message.content
|
|
1058
|
+
elapsed_seconds = time.perf_counter() - request_start
|
|
1059
|
+
|
|
1060
|
+
self._log_token_usage(params, response)
|
|
1061
|
+
|
|
1062
|
+
self.logger.info(
|
|
1063
|
+
"event=llm_request_end model=%s timeout=%s has_response_format=%s attempt=%d/%d elapsed_seconds=%.3f success=%s",
|
|
1064
|
+
params.get("model"),
|
|
1065
|
+
params.get("timeout"),
|
|
1066
|
+
response_format is not None,
|
|
1067
|
+
attempt + 1,
|
|
1068
|
+
effective_max_retries,
|
|
1069
|
+
elapsed_seconds,
|
|
1070
|
+
True,
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
# Tool-calling path: return a structured response instead of
|
|
1074
|
+
# going through _maybe_parse_structured_output.
|
|
1075
|
+
if "tools" in params:
|
|
1076
|
+
raw_usage = getattr(response, "usage", None)
|
|
1077
|
+
call_cost = self._compute_cost_usd(response, params.get("model"))
|
|
1078
|
+
return ToolCallingChatResponse(
|
|
1079
|
+
content=content,
|
|
1080
|
+
tool_calls=getattr(message, "tool_calls", None),
|
|
1081
|
+
finish_reason=response.choices[0].finish_reason, # type: ignore[reportAttributeAccessIssue]
|
|
1082
|
+
usage=raw_usage,
|
|
1083
|
+
cost_usd=call_cost,
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
return self._maybe_parse_structured_output(
|
|
1087
|
+
content, # type: ignore[reportArgumentType]
|
|
1088
|
+
response_format,
|
|
1089
|
+
parse_structured_output, # type: ignore[reportArgumentType]
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
except Exception as e:
|
|
1093
|
+
last_error = e
|
|
1094
|
+
elapsed_seconds = time.perf_counter() - request_start
|
|
1095
|
+
if (
|
|
1096
|
+
isinstance(e, StructuredOutputParseError)
|
|
1097
|
+
and not structured_parse_retry_granted
|
|
1098
|
+
):
|
|
1099
|
+
structured_parse_retry_granted = True
|
|
1100
|
+
effective_max_retries += 1
|
|
1101
|
+
self._handle_retry_or_raise(
|
|
1102
|
+
e,
|
|
1103
|
+
params,
|
|
1104
|
+
attempt,
|
|
1105
|
+
effective_max_retries,
|
|
1106
|
+
response_format,
|
|
1107
|
+
elapsed_seconds,
|
|
1108
|
+
)
|
|
1109
|
+
attempt += 1
|
|
1110
|
+
|
|
1111
|
+
raise LiteLLMClientError(
|
|
1112
|
+
f"API call failed after {effective_max_retries} retries: {str(last_error)}"
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
def _apply_prompt_caching(
|
|
1116
|
+
self, messages: list[dict[str, Any]], model: str
|
|
1117
|
+
) -> list[dict[str, Any]]:
|
|
1118
|
+
"""
|
|
1119
|
+
Apply prompt caching markers for supported providers.
|
|
1120
|
+
|
|
1121
|
+
For Anthropic models, transforms the system message content into content-block
|
|
1122
|
+
format with cache_control markers to enable prefix caching.
|
|
1123
|
+
For other providers, returns messages unchanged.
|
|
1124
|
+
|
|
1125
|
+
Args:
|
|
1126
|
+
messages: List of chat messages.
|
|
1127
|
+
model: Model name to determine provider.
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
list[dict]: Messages with cache control applied where appropriate.
|
|
1131
|
+
"""
|
|
1132
|
+
model_lower = model.lower()
|
|
1133
|
+
# The claude-code/* custom provider routes through the Claude Code CLI,
|
|
1134
|
+
# which does not accept Anthropic API cache_control content blocks.
|
|
1135
|
+
if model_lower.startswith("claude-code/"):
|
|
1136
|
+
return messages
|
|
1137
|
+
is_anthropic = "claude" in model_lower or "anthropic" in model_lower
|
|
1138
|
+
|
|
1139
|
+
if not is_anthropic:
|
|
1140
|
+
return messages
|
|
1141
|
+
|
|
1142
|
+
result = []
|
|
1143
|
+
for msg in messages:
|
|
1144
|
+
if msg.get("role") == "system" and isinstance(msg.get("content"), str):
|
|
1145
|
+
# Transform system message to content-block format with cache_control
|
|
1146
|
+
result.append(
|
|
1147
|
+
{
|
|
1148
|
+
"role": "system",
|
|
1149
|
+
"content": [
|
|
1150
|
+
{
|
|
1151
|
+
"type": "text",
|
|
1152
|
+
"text": msg["content"],
|
|
1153
|
+
"cache_control": {"type": "ephemeral"},
|
|
1154
|
+
}
|
|
1155
|
+
],
|
|
1156
|
+
}
|
|
1157
|
+
)
|
|
1158
|
+
else:
|
|
1159
|
+
result.append(msg)
|
|
1160
|
+
|
|
1161
|
+
return result
|
|
1162
|
+
|
|
1163
|
+
def _build_user_content(
|
|
1164
|
+
self,
|
|
1165
|
+
prompt: str,
|
|
1166
|
+
images: list[str | bytes | dict] | None = None,
|
|
1167
|
+
image_media_type: str | None = None,
|
|
1168
|
+
) -> str | list[dict[str, Any]]:
|
|
1169
|
+
"""
|
|
1170
|
+
Build user content with optional images.
|
|
1171
|
+
|
|
1172
|
+
Args:
|
|
1173
|
+
prompt: Text prompt.
|
|
1174
|
+
images: Optional list of images.
|
|
1175
|
+
image_media_type: Media type for byte images.
|
|
1176
|
+
|
|
1177
|
+
Returns:
|
|
1178
|
+
String for text-only, or list of content blocks for multi-modal.
|
|
1179
|
+
"""
|
|
1180
|
+
if not images:
|
|
1181
|
+
return prompt
|
|
1182
|
+
|
|
1183
|
+
content_blocks = [{"type": "text", "text": prompt}]
|
|
1184
|
+
|
|
1185
|
+
for image in images:
|
|
1186
|
+
if isinstance(image, dict):
|
|
1187
|
+
# Already formatted content block
|
|
1188
|
+
content_blocks.append(image)
|
|
1189
|
+
elif isinstance(image, bytes):
|
|
1190
|
+
# Raw bytes
|
|
1191
|
+
media_type = image_media_type or "image/png"
|
|
1192
|
+
base64_data = base64.b64encode(image).decode("utf-8")
|
|
1193
|
+
content_blocks.append(
|
|
1194
|
+
self._create_image_content_block(base64_data, media_type)
|
|
1195
|
+
)
|
|
1196
|
+
elif isinstance(image, str):
|
|
1197
|
+
# File path or URL
|
|
1198
|
+
if image.startswith(("http://", "https://")):
|
|
1199
|
+
# URL - use directly
|
|
1200
|
+
content_blocks.append(
|
|
1201
|
+
{"type": "image_url", "image_url": {"url": image}} # type: ignore[reportArgumentType]
|
|
1202
|
+
)
|
|
1203
|
+
else:
|
|
1204
|
+
# File path
|
|
1205
|
+
base64_data, media_type = self.encode_image_to_base64(image)
|
|
1206
|
+
content_blocks.append(
|
|
1207
|
+
self._create_image_content_block(base64_data, media_type)
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
return content_blocks
|
|
1211
|
+
|
|
1212
|
+
def _create_image_content_block(
|
|
1213
|
+
self, base64_data: str, media_type: str
|
|
1214
|
+
) -> dict[str, Any]:
|
|
1215
|
+
"""
|
|
1216
|
+
Create an image content block for the API.
|
|
1217
|
+
|
|
1218
|
+
Args:
|
|
1219
|
+
base64_data: Base64-encoded image data.
|
|
1220
|
+
media_type: MIME type of the image.
|
|
1221
|
+
|
|
1222
|
+
Returns:
|
|
1223
|
+
Image content block dictionary.
|
|
1224
|
+
"""
|
|
1225
|
+
return {
|
|
1226
|
+
"type": "image_url",
|
|
1227
|
+
"image_url": {"url": f"data:{media_type};base64,{base64_data}"},
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
def encode_image_to_base64(self, image_path: str) -> tuple[str, str]:
|
|
1231
|
+
"""
|
|
1232
|
+
Encode an image file to base64.
|
|
1233
|
+
|
|
1234
|
+
Delegates to :func:`reflexio.server.llm.image_utils.encode_image_to_base64`
|
|
1235
|
+
and wraps errors as :class:`LiteLLMClientError`.
|
|
1236
|
+
|
|
1237
|
+
Args:
|
|
1238
|
+
image_path (str): Path to the image file.
|
|
1239
|
+
|
|
1240
|
+
Returns:
|
|
1241
|
+
tuple[str, str]: ``(base64_data, media_type)`` pair.
|
|
1242
|
+
|
|
1243
|
+
Raises:
|
|
1244
|
+
LiteLLMClientError: If the image cannot be read or format is unsupported.
|
|
1245
|
+
"""
|
|
1246
|
+
try:
|
|
1247
|
+
return _encode_image_to_base64(image_path)
|
|
1248
|
+
except ImageEncodingError as exc:
|
|
1249
|
+
raise LiteLLMClientError(str(exc)) from exc
|
|
1250
|
+
|
|
1251
|
+
def _is_temperature_restricted_model(self, model: str) -> bool:
|
|
1252
|
+
"""
|
|
1253
|
+
Check if a model has temperature restrictions (e.g., GPT-5 and Gemini 3 models only support temperature=1.0).
|
|
1254
|
+
|
|
1255
|
+
Args:
|
|
1256
|
+
model: Model name to check.
|
|
1257
|
+
|
|
1258
|
+
Returns:
|
|
1259
|
+
True if the model has temperature restrictions.
|
|
1260
|
+
"""
|
|
1261
|
+
model_lower = model.lower()
|
|
1262
|
+
# Strip provider routing prefixes (e.g., "openrouter/openai/gpt-5-nano" -> "gpt-5-nano")
|
|
1263
|
+
model_name = model_lower.rsplit("/", 1)[-1]
|
|
1264
|
+
# Check if model starts with any of the restricted model prefixes
|
|
1265
|
+
return any(
|
|
1266
|
+
model_name.startswith(restricted) or model_name == restricted
|
|
1267
|
+
for restricted in self.TEMPERATURE_RESTRICTED_MODELS
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
def _maybe_parse_structured_output(
|
|
1271
|
+
self,
|
|
1272
|
+
content: str,
|
|
1273
|
+
response_format: Any,
|
|
1274
|
+
parse_structured_output: bool,
|
|
1275
|
+
) -> str | BaseModel:
|
|
1276
|
+
"""
|
|
1277
|
+
Parse structured output if applicable.
|
|
1278
|
+
|
|
1279
|
+
Args:
|
|
1280
|
+
content: Raw response content.
|
|
1281
|
+
response_format: Expected response format (must be a Pydantic BaseModel class).
|
|
1282
|
+
parse_structured_output: Whether to parse the output.
|
|
1283
|
+
|
|
1284
|
+
Returns:
|
|
1285
|
+
String for text responses, or BaseModel instance for structured responses.
|
|
1286
|
+
"""
|
|
1287
|
+
if not response_format or not parse_structured_output:
|
|
1288
|
+
return content
|
|
1289
|
+
|
|
1290
|
+
if content is None:
|
|
1291
|
+
return content
|
|
1292
|
+
|
|
1293
|
+
# If content is already a Pydantic model (some providers return parsed)
|
|
1294
|
+
if isinstance(content, BaseModel):
|
|
1295
|
+
return content
|
|
1296
|
+
|
|
1297
|
+
# Try to parse JSON and convert to Pydantic model
|
|
1298
|
+
# Extract JSON from markdown code blocks if present
|
|
1299
|
+
json_str = self._extract_json_from_string(content)
|
|
1300
|
+
try:
|
|
1301
|
+
parsed = json.loads(json_str)
|
|
1302
|
+
|
|
1303
|
+
# response_format must be a Pydantic model (validated at entry points)
|
|
1304
|
+
return response_format.model_validate(parsed)
|
|
1305
|
+
except Exception:
|
|
1306
|
+
# LLMs sometimes produce Python-style output (single quotes, True/False,
|
|
1307
|
+
# trailing commas). Try to sanitize before giving up.
|
|
1308
|
+
try:
|
|
1309
|
+
sanitized = self._sanitize_json_string(json_str)
|
|
1310
|
+
parsed = json.loads(sanitized)
|
|
1311
|
+
return response_format.model_validate(parsed)
|
|
1312
|
+
except Exception:
|
|
1313
|
+
# Last resort: json-repair can recover complete responses with
|
|
1314
|
+
# small syntax glitches, such as missing commas. Do not repair
|
|
1315
|
+
# likely truncation: the retry loop should request a fresh
|
|
1316
|
+
# complete response instead of accepting invented tail content.
|
|
1317
|
+
try:
|
|
1318
|
+
from json_repair import repair_json
|
|
1319
|
+
|
|
1320
|
+
if self._looks_truncated_json(json_str):
|
|
1321
|
+
raise StructuredOutputParseError(
|
|
1322
|
+
"Structured output appears truncated"
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
repaired = repair_json(json_str, return_objects=True)
|
|
1326
|
+
return response_format.model_validate(repaired)
|
|
1327
|
+
except Exception as e:
|
|
1328
|
+
model = self.config.model
|
|
1329
|
+
snippet = (
|
|
1330
|
+
content[:200]
|
|
1331
|
+
if isinstance(content, str)
|
|
1332
|
+
else repr(content)[:200]
|
|
1333
|
+
)
|
|
1334
|
+
raise StructuredOutputParseError(
|
|
1335
|
+
f"Structured output parse failed for model={model!r}: {e}. "
|
|
1336
|
+
f"Content snippet: {snippet!r}"
|
|
1337
|
+
) from e
|
|
1338
|
+
|
|
1339
|
+
def _extract_json_from_string(self, content: str) -> str:
|
|
1340
|
+
"""
|
|
1341
|
+
Extract JSON from a string, handling markdown code blocks.
|
|
1342
|
+
|
|
1343
|
+
Args:
|
|
1344
|
+
content: String potentially containing JSON.
|
|
1345
|
+
|
|
1346
|
+
Returns:
|
|
1347
|
+
Extracted JSON string.
|
|
1348
|
+
"""
|
|
1349
|
+
content = content.strip()
|
|
1350
|
+
|
|
1351
|
+
# Try to extract from markdown code blocks
|
|
1352
|
+
json_block_pattern = r"```(?:json)?\s*([\s\S]*?)```"
|
|
1353
|
+
matches = re.findall(json_block_pattern, content)
|
|
1354
|
+
if matches:
|
|
1355
|
+
return matches[0].strip()
|
|
1356
|
+
|
|
1357
|
+
# Try to find JSON object or array
|
|
1358
|
+
for start_char, end_char in [("{", "}"), ("[", "]")]:
|
|
1359
|
+
start_idx = content.find(start_char)
|
|
1360
|
+
end_idx = content.rfind(end_char)
|
|
1361
|
+
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
|
|
1362
|
+
return content[start_idx : end_idx + 1]
|
|
1363
|
+
|
|
1364
|
+
return content
|
|
1365
|
+
|
|
1366
|
+
def _looks_truncated_json(self, json_str: str) -> bool:
|
|
1367
|
+
"""
|
|
1368
|
+
Return True when a JSON-like string appears to end before it is complete.
|
|
1369
|
+
|
|
1370
|
+
This intentionally only treats content with a JSON container opener as
|
|
1371
|
+
truncation. Plain text that is not JSON should proceed to the normal
|
|
1372
|
+
parse failure path.
|
|
1373
|
+
|
|
1374
|
+
Args:
|
|
1375
|
+
json_str: Extracted JSON-like response text.
|
|
1376
|
+
|
|
1377
|
+
Returns:
|
|
1378
|
+
True if the response has unclosed containers or strings.
|
|
1379
|
+
"""
|
|
1380
|
+
stripped = json_str.strip()
|
|
1381
|
+
start_indices = [
|
|
1382
|
+
idx for idx in (stripped.find("{"), stripped.find("[")) if idx != -1
|
|
1383
|
+
]
|
|
1384
|
+
if not stripped or not start_indices:
|
|
1385
|
+
return False
|
|
1386
|
+
stripped = stripped[min(start_indices) :]
|
|
1387
|
+
|
|
1388
|
+
stack: list[str] = []
|
|
1389
|
+
in_str = False
|
|
1390
|
+
escape = False
|
|
1391
|
+
pairs = {"{": "}", "[": "]"}
|
|
1392
|
+
|
|
1393
|
+
for ch in stripped:
|
|
1394
|
+
if escape:
|
|
1395
|
+
escape = False
|
|
1396
|
+
continue
|
|
1397
|
+
if ch == "\\" and in_str:
|
|
1398
|
+
escape = True
|
|
1399
|
+
continue
|
|
1400
|
+
if ch == '"':
|
|
1401
|
+
in_str = not in_str
|
|
1402
|
+
continue
|
|
1403
|
+
if in_str:
|
|
1404
|
+
continue
|
|
1405
|
+
if ch in pairs:
|
|
1406
|
+
stack.append(pairs[ch])
|
|
1407
|
+
elif ch in ("}", "]") and (not stack or stack.pop() != ch):
|
|
1408
|
+
return False
|
|
1409
|
+
|
|
1410
|
+
return in_str or bool(stack)
|
|
1411
|
+
|
|
1412
|
+
def _sanitize_json_string(self, json_str: str) -> str:
|
|
1413
|
+
"""
|
|
1414
|
+
Sanitize a JSON-like string that uses Python-style syntax into valid JSON.
|
|
1415
|
+
|
|
1416
|
+
Handles common LLM issues: single quotes, Python True/False/None,
|
|
1417
|
+
and trailing commas before closing braces/brackets.
|
|
1418
|
+
|
|
1419
|
+
Args:
|
|
1420
|
+
json_str: A JSON-like string that may contain Python-style syntax.
|
|
1421
|
+
|
|
1422
|
+
Returns:
|
|
1423
|
+
A sanitized string closer to valid JSON.
|
|
1424
|
+
"""
|
|
1425
|
+
s = json_str
|
|
1426
|
+
|
|
1427
|
+
# Walk character-by-character to:
|
|
1428
|
+
# 1. Replace single-quoted strings with double-quoted strings
|
|
1429
|
+
# 2. Replace Python True/False/None with JSON true/false/null ONLY outside strings
|
|
1430
|
+
# 3. Handle escaped apostrophes inside single-quoted strings (e.g. 'didn\'t')
|
|
1431
|
+
# 4. Escape literal double quotes that end up inside double-quoted strings
|
|
1432
|
+
result = []
|
|
1433
|
+
in_double = False
|
|
1434
|
+
in_single = False
|
|
1435
|
+
i = 0
|
|
1436
|
+
while i < len(s):
|
|
1437
|
+
ch = s[i]
|
|
1438
|
+
if ch == "\\" and (in_double or in_single):
|
|
1439
|
+
# Escaped character inside a string
|
|
1440
|
+
if i + 1 < len(s):
|
|
1441
|
+
next_ch = s[i + 1]
|
|
1442
|
+
if in_single and next_ch == "'":
|
|
1443
|
+
# \' inside single-quoted string → literal apostrophe
|
|
1444
|
+
# In JSON double-quoted strings, apostrophe needs no escape
|
|
1445
|
+
result.append("'")
|
|
1446
|
+
i += 2
|
|
1447
|
+
continue
|
|
1448
|
+
result.append(ch)
|
|
1449
|
+
result.append(next_ch)
|
|
1450
|
+
i += 2
|
|
1451
|
+
continue
|
|
1452
|
+
result.append(ch)
|
|
1453
|
+
elif ch == '"' and not in_single:
|
|
1454
|
+
in_double = not in_double
|
|
1455
|
+
result.append(ch)
|
|
1456
|
+
elif ch == "'" and not in_double:
|
|
1457
|
+
in_single = not in_single
|
|
1458
|
+
result.append('"') # swap single → double
|
|
1459
|
+
else:
|
|
1460
|
+
# Escape unescaped double quotes inside single-quoted strings
|
|
1461
|
+
# (they become part of a double-quoted JSON string)
|
|
1462
|
+
if in_single and ch == '"':
|
|
1463
|
+
result.append('\\"')
|
|
1464
|
+
else:
|
|
1465
|
+
result.append(ch)
|
|
1466
|
+
i += 1
|
|
1467
|
+
s = "".join(result)
|
|
1468
|
+
|
|
1469
|
+
# Replace Python booleans/None with JSON equivalents only outside quoted strings.
|
|
1470
|
+
# We walk the already-double-quoted result so we only need to track double quotes.
|
|
1471
|
+
output = []
|
|
1472
|
+
in_str = False
|
|
1473
|
+
j = 0
|
|
1474
|
+
while j < len(s):
|
|
1475
|
+
if s[j] == "\\" and in_str:
|
|
1476
|
+
output.append(s[j : j + 2])
|
|
1477
|
+
j += 2
|
|
1478
|
+
continue
|
|
1479
|
+
if s[j] == '"':
|
|
1480
|
+
in_str = not in_str
|
|
1481
|
+
output.append(s[j])
|
|
1482
|
+
j += 1
|
|
1483
|
+
continue
|
|
1484
|
+
if not in_str:
|
|
1485
|
+
matched = False
|
|
1486
|
+
for py_val, json_val in _PYTHON_TO_JSON_REPLACEMENTS.items():
|
|
1487
|
+
if s[j : j + len(py_val)] == py_val:
|
|
1488
|
+
# Check word boundaries
|
|
1489
|
+
before = s[j - 1] if j > 0 else " "
|
|
1490
|
+
after = s[j + len(py_val)] if j + len(py_val) < len(s) else " "
|
|
1491
|
+
if (
|
|
1492
|
+
not before.isalnum()
|
|
1493
|
+
and before != "_"
|
|
1494
|
+
and not after.isalnum()
|
|
1495
|
+
and after != "_"
|
|
1496
|
+
):
|
|
1497
|
+
output.append(json_val)
|
|
1498
|
+
j += len(py_val)
|
|
1499
|
+
matched = True
|
|
1500
|
+
break
|
|
1501
|
+
if not matched:
|
|
1502
|
+
output.append(s[j])
|
|
1503
|
+
j += 1
|
|
1504
|
+
else:
|
|
1505
|
+
output.append(s[j])
|
|
1506
|
+
j += 1
|
|
1507
|
+
s = "".join(output)
|
|
1508
|
+
|
|
1509
|
+
# Remove trailing commas before } or ]
|
|
1510
|
+
return re.sub(r",\s*([}\]])", r"\1", s)
|
|
1511
|
+
|
|
1512
|
+
def _is_non_retryable_error(self, error_str: str) -> bool:
|
|
1513
|
+
"""
|
|
1514
|
+
Check if an error is non-retryable.
|
|
1515
|
+
|
|
1516
|
+
Args:
|
|
1517
|
+
error_str: Error message string.
|
|
1518
|
+
|
|
1519
|
+
Returns:
|
|
1520
|
+
True if error should not be retried.
|
|
1521
|
+
"""
|
|
1522
|
+
return any(pattern in error_str for pattern in self.NON_RETRYABLE_ERRORS)
|
|
1523
|
+
|
|
1524
|
+
def update_config(self, **kwargs) -> None:
|
|
1525
|
+
"""
|
|
1526
|
+
Update client configuration.
|
|
1527
|
+
|
|
1528
|
+
Args:
|
|
1529
|
+
**kwargs: Configuration parameters to update (model, temperature, etc.).
|
|
1530
|
+
"""
|
|
1531
|
+
for key, value in kwargs.items():
|
|
1532
|
+
if hasattr(self.config, key):
|
|
1533
|
+
setattr(self.config, key, value)
|
|
1534
|
+
self.logger.debug("Updated config: %s = %s", key, value)
|
|
1535
|
+
# Invalidate the embedding-default cache when the provider
|
|
1536
|
+
# surface changes — resolve_model_name(EMBEDDING) reads
|
|
1537
|
+
# api_key_config, so a swap must force a re-detect.
|
|
1538
|
+
if key == "api_key_config":
|
|
1539
|
+
self._default_embedding_model = None
|
|
1540
|
+
else:
|
|
1541
|
+
self.logger.warning("Unknown config parameter: %s", key)
|
|
1542
|
+
|
|
1543
|
+
def get_model(self) -> str:
|
|
1544
|
+
"""
|
|
1545
|
+
Get the current model being used.
|
|
1546
|
+
|
|
1547
|
+
Returns:
|
|
1548
|
+
Model name string.
|
|
1549
|
+
"""
|
|
1550
|
+
return self.config.model
|
|
1551
|
+
|
|
1552
|
+
def get_config(self) -> LiteLLMConfig:
|
|
1553
|
+
"""
|
|
1554
|
+
Get the current configuration.
|
|
1555
|
+
|
|
1556
|
+
Returns:
|
|
1557
|
+
Current LiteLLM configuration.
|
|
1558
|
+
"""
|
|
1559
|
+
return self.config
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
def create_litellm_client(
|
|
1563
|
+
model: str,
|
|
1564
|
+
temperature: float = 0.7,
|
|
1565
|
+
max_tokens: int | None = None,
|
|
1566
|
+
timeout: int = 60,
|
|
1567
|
+
max_retries: int = 3,
|
|
1568
|
+
api_key_config: APIKeyConfig | None = None,
|
|
1569
|
+
**kwargs,
|
|
1570
|
+
) -> LiteLLMClient:
|
|
1571
|
+
"""
|
|
1572
|
+
Create a LiteLLM client with simplified parameters.
|
|
1573
|
+
|
|
1574
|
+
Args:
|
|
1575
|
+
model: Model name to use (e.g., 'gpt-4o', 'claude-3-5-sonnet-20241022').
|
|
1576
|
+
temperature: Temperature for response generation.
|
|
1577
|
+
max_tokens: Maximum tokens to generate.
|
|
1578
|
+
timeout: Request timeout in seconds.
|
|
1579
|
+
max_retries: Maximum retry attempts.
|
|
1580
|
+
api_key_config: Optional API key configuration from Config (overrides env vars).
|
|
1581
|
+
**kwargs: Additional configuration parameters.
|
|
1582
|
+
|
|
1583
|
+
Returns:
|
|
1584
|
+
Configured LiteLLM client.
|
|
1585
|
+
"""
|
|
1586
|
+
config = LiteLLMConfig(
|
|
1587
|
+
model=model,
|
|
1588
|
+
temperature=temperature,
|
|
1589
|
+
max_tokens=max_tokens,
|
|
1590
|
+
timeout=timeout,
|
|
1591
|
+
max_retries=max_retries,
|
|
1592
|
+
api_key_config=api_key_config,
|
|
1593
|
+
**kwargs,
|
|
1594
|
+
)
|
|
1595
|
+
return LiteLLMClient(config)
|