claude-smart 0.2.45 → 0.2.47

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.
Files changed (353) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +47 -10
  3. package/bin/claude-smart.js +535 -23
  4. package/package.json +22 -3
  5. package/plugin/.claude-plugin/plugin.json +4 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +27 -6
  8. package/plugin/commands/clear-all.md +1 -0
  9. package/plugin/commands/dashboard.md +1 -0
  10. package/plugin/commands/learn.md +1 -0
  11. package/plugin/commands/restart.md +1 -0
  12. package/plugin/commands/show.md +1 -0
  13. package/plugin/dashboard/app/configure/env/page.tsx +40 -14
  14. package/plugin/dashboard/app/configure/server/page.tsx +51 -1
  15. package/plugin/dashboard/app/layout.tsx +20 -0
  16. package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
  17. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  18. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  19. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  20. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  21. package/plugin/dashboard/lib/status.ts +10 -3
  22. package/plugin/dashboard/lib/types.ts +18 -6
  23. package/plugin/hooks/codex-hooks.json +0 -1
  24. package/plugin/opencode/assistant-buffer.ts +108 -0
  25. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  26. package/plugin/opencode/dist/internal.js +12 -0
  27. package/plugin/opencode/dist/payload.js +85 -0
  28. package/plugin/opencode/dist/server.mjs +232 -0
  29. package/plugin/opencode/internal.ts +18 -0
  30. package/plugin/opencode/package.json +3 -0
  31. package/plugin/opencode/payload.ts +90 -0
  32. package/plugin/opencode/server.mts +251 -0
  33. package/plugin/opencode/tsconfig.json +13 -0
  34. package/plugin/pyproject.toml +25 -4
  35. package/plugin/scripts/_lib.sh +5 -1
  36. package/plugin/scripts/backend-service.sh +22 -2
  37. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  38. package/plugin/scripts/hook_entry.sh +1 -1
  39. package/plugin/scripts/opencode-claude-compat +4 -0
  40. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  41. package/plugin/scripts/opencode-claude-compat.js +225 -0
  42. package/plugin/scripts/smart-install.sh +17 -9
  43. package/plugin/src/README.md +1 -1
  44. package/plugin/src/claude_smart/cli.py +498 -12
  45. package/plugin/src/claude_smart/context_format.py +9 -9
  46. package/plugin/src/claude_smart/cs_cite.py +66 -19
  47. package/plugin/src/claude_smart/env_config.py +12 -4
  48. package/plugin/src/claude_smart/events/session_start.py +26 -7
  49. package/plugin/src/claude_smart/events/stop.py +1 -1
  50. package/plugin/src/claude_smart/ids.py +1 -1
  51. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  52. package/plugin/src/claude_smart/runtime.py +7 -1
  53. package/plugin/uv.lock +5 -5
  54. package/plugin/vendor/reflexio/.env.example +54 -30
  55. package/plugin/vendor/reflexio/README.md +17 -11
  56. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  57. package/plugin/vendor/reflexio/reflexio/README.md +4 -1
  58. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  59. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  60. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  63. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  64. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +212 -52
  65. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  66. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  67. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  68. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  69. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  70. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  71. package/plugin/vendor/reflexio/reflexio/client/client.py +154 -13
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  145. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +246 -10
  146. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  147. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  148. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +17 -5
  149. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  150. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +224 -21
  151. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  152. package/plugin/vendor/reflexio/reflexio/lib/_search.py +52 -25
  153. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +11 -1
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  157. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  158. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  159. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  160. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  161. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +97 -3
  162. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  163. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  164. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +104 -7
  165. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  166. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  167. package/plugin/vendor/reflexio/reflexio/server/README.md +58 -33
  168. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  169. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  170. package/plugin/vendor/reflexio/reflexio/server/api.py +899 -103
  171. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  172. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +4 -2
  173. package/plugin/vendor/reflexio/reflexio/server/auth.py +90 -0
  174. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  175. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  176. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  177. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  178. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  179. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  180. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  181. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  182. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  183. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
  184. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  185. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  186. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  187. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  188. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  189. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  197. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  198. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +278 -0
  199. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  200. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  201. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  202. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  203. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  204. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  205. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  206. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  207. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  208. package/plugin/vendor/reflexio/reflexio/server/services/README.md +28 -11
  209. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  211. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  212. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  214. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  215. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  216. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  217. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  218. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  219. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  220. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  221. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  222. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  223. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  225. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  226. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  227. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  228. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  229. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  230. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  231. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +47 -16
  232. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  233. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  234. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  235. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  237. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +219 -0
  238. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  239. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +18 -11
  241. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  242. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  243. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +334 -178
  244. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  245. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  246. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  247. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  248. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  249. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  250. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  251. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  252. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  253. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  254. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  255. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +31 -22
  256. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  257. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  258. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  259. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  260. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  261. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  262. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  263. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +99 -6
  264. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  265. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  266. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +31 -0
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +16 -2
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +644 -197
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +106 -119
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +630 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  277. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +31 -1369
  278. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +756 -210
  279. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +148 -34
  280. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  281. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  282. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +150 -23
  289. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  290. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +40 -57
  292. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +4 -677
  296. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +134 -1
  297. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +62 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +372 -34
  311. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  312. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  313. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  314. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  315. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  316. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  317. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  318. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  319. package/scripts/setup-claude-smart.sh +8 -3
  320. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  321. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  322. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  323. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  324. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  325. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  326. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  327. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  328. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  329. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  330. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  331. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  332. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  333. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  334. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  335. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  336. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  337. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  338. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  339. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  340. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  341. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  342. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  343. package/plugin/vendor/reflexio/reflexio/server/_auth.py +0 -25
  344. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  345. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  346. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  347. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  348. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  349. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  350. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  351. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  352. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  353. /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 claude-smart's local providers. All no-ops
70
- # unless the matching env var is set. Safe to call at import.
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 on the primary model. Passed
232
- directly to litellm's num_retries. Default 3.
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
- exhausts num_retries. Passed directly to litellm's fallbacks param.
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
- # Reasoning models that routinely exceed the default 120s provider timeout on
264
- # large extraction contexts. Values are floors, not overrides: the effective
265
- # timeout is max(configured, floor), and an explicit per-call timeout kwarg
266
- # always wins.
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": 240,
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
- try:
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
- LiteLLM says the target model supports JSON Schema response formats, we
1077
- send an explicit strict schema to constrain generation. Unsupported
1078
- providers keep the existing Pydantic response_format behavior.
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
- strict_response_format
1083
- and is_pydantic_model(response_format)
1084
- and self._supports_response_schema(model)
1085
- ):
1086
- return strict_response_format_for_model(response_format)
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 _completion_with_hard_timeout(self, params: dict[str, Any]) -> Any:
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
- try:
1119
- timeout_seconds = float(provider_timeout)
1120
- except (TypeError, ValueError):
1121
- timeout_seconds = float(self.config.timeout)
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, timeout_seconds) + max(0.0, grace_seconds)
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
- if isinstance(payload, _CompletionErrorSnapshot):
1160
- raise LiteLLMClientError(
1161
- f"litellm.completion raised {payload.type_name}: {payload.message}"
1162
- )
1163
- raise payload
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 retries and fallback to litellm.
1293
-
1294
- Retry and fallback semantics are handed to ``litellm.completion`` via
1295
- the native ``num_retries`` and ``fallbacks`` kwargs. Per the documented
1296
- flow at https://docs.litellm.ai/docs/router_architecture, the primary
1297
- model is tried ``num_retries+1`` times, then each fallback gets a single
1298
- attempt. The one piece we still own at the client level is a single
1299
- retry for ``StructuredOutputParseError``: LiteLLM cannot detect a
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, max_retries, fallbacks = (
1407
+ params, response_format, parse_structured_output, _max_retries, fallbacks = (
1317
1408
  self._build_completion_params(messages, **kwargs)
1318
1409
  )
1319
1410
 
1320
- # Hand retries + fallbacks to litellm. ``num_retries`` is the documented
1321
- # alias for max_retries on litellm.completion.
1322
- params["num_retries"] = max_retries
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=%d fallbacks=%s",
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=getattr(message, "tool_calls", None),
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
- # LiteLLM's num_retries covers API errors, but a Pydantic
1375
- # re-validation failure happens AFTER litellm sees a
1376
- # successful 200 — so we owe one explicit second attempt at
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``. Pydantic emits ``oneOf`` for
93
- # discriminated unions; fold it into ``anyOf`` so generation is
94
- # constrained to the same variants. Pydantic still enforces the
95
- # discriminator after parse, so semantics are preserved.
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 strict_response_format_for_model(model: type[BaseModel]) -> dict[str, Any]:
131
- """Build a LiteLLM/OpenAI-compatible strict ``json_schema`` response format."""
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(model.model_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
  }
@@ -195,12 +195,12 @@ _PROVIDER_DEFAULTS: dict[str, ProviderDefaults] = {
195
195
  extraction_agent="gpt-5.5",
196
196
  ),
197
197
  "anthropic": ProviderDefaults(
198
- generation="claude-sonnet-4-6",
199
- evaluation="claude-sonnet-4-6",
198
+ generation="claude-sonnet-5",
199
+ evaluation="claude-sonnet-5",
200
200
  should_run="claude-haiku-4-5-20251001",
201
201
  pre_retrieval="claude-haiku-4-5-20251001",
202
202
  embedding=None,
203
- extraction_agent="claude-sonnet-4-6",
203
+ extraction_agent="claude-sonnet-5",
204
204
  ),
205
205
  "gemini": ProviderDefaults(
206
206
  generation="gemini/gemini-3-flash-preview",
@@ -229,7 +229,7 @@ _PROVIDER_DEFAULTS: dict[str, ProviderDefaults] = {
229
229
  should_run="minimax/MiniMax-M3",
230
230
  pre_retrieval="minimax/MiniMax-M3",
231
231
  embedding=None,
232
- # Same M2.7 model handles resumable extraction. Surfaced by an
232
+ # Same M3 model handles resumable extraction. Surfaced by an
233
233
  # e2e run on a MiniMax-only VPS where publish printed
234
234
  # "No provider in ['minimax'] supports role=extraction_agent"
235
235
  # warnings and silently skipped profile creation. Without this,
@@ -69,7 +69,7 @@ _CODEX_COMPAT_SCRIPT_NAMES = (
69
69
  )
70
70
  _CODEX_COMPAT_SCRIPT_NAME_SET = set(_CODEX_COMPAT_SCRIPT_NAMES)
71
71
  _DEFAULT_TIMEOUT_SECONDS = 120
72
- _DEFAULT_CLI_MODEL = "claude-sonnet-4-6"
72
+ _DEFAULT_CLI_MODEL = "claude-sonnet-5"
73
73
 
74
74
  _TRUTHY_ENV_VALUES = {"1", "true", "yes"}
75
75
  _UNSUPPORTED_PARAMS_WARNED: set[str] = set()