chainlesschain 0.166.108 → 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.
- package/README.md +14 -7
- package/package.json +3 -2
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-B5HWUFaS.js → AIOps-sOceeExN.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DCIKpxoQ.js → ActionButton-BQs-vRUr.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-dsgvjCEQ.js → Analytics-sfAHHMhA.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DWDH5Qbm.js → AppLayout-0gAvE0c8.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-BD8S8kzU.js → Artifacts-Dh9s2Qmd.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CDXv4Aoj.js → Audit-CITm26SU.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BLAQ5Uy0.js → BackgroundAgents-BiMbBOh0.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-moPac6tF.js → Backup-CNpnhI3h.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C_6nzsLU.js → BaseInput-BI_lltkp.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-cYuslkFG.js → Chat-BgfB88xY.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-BDP3fGoz.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-Qjb-XOX_.js → Checkbox-4xy9rS3j.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-IYRWXlMA.js → Codegen-D93h9ihS.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CsQ9jv8M.js → Col-BRf3STY-.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BbNmfTwe.js → Community-f187ziSL.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-SShDx-nr.js → Compact-D4hXWuo1.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CLWhKIJp.js → Compliance-C6cYKI6H.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-D4tK9_s3.js → Cowork-C1i1f8LD.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-D7GnfQ1z.js → Cron-BacCE4LA.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-TG86SH3X.js → Crosschain-DXztxOoH.js} +1 -1
- package/src/assets/web-panel/assets/{DID-6pue0NCO.js → DID-rpf4HN8x.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BwkJ29du.js → Dashboard-h4bgtH7L.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DEvpYDof.js → Dropdown-CyQnoHmu.js} +1 -1
- package/src/assets/web-panel/assets/EmailListRenderer-FCtp4AcB.js +1 -0
- package/src/assets/web-panel/assets/{EvolutionSettings-tpvPMM9W.js → EvolutionSettings-JbISHTWR.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWYFW75O.js → FamilyGuardDashboard-DCIQLyfK.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-gRIi96rM.js → Federation-DDbFpwja.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-B7Nr3YkV.js → FormItemContext-CXF0jtXb.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-b1BgGgLT.js → GenericCardRenderer-CUaNfAM0.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Dis1jjO7.js → Git-Bhoz2gDf.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-C4Yg99Am.js → Governance-FiiXHe8e.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-D4UQRbGu.js → Inference-brpTW2Q3.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Co_kLMAq.js → KnowledgeGraph-BCWP-f6k.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-D4BRp7bg.js → Logs-CxYcduoK.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-12vD1nC1.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-ys0j1vr9.js → Marketplace-DANdmW7l.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-B3ZvRZe_.js → McpTools-Bi_EKkb5.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-BOa0jbr-.js → Memory-DecoMbYw.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CWJRmUbj.js → MobileBridge-DrOhFKp0.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-rRwP16dN.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-CCcOk5m5.js → Mtc-SLPII1iw.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-vlrrScbQ.js → MtcAudit-DWoVORYy.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-C8K59S_8.js → Multisig-Dxpnmq-m.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-oTpFYVme.js → NLProgramming-kNTpNqCP.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BhqAJWiz.js → Notes-D1uXCfVd.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DCnV1CJM.js → NotificationSettings-D3Qt8Gkl.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CZGnsrVW.js → OrderTableRenderer-qvaah2-m.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DX1LuQlJ.js → Organization-EhRl4JoJ.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CbfKrFdT.js → Overflow-BTt5C_5W.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BEwYZuHL.js → P2P-CeR2jyb0.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-YNmWbwd7.js → PdhVaultBrowser-BzD3OHWq.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DyZ18LiA.js → Permissions-BOPyeRlz.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-B2J3aw2g.js → PersonalDataHub-DcmNNsfO.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-CXl2ZlhQ.js → Pipeline-DuL3kwWC.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-I5SDmgAp.js → Privacy-CkQRdlSU.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Crl9tXw3.js → ProjectInit-moE_vQ1P.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DCFtFlSK.js → ProjectSettings-PnTWi4NQ.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-Do5BQq1i.js → Projects-DZvbT3D7.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Cb7OWa3y.js → Providers-CkcrHYy9.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CJmhAU-s.js → QrScannerModal-nYNFGf_H.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Cu6lEWou.js → QuickAsk-53WNo7xT.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Crip3jLB.js → Recommend-CmYN_NaS.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-KxYY-2CB.js → RemoteSession-BuOdNEjA.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-D0vN-htI.js → Reputation-BgkheZou.js} +1 -1
- package/src/assets/web-panel/assets/{Row-wqzd_xCD.js → Row-DvA3klj1.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-8lsPfb0a.js → RssFeed-CgkIL3PG.js} +3 -3
- package/src/assets/web-panel/assets/{Search-C-e8qAeU.js → Search-Cv2twJdL.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CMM9WgFy.js → Security-XrD2UNU6.js} +3 -3
- package/src/assets/web-panel/assets/{Services-CZdxjN2B.js → Services-LujCntCK.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BI3XNk35.js → Skeleton-ZZCOsxPX.js} +1 -1
- package/src/assets/web-panel/assets/Skills-BEXGkOrU.js +1 -0
- package/src/assets/web-panel/assets/{Sla-HQruznKY.js → Sla-CIqmCgdD.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-Dl-MFq3w.js → SpeechSettings-DkVbwJox.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BU224vDm.js → SyncSettings-BVGgUs0p.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-C1lh68l5.js +1 -0
- package/src/assets/web-panel/assets/{Templates-Jb3TPz4Q.js → Templates-BG4zNAFx.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CQ-GTrfa.js → Tenant-t2o6i5fY.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BbMl3Rue.js → Terminal-CH8UDdVO.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CS_vstKF.js → TimelineRenderer-B5sYY1IO.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BaopVl1f.js → Tokens-D0y7QkG5.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DtnD4WUN.js → Trigger-BA2nKko0.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CsvG4Tmf.js → Trust-CrFKCQIs.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C1-A-cOV.js → UkeySign-B3x_-Zun.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CNgJPdSL.js → VideoEditing-TEwxbLH6.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CdF41501.js → Wallet-Larp5rrt.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-D-JmxD5g.js → WebAuthn-DfWbEIZe.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-C0XunR13.js → WorkflowEditor-8LKiKExY.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BBAZpD_G.js → chat-ByxYOvYW.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BxSxO5BX.js → colors-Dh4yAClh.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BoxL8pg5.js → compact-item-f5n2j1WZ.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-W5JAyZmY.js → createContext-DIprYuXL.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DRIge35Z.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-HpiKDPVC.js → hasIn-CvYeap2I.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9VxE1C-.js → index-3ROY-lYB.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdEcQ1rp.js → index-6VEWxOyC.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4uI7WCB.js → index-7eG1J-GT.js} +1 -1
- package/src/assets/web-panel/assets/{index-_38IJqoP.js → index-B0AXUhiJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7GIUWWF.js → index-BS_dpgzu.js} +1 -1
- package/src/assets/web-panel/assets/{index-ooP1pQCt.js → index-BaXSBxy-.js} +1 -1
- package/src/assets/web-panel/assets/{index-gvfIRZxJ.js → index-Bk8oWRk6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSJ0cAle.js → index-BlImvgb7.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5kqii-G.js → index-BoayvJ48.js} +1 -1
- package/src/assets/web-panel/assets/index-Bq24rTYx.js +1 -0
- package/src/assets/web-panel/assets/{index-B05_YRHZ.js → index-BsU46c7D.js} +1 -1
- package/src/assets/web-panel/assets/{index-_p4e3KfB.js → index-BvyFQF8I.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bwn6k_qw.js → index-C2RGykST.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtFAEG08.js → index-C5aH2Pj2.js} +1 -1
- package/src/assets/web-panel/assets/{index-CehxfRUR.js → index-CD0s5ESV.js} +1 -1
- package/src/assets/web-panel/assets/{index-JrMBRi62.js → index-CIZQtz8G.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRg3aOoj.js → index-CJjc8ObW.js} +1 -1
- package/src/assets/web-panel/assets/{index-V6VIJe2N.js → index-CTqJNul4.js} +1 -1
- package/src/assets/web-panel/assets/{index-p6rNtYud.js → index-CcvA1nae.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6m6B4Wy.js → index-CpR0zD38.js} +1 -1
- package/src/assets/web-panel/assets/{index-IO2I7nzu.js → index-CzabQZ7I.js} +1 -1
- package/src/assets/web-panel/assets/index-D2f18WsK.js +1 -0
- package/src/assets/web-panel/assets/{index-9caMEwQ-.js → index-DGyTzElU.js} +1 -1
- package/src/assets/web-panel/assets/{index-DinY7IQa.js → index-DI8jC7gQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuM9I1If.js → index-DUj-sL9U.js} +1 -1
- package/src/assets/web-panel/assets/{index-DkSnlgK_.js → index-DaZVtafQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CM_gdohF.js → index-DjvON8oh.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcmAiq_Y.js → index-DlJCDpeN.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaJk9U9V.js → index-DlTwZIeX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgMViZIY.js → index-DlULGx8u.js} +1 -1
- package/src/assets/web-panel/assets/{index-BA3WWQAT.js → index-DmcAL4hd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSyfxxQS.js → index-DshB3-Up.js} +3 -3
- package/src/assets/web-panel/assets/{index-BZAbaY64.js → index-Du435yzt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BW4PP5GN.js → index-bt-0zMmH.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlPvZgv8.js → index-c_CxUQl3.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ba4usRFM.js → index-fpWv4u3Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwidpDEE.js → index-hwbaD3Nk.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcyzZloJ.js → index-uDiVjYrS.js} +1 -1
- package/src/assets/web-panel/assets/{index-QzlEbtRL.js → index-xU0eHLIH.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Dfn6M05J.js → initDefaultProps-DRuE92W6.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BRRwFqGO.js → motion-6Sgma5Vd.js} +1 -1
- package/src/assets/web-panel/assets/{move-DpDPN6zR.js → move-BbQsH8sb.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BHlSE87F.js → mtc-parser-BdC4INal.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CEOri_L5.js → omit-DeC5m_bi.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DqICh-ad.js → pickAttrs-DuehpBws.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DQizFFiV.js → placementArrow-CJEhv2ij.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DmFHPdvC.js → responsiveObserve-Dyw3lDGr.js} +1 -1
- package/src/assets/web-panel/assets/{slide-s2s1XS3T.js → slide-BMaLXTbS.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-IwEIbBan.js → statusUtils-c1K_SdJH.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CIFLlbXH.js → styleChecker-CefBry8k.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-CFkN2Cgh.js → useFlexGapSupport-Dj2IsZpo.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BSY6O48s.js → useFs-RLfB3yqb.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-iJl3VnU0.js → usePersonalDataHub-DbRVH_NA.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-vu8ReMs7.js → vnode-DTn_QVJz.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CeSrYAHp.js → zoom-DuFR0E7i.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/agent.js +12 -5
- package/src/commands/ask.js +96 -21
- package/src/commands/complete.js +14 -10
- package/src/commands/llm.js +23 -41
- package/src/commands/session.js +2 -2
- package/src/commands/stream.js +9 -5
- package/src/commands/ui.js +30 -2
- package/src/data/changelog.json +52 -0
- package/src/gateways/ws/llm-chat-protocol.js +23 -5
- package/src/gateways/ws/llm-creds.js +38 -60
- package/src/gateways/ws/ws-agent-handler.js +1 -1
- package/src/gateways/ws/ws-server.js +47 -0
- package/src/gateways/ws/ws-session-gateway.js +26 -12
- package/src/lib/__tests__/provider-stream.test.js +70 -4
- package/src/lib/chat-core.js +186 -24
- package/src/lib/cowork-adapter.js +29 -15
- package/src/lib/evolution/agent-evolution-runtime-composition.js +10 -7
- package/src/lib/evolution/evolution-deployment-config.js +27 -4
- package/src/lib/evolution/evolution-deployment-loader.js +58 -3
- package/src/lib/evolution/evolution-deployment-profile.js +2 -2
- package/src/lib/evolution/evolution-evidence-projector.js +18 -3
- package/src/lib/evolution/evolution-file-witness.js +10 -0
- package/src/lib/evolution/evolution-ledger.js +237 -28
- package/src/lib/evolution/evolution-windows-directory-durability.js +322 -0
- package/src/lib/evolution/governed-hub-llm.js +2 -1
- package/src/lib/evolution/governed-model-turn.js +4 -1
- package/src/lib/evolution/local-ui-evolution-deployment.js +682 -0
- package/src/lib/evolution/local-ui-evolution-host.mjs +26 -0
- package/src/lib/evolution/local-ui-evolution-runtime.js +601 -0
- package/src/lib/evolution/opaque-hex-id.js +28 -0
- package/src/lib/hub-llm-client.js +22 -14
- package/src/lib/image-input.js +19 -3
- package/src/lib/llm-config-defaults.js +19 -30
- package/src/lib/llm-connection-config.js +4 -0
- package/src/lib/llm-connection-probe.js +5 -1
- package/src/lib/llm-connection-resolution.js +94 -0
- package/src/lib/llm-providers.js +33 -11
- package/src/lib/ollama-base-url.js +14 -0
- package/src/lib/provider-protocol.js +104 -0
- package/src/lib/provider-stream.js +163 -4
- package/src/lib/runnable-provider.js +1 -0
- package/src/repl/chat-repl.js +58 -11
- package/src/runtime/agent-core.js +26 -43
- package/src/runtime/headless-runner.js +11 -4
- package/src/runtime/headless-stream.js +10 -4
- package/src/runtime/policies/agent-policy.js +17 -11
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Ceh3MG6M.js +0 -1
- package/src/assets/web-panel/assets/EmailListRenderer-CQj7UlKJ.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-Dgy8arQI.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-7ty8vTpj.js +0 -1
- package/src/assets/web-panel/assets/Skills-DMlsh9It.js +0 -1
- package/src/assets/web-panel/assets/Tasks-DESg3yCs.js +0 -1
- package/src/assets/web-panel/assets/devWarning-BI5TZ7EX.js +0 -1
- package/src/assets/web-panel/assets/index-9-JLDK5n.js +0 -1
- package/src/assets/web-panel/assets/index-B7m8ioaQ.js +0 -1
package/src/lib/image-input.js
CHANGED
|
@@ -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
|
-
|
|
344
|
+
sameProvider
|
|
332
345
|
? llm.model
|
|
333
346
|
: undefined;
|
|
334
347
|
const visionModel = supportsVisionModel(mainModel)
|
|
335
348
|
? mainModel
|
|
336
|
-
: llm.visionModel ||
|
|
349
|
+
: (sameProvider ? llm.visionModel : null) ||
|
|
350
|
+
DEFAULT_VISION_MODELS[provider] ||
|
|
351
|
+
mainModel ||
|
|
352
|
+
DEFAULT_VISION_MODEL;
|
|
337
353
|
return {
|
|
338
|
-
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.
|
|
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
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
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)
|
|
111
|
-
|
|
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 =
|
|
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
|
+
}
|
package/src/lib/llm-providers.js
CHANGED
|
@@ -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(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
378
|
+
geminiGenerateContentUrl(provider.baseUrl, testModel),
|
|
367
379
|
{
|
|
368
380
|
method: "POST",
|
|
369
|
-
headers: {
|
|
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 =
|
|
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(
|
|
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
|
-
`${
|
|
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(
|
|
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:
|
|
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);
|