claude-smart 0.2.44 → 0.2.46
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 +3 -3
- package/README.md +34 -5
- package/bin/claude-smart.js +295 -5
- package/package.json +22 -3
- package/plugin/.claude-plugin/plugin.json +4 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +22 -2
- package/plugin/commands/clear-all.md +1 -0
- package/plugin/commands/dashboard.md +1 -0
- package/plugin/commands/learn.md +1 -0
- package/plugin/commands/restart.md +1 -0
- package/plugin/commands/show.md +1 -0
- package/plugin/dashboard/app/configure/env/page.tsx +40 -14
- package/plugin/dashboard/app/configure/server/page.tsx +51 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
- package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
- package/plugin/dashboard/lib/reflexio-client.ts +16 -0
- package/plugin/dashboard/lib/status.ts +10 -3
- package/plugin/dashboard/lib/types.ts +18 -6
- package/plugin/hooks/codex-hooks.json +0 -1
- package/plugin/opencode/assistant-buffer.ts +108 -0
- package/plugin/opencode/dist/assistant-buffer.js +96 -0
- package/plugin/opencode/dist/internal.js +12 -0
- package/plugin/opencode/dist/payload.js +85 -0
- package/plugin/opencode/dist/server.mjs +158 -0
- package/plugin/opencode/internal.ts +18 -0
- package/plugin/opencode/package.json +3 -0
- package/plugin/opencode/payload.ts +90 -0
- package/plugin/opencode/server.mts +174 -0
- package/plugin/opencode/tsconfig.json +13 -0
- package/plugin/pyproject.toml +20 -3
- package/plugin/scripts/_lib.sh +77 -29
- package/plugin/scripts/backend-service.sh +27 -5
- package/plugin/scripts/dashboard-service.sh +7 -5
- package/plugin/scripts/ensure-plugin-root.sh +4 -4
- package/plugin/scripts/hook_entry.sh +3 -3
- package/plugin/scripts/opencode-claude-compat +4 -0
- package/plugin/scripts/opencode-claude-compat.cmd +3 -0
- package/plugin/scripts/opencode-claude-compat.js +225 -0
- package/plugin/scripts/smart-install.sh +10 -8
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +304 -6
- package/plugin/src/claude_smart/env_config.py +12 -4
- package/plugin/src/claude_smart/events/session_start.py +26 -7
- package/plugin/src/claude_smart/events/stop.py +1 -1
- package/plugin/src/claude_smart/ids.py +1 -1
- package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
- package/plugin/src/claude_smart/runtime.py +7 -1
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +54 -30
- package/plugin/vendor/reflexio/README.md +14 -8
- package/plugin/vendor/reflexio/pyproject.toml +13 -1
- package/plugin/vendor/reflexio/reflexio/README.md +1 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
- package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
- package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
- package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
- package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
- package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
- package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
- package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
- package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
- package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
- package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
- package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
- package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
- package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
- package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
- package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
- package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
- package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
- package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
- package/scripts/setup-claude-smart.sh +8 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
- /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
|
@@ -31,6 +31,7 @@ from reflexio.server.llm.image_utils import (
|
|
|
31
31
|
encode_image_to_base64 as _encode_image_to_base64,
|
|
32
32
|
)
|
|
33
33
|
from reflexio.server.llm.llm_utils import (
|
|
34
|
+
assert_provider_safe_schema,
|
|
34
35
|
is_pydantic_model,
|
|
35
36
|
strict_response_format_for_model,
|
|
36
37
|
)
|
|
@@ -62,13 +63,17 @@ from reflexio.server.llm.providers.nomic_embedding_provider import (
|
|
|
62
63
|
from reflexio.server.llm.providers.nomic_embedding_provider import (
|
|
63
64
|
register_if_enabled as _register_nomic_embedder,
|
|
64
65
|
)
|
|
66
|
+
from reflexio.server.llm.providers.openclaw_provider import (
|
|
67
|
+
register_if_enabled as _register_openclaw,
|
|
68
|
+
)
|
|
65
69
|
|
|
66
70
|
# Suppress LiteLLM's verbose logging
|
|
67
71
|
litellm.suppress_debug_info = True
|
|
68
72
|
|
|
69
|
-
# Opt-in registration of
|
|
70
|
-
#
|
|
73
|
+
# Opt-in registration of local CLI providers. All no-ops unless the
|
|
74
|
+
# matching env var is set. Safe to call at import.
|
|
71
75
|
_register_claude_code()
|
|
76
|
+
_register_openclaw()
|
|
72
77
|
_register_local_embedder()
|
|
73
78
|
_register_nomic_embedder()
|
|
74
79
|
|
|
@@ -228,14 +233,17 @@ class LiteLLMConfig:
|
|
|
228
233
|
temperature: Temperature for response generation (0.0 to 2.0).
|
|
229
234
|
max_tokens: Maximum tokens to generate.
|
|
230
235
|
timeout: Request timeout in seconds.
|
|
231
|
-
max_retries: Maximum retry attempts
|
|
232
|
-
|
|
236
|
+
max_retries: Maximum same-model retry attempts. Used by the embedding
|
|
237
|
+
path (litellm's num_retries) and clamped in _build_completion_params.
|
|
238
|
+
NOT used on the chat-completion path — that forces num_retries=0 so a
|
|
239
|
+
hung primary can't be retried before the fallback (PYTHON-FASTAPI-62).
|
|
240
|
+
Default 3.
|
|
233
241
|
retry_delay: Currently unused — LiteLLM owns retry backoff. Kept for
|
|
234
242
|
backward compatibility; remove in a follow-up sweep.
|
|
235
243
|
top_p: Top-p sampling parameter.
|
|
236
244
|
api_key_config: Optional API key configuration from Config (overrides env vars).
|
|
237
|
-
fallback_models: Models LiteLLM tries in order after the primary
|
|
238
|
-
|
|
245
|
+
fallback_models: Models LiteLLM tries in order after the primary's single
|
|
246
|
+
attempt. Passed directly to litellm's fallbacks param.
|
|
239
247
|
Default is an empty list (no fallback) so local reflexio and the
|
|
240
248
|
claude-smart integration are never silently routed to an unintended
|
|
241
249
|
provider. Production opts in via the env var
|
|
@@ -260,12 +268,18 @@ class LiteLLMConfig:
|
|
|
260
268
|
)
|
|
261
269
|
|
|
262
270
|
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
#
|
|
271
|
+
# Per-model provider-timeout floors. Values are floors, not overrides: the
|
|
272
|
+
# effective timeout is max(configured, floor), and an explicit per-call timeout
|
|
273
|
+
# kwarg always wins.
|
|
274
|
+
#
|
|
275
|
+
# MiniMax-M3 was pinned to 240s when it was the sole model. That let a *hung*
|
|
276
|
+
# primary block ~240s before falling back, dominating the wasted time behind
|
|
277
|
+
# Sentry PYTHON-FASTAPI-62. It is now floored at the 120s default so a hang is
|
|
278
|
+
# abandoned sooner and the fallback (e.g. gpt-5-mini) is reached faster. This is
|
|
279
|
+
# the key post-deploy tuning knob: raise it if legitimately-slow calls start
|
|
280
|
+
# timing out, lower it to cut more waste.
|
|
267
281
|
_MODEL_TIMEOUT_FLOOR_SECONDS: dict[str, int] = {
|
|
268
|
-
"minimax/MiniMax-M3":
|
|
282
|
+
"minimax/MiniMax-M3": 120,
|
|
269
283
|
}
|
|
270
284
|
|
|
271
285
|
|
|
@@ -285,6 +299,10 @@ class ToolCallingChatResponse:
|
|
|
285
299
|
usage: Raw usage object from the LLM response (provider-dependent shape), or None.
|
|
286
300
|
cost_usd: Estimated cost in USD for this call via litellm price table, or None when
|
|
287
301
|
the provider is not in the table (local ONNX, claude-code CLI, etc.).
|
|
302
|
+
parsed_output: When ``response_format`` is passed alongside ``tools`` and the model
|
|
303
|
+
ends the turn with a plain (non-tool) response, the content parsed into the
|
|
304
|
+
``response_format`` schema. None when the turn emitted tool calls, when no
|
|
305
|
+
``response_format`` was requested, or when the content was not parseable.
|
|
288
306
|
"""
|
|
289
307
|
|
|
290
308
|
content: str | None
|
|
@@ -292,6 +310,7 @@ class ToolCallingChatResponse:
|
|
|
292
310
|
finish_reason: str | None
|
|
293
311
|
usage: Any | None = None
|
|
294
312
|
cost_usd: float | None = None
|
|
313
|
+
parsed_output: BaseModel | None = None
|
|
295
314
|
|
|
296
315
|
|
|
297
316
|
class LiteLLMClientError(Exception):
|
|
@@ -349,6 +368,21 @@ class _CompletionResponseSnapshot:
|
|
|
349
368
|
class _CompletionErrorSnapshot:
|
|
350
369
|
type_name: str
|
|
351
370
|
message: str
|
|
371
|
+
model: str | None = None
|
|
372
|
+
llm_provider: str | None = None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _snapshot_completion_error(
|
|
376
|
+
exc: BaseException, params: dict[str, Any]
|
|
377
|
+
) -> _CompletionErrorSnapshot:
|
|
378
|
+
model = getattr(exc, "model", None) or params.get("model")
|
|
379
|
+
llm_provider = getattr(exc, "llm_provider", None)
|
|
380
|
+
return _CompletionErrorSnapshot(
|
|
381
|
+
type_name=type(exc).__name__,
|
|
382
|
+
message=str(exc),
|
|
383
|
+
model=str(model) if model else None,
|
|
384
|
+
llm_provider=str(llm_provider) if llm_provider else None,
|
|
385
|
+
)
|
|
352
386
|
|
|
353
387
|
|
|
354
388
|
def _ensure_picklable(value: Any) -> Any:
|
|
@@ -421,14 +455,7 @@ def _litellm_completion_worker(
|
|
|
421
455
|
("ok", _picklable_completion_result(litellm.completion(**params)))
|
|
422
456
|
)
|
|
423
457
|
except BaseException as exc:
|
|
424
|
-
|
|
425
|
-
pickle.dumps(exc)
|
|
426
|
-
except Exception:
|
|
427
|
-
result_queue.put(
|
|
428
|
-
("error", _CompletionErrorSnapshot(type(exc).__name__, str(exc)))
|
|
429
|
-
)
|
|
430
|
-
else:
|
|
431
|
-
result_queue.put(("error", exc))
|
|
458
|
+
result_queue.put(("error", _snapshot_completion_error(exc, params)))
|
|
432
459
|
|
|
433
460
|
|
|
434
461
|
class LiteLLMClient:
|
|
@@ -452,6 +479,15 @@ class LiteLLMClient:
|
|
|
452
479
|
"xai/": "xai",
|
|
453
480
|
}
|
|
454
481
|
|
|
482
|
+
# OpenAI-compatible providers that accept a ``json_schema`` response_format
|
|
483
|
+
# but that ``litellm.supports_response_schema`` reports as unsupported. For
|
|
484
|
+
# these, the gate below would fall back to handing LiteLLM the raw Pydantic
|
|
485
|
+
# model; LiteLLM then builds the ``json_schema`` itself and emits ``oneOf``
|
|
486
|
+
# for discriminated unions, which strict structured-output endpoints reject
|
|
487
|
+
# (Sentry PYTHON-FASTAPI-9J). Listing the provider here forces our own
|
|
488
|
+
# normalized strict schema (``oneOf`` folded into ``anyOf``) to be sent.
|
|
489
|
+
_JSON_SCHEMA_PROVIDER_ALLOWLIST: frozenset[str] = frozenset({"minimax"})
|
|
490
|
+
|
|
455
491
|
# Models that only support temperature=1.0 (custom values cause errors or degraded performance)
|
|
456
492
|
TEMPERATURE_RESTRICTED_MODELS = {
|
|
457
493
|
"gpt-5",
|
|
@@ -1063,6 +1099,28 @@ class LiteLLMClient:
|
|
|
1063
1099
|
except Exception:
|
|
1064
1100
|
return False
|
|
1065
1101
|
|
|
1102
|
+
@staticmethod
|
|
1103
|
+
@lru_cache(maxsize=256)
|
|
1104
|
+
def _provider_for_model(model: str) -> str | None:
|
|
1105
|
+
try:
|
|
1106
|
+
return litellm.get_llm_provider(model)[1]
|
|
1107
|
+
except Exception:
|
|
1108
|
+
return None
|
|
1109
|
+
|
|
1110
|
+
@classmethod
|
|
1111
|
+
def _accepts_json_schema_response_format(cls, model: str) -> bool:
|
|
1112
|
+
"""Whether to send ``model`` an explicit strict ``json_schema`` schema.
|
|
1113
|
+
|
|
1114
|
+
True when LiteLLM reports native response-schema support, or when the
|
|
1115
|
+
provider is a known OpenAI-compatible endpoint that LiteLLM
|
|
1116
|
+
under-reports (see ``_JSON_SCHEMA_PROVIDER_ALLOWLIST``). In the latter
|
|
1117
|
+
case LiteLLM would otherwise forward a ``json_schema`` it built itself,
|
|
1118
|
+
emitting ``oneOf`` for discriminated unions that the endpoint rejects.
|
|
1119
|
+
"""
|
|
1120
|
+
if cls._supports_response_schema(model):
|
|
1121
|
+
return True
|
|
1122
|
+
return cls._provider_for_model(model) in cls._JSON_SCHEMA_PROVIDER_ALLOWLIST
|
|
1123
|
+
|
|
1066
1124
|
def _provider_response_format(
|
|
1067
1125
|
self,
|
|
1068
1126
|
*,
|
|
@@ -1072,18 +1130,28 @@ class LiteLLMClient:
|
|
|
1072
1130
|
) -> Any:
|
|
1073
1131
|
"""Return the provider-facing response_format while preserving parser schema.
|
|
1074
1132
|
|
|
1075
|
-
Callers pass a Pydantic model so local parsing stays type-safe. When
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1133
|
+
Callers pass a Pydantic model so local parsing stays type-safe. When the
|
|
1134
|
+
target model accepts a JSON Schema response format — either LiteLLM
|
|
1135
|
+
reports native support, or the provider is an OpenAI-compatible endpoint
|
|
1136
|
+
LiteLLM under-reports (see ``_accepts_json_schema_response_format``) — we
|
|
1137
|
+
send an explicit strict schema to constrain generation. Truly
|
|
1138
|
+
unsupported providers keep the existing Pydantic response_format
|
|
1139
|
+
behavior.
|
|
1079
1140
|
"""
|
|
1080
1141
|
|
|
1081
|
-
if (
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
)
|
|
1086
|
-
|
|
1142
|
+
if not is_pydantic_model(response_format):
|
|
1143
|
+
return response_format
|
|
1144
|
+
|
|
1145
|
+
# Build the native schema once and reuse it for both the boundary guard and
|
|
1146
|
+
# (when applicable) the strict normalizer, avoiding a second schema build.
|
|
1147
|
+
# Boundary guard: models inheriting StrictStructuredOutput are safe by
|
|
1148
|
+
# construction; this catches a model that forgot the base (raises under
|
|
1149
|
+
# tests, warns in prod) regardless of which path is taken below.
|
|
1150
|
+
schema = response_format.model_json_schema()
|
|
1151
|
+
assert_provider_safe_schema(schema, name=response_format.__name__)
|
|
1152
|
+
|
|
1153
|
+
if strict_response_format and self._accepts_json_schema_response_format(model):
|
|
1154
|
+
return strict_response_format_for_model(response_format, schema=schema)
|
|
1087
1155
|
return response_format
|
|
1088
1156
|
|
|
1089
1157
|
def _compute_cost_usd(self, response: Any, model: str | None) -> float | None:
|
|
@@ -1107,20 +1175,38 @@ class LiteLLMClient:
|
|
|
1107
1175
|
except Exception:
|
|
1108
1176
|
return None
|
|
1109
1177
|
|
|
1110
|
-
def
|
|
1178
|
+
def _coerce_timeout_seconds(self, params: dict[str, Any]) -> float:
|
|
1179
|
+
"""Coerce ``params['timeout']`` to a float, falling back to the config
|
|
1180
|
+
default when it is missing or non-numeric."""
|
|
1181
|
+
try:
|
|
1182
|
+
return float(params.get("timeout", self.config.timeout))
|
|
1183
|
+
except (TypeError, ValueError):
|
|
1184
|
+
return float(self.config.timeout)
|
|
1185
|
+
|
|
1186
|
+
def _completion_with_hard_timeout(
|
|
1187
|
+
self, params: dict[str, Any], hard_timeout: float
|
|
1188
|
+
) -> Any:
|
|
1111
1189
|
"""Run ``litellm.completion`` with a client-side wall-clock bound.
|
|
1112
1190
|
|
|
1113
1191
|
Some providers can exceed LiteLLM's ``timeout`` kwarg. Run the blocking
|
|
1114
1192
|
call in a child process so the caller can fail, release locks, and
|
|
1115
1193
|
terminate the in-flight provider request instead of waiting indefinitely.
|
|
1194
|
+
|
|
1195
|
+
``hard_timeout`` is the wall-clock kill bound for the whole subprocess.
|
|
1196
|
+
Because LiteLLM walks ``[primary, *fallbacks]`` inside this one call
|
|
1197
|
+
(copying ``timeout`` unchanged into each rung), the caller sizes
|
|
1198
|
+
``hard_timeout`` to cover the entire fallback ladder, not a single
|
|
1199
|
+
attempt — otherwise the subprocess would be killed before LiteLLM ever
|
|
1200
|
+
reaches a fallback (the root cause of Sentry PYTHON-FASTAPI-62).
|
|
1116
1201
|
"""
|
|
1117
1202
|
provider_timeout = params.get("timeout", self.config.timeout)
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1203
|
+
# timeout_seconds + grace_seconds below only classify test doubles in
|
|
1204
|
+
# _should_process_isolate_completion (real litellm vs a monkeypatched
|
|
1205
|
+
# closure) — they do NOT size the kill bound, which is the caller's
|
|
1206
|
+
# ladder-wide ``hard_timeout``.
|
|
1207
|
+
timeout_seconds = self._coerce_timeout_seconds(params)
|
|
1122
1208
|
grace_seconds = self._hard_timeout_grace_seconds()
|
|
1123
|
-
hard_timeout = max(0.001,
|
|
1209
|
+
hard_timeout = max(0.001, hard_timeout)
|
|
1124
1210
|
|
|
1125
1211
|
if not self._should_process_isolate_completion(timeout_seconds, grace_seconds):
|
|
1126
1212
|
return litellm.completion(**params)
|
|
@@ -1156,11 +1242,15 @@ class LiteLLMClient:
|
|
|
1156
1242
|
|
|
1157
1243
|
if status == "ok":
|
|
1158
1244
|
return payload
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
)
|
|
1163
|
-
raise
|
|
1245
|
+
# The worker always reports errors as a picklable snapshot.
|
|
1246
|
+
context_parts = [f"model={payload.model}"]
|
|
1247
|
+
if payload.llm_provider:
|
|
1248
|
+
context_parts.append(f"provider={payload.llm_provider}")
|
|
1249
|
+
raise LiteLLMClientError(
|
|
1250
|
+
"litellm.completion failed in isolated worker: "
|
|
1251
|
+
f"{payload.type_name}: {payload.message} "
|
|
1252
|
+
f"({', '.join(context_parts)})"
|
|
1253
|
+
)
|
|
1164
1254
|
finally:
|
|
1165
1255
|
result_queue.close()
|
|
1166
1256
|
result_queue.join_thread()
|
|
@@ -1289,14 +1379,15 @@ class LiteLLMClient:
|
|
|
1289
1379
|
self, messages: list[dict[str, Any]], **kwargs: Any
|
|
1290
1380
|
) -> str | BaseModel | ToolCallingChatResponse:
|
|
1291
1381
|
"""
|
|
1292
|
-
Make a request to the LLM, delegating
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
retry
|
|
1382
|
+
Make a request to the LLM, delegating cross-model fallback to litellm.
|
|
1383
|
+
|
|
1384
|
+
Fallback is handed to ``litellm.completion`` via the native ``fallbacks``
|
|
1385
|
+
kwarg, but ``num_retries`` is forced to 0: same-model retry of a *hung*
|
|
1386
|
+
primary is what made the fallback unreachable and produced the 490s in
|
|
1387
|
+
Sentry PYTHON-FASTAPI-62 (see the body comment). So the primary is tried
|
|
1388
|
+
once, then each fallback once. The subprocess hard timeout is sized to
|
|
1389
|
+
cover that whole ladder. The one retry we still own at the client level
|
|
1390
|
+
is a single ``StructuredOutputParseError`` retry: LiteLLM cannot detect a
|
|
1300
1391
|
post-hoc Pydantic re-validation failure because it sees a successful
|
|
1301
1392
|
HTTP response.
|
|
1302
1393
|
|
|
@@ -1313,28 +1404,53 @@ class LiteLLMClient:
|
|
|
1313
1404
|
LiteLLMClientError: If the request fails after all retries and
|
|
1314
1405
|
fallbacks have been exhausted by litellm.
|
|
1315
1406
|
"""
|
|
1316
|
-
params, response_format, parse_structured_output,
|
|
1407
|
+
params, response_format, parse_structured_output, _max_retries, fallbacks = (
|
|
1317
1408
|
self._build_completion_params(messages, **kwargs)
|
|
1318
1409
|
)
|
|
1319
1410
|
|
|
1320
|
-
# Hand
|
|
1321
|
-
#
|
|
1322
|
-
|
|
1411
|
+
# Hand the fallback ladder to litellm, but DISABLE same-model retries.
|
|
1412
|
+
# litellm walks [primary, *fallbacks] inside one litellm.completion call,
|
|
1413
|
+
# copying ``timeout`` unchanged into each rung. With num_retries>=1 it
|
|
1414
|
+
# retries a *hung* primary num_retries+1 times (each up to a full
|
|
1415
|
+
# provider timeout) before ever reaching a fallback — making the fallback
|
|
1416
|
+
# unreachable within any sane wall-clock bound (root cause of Sentry
|
|
1417
|
+
# PYTHON-FASTAPI-62). num_retries=0 makes the fallback LIST the resilience
|
|
1418
|
+
# mechanism: each model is tried once, in order.
|
|
1419
|
+
params["num_retries"] = 0
|
|
1323
1420
|
if fallbacks:
|
|
1324
1421
|
params["fallbacks"] = fallbacks
|
|
1325
1422
|
|
|
1423
|
+
# Size the hard (wall-clock) timeout to cover the WHOLE ladder. litellm
|
|
1424
|
+
# copies this single ``params["timeout"]`` into EVERY rung (primary + each
|
|
1425
|
+
# fallback), so every rung shares the primary's per-attempt budget and the
|
|
1426
|
+
# subprocess must be allowed to run ``(1 + len(fallbacks))`` of them plus
|
|
1427
|
+
# one grace buffer before being killed — otherwise it is killed before
|
|
1428
|
+
# litellm can reach a fallback.
|
|
1429
|
+
#
|
|
1430
|
+
# ASYMMETRIC-FLOOR FOOTGUN: because every rung shares one timeout, a
|
|
1431
|
+
# fallback whose _MODEL_TIMEOUT_FLOOR_SECONDS floor is HIGHER than the
|
|
1432
|
+
# primary's would run — and be killed — at the primary's shorter timeout,
|
|
1433
|
+
# reintroducing the "fallback killed early" failure this fix removes. The
|
|
1434
|
+
# floor table is single-valued today (MiniMax-M3 == the 120 default), so
|
|
1435
|
+
# this is latent; revisit the sizing (e.g. max floor across rungs, passed
|
|
1436
|
+
# as ``params["timeout"]``) before adding an asymmetric floor entry.
|
|
1437
|
+
per_attempt_timeout = self._coerce_timeout_seconds(params)
|
|
1438
|
+
hard_timeout = (
|
|
1439
|
+
1 + len(fallbacks)
|
|
1440
|
+
) * per_attempt_timeout + self._hard_timeout_grace_seconds()
|
|
1441
|
+
|
|
1326
1442
|
request_start = time.perf_counter()
|
|
1327
1443
|
self.logger.info(
|
|
1328
|
-
"event=llm_request_start model=%s timeout=%s has_response_format=%s num_retries
|
|
1444
|
+
"event=llm_request_start model=%s timeout=%s has_response_format=%s num_retries=0 fallbacks=%s hard_timeout=%.3f",
|
|
1329
1445
|
params.get("model"),
|
|
1330
1446
|
params.get("timeout"),
|
|
1331
1447
|
response_format is not None,
|
|
1332
|
-
max_retries,
|
|
1333
1448
|
fallbacks,
|
|
1449
|
+
hard_timeout,
|
|
1334
1450
|
)
|
|
1335
1451
|
|
|
1336
1452
|
def _call_and_parse() -> str | BaseModel | ToolCallingChatResponse:
|
|
1337
|
-
response = self._completion_with_hard_timeout(params)
|
|
1453
|
+
response = self._completion_with_hard_timeout(params, hard_timeout)
|
|
1338
1454
|
self._emit_fallback_observability(response, params)
|
|
1339
1455
|
message = response.choices[0].message # type: ignore[reportAttributeAccessIssue]
|
|
1340
1456
|
content = message.content
|
|
@@ -1353,12 +1469,28 @@ class LiteLLMClient:
|
|
|
1353
1469
|
if "tools" in params:
|
|
1354
1470
|
raw_usage = getattr(response, "usage", None)
|
|
1355
1471
|
call_cost = self._compute_cost_usd(response, params.get("model"))
|
|
1472
|
+
tool_calls = getattr(message, "tool_calls", None)
|
|
1473
|
+
# Structured-output + tools: when the model ends the turn with a
|
|
1474
|
+
# plain (non-tool) response and a response_format was requested,
|
|
1475
|
+
# the content IS the final structured answer. Parse it here so a
|
|
1476
|
+
# tool-loop caller can finish on it. A malformed parse raises
|
|
1477
|
+
# StructuredOutputParseError, which the outer wrapper retries once.
|
|
1478
|
+
parsed_output: BaseModel | None = None
|
|
1479
|
+
if response_format is not None and not tool_calls:
|
|
1480
|
+
parsed = self._maybe_parse_structured_output(
|
|
1481
|
+
content, # type: ignore[reportArgumentType]
|
|
1482
|
+
response_format,
|
|
1483
|
+
parse_structured_output,
|
|
1484
|
+
)
|
|
1485
|
+
if isinstance(parsed, BaseModel):
|
|
1486
|
+
parsed_output = parsed
|
|
1356
1487
|
return ToolCallingChatResponse(
|
|
1357
1488
|
content=content,
|
|
1358
|
-
tool_calls=
|
|
1489
|
+
tool_calls=tool_calls,
|
|
1359
1490
|
finish_reason=response.choices[0].finish_reason, # type: ignore[reportAttributeAccessIssue]
|
|
1360
1491
|
usage=raw_usage,
|
|
1361
1492
|
cost_usd=call_cost,
|
|
1493
|
+
parsed_output=parsed_output,
|
|
1362
1494
|
)
|
|
1363
1495
|
|
|
1364
1496
|
return self._maybe_parse_structured_output(
|
|
@@ -1371,25 +1503,25 @@ class LiteLLMClient:
|
|
|
1371
1503
|
try:
|
|
1372
1504
|
return _call_and_parse()
|
|
1373
1505
|
except StructuredOutputParseError:
|
|
1374
|
-
#
|
|
1375
|
-
# re-validation failure happens AFTER litellm sees a
|
|
1376
|
-
#
|
|
1377
|
-
# the model. PR #121 documented this as a MiniMax-M3
|
|
1378
|
-
# mitigation.
|
|
1506
|
+
# litellm's fallbacks cover API/timeout errors, but a Pydantic
|
|
1507
|
+
# re-validation failure happens AFTER litellm sees a successful
|
|
1508
|
+
# 200 — litellm can't detect it, so we owe one explicit second
|
|
1509
|
+
# attempt at the model. PR #121 documented this as a MiniMax-M3
|
|
1510
|
+
# mitigation. (A hard timeout is NOT retried here: same-model
|
|
1511
|
+
# retry of a hang is what produced the 490s in PYTHON-FASTAPI-62;
|
|
1512
|
+
# the fallback ladder inside _call_and_parse handles it instead.)
|
|
1513
|
+
#
|
|
1514
|
+
# This second pass re-walks the full ladder, so the worst-case
|
|
1515
|
+
# wall clock is ~2x the ladder bound. That ceiling is only reached
|
|
1516
|
+
# if a model returns a malformed-but-successful 200 AND runs near
|
|
1517
|
+
# the timeout on BOTH passes — a hang (the common case) raises
|
|
1518
|
+
# LLMHardTimeoutError, which is not caught here and exits after a
|
|
1519
|
+
# single ladder.
|
|
1379
1520
|
self.logger.warning(
|
|
1380
1521
|
"event=llm_parse_retry model=%s — primary returned malformed structured output, retrying once",
|
|
1381
1522
|
params.get("model"),
|
|
1382
1523
|
)
|
|
1383
1524
|
return _call_and_parse()
|
|
1384
|
-
except LLMHardTimeoutError:
|
|
1385
|
-
# The hard timeout kills the litellm subprocess, so litellm's
|
|
1386
|
-
# num_retries never gets a chance — we owe one explicit retry
|
|
1387
|
-
# at this level to cover transient provider hangs.
|
|
1388
|
-
self.logger.warning(
|
|
1389
|
-
"event=llm_hard_timeout_retry model=%s — request hit hard timeout, retrying once",
|
|
1390
|
-
params.get("model"),
|
|
1391
|
-
)
|
|
1392
|
-
return _call_and_parse()
|
|
1393
1525
|
except Exception as e:
|
|
1394
1526
|
self.logger.error(
|
|
1395
1527
|
"event=llm_request_end model=%s elapsed_seconds=%.3f success=False error_type=%s error=%s",
|
|
@@ -1636,21 +1768,76 @@ class LiteLLMClient:
|
|
|
1636
1768
|
"""
|
|
1637
1769
|
content = content.strip()
|
|
1638
1770
|
|
|
1771
|
+
# Prefer a balanced JSON container first. Structured JSON may contain
|
|
1772
|
+
# markdown fences inside string values; grabbing the first code block
|
|
1773
|
+
# would extract the inner snippet instead of the response object.
|
|
1774
|
+
json_container = self._extract_first_json_container(content)
|
|
1775
|
+
if json_container is not None:
|
|
1776
|
+
return json_container
|
|
1777
|
+
|
|
1639
1778
|
# Try to extract from markdown code blocks
|
|
1640
1779
|
json_block_pattern = r"```(?:json)?\s*([\s\S]*?)```"
|
|
1641
1780
|
matches = re.findall(json_block_pattern, content)
|
|
1642
1781
|
if matches:
|
|
1643
1782
|
return matches[0].strip()
|
|
1644
1783
|
|
|
1645
|
-
# Try to find JSON object or array
|
|
1646
|
-
for start_char, end_char in [("{", "}"), ("[", "]")]:
|
|
1647
|
-
start_idx = content.find(start_char)
|
|
1648
|
-
end_idx = content.rfind(end_char)
|
|
1649
|
-
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
|
|
1650
|
-
return content[start_idx : end_idx + 1]
|
|
1651
|
-
|
|
1652
1784
|
return content
|
|
1653
1785
|
|
|
1786
|
+
def _extract_first_json_container(self, content: str) -> str | None:
|
|
1787
|
+
"""Return the first balanced JSON-like object/array in ``content``."""
|
|
1788
|
+
for start_idx, ch in enumerate(content):
|
|
1789
|
+
if ch not in "{[":
|
|
1790
|
+
continue
|
|
1791
|
+
end_idx = self._find_json_container_end(content, start_idx)
|
|
1792
|
+
if end_idx is None:
|
|
1793
|
+
continue
|
|
1794
|
+
candidate = content[start_idx : end_idx + 1]
|
|
1795
|
+
if self._is_parseable_json_candidate(candidate):
|
|
1796
|
+
return candidate
|
|
1797
|
+
return None
|
|
1798
|
+
|
|
1799
|
+
@staticmethod
|
|
1800
|
+
def _find_json_container_end(content: str, start_idx: int) -> int | None:
|
|
1801
|
+
"""Find the matching end of a JSON container, respecting strings."""
|
|
1802
|
+
pairs = {"{": "}", "[": "]"}
|
|
1803
|
+
stack = [pairs[content[start_idx]]]
|
|
1804
|
+
in_str = False
|
|
1805
|
+
escape = False
|
|
1806
|
+
|
|
1807
|
+
for idx in range(start_idx + 1, len(content)):
|
|
1808
|
+
ch = content[idx]
|
|
1809
|
+
if escape:
|
|
1810
|
+
escape = False
|
|
1811
|
+
continue
|
|
1812
|
+
if ch == "\\" and in_str:
|
|
1813
|
+
escape = True
|
|
1814
|
+
continue
|
|
1815
|
+
if ch == '"':
|
|
1816
|
+
in_str = not in_str
|
|
1817
|
+
continue
|
|
1818
|
+
if in_str:
|
|
1819
|
+
continue
|
|
1820
|
+
if ch in pairs:
|
|
1821
|
+
stack.append(pairs[ch])
|
|
1822
|
+
elif ch in ("}", "]"):
|
|
1823
|
+
if not stack or stack.pop() != ch:
|
|
1824
|
+
return None
|
|
1825
|
+
if not stack:
|
|
1826
|
+
return idx
|
|
1827
|
+
return None
|
|
1828
|
+
|
|
1829
|
+
def _is_parseable_json_candidate(self, candidate: str) -> bool:
|
|
1830
|
+
"""Return True if a balanced candidate can parse after normal sanitizing."""
|
|
1831
|
+
try:
|
|
1832
|
+
json.loads(candidate)
|
|
1833
|
+
return True
|
|
1834
|
+
except Exception:
|
|
1835
|
+
try:
|
|
1836
|
+
json.loads(self._sanitize_json_string(candidate))
|
|
1837
|
+
return True
|
|
1838
|
+
except Exception:
|
|
1839
|
+
return False
|
|
1840
|
+
|
|
1654
1841
|
def _looks_truncated_json(self, json_str: str) -> bool:
|
|
1655
1842
|
"""
|
|
1656
1843
|
Return True when a JSON-like string appears to end before it is complete.
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
from copy import deepcopy
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from pydantic import BaseModel
|
|
8
9
|
|
|
10
|
+
from reflexio.models.structured_output import find_schema_keyword
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# JSON-Schema keywords that strict structured-output endpoints (OpenAI, minimax)
|
|
15
|
+
# reject; see PYTHON-FASTAPI-9J.
|
|
16
|
+
PROVIDER_UNSAFE_KEYWORDS = ("oneOf", "discriminator")
|
|
17
|
+
|
|
9
18
|
|
|
10
19
|
def positive_int_env(name: str, default: int, logger: logging.Logger) -> int:
|
|
11
20
|
"""Resolve a strictly-positive int from environment variable ``name``.
|
|
@@ -89,10 +98,12 @@ def make_strict_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
89
98
|
node.pop(keyword, None)
|
|
90
99
|
|
|
91
100
|
# Strict structured output (OpenAI) permits ``anyOf`` but rejects
|
|
92
|
-
# ``oneOf`` and ``discriminator``.
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
101
|
+
# ``oneOf`` and ``discriminator``. Folded inline here, fused into this
|
|
102
|
+
# single structure-aware pass (the shared ``_fold_oneof_to_anyof`` helper
|
|
103
|
+
# does the same fold for the model-boundary hook; kept inline here to
|
|
104
|
+
# avoid a second full-tree walk). ``visit`` only ever recurses into
|
|
105
|
+
# property/$def *values*, never the name maps, so a field literally named
|
|
106
|
+
# ``oneOf`` is preserved — same contract as the helper.
|
|
96
107
|
one_of = node.pop("oneOf", None)
|
|
97
108
|
node.pop("discriminator", None)
|
|
98
109
|
if isinstance(one_of, list):
|
|
@@ -127,14 +138,65 @@ def make_strict_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
127
138
|
return strict_schema
|
|
128
139
|
|
|
129
140
|
|
|
130
|
-
def
|
|
131
|
-
"""
|
|
141
|
+
def assert_provider_safe_schema(schema: dict[str, Any], *, name: str = "") -> None:
|
|
142
|
+
"""Enforce that an emitted structured-output schema is provider-safe.
|
|
143
|
+
|
|
144
|
+
Strict structured-output endpoints (OpenAI, minimax) reject ``oneOf`` /
|
|
145
|
+
``discriminator`` (Sentry PYTHON-FASTAPI-9J). Models that inherit
|
|
146
|
+
``StrictStructuredOutput`` are safe by construction; this is the runtime net
|
|
147
|
+
at the call boundary for anything that bypasses that guarantee — a model that
|
|
148
|
+
forgot the base, or a tool-argument / dynamically-built schema not covered by
|
|
149
|
+
the registry contract test.
|
|
150
|
+
|
|
151
|
+
Enforcement: under pytest (``"pytest" in sys.modules``) it RAISES so a
|
|
152
|
+
regression fails CI loudly — including at import/collection time, which a
|
|
153
|
+
per-test signal like ``PYTEST_CURRENT_TEST`` would miss. In prod it logs a
|
|
154
|
+
warning (observability) and returns; it does NOT mutate what is sent. So a
|
|
155
|
+
forgot-the-base model is meant to be caught **pre-merge** (by this raise plus
|
|
156
|
+
the registry contract test), not auto-repaired at runtime: on the strict /
|
|
157
|
+
allowlisted path ``make_strict_json_schema`` independently folds the schema,
|
|
158
|
+
but on the raw passthrough path the warning is the only signal and an unfolded
|
|
159
|
+
``oneOf`` would still reach the provider. Keep every output model on
|
|
160
|
+
``StrictStructuredOutput``.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
schema (dict[str, Any]): The emitted JSON schema to check.
|
|
164
|
+
name (str): Identifier for the schema's source, used in the message.
|
|
165
|
+
"""
|
|
166
|
+
offenders = [
|
|
167
|
+
kw for kw in PROVIDER_UNSAFE_KEYWORDS if find_schema_keyword(schema, kw)
|
|
168
|
+
]
|
|
169
|
+
if not offenders:
|
|
170
|
+
return
|
|
171
|
+
msg = (
|
|
172
|
+
f"Structured-output schema {name or '<unnamed>'!r} contains provider-unsafe "
|
|
173
|
+
f"keyword(s) {offenders}; strict providers reject these. Inherit "
|
|
174
|
+
"StrictStructuredOutput so the schema folds oneOf->anyOf by construction "
|
|
175
|
+
"(Sentry PYTHON-FASTAPI-9J)."
|
|
176
|
+
)
|
|
177
|
+
if "pytest" in sys.modules:
|
|
178
|
+
raise ValueError(msg)
|
|
179
|
+
logger.warning(msg)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def strict_response_format_for_model(
|
|
183
|
+
model: type[BaseModel], schema: dict[str, Any] | None = None
|
|
184
|
+
) -> dict[str, Any]:
|
|
185
|
+
"""Build a LiteLLM/OpenAI-compatible strict ``json_schema`` response format.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
model: The Pydantic model (supplies the schema ``name``).
|
|
189
|
+
schema: Optional pre-built ``model.model_json_schema()`` to reuse, avoiding
|
|
190
|
+
a second schema build when the caller already has one.
|
|
191
|
+
"""
|
|
132
192
|
|
|
133
193
|
return {
|
|
134
194
|
"type": "json_schema",
|
|
135
195
|
"json_schema": {
|
|
136
196
|
"name": model.__name__,
|
|
137
|
-
"schema": make_strict_json_schema(
|
|
197
|
+
"schema": make_strict_json_schema(
|
|
198
|
+
schema if schema is not None else model.model_json_schema()
|
|
199
|
+
),
|
|
138
200
|
"strict": True,
|
|
139
201
|
},
|
|
140
202
|
}
|