utim-cli 2.3.24__tar.gz → 2.3.26__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. {utim_cli-2.3.24 → utim_cli-2.3.26}/CHANGELOG.md +12 -0
  2. {utim_cli-2.3.24/utim_cli.egg-info → utim_cli-2.3.26}/PKG-INFO +1 -1
  3. {utim_cli-2.3.24 → utim_cli-2.3.26}/pyproject.toml +1 -1
  4. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim/__init__.py +1 -1
  5. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/_version.py +1 -1
  7. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/constants.py +12 -1
  8. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/orchestrator.py +33 -29
  9. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/orchestrator_helpers.py +12 -12
  10. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/orchestrator_llm_transport.py +40 -86
  11. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/orchestrator_tool_exec.py +24 -5
  12. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/key_rotator.py +103 -80
  13. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/models.py +88 -2
  14. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/router.py +131 -137
  15. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/completion_routes.py +588 -297
  16. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/state.py +1 -0
  17. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tools.py +90 -10
  18. utim_cli-2.3.26/utim_cli/tui/compression_dialog.py +188 -0
  19. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/utim.py +154 -167
  20. {utim_cli-2.3.24 → utim_cli-2.3.26/utim_cli.egg-info}/PKG-INFO +1 -1
  21. utim_cli-2.3.24/utim_cli/tui/compression_dialog.py +0 -230
  22. {utim_cli-2.3.24 → utim_cli-2.3.26}/LICENSE +0 -0
  23. {utim_cli-2.3.24 → utim_cli-2.3.26}/MANIFEST.in +0 -0
  24. {utim_cli-2.3.24 → utim_cli-2.3.26}/README.md +0 -0
  25. {utim_cli-2.3.24 → utim_cli-2.3.26}/setup.cfg +0 -0
  26. {utim_cli-2.3.24 → utim_cli-2.3.26}/setup.py +0 -0
  27. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/_chip_test.py +0 -0
  28. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/_verify_repro.py +0 -0
  29. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/_verify_syntax.py +0 -0
  30. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/_wizard_smoke_test.py +0 -0
  31. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/agent.py +0 -0
  32. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/ask_helper.py +0 -0
  33. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/auth.py +0 -0
  34. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/backup.py +0 -0
  35. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/billing.py +0 -0
  36. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/blender_agent.py +0 -0
  37. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/bootstrap.py +0 -0
  38. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/brain.py +0 -0
  39. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
  40. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/client_utils.py +0 -0
  41. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/config.py +0 -0
  42. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/context_pruner.py +0 -0
  43. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/crawler/__init__.py +0 -0
  44. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/crawler/engine.py +0 -0
  45. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/crawler/markdown_distiller.py +0 -0
  46. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/dependency_manager.py +0 -0
  47. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/doctor.py +0 -0
  48. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/evidence_registry.py +0 -0
  49. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/harbor.py +0 -0
  50. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/knowledge_graph.py +0 -0
  51. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/local_db.py +0 -0
  52. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/logger.py +0 -0
  53. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/mcp_clean_wrapper.py +0 -0
  54. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/mcp_client.py +0 -0
  55. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/mcp_registry.json +0 -0
  56. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/models.txt +0 -0
  57. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/orchestrator_turn_history.py +0 -0
  58. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/personalities.py +0 -0
  59. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/platform_installer.py +0 -0
  60. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/plugins/__init__.py +0 -0
  61. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/plugins/hooks.py +0 -0
  62. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/plugins/loader.py +0 -0
  63. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/plugins/manager.py +0 -0
  64. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/plugins/registry.py +0 -0
  65. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/plugins/runtime.py +0 -0
  66. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/plugins/schema.py +0 -0
  67. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/plugins/tool_loader.py +0 -0
  68. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/reflection.py +0 -0
  69. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/report.py +0 -0
  70. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/scrapy_search.py +0 -0
  71. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/sdk/__init__.py +0 -0
  72. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/sdk/config.py +0 -0
  73. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/sdk/context.py +0 -0
  74. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/sdk/events.py +0 -0
  75. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/sdk/handlers.py +0 -0
  76. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/sdk/session.py +0 -0
  77. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/sdk/sidecar.py +0 -0
  78. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/__init__.py +0 -0
  79. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/admin_auth.py +0 -0
  80. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/attribution.py +0 -0
  81. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/audit_log.py +0 -0
  82. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/auth.py +0 -0
  83. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/batch_processor.py +0 -0
  84. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/captcha.py +0 -0
  85. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/cli_auth.py +0 -0
  86. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/concurrency.py +0 -0
  87. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/db.py +0 -0
  88. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  89. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/about.md +0 -0
  90. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/changelog.md +0 -0
  91. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/docs.md +0 -0
  92. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/features.md +0 -0
  93. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/license.md +0 -0
  94. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/pricing.md +0 -0
  95. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/privacy.md +0 -0
  96. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/refund.md +0 -0
  97. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/support.md +0 -0
  98. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/docs_md/terms.md +0 -0
  99. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/email_utils.py +0 -0
  100. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/exchange_rate.py +0 -0
  101. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/firebase.py +0 -0
  102. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/fix_duplicate_users.py +0 -0
  103. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/history.py +0 -0
  104. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/local_llm.py +0 -0
  105. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/logging_config.py +0 -0
  106. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/micro_batcher.py +0 -0
  107. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/model_agent.py +0 -0
  108. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/plan_integrity.py +0 -0
  109. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/pricing_updater.py +0 -0
  110. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/provision_build.py +0 -0
  111. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/rate_limit.py +0 -0
  112. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/rewards_engine.py +0 -0
  113. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/__init__.py +0 -0
  114. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
  115. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/admin_db_routes.py +0 -0
  116. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/auth_routes.py +0 -0
  117. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/billing_routes.py +0 -0
  118. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/credit_routes.py +0 -0
  119. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/feedback_routes.py +0 -0
  120. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/marketplace_routes.py +0 -0
  121. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/quota_routes.py +0 -0
  122. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/quota_share_routes.py +0 -0
  123. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/referral_routes.py +0 -0
  124. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/rewards_routes.py +0 -0
  125. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/security_routes.py +0 -0
  126. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/session_routes.py +0 -0
  127. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/share_routes.py +0 -0
  128. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/routes/webhook_routes.py +0 -0
  129. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/security_alerts.py +0 -0
  130. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/server.py +0 -0
  131. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/storage_nodes.py +0 -0
  132. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/server/utils.py +0 -0
  133. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/share.py +0 -0
  134. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/share_tui.py +0 -0
  135. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/situational_scoring.py +0 -0
  136. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/subagent_manager.py +0 -0
  137. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/task_dispatcher.py +0 -0
  138. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/task_logger.py +0 -0
  139. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tools_grep.py +0 -0
  140. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/__init__.py +0 -0
  141. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/feedback_dialog.py +0 -0
  142. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/history_dialog.py +0 -0
  143. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/marketplace_app_state.py +0 -0
  144. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/marketplace_dialog.py +0 -0
  145. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  146. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/mcp_dialog.py +0 -0
  147. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/miniagents_dialog.py +0 -0
  148. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/model_dialog.py +0 -0
  149. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/personality_dialog.py +0 -0
  150. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/plugins_dialog.py +0 -0
  151. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/publish_dialog.py +0 -0
  152. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/quota_dialog.py +0 -0
  153. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  154. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/quota_share_dialog.py +0 -0
  155. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/resume_dialog.py +0 -0
  156. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/rewards_tui.py +0 -0
  157. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/skills_dialog.py +0 -0
  158. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/subagents_dialog.py +0 -0
  159. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/tasks_dialog.py +0 -0
  160. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/thinking_display.py +0 -0
  161. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/tools_dialog.py +0 -0
  162. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/tui/update_dialog.py +0 -0
  163. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/update_checker.py +0 -0
  164. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/utilities.py +0 -0
  165. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/utimmodel.txt +0 -0
  166. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/vector_memory.py +0 -0
  167. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/wheel.py +0 -0
  168. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli/workspace.py +0 -0
  169. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli.egg-info/SOURCES.txt +0 -0
  170. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli.egg-info/dependency_links.txt +0 -0
  171. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli.egg-info/entry_points.txt +0 -0
  172. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli.egg-info/requires.txt +0 -0
  173. {utim_cli-2.3.24 → utim_cli-2.3.26}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.26] - 2026-09-08
4
+
5
+ ### 🛡️ Zero Rate-Limit Failover & Low-Latency Key Rotation
6
+ - **Deterministic APINEX Failover Chain**: Upstream APINEX failures (429/402/exhaustion) seamlessly cascade through `openrouter/free` -> `upstage/solar-pro4` -> `qwen/qwen3.7-flash` -> `deepseek/deepseek-v4-flash-0731` with zero wait latency and direct key pool acquisition.
7
+ - **Silent Rate-Limit Recovery**: Completely eliminated user-facing rate-limit interruptions, warnings, and 24-hour key lockout bans on transient provider 429s.
8
+ - **Zero-Latency Paid 402/429 Key Rotation**: OpenRouter keys returning 402 or 429 immediately rotate across candidate keys and seamlessly fail over to backup chain models without aborting active turns.
9
+
10
+ ## [2.3.25] - 2026-09-07
11
+
12
+ ### 🛠️ Bug Fixes
13
+ - Fixed usage dialog glitches
14
+
3
15
  ## [2.3.24] - 2026-09-07
4
16
 
5
17
  ### ⚡ Performance
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.3.24
3
+ Version: 2.3.26
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: Emend AI Proprietary EULA
6
6
  Project-URL: Homepage, https://utim.dev
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.3.24"
7
+ version = "2.3.26"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -14,7 +14,7 @@ Quickstart:
14
14
  try:
15
15
  from utim_cli._version import VERSION as __version__
16
16
  except ImportError:
17
- __version__ = "2.3.24"
17
+ __version__ = "2.3.26"
18
18
 
19
19
  # Developer-friendly core classes (similar to `import genai` / `import openai`)
20
20
  from utim_cli.sdk.session import UtimSession as Session
@@ -5,7 +5,7 @@ try:
5
5
  from utim_cli._version import VERSION as __version__
6
6
  except ImportError:
7
7
  # Fallback during first-install / editable-install edge cases
8
- __version__ = "2.3.24"
8
+ __version__ = "2.3.26"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -11,6 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.3.24"
14
+ VERSION = "2.3.26"
15
15
 
16
16
 
@@ -11,7 +11,17 @@ SUBAGENT_DEFAULTS = {
11
11
  "analyze_image": "google/gemma-4-31b-it:free",
12
12
  }
13
13
 
14
- APINEX_MODEL_IDS = {
14
+ ORCAROUTER_MODEL_IDS = {
15
+ "orcarouter/free",
16
+ "orcarouter/fusion",
17
+ "orcarouter/fusion-flash",
18
+ "orcarouter/fusion-mini",
19
+ "anthropic/claude-sonnet-5",
20
+ "anthropic/claude-opus-5",
21
+ "deepseek/deepseek-v4-flash-0731",
22
+ "deepseek/deepseek-v4-flash-free",
23
+ "deepseek/deepseek-v4-pro-0813",
24
+ "google/gemini-2.5-flash",
15
25
  "claude/opus-5",
16
26
  "claude/sonnet-5",
17
27
  "deepseek/v4-flash-0731",
@@ -33,6 +43,7 @@ APINEX_MODEL_IDS = {
33
43
  "qwen/3.8-max",
34
44
  "free/qwen-3.8-max",
35
45
  }
46
+ APINEX_MODEL_IDS = ORCAROUTER_MODEL_IDS
36
47
 
37
48
 
38
49
  import sys
@@ -854,24 +854,6 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
854
854
 
855
855
  break # success
856
856
  except _ServerUnavailableError as exc:
857
- _err_text = str(exc).lower()
858
- if "rate limit" in _err_text or "rate-limit" in _err_text or "429" in _err_text or "too many requests" in _err_text:
859
- # Mark the current API key as exhausted for 24 hours
860
- from utim_cli.config import config
861
- current_key = config.get("api_key", "")
862
- if current_key:
863
- config.mark_api_key_exhausted(current_key, duration_hours=24)
864
- self.console.print(f"\n[bold yellow]⏳ Rate-limited on API key {current_key[:12]}... — excluded for 24 hours.[/bold yellow]")
865
- else:
866
- self.console.print(f"\n[bold yellow]⏳ Model is temporarily rate-limited by the provider.[/bold yellow]")
867
- self.console.print(" [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n")
868
- if len(self.messages) <= turn_msg_start + 1:
869
- self.messages.append({
870
- "role": "assistant",
871
- "content": "⏳ **Rate Limited**: The API key has been temporarily exhausted (24h cooldown). Please wait and retry, or use `/model` to switch models."
872
- })
873
- self._persist_messages()
874
- return
875
857
  if self.cancel_event.is_set():
876
858
  del self.messages[turn_msg_start + 1:]
877
859
  self._persist_messages()
@@ -918,17 +900,6 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
918
900
  self.messages.append({"role": "assistant", "content": "[Aborted by user]"})
919
901
  break
920
902
  except Exception as exc:
921
- _err_text = str(exc).lower()
922
- if "rate limit" in _err_text or "rate-limit" in _err_text or "429" in _err_text or "too many requests" in _err_text:
923
- self.console.print(f"\n[bold yellow]⏳ Model is temporarily rate-limited by the provider.[/bold yellow]")
924
- self.console.print(" [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n")
925
- if len(self.messages) <= turn_msg_start + 1:
926
- self.messages.append({
927
- "role": "assistant",
928
- "content": "⏳ **Model Rate Limited**: The model is temporarily rate-limited by the provider. Please wait a short moment and retry, or use `/model` to switch models."
929
- })
930
- self._persist_messages()
931
- return
932
903
  if self.cancel_event.is_set():
933
904
  if len(self.messages) <= turn_msg_start + 1:
934
905
  self.messages.append({"role": "assistant", "content": "[Aborted by user]"})
@@ -1234,6 +1205,39 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
1234
1205
  # Got a valid response — reset streak counter
1235
1206
  _empty_response_streak = 0
1236
1207
 
1208
+ # ── Nudge if model mistakenly claims it lacks web_search ──────────
1209
+ _no_web_search_claims = [
1210
+ "don't have a web_search",
1211
+ "do not have a web_search",
1212
+ "don't have a web search",
1213
+ "do not have a web search",
1214
+ "no web search tool",
1215
+ "don't have access to a web search",
1216
+ "do not have access to a web search",
1217
+ "cannot search the web",
1218
+ "unable to search the web",
1219
+ "don't have internet access",
1220
+ "do not have internet access",
1221
+ "cannot access the internet",
1222
+ "cannot browse the web",
1223
+ "don't have browsing",
1224
+ ]
1225
+ if (
1226
+ not tool_calls
1227
+ and iteration < max_iterations - 1
1228
+ and any(claim in _lower_content for claim in _no_web_search_claims)
1229
+ and "web_search" not in (config.get("disabled_tools") or [])
1230
+ ):
1231
+ self.messages.append({"role": "assistant", "content": content})
1232
+ self.messages.append({
1233
+ "role": "user",
1234
+ "content": (
1235
+ "SYSTEM REMINDER: You DO have the 'web_search' tool in your active tool registry. "
1236
+ "Call the web_search tool directly now using web_search(prompt=...) to perform the requested web search."
1237
+ )
1238
+ })
1239
+ continue
1240
+
1237
1241
  # ── Universal Required Deliverables Checklist ──────────────────────────────
1238
1242
  if getattr(self, "_deliverable_check_attempts", 0) < 2:
1239
1243
  _deliverable_matches = re.findall(
@@ -350,11 +350,11 @@ If a workspace skill or rule is relevant to your task (or mentioned by the user)
350
350
  1. EXECUTE
351
351
  Take the shortest reliable path. Inspect → modify → verify. Avoid redundant exploration.
352
352
 
353
- 2. SEARCH
354
- For known symbols/errors, use grep_search. For known files, use scoped read_file. Avoid large unsliced reads and redundant listings.
353
+ 2. SEARCH & WEB RESEARCH
354
+ For known symbols/errors in the codebase, use grep_search. For known files, use scoped read_file. For live internet research, external library documentation, API references, latest package details, or web queries, use web_search. Avoid large unsliced reads and redundant listings.
355
355
 
356
356
  3. PLAN
357
- Use plan_project for new features, architectural designs, and complex multi-file refactors. Skip planning for localized edits and direct fixes.
357
+ Use plan_project for new features, architectural designs, and complex multi-file refactors. Generated plans are saved under .utim_tmp/plans/ and can be read with read_file. Skip planning for localized edits and direct fixes.
358
358
 
359
359
  4. IMAGES
360
360
  If the user provides an image/screenshot path, read that exact path with read_file on Turn 1.
@@ -366,7 +366,7 @@ After modifications, verify with run_command using the appropriate build/test/li
366
366
  Use is_background=True or schedule for long-running commands, tests, or servers, then conclude the turn.
367
367
 
368
368
  7. TOOLS
369
- Use native function calls directly. Never emit raw tool JSON, <think> tags, or filler before tool calls.
369
+ Use native function calls directly. You have full access to tools including read_file, edit_file, write_file, run_command, grep_search, list_directory, web_search, plan_project, schedule, and background_tasks. Never emit raw tool JSON, <think> tags, or filler before tool calls. Never claim you lack web search or terminal execution capabilities when these tools are present in your registry.
370
370
 
371
371
  8. OUTPUT
372
372
  After execution, give a concise result and relevant verification status.
@@ -460,11 +460,11 @@ If a workspace skill or rule is relevant to your task (or mentioned by the user)
460
460
  1. EXECUTE
461
461
  Take the shortest reliable path. Inspect → modify → verify. Avoid redundant exploration.
462
462
 
463
- 2. SEARCH
464
- For known symbols/errors, use grep_search. For known files, use scoped read_file. Avoid large unsliced reads and redundant listings.
463
+ 2. SEARCH & WEB RESEARCH
464
+ For known symbols/errors in the codebase, use grep_search. For known files, use scoped read_file. For live internet research, external library documentation, API references, latest package details, or web queries, use web_search. Avoid large unsliced reads and redundant listings.
465
465
 
466
466
  3. PLAN
467
- Use plan_project for new features, architectural designs, and complex multi-file refactors. Skip planning for localized edits and direct fixes.
467
+ Use plan_project for new features, architectural designs, and complex multi-file refactors. Generated plans are saved under .utim_tmp/plans/ and can be read with read_file. Skip planning for localized edits and direct fixes.
468
468
 
469
469
  4. IMAGES
470
470
  If the user provides an image/screenshot path, read that exact path with read_file on Turn 1.
@@ -476,7 +476,7 @@ After modifications, verify with run_command using the appropriate build/test/li
476
476
  Use is_background=True or schedule for long-running commands, tests, or servers, then conclude the turn.
477
477
 
478
478
  7. TOOLS
479
- Use native function calls directly. Never emit raw tool JSON, <think> tags, or filler before tool calls.
479
+ Use native function calls directly. You have full access to tools including read_file, edit_file, write_file, run_command, grep_search, list_directory, web_search, plan_project, schedule, and background_tasks. Never emit raw tool JSON, <think> tags, or filler before tool calls. Never claim you lack web search or terminal execution capabilities when these tools are present in your registry.
480
480
 
481
481
  8. OUTPUT
482
482
  After execution, give a concise result and relevant verification status.
@@ -1136,11 +1136,11 @@ def _get_dynamic_directives(is_compressed: bool, messages: Optional[List[Dict]]
1136
1136
  "1. EXECUTE",
1137
1137
  "Take the shortest reliable path. Inspect → modify → verify. Avoid redundant exploration.",
1138
1138
  "",
1139
- "2. SEARCH",
1140
- "For known symbols/errors, use grep_search. For known files, use scoped read_file. Avoid large unsliced reads and redundant listings.",
1139
+ "2. SEARCH & WEB RESEARCH",
1140
+ "For known symbols/errors in the codebase, use grep_search. For known files, use scoped read_file. For live internet research, external library documentation, API references, latest package details, or web queries, use web_search. Avoid large unsliced reads and redundant listings.",
1141
1141
  "",
1142
1142
  "3. PLAN",
1143
- "Use plan_project for new features, architectural designs, and complex multi-file refactors. Skip planning for localized edits and direct fixes.",
1143
+ "Use plan_project for new features, architectural designs, and complex multi-file refactors. Generated plans are saved under .utim_tmp/plans/ and can be read with read_file. Skip planning for localized edits and direct fixes.",
1144
1144
  "",
1145
1145
  "4. IMAGES",
1146
1146
  "If the user provides an image/screenshot path, read that exact path with read_file on Turn 1.",
@@ -1152,7 +1152,7 @@ def _get_dynamic_directives(is_compressed: bool, messages: Optional[List[Dict]]
1152
1152
  "Use is_background=True or schedule for long-running commands, tests, or servers, then conclude the turn.",
1153
1153
  "",
1154
1154
  "7. TOOLS",
1155
- "Use native function calls directly. Never emit raw tool JSON, <think> tags, or filler before tool calls.",
1155
+ "Use native function calls directly. You have full access to tools including read_file, edit_file, write_file, run_command, grep_search, list_directory, web_search, plan_project, schedule, and background_tasks. Never emit raw tool JSON, <think> tags, or filler before tool calls. Never claim you lack web search or terminal execution capabilities when these tools are present in your registry.",
1156
1156
  "",
1157
1157
  "8. OUTPUT",
1158
1158
  "After execution, give a concise result and relevant verification status.",
@@ -268,11 +268,6 @@ class LLMTransportMixin:
268
268
  if not race_candidates:
269
269
  return None
270
270
 
271
- if not silent:
272
- self.console.print(
273
- f"\n[dim yellow]↪ Primary model unavailable — racing {len(race_candidates)} fallback models for lowest latency response…[/dim yellow]\n"
274
- )
275
-
276
271
  winner_lock = threading.Lock()
277
272
  winner_model = [None]
278
273
  winner_result = [None]
@@ -832,8 +827,8 @@ class LLMTransportMixin:
832
827
  is_custom = self._is_custom_model_selection(chosen_model, is_primary=not override_model)
833
828
  is_free_user = (quota.get("plan") == "free" or str(quota.get("display_name", "")).lower() == "free")
834
829
 
835
- from utim_cli.constants import APINEX_MODEL_IDS
836
- is_apinex = (chosen_model in APINEX_MODEL_IDS)
830
+ from utim_cli.constants import APINEX_MODEL_IDS, ORCAROUTER_MODEL_IDS
831
+ is_apinex = (chosen_model in APINEX_MODEL_IDS or chosen_model in ORCAROUTER_MODEL_IDS or chosen_model.startswith("orcarouter/"))
837
832
  is_free_model = (chosen_model.endswith(":free") or chosen_model.startswith("free/"))
838
833
 
839
834
  if not is_custom:
@@ -1135,10 +1130,6 @@ class LLMTransportMixin:
1135
1130
  _exhausted_key = _api_key
1136
1131
 
1137
1132
  if _exhausted_key:
1138
- remaining = config.get_exhaustion_countdown(_exhausted_key)
1139
- remaining_str = f"{remaining // 3600}h {remaining % 3600 // 60}m" if remaining else "24h"
1140
- if not silent:
1141
- self.console.print(f"[dim]Skipping exhausted API key {_exhausted_key[:12]}... (cooldown: {remaining_str})[/dim]")
1142
1133
  raise _ServerUnavailableError(f"API key exhausted: {_exhausted_key[:12]}...")
1143
1134
 
1144
1135
  # Build auth headers or query params depending on provider
@@ -1610,52 +1601,26 @@ class LLMTransportMixin:
1610
1601
  _paid_slug = current_model.rsplit(":free", 1)[0]
1611
1602
  _remaining = models_to_try[model_idx + 1:]
1612
1603
  if _paid_slug not in _remaining:
1613
- if not silent:
1614
- self.console.print(
1615
- f"\n[dim yellow]↪ Free model rate-limited — trying paid version '{_paid_slug}'…[/dim yellow]"
1616
- )
1617
1604
  # Insert paid slug as the very next model, then break
1618
1605
  models_to_try[model_idx + 1:model_idx + 1] = [_paid_slug]
1619
- else:
1620
- if not silent:
1621
- self.console.print(
1622
- f"\n[dim yellow]↪ Free model rate-limited — paid version already in queue, skipping…[/dim yellow]"
1623
- )
1624
1606
  break # inner attempt loop → outer loop picks up injected paid slug
1625
- # Non-free model: nothing to fall back to show message and return
1626
- rate_limit_display = (
1627
- f"⏳ **Model Rate Limited**: `{current_model}` is currently experiencing high demand and is temporarily rate-limited by the provider.\n\n"
1628
- f"- **What to do:** Please wait a moment before retrying, or switch to a different model with `/model` or `Ctrl+M`.\n"
1629
- f"- **Retry:** You can resend your prompt anytime."
1630
- )
1631
- if not silent:
1632
- self.console.print(
1633
- f"\n[bold yellow]⏳ Model '{current_model}' is temporarily rate-limited by the provider.[/bold yellow]"
1634
- )
1635
- self.console.print(
1636
- " [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n"
1637
- )
1638
- return {
1639
- "role": "assistant",
1640
- "content": rate_limit_display,
1641
- "tool_calls": None,
1642
- }, False
1607
+ # Never display rate limit warnings to the user.
1608
+ # Seamlessly append the failover chain to models_to_try and continue trying models.
1609
+ _failover_chain = [
1610
+ "openrouter/free",
1611
+ "upstage/solar-pro4",
1612
+ "qwen/qwen3.7-flash",
1613
+ "deepseek/deepseek-v4-flash-0731",
1614
+ ]
1615
+ _remaining = models_to_try[model_idx + 1:]
1616
+ _to_inject = [m for m in _failover_chain if m not in _remaining and m != current_model]
1617
+ if _to_inject:
1618
+ models_to_try.extend(_to_inject)
1619
+ break # inner attempt loop → outer loop picks up next failover model
1643
1620
  elif _is_provider_upstream:
1644
- if not silent:
1645
- self.console.print(
1646
- f"\n[dim yellow]↪ {_friendly_name}'s provider is having trouble — "
1647
- f"switching to the next available model…[/dim yellow]"
1648
- )
1649
1621
  raise RuntimeError(f"__PROVIDER_ERROR__:{current_model}")
1650
1622
  else:
1651
- # Unknown error: show a concise message, not the raw blob
1652
- _short = err_msg.split("{'error'")[0].strip() or err_msg[:120]
1653
- if not silent:
1654
- self.console.print(
1655
- f"\n[bold red]Model error on {_friendly_name}: {_short}[/bold red]\n"
1656
- f"[dim]Trying the next available model…[/dim]"
1657
- )
1658
- raise RuntimeError(f"Server completion error ({current_model}): {err_msg}")
1623
+ raise RuntimeError(f"Server completion error ({current_model}): {err_msg}")
1659
1624
  if "finish_reason" in chunk and chunk["finish_reason"]:
1660
1625
  _stream_finish_reason = chunk["finish_reason"]
1661
1626
  elif not _stream_finish_reason:
@@ -2045,7 +2010,14 @@ class LLMTransportMixin:
2045
2010
  self.console.print(f"\n[red]HTTP 400 Error Details from {current_model}: {escape(err_msg)}[/red]")
2046
2011
  except Exception:
2047
2012
  from rich.markup import escape
2048
- self.console.print(f"\n[red]HTTP 400 Error Details from {current_model}: {escape(exc.response.text or 'Bad Request')}[/red]")
2013
+ if code == 404:
2014
+ _not_avail_msg = 'This model isn\'t currently available on UTIM in free plan please enter "/model" and select model'
2015
+ self.console.print(f"\n[bold yellow]{_not_avail_msg}[/bold yellow]\n")
2016
+ return {
2017
+ "role": "assistant",
2018
+ "content": _not_avail_msg,
2019
+ "tool_calls": None,
2020
+ }, False
2049
2021
  if code == 403:
2050
2022
  # Invalidate cached quota immediately to force fresh check
2051
2023
  global _QUOTA_CACHE
@@ -2069,13 +2041,15 @@ class LLMTransportMixin:
2069
2041
  break
2070
2042
  # Bonus exhausted (or plan-gated): auto-degrade to the free
2071
2043
  # default model instead of aborting the user's turn.
2072
- from utim_cli.constants import APINEX_MODEL_IDS
2044
+ from utim_cli.constants import APINEX_MODEL_IDS, ORCAROUTER_MODEL_IDS
2073
2045
  _free_fallback = DEFAULT_MODEL
2074
2046
  _is_cur_free_or_apx = (
2075
2047
  current_model == _free_fallback
2076
2048
  or current_model.endswith(":free")
2077
2049
  or current_model.startswith("free/")
2050
+ or current_model.startswith("orcarouter/")
2078
2051
  or current_model in APINEX_MODEL_IDS
2052
+ or current_model in ORCAROUTER_MODEL_IDS
2079
2053
  )
2080
2054
  if current_model != _free_fallback and not _is_cur_free_or_apx:
2081
2055
  self.console.print(f"\n[bold yellow]⚠ Model '{current_model}' needs bonus credits and your bonus balance is empty.[/bold yellow]")
@@ -2188,46 +2162,26 @@ class LLMTransportMixin:
2188
2162
  _paid_slug = current_model.rsplit(":free", 1)[0]
2189
2163
  _remaining = models_to_try[model_idx + 1:]
2190
2164
  if _paid_slug not in _remaining:
2191
- if not silent:
2192
- self.console.print(
2193
- f"\n[dim yellow]↪ Free model rate-limited — trying paid version '{_paid_slug}'…[/dim yellow]"
2194
- )
2195
2165
  # Insert paid slug as the very next model, then break
2196
2166
  models_to_try[model_idx + 1:model_idx + 1] = [_paid_slug]
2197
- else:
2198
- if not silent:
2199
- self.console.print(
2200
- f"\n[dim yellow]↪ Free model rate-limited — paid version already in queue, skipping…[/dim yellow]"
2201
- )
2202
2167
  break # inner attempt loop → outer loop picks up injected paid slug
2203
2168
  # If primary model is rate-limited and fallback models exist, race them
2204
2169
  if current_is_primary and fallback_list:
2205
- if not silent:
2206
- self.console.print(
2207
- f"\n[dim yellow]⏳ Model '{current_model}' is temporarily rate-limited by the provider — racing fallback models…[/dim yellow]\n"
2208
- )
2209
2170
  raise _ServerUnavailableError(f"Rate-limited on model '{current_model}'")
2210
2171
 
2211
- # Non-free model with no fallback: show message and return
2212
- rate_limit_display = (
2213
- f"⏳ **Model Rate Limited**: `{current_model}` is currently experiencing high demand and is temporarily rate-limited by the provider.\n\n"
2214
- f"- **What to do:** Please wait a moment before retrying, or switch to a different model with `/model` or `Ctrl+M`.\n"
2215
- f"- **Retry:** You can resend your prompt anytime."
2216
- )
2217
- if not silent:
2218
- self.console.print(
2219
- f"\n[bold yellow]⏳ Model '{current_model}' is temporarily rate-limited by the provider.[/bold yellow]"
2220
- )
2221
- self.console.print(
2222
- " [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n"
2223
- )
2224
- return {
2225
- "role": "assistant",
2226
- "content": rate_limit_display,
2227
- "tool_calls": None,
2228
- }, False
2229
- # When silent=True (free model flow), raise exception to continue to next model
2230
- raise _ServerUnavailableError(f"Rate-limited on model '{current_model}'")
2172
+ # Never display rate limit warnings to the user.
2173
+ # Seamlessly append the failover chain to models_to_try and continue trying models.
2174
+ _failover_chain = [
2175
+ "openrouter/free",
2176
+ "upstage/solar-pro4",
2177
+ "qwen/qwen3.7-flash",
2178
+ "deepseek/deepseek-v4-flash-0731",
2179
+ ]
2180
+ _remaining = models_to_try[model_idx + 1:]
2181
+ _to_inject = [m for m in _failover_chain if m not in _remaining and m != current_model]
2182
+ if _to_inject:
2183
+ models_to_try.extend(_to_inject)
2184
+ break # outer loop will immediately pick up next failover model
2231
2185
  if attempt < model_retries:
2232
2186
  delay = 3 * (attempt + 1)
2233
2187
  # Interruptible wait — so Ctrl+C can abort immediately
@@ -659,6 +659,13 @@ class ToolExecutionMixin:
659
659
  self.console.print(bl)
660
660
  rendered_text = capture.get()
661
661
  if print_to_console and getattr(self, "_subagent_depth", 0) == 0:
662
+ try:
663
+ from utim_cli.state import STATE
664
+ if STATE.get("dialog_active"):
665
+ self.console.print(rendered_text, end="", markup=False, highlight=False)
666
+ return rendered_text
667
+ except Exception:
668
+ pass
662
669
  if getattr(self.console, "file", None) is not sys.stdout and getattr(self.console, "file", None) is not sys.__stdout__:
663
670
  # IMPORTANT: rendered_text is a *plain string* that was already
664
671
  # produced by Rich (via capture.get()). It can legitimately
@@ -796,9 +803,20 @@ class ToolExecutionMixin:
796
803
  v_strip = v.strip()
797
804
  v_norm = v_strip.replace("\\", "/")
798
805
 
806
+ # 0. Prioritize workspace .utim_tmp and plans normalization so model paths like
807
+ # /.utim/_tmp/plans, /.utim.tmp/plans, etc. resolve to workspace .utim_tmp, NOT ~/.utim
808
+ import re as _re_path
809
+ if _re_path.search(
810
+ r'(?:^|[/\\])\.utim(?:[._/]tmp|[/\\]_tmp|[/\\]tmp|[._/]plans|[/\\]plans)($|[/\\])',
811
+ v_norm,
812
+ _re_path.IGNORECASE
813
+ ):
814
+ from utim_cli.tools import resolve_project_path
815
+ arguments[k] = resolve_project_path(v_strip)
816
+ continue
817
+
799
818
  # 1. First prioritize dynamic .utim mapping for all users across any virtual prefix
800
819
  # NOTE: use path-component regex to avoid matching .utim_tmp (which contains .utim as substring)
801
- import re as _re_path
802
820
  if _re_path.search(r'(^|[/\\])\.utim($|[/\\])', v_norm):
803
821
  from utim_cli.config import get_utim_dir
804
822
  utim_dir = get_utim_dir()
@@ -1349,10 +1367,11 @@ class ToolExecutionMixin:
1349
1367
  ("run_command", "input_text"): "input",
1350
1368
  ("run_command", "enter"): "press_enter",
1351
1369
  ("run_command", "target_process_id"):"process_id",
1352
- ("run_command", "task_id"): "process_id",
1353
- # web_search: model sometimes uses 'prompt'
1354
- ("web_search", "prompt"): "query",
1355
- ("web_search", "text"): "query",
1370
+ # web_search: model sometimes uses 'query', 'search_query', 'q', or 'text' instead of 'prompt'
1371
+ ("web_search", "query"): "prompt",
1372
+ ("web_search", "search_query"): "prompt",
1373
+ ("web_search", "q"): "prompt",
1374
+ ("web_search", "text"): "prompt",
1356
1375
  }
1357
1376
  # Apply aliasing before filtering (don't overwrite existing correct keys)
1358
1377
  remapped = dict(call_args)