chainlesschain 0.166.109 → 0.166.112

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 (206) hide show
  1. package/README.md +6 -4
  2. package/package.json +3 -2
  3. package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
  4. package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
  5. package/src/assets/web-panel/assets/{AIOps-DkrNbDzS.js → AIOps-sOceeExN.js} +1 -1
  6. package/src/assets/web-panel/assets/{ActionButton-DJjFXDhv.js → ActionButton-BQs-vRUr.js} +1 -1
  7. package/src/assets/web-panel/assets/{Analytics-DyUbagRn.js → Analytics-sfAHHMhA.js} +2 -2
  8. package/src/assets/web-panel/assets/{AppLayout-p7kJWSvn.js → AppLayout-0gAvE0c8.js} +4 -4
  9. package/src/assets/web-panel/assets/{Artifacts-C3rVh4Ak.js → Artifacts-Dh9s2Qmd.js} +1 -1
  10. package/src/assets/web-panel/assets/{Audit-Bch_L_YO.js → Audit-CITm26SU.js} +1 -1
  11. package/src/assets/web-panel/assets/{BackgroundAgents-BNHy8zP6.js → BackgroundAgents-BiMbBOh0.js} +1 -1
  12. package/src/assets/web-panel/assets/{Backup-BUaye_Ys.js → Backup-CNpnhI3h.js} +1 -1
  13. package/src/assets/web-panel/assets/{BaseInput-D49mzAVz.js → BaseInput-BI_lltkp.js} +1 -1
  14. package/src/assets/web-panel/assets/{Chat-2MDzM_us.js → Chat-BgfB88xY.js} +5 -5
  15. package/src/assets/web-panel/assets/ChatBubbleRenderer-BDP3fGoz.js +1 -0
  16. package/src/assets/web-panel/assets/{Checkbox-5p1T_KEq.js → Checkbox-4xy9rS3j.js} +1 -1
  17. package/src/assets/web-panel/assets/{Codegen-TKtiLaoZ.js → Codegen-D93h9ihS.js} +1 -1
  18. package/src/assets/web-panel/assets/{Col-BVCMG9mS.js → Col-BRf3STY-.js} +1 -1
  19. package/src/assets/web-panel/assets/{Community-D09-QczH.js → Community-f187ziSL.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compact-QuRMCnj7.js → Compact-D4hXWuo1.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compliance-CN0PV7fk.js → Compliance-C6cYKI6H.js} +1 -1
  22. package/src/assets/web-panel/assets/{Cowork-5N2zhAty.js → Cowork-C1i1f8LD.js} +2 -2
  23. package/src/assets/web-panel/assets/{Cron-BxT8UZ0S.js → Cron-BacCE4LA.js} +2 -2
  24. package/src/assets/web-panel/assets/{Crosschain-Dp-noQl4.js → Crosschain-DXztxOoH.js} +1 -1
  25. package/src/assets/web-panel/assets/{DID-B7MZFkjG.js → DID-rpf4HN8x.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dashboard-DulJ3rKV.js → Dashboard-h4bgtH7L.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dropdown-DV4ov4zF.js → Dropdown-CyQnoHmu.js} +1 -1
  28. package/src/assets/web-panel/assets/{EmailListRenderer-DB2-ooz6.js → EmailListRenderer-FCtp4AcB.js} +1 -1
  29. package/src/assets/web-panel/assets/{EvolutionSettings-B1Rb6p7q.js → EvolutionSettings-JbISHTWR.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-U8aHayx-.js → FamilyGuardDashboard-DCIQLyfK.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-BkQULBHY.js → Federation-DDbFpwja.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-CtviG260.js → FormItemContext-CXF0jtXb.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-DnPirB24.js → GenericCardRenderer-CUaNfAM0.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-CK57Oro6.js → Git-Bhoz2gDf.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-DbI-LB8-.js → Governance-FiiXHe8e.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-sEG4T1Jg.js → Inference-brpTW2Q3.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-9SLJR3h9.js → KnowledgeGraph-BCWP-f6k.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-BIZYcISQ.js → Logs-CxYcduoK.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-12vD1nC1.js +1 -0
  40. package/src/assets/web-panel/assets/{Marketplace-CRmLLwAE.js → Marketplace-DANdmW7l.js} +1 -1
  41. package/src/assets/web-panel/assets/{McpTools-DkOFkSO1.js → McpTools-Bi_EKkb5.js} +3 -3
  42. package/src/assets/web-panel/assets/{Memory-M29Kd9TU.js → Memory-DecoMbYw.js} +2 -2
  43. package/src/assets/web-panel/assets/{MobileBridge-Tl6JvPw_.js → MobileBridge-DrOhFKp0.js} +1 -1
  44. package/src/assets/web-panel/assets/MobileProjects-rRwP16dN.js +1 -0
  45. package/src/assets/web-panel/assets/{Mtc-DkUjX1F5.js → Mtc-SLPII1iw.js} +2 -2
  46. package/src/assets/web-panel/assets/{MtcAudit-BCqr1I4q.js → MtcAudit-DWoVORYy.js} +2 -2
  47. package/src/assets/web-panel/assets/{Multisig-DmoqYGQ5.js → Multisig-Dxpnmq-m.js} +3 -3
  48. package/src/assets/web-panel/assets/{NLProgramming-DNRQeRke.js → NLProgramming-kNTpNqCP.js} +1 -1
  49. package/src/assets/web-panel/assets/{Notes-CQSBeiY6.js → Notes-D1uXCfVd.js} +3 -3
  50. package/src/assets/web-panel/assets/{NotificationSettings-gNY9bbH6.js → NotificationSettings-D3Qt8Gkl.js} +1 -1
  51. package/src/assets/web-panel/assets/{OrderTableRenderer-DW0CNjh8.js → OrderTableRenderer-qvaah2-m.js} +1 -1
  52. package/src/assets/web-panel/assets/{Organization-CthnbKag.js → Organization-EhRl4JoJ.js} +4 -4
  53. package/src/assets/web-panel/assets/{Overflow-_GQ3sbXu.js → Overflow-BTt5C_5W.js} +1 -1
  54. package/src/assets/web-panel/assets/{P2P-BTr7ea6W.js → P2P-CeR2jyb0.js} +2 -2
  55. package/src/assets/web-panel/assets/PdhVaultBrowser-BzD3OHWq.js +7 -0
  56. package/src/assets/web-panel/assets/{Permissions-DMny2Cjs.js → Permissions-BOPyeRlz.js} +3 -3
  57. package/src/assets/web-panel/assets/{PersonalDataHub-CyYlMSoI.js → PersonalDataHub-DcmNNsfO.js} +4 -4
  58. package/src/assets/web-panel/assets/{Pipeline-FASu2Oc4.js → Pipeline-DuL3kwWC.js} +1 -1
  59. package/src/assets/web-panel/assets/{Privacy-9bC5FnsJ.js → Privacy-CkQRdlSU.js} +1 -1
  60. package/src/assets/web-panel/assets/{ProjectInit-BOYZwB5G.js → ProjectInit-moE_vQ1P.js} +2 -2
  61. package/src/assets/web-panel/assets/{ProjectSettings-45DViNG-.js → ProjectSettings-PnTWi4NQ.js} +2 -2
  62. package/src/assets/web-panel/assets/{Projects-C2fZzMZc.js → Projects-DZvbT3D7.js} +1 -1
  63. package/src/assets/web-panel/assets/{Providers-BgjvZx9F.js → Providers-CkcrHYy9.js} +1 -1
  64. package/src/assets/web-panel/assets/{QrScannerModal-Brw-3csB.js → QrScannerModal-nYNFGf_H.js} +1 -1
  65. package/src/assets/web-panel/assets/{QuickAsk-DWG-A5zB.js → QuickAsk-53WNo7xT.js} +1 -1
  66. package/src/assets/web-panel/assets/{Recommend-DuQGxjIR.js → Recommend-CmYN_NaS.js} +1 -1
  67. package/src/assets/web-panel/assets/{RemoteSession-T1cdSSRL.js → RemoteSession-BuOdNEjA.js} +2 -2
  68. package/src/assets/web-panel/assets/{Reputation-9FT2dI4G.js → Reputation-BgkheZou.js} +1 -1
  69. package/src/assets/web-panel/assets/{Row-yNIbNftX.js → Row-DvA3klj1.js} +1 -1
  70. package/src/assets/web-panel/assets/{RssFeed-BIchNoPd.js → RssFeed-CgkIL3PG.js} +3 -3
  71. package/src/assets/web-panel/assets/{Search-msDJ8UTZ.js → Search-Cv2twJdL.js} +1 -1
  72. package/src/assets/web-panel/assets/{Security-DE46oBYT.js → Security-XrD2UNU6.js} +2 -2
  73. package/src/assets/web-panel/assets/{Services-Dori7vc3.js → Services-LujCntCK.js} +2 -2
  74. package/src/assets/web-panel/assets/{Skeleton-D3O3sUFz.js → Skeleton-ZZCOsxPX.js} +1 -1
  75. package/src/assets/web-panel/assets/{Skills-BV0ct8SO.js → Skills-BEXGkOrU.js} +1 -1
  76. package/src/assets/web-panel/assets/{Sla-DflIVOrh.js → Sla-CIqmCgdD.js} +1 -1
  77. package/src/assets/web-panel/assets/{SpeechSettings-Cpbv2Zz6.js → SpeechSettings-DkVbwJox.js} +1 -1
  78. package/src/assets/web-panel/assets/{SyncSettings-Dgg6-wbe.js → SyncSettings-BVGgUs0p.js} +2 -2
  79. package/src/assets/web-panel/assets/{Tasks-Cb6e4Klx.js → Tasks-C1lh68l5.js} +1 -1
  80. package/src/assets/web-panel/assets/{Templates-GfdAATGz.js → Templates-BG4zNAFx.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tenant-D0gecLXl.js → Tenant-t2o6i5fY.js} +1 -1
  82. package/src/assets/web-panel/assets/{Terminal-BFRPJt-0.js → Terminal-CH8UDdVO.js} +2 -2
  83. package/src/assets/web-panel/assets/{TimelineRenderer-BobtChxX.js → TimelineRenderer-B5sYY1IO.js} +1 -1
  84. package/src/assets/web-panel/assets/{Tokens-CifeZtHV.js → Tokens-D0y7QkG5.js} +1 -1
  85. package/src/assets/web-panel/assets/{Trigger-BHBvbBCf.js → Trigger-BA2nKko0.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trust-6lvqjKYp.js → Trust-CrFKCQIs.js} +1 -1
  87. package/src/assets/web-panel/assets/{UkeySign-CqzMc-G5.js → UkeySign-B3x_-Zun.js} +1 -1
  88. package/src/assets/web-panel/assets/{VideoEditing-OIiRG582.js → VideoEditing-TEwxbLH6.js} +1 -1
  89. package/src/assets/web-panel/assets/{Wallet-4MeKq0q_.js → Wallet-Larp5rrt.js} +2 -2
  90. package/src/assets/web-panel/assets/{WebAuthn-eHBRjGQG.js → WebAuthn-DfWbEIZe.js} +4 -4
  91. package/src/assets/web-panel/assets/{WorkflowEditor-DlzYdEqW.js → WorkflowEditor-8LKiKExY.js} +1 -1
  92. package/src/assets/web-panel/assets/{chat-CBcjOzkX.js → chat-ByxYOvYW.js} +1 -1
  93. package/src/assets/web-panel/assets/{colors-zgwsYQSa.js → colors-Dh4yAClh.js} +1 -1
  94. package/src/assets/web-panel/assets/{compact-item-Dcd9H5Bn.js → compact-item-f5n2j1WZ.js} +1 -1
  95. package/src/assets/web-panel/assets/{createContext-DR1tO-8r.js → createContext-DIprYuXL.js} +1 -1
  96. package/src/assets/web-panel/assets/devWarning-DRIge35Z.js +1 -0
  97. package/src/assets/web-panel/assets/{hasIn-CtfbUH_p.js → hasIn-CvYeap2I.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-D1GxtY-h.js → index-3ROY-lYB.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CT7zIkqP.js → index-6VEWxOyC.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Cdhy5ckh.js → index-7eG1J-GT.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DvPOpvvH.js → index-B0AXUhiJ.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cur8LNNn.js → index-BS_dpgzu.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DWG98orp.js → index-BaXSBxy-.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-C5XGvJb0.js → index-Bk8oWRk6.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DA2-Bhy8.js → index-BlImvgb7.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CwHgaYVH.js → index-BoayvJ48.js} +1 -1
  107. package/src/assets/web-panel/assets/index-Bq24rTYx.js +1 -0
  108. package/src/assets/web-panel/assets/{index-DHEiuvhA.js → index-BsU46c7D.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DSvcz0RO.js → index-BvyFQF8I.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-D7ZOiIKb.js → index-C2RGykST.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CmzQzTY5.js → index-C5aH2Pj2.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-oWOlF7CM.js → index-CD0s5ESV.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DVpp9YSQ.js → index-CIZQtz8G.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-DWuCGhHa.js → index-CJjc8ObW.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-JSfldpw4.js → index-CTqJNul4.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-dvF6C39V.js → index-CcvA1nae.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CmVIM-m2.js → index-CpR0zD38.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CaGNqkY0.js → index-CzabQZ7I.js} +1 -1
  119. package/src/assets/web-panel/assets/index-D2f18WsK.js +1 -0
  120. package/src/assets/web-panel/assets/{index-BXfGHUz_.js → index-DGyTzElU.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-LN7Hkl1C.js → index-DI8jC7gQ.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DfzOYmOj.js → index-DUj-sL9U.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BoxowMYS.js → index-DaZVtafQ.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CSQCHpDo.js → index-DjvON8oh.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-B3QUK1ks.js → index-DlJCDpeN.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-J-ORW1i6.js → index-DlTwZIeX.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-z8lrkcb-.js → index-DlULGx8u.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-hTcre7U9.js → index-DmcAL4hd.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DYPMJ5Mb.js → index-DshB3-Up.js} +3 -3
  130. package/src/assets/web-panel/assets/{index-5XaB_Tjg.js → index-Du435yzt.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-C0kP88Pb.js → index-bt-0zMmH.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D8WPohye.js → index-c_CxUQl3.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-DeAi4CNt.js → index-fpWv4u3Z.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-BndPtS9D.js → index-hwbaD3Nk.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-DTx4FyLU.js → index-uDiVjYrS.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-CPLLgPxA.js → index-xU0eHLIH.js} +1 -1
  137. package/src/assets/web-panel/assets/{initDefaultProps-CVMtDrWZ.js → initDefaultProps-DRuE92W6.js} +1 -1
  138. package/src/assets/web-panel/assets/{motion-Dye3IU8k.js → motion-6Sgma5Vd.js} +1 -1
  139. package/src/assets/web-panel/assets/{move-C251Zo34.js → move-BbQsH8sb.js} +1 -1
  140. package/src/assets/web-panel/assets/{mtc-parser-BDiOORoQ.js → mtc-parser-BdC4INal.js} +1 -1
  141. package/src/assets/web-panel/assets/{omit-DTDA_RHv.js → omit-DeC5m_bi.js} +1 -1
  142. package/src/assets/web-panel/assets/{pickAttrs-CqEUTPcT.js → pickAttrs-DuehpBws.js} +1 -1
  143. package/src/assets/web-panel/assets/{placementArrow-BOB8QWH3.js → placementArrow-CJEhv2ij.js} +1 -1
  144. package/src/assets/web-panel/assets/{responsiveObserve-CZDXP1d9.js → responsiveObserve-Dyw3lDGr.js} +1 -1
  145. package/src/assets/web-panel/assets/{slide-DPaRw3E9.js → slide-BMaLXTbS.js} +1 -1
  146. package/src/assets/web-panel/assets/{statusUtils-Ba9d7XZ7.js → statusUtils-c1K_SdJH.js} +1 -1
  147. package/src/assets/web-panel/assets/{styleChecker-mZKU3lvW.js → styleChecker-CefBry8k.js} +1 -1
  148. package/src/assets/web-panel/assets/{useFlexGapSupport-VcauLq0M.js → useFlexGapSupport-Dj2IsZpo.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFs-Cal9KRcx.js → useFs-RLfB3yqb.js} +1 -1
  150. package/src/assets/web-panel/assets/{usePersonalDataHub-DISoRyQB.js → usePersonalDataHub-DbRVH_NA.js} +1 -1
  151. package/src/assets/web-panel/assets/{vnode-B58C6w6B.js → vnode-DTn_QVJz.js} +1 -1
  152. package/src/assets/web-panel/assets/{zoom-BPlBAMKB.js → zoom-DuFR0E7i.js} +1 -1
  153. package/src/assets/web-panel/index.html +1 -1
  154. package/src/command-help-index.json +1 -1
  155. package/src/commands/agent.js +12 -5
  156. package/src/commands/ask.js +96 -21
  157. package/src/commands/complete.js +14 -10
  158. package/src/commands/llm.js +23 -41
  159. package/src/commands/session.js +2 -2
  160. package/src/commands/stream.js +9 -5
  161. package/src/commands/ui.js +30 -2
  162. package/src/data/changelog.json +39 -0
  163. package/src/gateways/ws/llm-chat-protocol.js +18 -10
  164. package/src/gateways/ws/llm-creds.js +29 -79
  165. package/src/gateways/ws/ws-agent-handler.js +1 -1
  166. package/src/gateways/ws/ws-session-gateway.js +26 -12
  167. package/src/lib/__tests__/provider-stream.test.js +70 -4
  168. package/src/lib/chat-core.js +186 -24
  169. package/src/lib/cowork-adapter.js +29 -15
  170. package/src/lib/evolution/agent-evolution-runtime-composition.js +10 -7
  171. package/src/lib/evolution/evolution-deployment-config.js +27 -4
  172. package/src/lib/evolution/evolution-deployment-loader.js +58 -3
  173. package/src/lib/evolution/evolution-deployment-profile.js +2 -2
  174. package/src/lib/evolution/evolution-evidence-projector.js +18 -3
  175. package/src/lib/evolution/evolution-file-witness.js +10 -0
  176. package/src/lib/evolution/evolution-ledger.js +237 -28
  177. package/src/lib/evolution/evolution-windows-directory-durability.js +322 -0
  178. package/src/lib/evolution/governed-hub-llm.js +2 -1
  179. package/src/lib/evolution/governed-model-turn.js +4 -1
  180. package/src/lib/evolution/local-ui-evolution-deployment.js +682 -0
  181. package/src/lib/evolution/local-ui-evolution-host.mjs +26 -0
  182. package/src/lib/evolution/local-ui-evolution-runtime.js +601 -0
  183. package/src/lib/evolution/opaque-hex-id.js +28 -0
  184. package/src/lib/hub-llm-client.js +22 -14
  185. package/src/lib/image-input.js +19 -3
  186. package/src/lib/llm-config-defaults.js +19 -30
  187. package/src/lib/llm-connection-config.js +4 -0
  188. package/src/lib/llm-connection-probe.js +5 -1
  189. package/src/lib/llm-connection-resolution.js +94 -0
  190. package/src/lib/llm-providers.js +33 -11
  191. package/src/lib/ollama-base-url.js +14 -0
  192. package/src/lib/provider-protocol.js +104 -0
  193. package/src/lib/provider-stream.js +163 -4
  194. package/src/lib/runnable-provider.js +1 -0
  195. package/src/repl/chat-repl.js +58 -11
  196. package/src/runtime/agent-core.js +26 -43
  197. package/src/runtime/headless-runner.js +11 -4
  198. package/src/runtime/headless-stream.js +10 -4
  199. package/src/runtime/policies/agent-policy.js +17 -11
  200. package/src/assets/web-panel/assets/ChatBubbleRenderer-DOX3Y5EJ.js +0 -1
  201. package/src/assets/web-panel/assets/MarkdownRenderer-9nfWupWP.js +0 -1
  202. package/src/assets/web-panel/assets/MobileProjects-Bq-FPDzS.js +0 -1
  203. package/src/assets/web-panel/assets/PdhVaultBrowser-D9McHF9I.js +0 -7
  204. package/src/assets/web-panel/assets/devWarning-DYhpRtve.js +0 -1
  205. package/src/assets/web-panel/assets/index-DPJXFIY7.js +0 -1
  206. package/src/assets/web-panel/assets/index-DjBhR5M_.js +0 -1
@@ -276,6 +276,17 @@ export function imageUrlBlockToAnthropic(block) {
276
276
  * to pin a different snapshot.
277
277
  */
278
278
  export const DEFAULT_VISION_MODEL = "doubao-seed-2-0-lite-260215";
279
+ const DEFAULT_VISION_MODELS = Object.freeze({
280
+ ollama: "qwen2.5vl:7b",
281
+ openai: "gpt-4o-mini",
282
+ anthropic: "claude-sonnet-4-6",
283
+ gemini: "gemini-2.5-flash",
284
+ volcengine: DEFAULT_VISION_MODEL,
285
+ dashscope: "qwen-vl-plus",
286
+ kimi: "kimi-k2.5",
287
+ minimax: "MiniMax-VL-01",
288
+ mistral: "pixtral-12b-2409",
289
+ });
279
290
 
280
291
  // Recognize image-input families, including dated snapshots and local tags.
281
292
  // Unknown models keep using the configured vision fallback. Do not infer
@@ -327,15 +338,20 @@ export function resolveVisionLlm({ hasImage, flags = {}, llm = {} } = {}) {
327
338
  apiKey: undefined,
328
339
  };
329
340
  }
341
+ const provider = flags.provider || llm.provider;
342
+ const sameProvider = !flags.provider || !llm.provider || flags.provider === llm.provider;
330
343
  const mainModel =
331
- !flags.provider || !llm.provider || flags.provider === llm.provider
344
+ sameProvider
332
345
  ? llm.model
333
346
  : undefined;
334
347
  const visionModel = supportsVisionModel(mainModel)
335
348
  ? mainModel
336
- : llm.visionModel || DEFAULT_VISION_MODEL;
349
+ : (sameProvider ? llm.visionModel : null) ||
350
+ DEFAULT_VISION_MODELS[provider] ||
351
+ mainModel ||
352
+ DEFAULT_VISION_MODEL;
337
353
  return {
338
- provider: flags.provider || llm.provider,
354
+ provider,
339
355
  model: flags.model || flags.visionModel || visionModel,
340
356
  baseUrl: flags.baseUrl || llm.baseUrl,
341
357
  apiKey: flags.apiKey || llm.apiKey,
@@ -56,26 +56,12 @@ export async function reconcileConfigLlmProvider(cfgLlm = {}) {
56
56
  return { llm: corrected, changed: true };
57
57
  }
58
58
 
59
- import { resolveApiKeyFromHelper } from "./api-key-helper.js";
60
59
  import { modelForeignToProvider } from "./runnable-provider.js";
61
-
62
- /**
63
- * Key backfill order: config llm.apiKey (plaintext, legacy) first, then
64
- * llm.apiKeyHelper (external command → OS credential store). The helper only
65
- * runs when no key is otherwise present, so configured setups never pay a
66
- * subprocess spawn.
67
- */
68
- function backfillApiKey(options, cfgLlm) {
69
- if (options.apiKey) return;
70
- if (cfgLlm.apiKey) {
71
- options.apiKey = cfgLlm.apiKey;
72
- } else if (cfgLlm.apiKeyHelper) {
73
- const key = resolveApiKeyFromHelper(cfgLlm.apiKeyHelper);
74
- if (key) options.apiKey = key;
75
- }
76
- }
60
+ import { resolveLlmConnection } from "./llm-connection-resolution.js";
77
61
 
78
62
  export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
63
+ const connection = resolveLlmConnection(options, [cfgLlm]);
64
+ const configMatchesTarget = connection.endpointSources.includes(cfgLlm);
79
65
  // The IDE chat panel pins --provider/--model (read from config) but DROPS
80
66
  // --base-url/--api-key. With an explicit --provider the block below bails, so
81
67
  // a cloud provider would lose its endpoint + key → a 401, or a silent
@@ -84,31 +70,34 @@ export function applyConfigLlmDefaults(options = {}, cfgLlm = {}, opts = {}) {
84
70
  // baseUrl/apiKey (and model) from config — it's the SAME provider, so its
85
71
  // credentials are exactly right. Mixing config into a DIFFERENT explicit
86
72
  // provider is still avoided (handled by the early return below).
87
- if (
88
- options.provider &&
89
- cfgLlm.provider &&
90
- options.provider === cfgLlm.provider
91
- ) {
92
- if (!options.baseUrl && cfgLlm.baseUrl) options.baseUrl = cfgLlm.baseUrl;
73
+ if (options.provider && configMatchesTarget) {
74
+ if (!options.baseUrl && cfgLlm.baseUrl) options.baseUrl = connection.baseUrl;
93
75
  if (options.serviceTier == null && cfgLlm.serviceTier != null)
94
76
  options.serviceTier = cfgLlm.serviceTier;
95
- backfillApiKey(options, cfgLlm);
77
+ if (!options.apiKey && connection.apiKey) options.apiKey = connection.apiKey;
96
78
  if (!options.model && !opts.explicitModel && cfgLlm.model) {
97
79
  options.model = cfgLlm.model;
98
80
  }
99
81
  return options;
100
82
  }
101
- if (options.provider || !cfgLlm.provider) return options;
102
- options.provider = cfgLlm.provider;
103
- if (options.serviceTier == null && cfgLlm.serviceTier != null)
83
+ if (options.provider) {
84
+ if (!options.apiKey && connection.apiKey) options.apiKey = connection.apiKey;
85
+ return options;
86
+ }
87
+ if (!cfgLlm.provider) return options;
88
+ options.provider = connection.provider;
89
+ if (configMatchesTarget && options.serviceTier == null && cfgLlm.serviceTier != null)
104
90
  options.serviceTier = cfgLlm.serviceTier;
105
91
  if (opts.explicitModel) {
106
92
  options.model = opts.explicitModel;
107
- } else if (cfgLlm.model) {
93
+ } else if (configMatchesTarget && cfgLlm.model) {
108
94
  options.model = cfgLlm.model;
95
+ } else if (!configMatchesTarget) {
96
+ options.model = connection.model;
109
97
  }
110
- if (!options.baseUrl && cfgLlm.baseUrl) options.baseUrl = cfgLlm.baseUrl;
111
- backfillApiKey(options, cfgLlm);
98
+ if (configMatchesTarget && !options.baseUrl && cfgLlm.baseUrl)
99
+ options.baseUrl = connection.baseUrl;
100
+ if (!options.apiKey && connection.apiKey) options.apiKey = connection.apiKey;
112
101
  return options;
113
102
  }
114
103
 
@@ -1,4 +1,5 @@
1
1
  import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
2
+ import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
2
3
  import {
3
4
  setSecretConfigValue,
4
5
  updateConfigAtomically,
@@ -65,6 +66,9 @@ export function normalizeLlmConnection(input) {
65
66
  "Use the API base path, not a completion/messages endpoint",
66
67
  );
67
68
  value.baseUrl = url.href.replace(/\/+$/, "");
69
+ if (value.provider === "ollama") {
70
+ value.baseUrl = normalizeOllamaBaseUrl(value.baseUrl);
71
+ }
68
72
  return value;
69
73
  }
70
74
 
@@ -2,6 +2,7 @@ import { formatProviderResponseError } from "./provider-http-error.js";
2
2
  import { estimateCost } from "./llm-pricing.js";
3
3
  import { providerFetch } from "./formal-budget-provider-scope.js";
4
4
  import { isFormalBudgetError } from "./formal-budget-errors.js";
5
+ import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
5
6
 
6
7
  // Reasoning models spend the same output allowance on thinking before the
7
8
  // final answer. A 16-token probe can therefore receive HTTP 200 with only
@@ -139,7 +140,10 @@ export async function probeLlmConnection(
139
140
  target,
140
141
  { fetchImpl = fetch, timeoutMs = 20_000, includeUsage = false } = {},
141
142
  ) {
142
- const base = target.baseUrl.replace(/\/+$/, "");
143
+ const base =
144
+ target.provider === "ollama"
145
+ ? normalizeOllamaBaseUrl(target.baseUrl)
146
+ : target.baseUrl.replace(/\/+$/, "");
143
147
  const headers = { "Content-Type": "application/json" };
144
148
  let url, body;
145
149
  if (target.provider === "ollama") {
@@ -0,0 +1,94 @@
1
+ import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
2
+ import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
3
+ import { normalizeProviderBaseUrl } from "./provider-protocol.js";
4
+ import { resolveApiKeyFromHelper } from "./api-key-helper.js";
5
+
6
+ const DEFAULT_PROVIDER = "ollama";
7
+ const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
8
+
9
+ function nonEmpty(value) {
10
+ return typeof value === "string" && value.trim().length > 0;
11
+ }
12
+
13
+ export function normalizeLlmEndpoint(provider, baseUrl) {
14
+ if (!nonEmpty(baseUrl)) return "";
15
+ return provider === "ollama"
16
+ ? normalizeOllamaBaseUrl(baseUrl)
17
+ : normalizeProviderBaseUrl(baseUrl);
18
+ }
19
+
20
+ /**
21
+ * Resolve provider, model, endpoint, and credential as one configuration.
22
+ * Saved models and credentials are reusable only for the same provider and
23
+ * normalized endpoint. An explicit key always belongs to the explicit target.
24
+ */
25
+ export function resolveLlmConnection(
26
+ options = {},
27
+ configuredSources = [],
28
+ {
29
+ env = process.env,
30
+ builtIns = BUILT_IN_PROVIDERS,
31
+ defaultProvider = DEFAULT_PROVIDER,
32
+ defaultModel = null,
33
+ defaultBaseUrl = null,
34
+ } = {},
35
+ ) {
36
+ const sources = (Array.isArray(configuredSources)
37
+ ? configuredSources
38
+ : [configuredSources]
39
+ ).filter((source) => source && typeof source === "object");
40
+ const provider =
41
+ (nonEmpty(options.provider) && options.provider) ||
42
+ sources.find((source) => nonEmpty(source.provider))?.provider ||
43
+ defaultProvider;
44
+ const definition = builtIns[provider];
45
+ const matchingSources = sources.filter(
46
+ (source) => source.provider === provider,
47
+ );
48
+ const providerBaseUrl =
49
+ definition?.baseUrl ||
50
+ defaultBaseUrl ||
51
+ (provider === "ollama" ? DEFAULT_OLLAMA_BASE_URL : null);
52
+ const baseUrl =
53
+ (nonEmpty(options.baseUrl) && options.baseUrl) ||
54
+ matchingSources.find((source) => nonEmpty(source.baseUrl))?.baseUrl ||
55
+ providerBaseUrl;
56
+ const normalizedBaseUrl = normalizeLlmEndpoint(provider, baseUrl);
57
+ const endpointSources = matchingSources.filter((source) => {
58
+ const sourceBaseUrl = source.baseUrl || providerBaseUrl;
59
+ return (
60
+ normalizedBaseUrl !== "" &&
61
+ normalizeLlmEndpoint(provider, sourceBaseUrl) === normalizedBaseUrl
62
+ );
63
+ });
64
+ const model =
65
+ (nonEmpty(options.model) && options.model) ||
66
+ endpointSources.find((source) => nonEmpty(source.model))?.model ||
67
+ defaultModel ||
68
+ definition?.models?.[0] ||
69
+ null;
70
+ let apiKey =
71
+ (nonEmpty(options.apiKey) && options.apiKey) ||
72
+ (nonEmpty(env?.CC_API_KEY) && env.CC_API_KEY) ||
73
+ endpointSources.find((source) => nonEmpty(source.apiKey))?.apiKey ||
74
+ null;
75
+ if (!apiKey) {
76
+ const helper = endpointSources.find((source) =>
77
+ nonEmpty(source.apiKeyHelper),
78
+ )?.apiKeyHelper;
79
+ if (helper) apiKey = resolveApiKeyFromHelper(helper);
80
+ }
81
+ if (!apiKey && definition?.apiKeyEnv) {
82
+ apiKey = nonEmpty(env?.[definition.apiKeyEnv])
83
+ ? env[definition.apiKeyEnv]
84
+ : null;
85
+ }
86
+ return {
87
+ provider,
88
+ model,
89
+ baseUrl: baseUrl || null,
90
+ apiKey,
91
+ endpoint: normalizedBaseUrl,
92
+ endpointSources,
93
+ };
94
+ }
@@ -7,6 +7,11 @@
7
7
  */
8
8
  import { providerFetch } from "./formal-budget-provider-scope.js";
9
9
  import { isFormalBudgetError } from "./formal-budget-errors.js";
10
+ import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
11
+ import {
12
+ geminiGenerateContentUrl,
13
+ normalizeProviderBaseUrl,
14
+ } from "./provider-protocol.js";
10
15
  export const BUILT_IN_PROVIDERS = {
11
16
  ollama: {
12
17
  name: "ollama",
@@ -345,11 +350,18 @@ export class LLMProviderRegistry {
345
350
  const testModel = model || provider.models[0];
346
351
 
347
352
  if (name === "ollama") {
348
- const res = await _fetchWithTimeout(`${provider.baseUrl}/api/generate`, {
349
- method: "POST",
350
- headers: { "Content-Type": "application/json" },
351
- body: JSON.stringify({ model: testModel, prompt: "Hi", stream: false }),
352
- });
353
+ const res = await _fetchWithTimeout(
354
+ `${normalizeOllamaBaseUrl(provider.baseUrl)}/api/generate`,
355
+ {
356
+ method: "POST",
357
+ headers: { "Content-Type": "application/json" },
358
+ body: JSON.stringify({
359
+ model: testModel,
360
+ prompt: "Hi",
361
+ stream: false,
362
+ }),
363
+ },
364
+ );
353
365
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
354
366
  const data = await res.json();
355
367
  return {
@@ -363,23 +375,32 @@ export class LLMProviderRegistry {
363
375
  const key = this.getApiKey(name);
364
376
  if (!key) throw new Error("GEMINI_API_KEY not set");
365
377
  const res = await _fetchWithTimeout(
366
- `${provider.baseUrl}/models/${testModel}:generateContent?key=${key}`,
378
+ geminiGenerateContentUrl(provider.baseUrl, testModel),
367
379
  {
368
380
  method: "POST",
369
- headers: { "Content-Type": "application/json" },
381
+ headers: {
382
+ "Content-Type": "application/json",
383
+ "x-goog-api-key": key,
384
+ },
370
385
  body: JSON.stringify({ contents: [{ parts: [{ text: "Hi" }] }] }),
371
386
  },
372
387
  );
373
388
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
374
389
  const data = await res.json();
375
- const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
390
+ const text =
391
+ data.candidates?.[0]?.content?.parts
392
+ ?.filter((part) => part?.thought !== true)
393
+ ?.map((part) => part?.text || "")
394
+ ?.join("") || "";
376
395
  return { ok: true, elapsed: Date.now() - start, response: text.trim() };
377
396
  }
378
397
 
379
398
  if (name === "anthropic") {
380
399
  const key = this.getApiKey(name);
381
400
  if (!key) throw new Error("ANTHROPIC_API_KEY not set");
382
- const res = await _fetchWithTimeout(`${provider.baseUrl}/messages`, {
401
+ const res = await _fetchWithTimeout(
402
+ `${normalizeProviderBaseUrl(provider.baseUrl)}/messages`,
403
+ {
383
404
  method: "POST",
384
405
  headers: {
385
406
  "Content-Type": "application/json",
@@ -391,7 +412,8 @@ export class LLMProviderRegistry {
391
412
  max_tokens: 10,
392
413
  messages: [{ role: "user", content: "Hi" }],
393
414
  }),
394
- });
415
+ },
416
+ );
395
417
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
396
418
  const data = await res.json();
397
419
  const text = data.content?.[0]?.text || "";
@@ -402,7 +424,7 @@ export class LLMProviderRegistry {
402
424
  const key = this.getApiKey(name);
403
425
  if (!key) throw new Error(`${provider.apiKeyEnv} not set`);
404
426
  const res = await _fetchWithTimeout(
405
- `${provider.baseUrl}/chat/completions`,
427
+ `${normalizeProviderBaseUrl(provider.baseUrl)}/chat/completions`,
406
428
  {
407
429
  method: "POST",
408
430
  headers: {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalize an Ollama server URL for native `/api/*` requests.
3
+ *
4
+ * Ollama's OpenAI-compatible examples commonly use a `/v1` base path, while
5
+ * this CLI provider speaks Ollama's native API. Accept both forms in saved
6
+ * settings and remove trailing OpenAI/native API suffixes before appending a
7
+ * native `/api/*` path.
8
+ */
9
+ export function normalizeOllamaBaseUrl(value) {
10
+ return String(value || "http://localhost:11434")
11
+ .trim()
12
+ .replace(/\/+$/, "")
13
+ .replace(/(?:\/(?:v1|api))+$/i, "");
14
+ }
@@ -0,0 +1,104 @@
1
+ /** Shared URL and request-shape helpers for provider-specific adapters. */
2
+
3
+ export function normalizeProviderBaseUrl(baseUrl) {
4
+ return String(baseUrl || "").replace(/\/+$/u, "");
5
+ }
6
+
7
+ /**
8
+ * Google's OpenAI-compatible API lives below `/v1beta/openai`. The provider
9
+ * registry stores the native API root (`/v1beta`), so adapt only that exact
10
+ * official root when a caller explicitly uses the OpenAI-compatible protocol.
11
+ */
12
+ export function openAICompatibleBaseUrl(provider, baseUrl) {
13
+ const base = normalizeProviderBaseUrl(baseUrl);
14
+ if (provider !== "gemini") return base;
15
+ try {
16
+ const url = new URL(base);
17
+ if (
18
+ url.hostname === "generativelanguage.googleapis.com" &&
19
+ url.pathname.replace(/\/+$/u, "") === "/v1beta"
20
+ ) {
21
+ url.pathname = "/v1beta/openai";
22
+ return normalizeProviderBaseUrl(url.href);
23
+ }
24
+ } catch {
25
+ // Keep custom compatibility endpoints intact; the request layer reports
26
+ // malformed URLs with the provider's normal transport error.
27
+ }
28
+ return base;
29
+ }
30
+
31
+ export function geminiGenerateContentUrl(baseUrl, model, { stream = false } = {}) {
32
+ const action = stream ? "streamGenerateContent" : "generateContent";
33
+ const endpoint = `${normalizeProviderBaseUrl(baseUrl)}/models/${encodeURIComponent(model)}:${action}`;
34
+ return stream ? `${endpoint}?alt=sse` : endpoint;
35
+ }
36
+
37
+ function geminiTextParts(content) {
38
+ if (typeof content === "string") return [{ text: content }];
39
+ if (!Array.isArray(content)) return [];
40
+ return content.flatMap((part) => {
41
+ if (typeof part === "string") return [{ text: part }];
42
+ if (part && typeof part.text === "string") return [{ text: part.text }];
43
+ return [];
44
+ });
45
+ }
46
+
47
+ export function geminiRequestBody(
48
+ messages,
49
+ { maxOutputTokens = 4096, temperature } = {},
50
+ ) {
51
+ const contents = [];
52
+ const systemParts = [];
53
+ for (const message of messages || []) {
54
+ const parts = geminiTextParts(message?.content);
55
+ if (parts.length === 0) continue;
56
+ if (message.role === "system") {
57
+ systemParts.push(...parts);
58
+ continue;
59
+ }
60
+ contents.push({
61
+ role: message.role === "assistant" || message.role === "model" ? "model" : "user",
62
+ parts,
63
+ });
64
+ }
65
+ const generationConfig = { maxOutputTokens };
66
+ if (Number.isFinite(temperature)) generationConfig.temperature = temperature;
67
+ return {
68
+ contents,
69
+ ...(systemParts.length ? { systemInstruction: { parts: systemParts } } : {}),
70
+ generationConfig,
71
+ };
72
+ }
73
+
74
+ export function geminiResponseText(data) {
75
+ return (data?.candidates?.[0]?.content?.parts || [])
76
+ .filter((part) => part?.thought !== true)
77
+ .map((part) => (typeof part?.text === "string" ? part.text : ""))
78
+ .join("");
79
+ }
80
+
81
+ export function geminiUsage(data) {
82
+ const usage = data?.usageMetadata;
83
+ if (!usage || typeof usage !== "object") return null;
84
+ const promptTokens = Number(usage.promptTokenCount);
85
+ const outputTokens =
86
+ Number(usage.candidatesTokenCount || 0) +
87
+ Number(usage.thoughtsTokenCount || 0);
88
+ const cacheReadTokens = Number(usage.cachedContentTokenCount || 0);
89
+ if (
90
+ !Number.isSafeInteger(promptTokens) ||
91
+ promptTokens < 0 ||
92
+ !Number.isSafeInteger(outputTokens) ||
93
+ outputTokens < 0 ||
94
+ !Number.isSafeInteger(cacheReadTokens) ||
95
+ cacheReadTokens < 0 ||
96
+ cacheReadTokens > promptTokens
97
+ )
98
+ return null;
99
+ return {
100
+ inputTokens: promptTokens - cacheReadTokens,
101
+ outputTokens,
102
+ cacheReadTokens,
103
+ };
104
+ }
@@ -9,6 +9,13 @@ import {
9
9
  */
10
10
 
11
11
  import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
12
+ import { normalizeOllamaBaseUrl } from "./ollama-base-url.js";
13
+ import {
14
+ geminiGenerateContentUrl,
15
+ geminiRequestBody,
16
+ normalizeProviderBaseUrl,
17
+ openAICompatibleBaseUrl,
18
+ } from "./provider-protocol.js";
12
19
 
13
20
  async function* responseLines(body) {
14
21
  const reader = body.getReader();
@@ -51,8 +58,9 @@ export async function* ollamaTokenStream({
51
58
  signal,
52
59
  requireCompletion = false,
53
60
  }) {
61
+ const normalizedBaseUrl = normalizeOllamaBaseUrl(baseUrl);
54
62
  const res = await providerFetch(
55
- `${baseUrl}/api/${messages ? "chat" : "generate"}`,
63
+ `${normalizedBaseUrl}/api/${messages ? "chat" : "generate"}`,
56
64
  {
57
65
  method: "POST",
58
66
  headers: { "Content-Type": "application/json" },
@@ -96,7 +104,9 @@ export async function* openAIStream({
96
104
  requireCompletion = false,
97
105
  }) {
98
106
  const governed = hasFormalBudgetProviderScope();
99
- const res = await providerFetch(`${baseUrl}/chat/completions`, {
107
+ const res = await providerFetch(
108
+ `${normalizeProviderBaseUrl(baseUrl)}/chat/completions`,
109
+ {
100
110
  method: "POST",
101
111
  headers: {
102
112
  "Content-Type": "application/json",
@@ -109,7 +119,8 @@ export async function* openAIStream({
109
119
  ...(governed ? { stream_options: { include_usage: true } } : {}),
110
120
  }),
111
121
  signal,
112
- });
122
+ },
123
+ );
113
124
  if (!res.ok || !res.body) {
114
125
  throw new Error(`${res.status} ${res.statusText}`);
115
126
  }
@@ -139,6 +150,126 @@ export async function* openAIStream({
139
150
  throw new Error("OpenAI stream ended before its completion marker");
140
151
  }
141
152
 
153
+ export async function* geminiTokenStream({
154
+ baseUrl,
155
+ apiKey,
156
+ model,
157
+ prompt,
158
+ messages,
159
+ signal,
160
+ requireCompletion = false,
161
+ }) {
162
+ const requestMessages = messages ?? [{ role: "user", content: prompt }];
163
+ const res = await providerFetch(
164
+ geminiGenerateContentUrl(baseUrl, model, { stream: true }),
165
+ {
166
+ method: "POST",
167
+ headers: {
168
+ "Content-Type": "application/json",
169
+ "x-goog-api-key": apiKey,
170
+ },
171
+ body: JSON.stringify(geminiRequestBody(requestMessages)),
172
+ signal,
173
+ },
174
+ );
175
+ if (!res.ok || !res.body) {
176
+ throw new Error(`Gemini ${res.status} ${res.statusText}`);
177
+ }
178
+
179
+ let completed = false;
180
+ for await (const raw of responseLines(res.body)) {
181
+ const line = raw.trim();
182
+ if (!line.startsWith("data:")) continue;
183
+ const payload = line.slice(5).trim();
184
+ if (!payload || payload === "[DONE]") continue;
185
+ try {
186
+ const data = JSON.parse(payload);
187
+ if (requireCompletion && data.error)
188
+ throw new Error("Gemini stream returned an error");
189
+ const candidate = data.candidates?.[0];
190
+ if (candidate?.finishReason || data.promptFeedback?.blockReason)
191
+ completed = true;
192
+ for (const part of candidate?.content?.parts || []) {
193
+ if (part?.thought !== true && typeof part?.text === "string")
194
+ yield part.text;
195
+ }
196
+ } catch (error) {
197
+ if (requireCompletion) throw error;
198
+ /* skip malformed chunks when completion isn't enforced */
199
+ }
200
+ }
201
+ if (requireCompletion && !completed)
202
+ throw new Error("Gemini stream ended before its completion marker");
203
+ }
204
+
205
+ export async function* anthropicTokenStream({
206
+ baseUrl,
207
+ apiKey,
208
+ model,
209
+ prompt,
210
+ messages,
211
+ signal,
212
+ requireCompletion = false,
213
+ }) {
214
+ const requestMessages = messages ?? [{ role: "user", content: prompt }];
215
+ const system = requestMessages
216
+ .filter((message) => message.role === "system")
217
+ .map((message) => message.content)
218
+ .join("\n\n");
219
+ const turns = requestMessages
220
+ .filter((message) => message.role !== "system")
221
+ .map((message) => ({
222
+ role: message.role === "assistant" ? "assistant" : "user",
223
+ content: message.content,
224
+ }));
225
+ const res = await providerFetch(
226
+ `${normalizeProviderBaseUrl(baseUrl)}/messages`,
227
+ {
228
+ method: "POST",
229
+ headers: {
230
+ "Content-Type": "application/json",
231
+ "x-api-key": apiKey,
232
+ "anthropic-version": "2023-06-01",
233
+ },
234
+ body: JSON.stringify({
235
+ model,
236
+ max_tokens: 4096,
237
+ stream: true,
238
+ ...(system ? { system } : {}),
239
+ messages: turns,
240
+ }),
241
+ signal,
242
+ },
243
+ );
244
+ if (!res.ok || !res.body) {
245
+ throw new Error(`Anthropic ${res.status} ${res.statusText}`);
246
+ }
247
+
248
+ let completed = false;
249
+ for await (const raw of responseLines(res.body)) {
250
+ const line = raw.trim();
251
+ if (!line.startsWith("data:")) continue;
252
+ const payload = line.slice(5).trim();
253
+ if (!payload) continue;
254
+ try {
255
+ const data = JSON.parse(payload);
256
+ if (requireCompletion && (data.error || data.type === "error"))
257
+ throw new Error("Anthropic stream returned an error");
258
+ if (data.type === "message_stop") completed = true;
259
+ if (
260
+ data.type === "content_block_delta" &&
261
+ typeof data.delta?.text === "string"
262
+ )
263
+ yield data.delta.text;
264
+ } catch (error) {
265
+ if (requireCompletion) throw error;
266
+ /* skip malformed chunks when completion isn't enforced */
267
+ }
268
+ }
269
+ if (requireCompletion && !completed)
270
+ throw new Error("Anthropic stream ended before its completion marker");
271
+ }
272
+
142
273
  /**
143
274
  * Build an AsyncIterable<string> token stream for the given provider.
144
275
  * Throws on unsupported provider / missing API key.
@@ -172,8 +303,36 @@ export function buildProviderSource(provider, opts = {}) {
172
303
  `API key required for ${provider} (--api-key or ${def.apiKeyEnv})`,
173
304
  );
174
305
  }
306
+ if (provider === "anthropic") {
307
+ return anthropicTokenStream({
308
+ baseUrl: baseUrl || def.baseUrl,
309
+ apiKey: finalKey,
310
+ model: model || def.models[0],
311
+ prompt,
312
+ messages,
313
+ signal,
314
+ requireCompletion,
315
+ });
316
+ }
317
+ if (provider === "gemini") {
318
+ return geminiTokenStream({
319
+ baseUrl:
320
+ baseUrl ||
321
+ def.baseUrl ||
322
+ "https://generativelanguage.googleapis.com/v1beta",
323
+ apiKey: finalKey,
324
+ model: model || def.models[0],
325
+ prompt,
326
+ messages,
327
+ signal,
328
+ requireCompletion,
329
+ });
330
+ }
175
331
  return openAIStream({
176
- baseUrl: baseUrl || def.baseUrl,
332
+ baseUrl: openAICompatibleBaseUrl(
333
+ provider,
334
+ normalizeProviderBaseUrl(baseUrl || def.baseUrl),
335
+ ),
177
336
  apiKey: finalKey,
178
337
  model: model || def.models[0],
179
338
  prompt,
@@ -198,6 +198,7 @@ export function makeRunnableProviderFallback(
198
198
  // swap ONLY the model to the provider's own default so the call succeeds.
199
199
  if (
200
200
  nonEmpty(options.provider) &&
201
+ inferProviderFromBaseUrl(options.baseUrl) === options.provider &&
201
202
  modelForeignToProvider(options.provider, options.model)
202
203
  ) {
203
204
  const def = defaultModelFor(options.provider);