chainlesschain 0.162.77 → 0.162.79
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 +37 -1
- package/bin/chainlesschain.js +20 -1
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps--t6qElO3.js → AIOps-D89fD_7T.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-vRiifAww.js → ActionButton-d75flwX8.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BfSlGprA.js → Analytics-gvvBEb4T.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-B6L2I5ld.js → AppLayout-DzHOVS64.js} +4 -4
- package/src/assets/web-panel/assets/{Audit-Dwpk7vO2.js → Audit-15K7MhRC.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D7UuR3M8.js → Backup-BgVHsglI.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BzMDBTyZ.js → BaseInput-BRnvd4sF.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BxYUynGU.js → Chat-B2vtXu7s.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BYyCFqO0.js → ChatBubbleRenderer-d1EFM3dh.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DgpLcrjA.js → Checkbox-B5uV5Jhr.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-N5dSkUbq.js → Codegen-cVDMBVGV.js} +1 -1
- package/src/assets/web-panel/assets/{Col-C1nf7jzT.js → Col-DtL6q7Hw.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BuX21CEd.js → Community-DSyPZkqQ.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-M37IFQe_.js → Compact-CzfV_q4O.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-B_OBnXJx.js → Compliance-B_xhhLE1.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BcIn7Sb2.js → Cowork-Y49Qf0JX.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-BtmA9AMI.js → Cron-B7JG-oLj.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DlAiW2Iy.js → Crosschain-CukEkQtf.js} +1 -1
- package/src/assets/web-panel/assets/{DID-91Skenc5.js → DID-Bd4UVKdn.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BTzcKrw1.js → Dashboard-DRCQl7H1.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-VNCZqfkL.js → Dropdown-De47CuRY.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-B_o62liK.js → EmailListRenderer-C-wAkceB.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqceYpym.js → FamilyGuardDashboard-DhLF_E0k.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BJFcp8S0.js → Federation-DAipUS1m.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DwsPen2h.js → FormItemContext-BCFyAd2j.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BOU0nfJP.js → GenericCardRenderer-wMdxrEV9.js} +1 -1
- package/src/assets/web-panel/assets/{Git-C_92Ngor.js → Git-CcwP7HUN.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-SIBJc353.js → Governance-VF760JcB.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DcEwRh8C.js → Inference-DnR-GIn2.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-C4LQ8MJI.js → KnowledgeGraph-j-ewVKWO.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BXz2tihi.js → Logs-DgB7wSor.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-DqXKQJ2n.js → Marketplace-fiKjzVWE.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CXVGQSUd.js → McpTools-BjXSMQrd.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-Bp8huWkt.js → Memory-C0L5lnSS.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-B3duw6FB.js → MobileBridge-C0Rgg1Su.js} +3 -3
- package/src/assets/web-panel/assets/{MobileProjects-Cn5YO60O.js → MobileProjects-BPA50hQb.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CE_M_-1m.js → Mtc-BUsHuAjB.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Dev-y1Ei.js → MtcAudit-dlH8Q_7U.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-RILPE0-i.js → Multisig-L3_9beuW.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DsBeFM0w.js → NLProgramming-BG4sXy27.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-MONi8b1b.js → Notes-felgIvGS.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-mXNink9t.js → NotificationSettings-DHDT96AK.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Bvtv8CHQ.js → OrderTableRenderer-BYrkEfvR.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-_7rEqDWP.js → Organization-DdtOLkHG.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CuqybwI0.js → Overflow-DLw5Pni7.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BvYN2SXJ.js → P2P-B-mZ5EXz.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BWGnb4i0.js → PdhVaultBrowser-DeOmNCwK.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-D2vyDMm6.js → Permissions-BeQMX71K.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BWaT70CA.js → PersonalDataHub-D5PoqtQI.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-T_Pztk-K.js → Pipeline-B0f5sqKF.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BTChzeM8.js → Privacy-Dj_gcxio.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BQQBICYM.js → ProjectInit-DmfPgied.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BOLO_fhW.js → ProjectSettings-BmBfocrv.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BfqkBhzi.js → Projects-BUif48cc.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DpWEAYh2.js → Providers-B0ztEAOV.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-MX8kg-uO.js → QuickAsk-BtMG4jH5.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-UOCp-h2X.js → Recommend-DQKN1En8.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-H_vE2kEf.js → Reputation-D34CvUxg.js} +1 -1
- package/src/assets/web-panel/assets/{Row-dupAolkD.js → Row-nnuDNx31.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DVoerh51.js → RssFeed-DHx9x8_B.js} +2 -2
- package/src/assets/web-panel/assets/{Search-5zxC_iWb.js → Search-CH-JYtn_.js} +1 -1
- package/src/assets/web-panel/assets/{Security-vg3XxdEZ.js → Security-nNKBmzm4.js} +3 -3
- package/src/assets/web-panel/assets/{Services-CWjum0P4.js → Services-xYk_lDxy.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BkVnAX7_.js → Skeleton-DTYyRduA.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-2oMJr3sU.js → Skills-DS19-9sF.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Bv8uLcr_.js → Sla-Cr_ir42Y.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BfvawAGM.js → SpeechSettings-Ch5Iq7Wy.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DzWmsU8K.js → SyncSettings-CTblfa_E.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-cbtuyShK.js → Tasks-z7lz3hjG.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-PP2omXNk.js → Templates-DGoMFd4r.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Cab9qgCx.js → Tenant-DQUPvHxx.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-9kFA0Vf8.js → Terminal-Wt2wrbE6.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Cx8kncEg.js → TimelineRenderer-DGTc2UqL.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-D19NKMv_.js → Tokens-DPVnuARF.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DbRaTJsm.js → Trigger-DV5MPXC1.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-8EFY9ZrM.js → Trust-BBkMKqwS.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign--wxy-nr4.js → UkeySign-B_2E0qev.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BlRqqo2c.js → VideoEditing-CA-qKeVb.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-B2x_RfaK.js → Wallet-Ds4WURh-.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-DISokPYb.js → WebAuthn-BApiHjzz.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DkuLYA12.js → WorkflowEditor-DFzmAnzV.js} +1 -1
- package/src/assets/web-panel/assets/{chat-MMwBSP3l.js → chat-DZ6sHPit.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CPP3K6Jb.js → colors-DgbwhHtE.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-2ruYr7FB.js → compact-item-C4QVYSzd.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BPpiGxaR.js → createContext-yXIs4TwU.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-C-HfIeF7.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CtP3Uqy-.js → hasIn-CPtuUtBl.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpBaxHIL.js → index-4g6pGxnX.js} +1 -1
- package/src/assets/web-panel/assets/{index-9SEWDDhb.js → index-B91hax9S.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2N_-y_W.js → index-B9DUiQ24.js} +3 -3
- package/src/assets/web-panel/assets/index-BBgWatYO.js +1 -0
- package/src/assets/web-panel/assets/{index-2cESIQ4O.js → index-BCb7MYMS.js} +1 -1
- package/src/assets/web-panel/assets/{index-D05imMj-.js → index-BIAxMDe9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bq8gB01i.js → index-BJiGrT6V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM_RMGj5.js → index-BOKVSmKh.js} +1 -1
- package/src/assets/web-panel/assets/{index-COSm6DcE.js → index-BWJGry74.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bf1tNEoB.js → index-BaVYCMJa.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyCZ3fz1.js → index-Bd0JznCS.js} +1 -1
- package/src/assets/web-panel/assets/{index-oaNSovPm.js → index-BpBCKK6W.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSNoOIwN.js → index-BsXF9cn5.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIQO-prS.js → index-BxclR5gc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6qsWs-4.js → index-C0LyE6R6.js} +1 -1
- package/src/assets/web-panel/assets/{index-COPqJgcW.js → index-CBAgy5pf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BLAT0M1t.js → index-CJ550XXg.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7-Zo0uW.js → index-CTlz3MP6.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dc1ysHqq.js → index-CaC4gaQZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-6Sly6NDj.js → index-CnP2ftM5.js} +1 -1
- package/src/assets/web-panel/assets/index-CvCTol_u.js +1 -0
- package/src/assets/web-panel/assets/{index-Bjthf-eJ.js → index-D2KRfjEI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bfr1HIma.js → index-DA0ePxNn.js} +1 -1
- package/src/assets/web-panel/assets/{index-Byddazfj.js → index-DC5iP1VB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkqTFggQ.js → index-DCGmeXfl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM7y07U3.js → index-DHnWI0jj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZrah8Gb.js → index-DN4qpkKQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DS_ISOVj.js → index-DTLaRwKx.js} +1 -1
- package/src/assets/web-panel/assets/{index-DO_a65ut.js → index-DdFjcgqH.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dy85x86R.js → index-DopuoTzG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXGebJnR.js → index-DpDAGvyU.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUQp8MdV.js → index-EGVlUtH-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cru4qTvK.js → index-IFh9qCi0.js} +1 -1
- package/src/assets/web-panel/assets/{index-oD_rJWBp.js → index-UEeNpaDD.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzFVlsbg.js → index-ZCyyy3Zt.js} +1 -1
- package/src/assets/web-panel/assets/{index-h-zAHKNr.js → index-aKZjRwjv.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5g378QF.js → index-jBlY6-bF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZyr02ib.js → index-kbcTc6Pf.js} +1 -1
- package/src/assets/web-panel/assets/{index-P_R2pJ9q.js → index-x9bXmSNB.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BrJf63uk.js → initDefaultProps-BDsxioXk.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BD8UoaTG.js → motion-erZ2fiIQ.js} +1 -1
- package/src/assets/web-panel/assets/{move-DbunBKV-.js → move-BFiiw4WK.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CkGNCa-h.js → omit-BRMVd4pM.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DTvTrHC2.js → pickAttrs-CGjoFXsq.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Cb7oz9vL.js → placementArrow-DU_Bnf7w.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-O7lB6MAE.js → responsiveObserve-DPdkYupg.js} +1 -1
- package/src/assets/web-panel/assets/{slide-C24m1SKv.js → slide-JfHiG96y.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-D_-9GtZ3.js → statusUtils-Dy-1guyd.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CrAYF9jD.js → styleChecker-Chaljnux.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-CmgeoVPN.js → useFlexGapSupport-DOHIHBgY.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DzJSzGsy.js → useFs-BxSoQhIY.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-Bgqky5YK.js → usePersonalDataHub-MOR76PYB.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-ErTJLgr4.js → vnode-BBGIyeYD.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-wlf3cppM.js → zoom-C1oFENec.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/audit.js +4 -3
- package/src/commands/automation.js +6 -14
- package/src/commands/bi.js +10 -9
- package/src/commands/codegen.js +5 -13
- package/src/commands/dao.js +8 -6
- package/src/commands/dbevo.js +13 -14
- package/src/commands/economy.js +3 -2
- package/src/commands/evolution.js +3 -2
- package/src/commands/federation.js +4 -3
- package/src/commands/governance.js +9 -4
- package/src/commands/hardening.js +5 -4
- package/src/commands/incentive.js +6 -5
- package/src/commands/kg.js +17 -10
- package/src/commands/lowcode.js +23 -11
- package/src/commands/marketplace.js +4 -3
- package/src/commands/mcp.js +17 -5
- package/src/commands/ops.js +9 -4
- package/src/commands/recommend.js +7 -5
- package/src/commands/scim.js +3 -2
- package/src/commands/session.js +9 -6
- package/src/commands/social.js +4 -3
- package/src/commands/sync.js +3 -2
- package/src/commands/tenant.js +11 -6
- package/src/commands/zkp.js +8 -9
- package/src/gateways/ws/ws-agent-handler.js +12 -3
- package/src/gateways/ws/ws-server.js +6 -0
- package/src/harness/background-task-manager.js +44 -18
- package/src/harness/mcp-client.js +125 -46
- package/src/lib/agent-core.js +2 -1
- package/src/lib/chat-core.js +209 -107
- package/src/lib/claude-code-bridge.js +13 -1
- package/src/lib/dao-governance.js +3 -3
- package/src/lib/downloader.js +82 -25
- package/src/lib/headless-config-command.js +62 -0
- package/src/lib/json-schema-output.js +55 -11
- package/src/lib/mcp-oauth.js +110 -21
- package/src/lib/mcp-serve.js +70 -11
- package/src/lib/multisig-runtime.js +22 -3
- package/src/lib/parse-json-option.js +35 -0
- package/src/lib/parse-number-option.js +27 -0
- package/src/lib/runnable-provider.js +97 -0
- package/src/repl/agent-repl.js +76 -17
- package/src/repl/config-summary.js +66 -0
- package/src/runtime/agent-core.js +189 -36
- package/src/runtime/headless-runner.js +49 -1
- package/src/runtime/headless-stream.js +34 -0
- package/src/assets/web-panel/assets/devWarning-D7iybGpP.js +0 -1
- package/src/assets/web-panel/assets/index-BzJrOJ0f.js +0 -1
- package/src/assets/web-panel/assets/index-QN-iyhAl.js +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runnability guards for LLM model/provider selection.
|
|
3
|
+
*
|
|
4
|
+
* The task-based auto model-selector (lib/task-model-selector.js) switches the
|
|
5
|
+
* model by detected task type — e.g. a "quick" message on the `anthropic`
|
|
6
|
+
* provider becomes `claude-haiku`. That is WRONG when the target provider has
|
|
7
|
+
* no usable API key: it silently routes you onto a model you can't call and you
|
|
8
|
+
* get a 401. These helpers make selection "runnable-first": never switch onto a
|
|
9
|
+
* provider that has no usable (present) key, and let callers detect that the
|
|
10
|
+
* configured provider itself is not runnable.
|
|
11
|
+
*
|
|
12
|
+
* "Usable key" = a keyless local provider (ollama), OR the active session's
|
|
13
|
+
* configured key, OR the provider's standard env var (`<PROVIDER>_API_KEY`).
|
|
14
|
+
* Empty/whitespace keys never count (a missing or cleared key is not usable).
|
|
15
|
+
*/
|
|
16
|
+
import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
|
|
17
|
+
|
|
18
|
+
function nonEmpty(v) {
|
|
19
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Can we actually call `provider` right now?
|
|
24
|
+
*
|
|
25
|
+
* @param {string} provider
|
|
26
|
+
* @param {object} [opts]
|
|
27
|
+
* @param {string} [opts.apiKey] the session's configured key — only counts
|
|
28
|
+
* when `isActive` (it belongs to the ACTIVE
|
|
29
|
+
* provider, not an arbitrary one).
|
|
30
|
+
* @param {boolean} [opts.isActive=true]
|
|
31
|
+
* @param {object} [opts.env=process.env]
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function hasUsableKey(
|
|
35
|
+
provider,
|
|
36
|
+
{ apiKey, isActive = true, env = process.env } = {},
|
|
37
|
+
) {
|
|
38
|
+
const def = BUILT_IN_PROVIDERS[provider];
|
|
39
|
+
if (!def) return false;
|
|
40
|
+
if (!def.apiKeyEnv) return true; // keyless local provider (e.g. ollama)
|
|
41
|
+
if (isActive && nonEmpty(apiKey)) return true;
|
|
42
|
+
return nonEmpty(env[def.apiKeyEnv]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Gate the task-based auto model-switch on runnability. Returns the recommended
|
|
47
|
+
* model ONLY when the (active) provider is runnable; otherwise returns null so
|
|
48
|
+
* the caller keeps the user's configured model instead of switching onto
|
|
49
|
+
* something it can't call.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} args
|
|
52
|
+
* @param {string} args.provider
|
|
53
|
+
* @param {string} [args.currentModel]
|
|
54
|
+
* @param {string|null} [args.recommended] from selectModelForTask()
|
|
55
|
+
* @param {string} [args.apiKey]
|
|
56
|
+
* @param {object} [args.env=process.env]
|
|
57
|
+
* @returns {string|null} a model to switch to, or null to keep the current one
|
|
58
|
+
*/
|
|
59
|
+
export function runnableTaskModel({
|
|
60
|
+
provider,
|
|
61
|
+
currentModel,
|
|
62
|
+
recommended,
|
|
63
|
+
apiKey,
|
|
64
|
+
env = process.env,
|
|
65
|
+
} = {}) {
|
|
66
|
+
if (!recommended || recommended === currentModel) return null;
|
|
67
|
+
return hasUsableKey(provider, { apiKey, isActive: true, env })
|
|
68
|
+
? recommended
|
|
69
|
+
: null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Find a provider we can actually run, "runnable-first": keep `provider` when
|
|
74
|
+
* it has a usable key; otherwise fall back to the first built-in provider whose
|
|
75
|
+
* env key is set; otherwise the keyless local provider (ollama). Returns
|
|
76
|
+
* `{ provider, runnable, fellBackFrom?, keyless? }`. Pure given `env`.
|
|
77
|
+
*/
|
|
78
|
+
export function pickRunnableProvider({
|
|
79
|
+
provider,
|
|
80
|
+
apiKey,
|
|
81
|
+
env = process.env,
|
|
82
|
+
} = {}) {
|
|
83
|
+
if (provider && hasUsableKey(provider, { apiKey, isActive: true, env })) {
|
|
84
|
+
return { provider, runnable: true };
|
|
85
|
+
}
|
|
86
|
+
for (const [name, def] of Object.entries(BUILT_IN_PROVIDERS)) {
|
|
87
|
+
if (def.apiKeyEnv && nonEmpty(env[def.apiKeyEnv])) {
|
|
88
|
+
return { provider: name, runnable: true, fellBackFrom: provider || null };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
provider: "ollama",
|
|
93
|
+
runnable: true,
|
|
94
|
+
keyless: true,
|
|
95
|
+
fellBackFrom: provider || null,
|
|
96
|
+
};
|
|
97
|
+
}
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
detectTaskType,
|
|
53
53
|
selectModelForTask,
|
|
54
54
|
} from "../lib/task-model-selector.js";
|
|
55
|
+
import { runnableTaskModel, hasUsableKey } from "../lib/runnable-provider.js";
|
|
55
56
|
import { CLIPermanentMemory } from "../lib/permanent-memory.js";
|
|
56
57
|
import { CLIAutonomousAgent, GoalStatus } from "../lib/autonomous-agent.js";
|
|
57
58
|
import {
|
|
@@ -743,7 +744,9 @@ export async function startAgentRepl(options = {}) {
|
|
|
743
744
|
_bundleResolved.approvalPolicy.default,
|
|
744
745
|
);
|
|
745
746
|
// Mirror it so Shift+Tab cycling starts from the real tier.
|
|
746
|
-
const applied = parsePermissionTier(
|
|
747
|
+
const applied = parsePermissionTier(
|
|
748
|
+
_bundleResolved.approvalPolicy.default,
|
|
749
|
+
);
|
|
747
750
|
if (applied) _sessionTier = applied;
|
|
748
751
|
} catch (_err) {
|
|
749
752
|
// Non-critical — invalid policy value is silently ignored
|
|
@@ -1307,7 +1310,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
1307
1310
|
` ${chalk.cyan("/theme")} Color theme (/theme <auto|dark|light|mono>; mono = no color)`,
|
|
1308
1311
|
);
|
|
1309
1312
|
logger.log(
|
|
1310
|
-
` ${chalk.cyan("/config")}
|
|
1313
|
+
` ${chalk.cyan("/config")} Show config; ${chalk.cyan("/config <key>")} read, ${chalk.cyan("/config <key>=<val>")} set`,
|
|
1311
1314
|
);
|
|
1312
1315
|
logger.log(
|
|
1313
1316
|
` ${chalk.cyan("/doctor")} Session health check (provider/key/IDE/MCP/hooks)`,
|
|
@@ -2695,18 +2698,43 @@ export async function startAgentRepl(options = {}) {
|
|
|
2695
2698
|
|
|
2696
2699
|
// `/config` — effective configuration (secret-safe): the LLM provider/model
|
|
2697
2700
|
// in effect, whether keys are set, web-search backend, config path.
|
|
2698
|
-
|
|
2701
|
+
// `/config <key>` reads a value; `/config <key>=<value>` (Claude-Code
|
|
2702
|
+
// parity) or `/config <key> <value>` persists one. Secrets stay masked.
|
|
2703
|
+
if (trimmed === "/config" || trimmed.startsWith("/config ")) {
|
|
2699
2704
|
try {
|
|
2700
|
-
const
|
|
2705
|
+
const cm = await import("../lib/config-manager.js");
|
|
2701
2706
|
const { getConfigPath } = await import("../lib/paths.js");
|
|
2702
|
-
const {
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
)
|
|
2707
|
+
const {
|
|
2708
|
+
renderConfigSummary,
|
|
2709
|
+
parseConfigCommand,
|
|
2710
|
+
renderConfigGet,
|
|
2711
|
+
renderConfigSet,
|
|
2712
|
+
} = await import("./config-summary.js");
|
|
2713
|
+
const cmd = parseConfigCommand(trimmed.slice("/config".length));
|
|
2714
|
+
if (cmd.action === "error") {
|
|
2715
|
+
logger.error(chalk.red(`/config: ${cmd.message}`));
|
|
2716
|
+
} else if (cmd.action === "get") {
|
|
2717
|
+
logger.log(renderConfigGet(cmd.key, cm.getConfigValue(cmd.key)));
|
|
2718
|
+
} else if (cmd.action === "set") {
|
|
2719
|
+
cm.setConfigValue(cmd.key, cmd.value);
|
|
2720
|
+
logger.log(
|
|
2721
|
+
chalk.green("✓ ") +
|
|
2722
|
+
renderConfigSet(cmd.key, cm.getConfigValue(cmd.key)),
|
|
2723
|
+
);
|
|
2724
|
+
logger.log(
|
|
2725
|
+
chalk.gray(
|
|
2726
|
+
" (persisted; provider/model changes apply to new sessions)",
|
|
2727
|
+
),
|
|
2728
|
+
);
|
|
2729
|
+
} else {
|
|
2730
|
+
logger.log(
|
|
2731
|
+
renderConfigSummary(cm.loadConfig(), {
|
|
2732
|
+
path: getConfigPath(),
|
|
2733
|
+
activeProvider: provider,
|
|
2734
|
+
activeModel: _curModel || model,
|
|
2735
|
+
}),
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2710
2738
|
} catch (err) {
|
|
2711
2739
|
logger.error(chalk.red(`/config failed: ${err.message}`));
|
|
2712
2740
|
}
|
|
@@ -3080,16 +3108,35 @@ export async function startAgentRepl(options = {}) {
|
|
|
3080
3108
|
// Slot-filling failure is non-critical
|
|
3081
3109
|
}
|
|
3082
3110
|
|
|
3083
|
-
// Auto-select best model based on task type
|
|
3111
|
+
// Auto-select best model based on task type — but ONLY onto a runnable
|
|
3112
|
+
// provider. The selector maps e.g. "fast" → claude-haiku on anthropic; if
|
|
3113
|
+
// there's no usable key for the provider, never switch there (you'd just
|
|
3114
|
+
// get a 401). Runnable-first: keep the configured (working) model instead.
|
|
3084
3115
|
let activeModel = model;
|
|
3085
3116
|
const taskDetection = detectTaskType(promptText);
|
|
3086
3117
|
if (taskDetection.confidence > 0.3) {
|
|
3087
3118
|
const recommended = selectModelForTask(provider, taskDetection.taskType);
|
|
3088
|
-
|
|
3089
|
-
|
|
3119
|
+
const switchTo = runnableTaskModel({
|
|
3120
|
+
provider,
|
|
3121
|
+
currentModel: activeModel,
|
|
3122
|
+
recommended,
|
|
3123
|
+
apiKey,
|
|
3124
|
+
});
|
|
3125
|
+
if (switchTo) {
|
|
3126
|
+
activeModel = switchTo;
|
|
3090
3127
|
logger.info(
|
|
3091
3128
|
chalk.gray(`[auto] ${taskDetection.name} → ${activeModel}`),
|
|
3092
3129
|
);
|
|
3130
|
+
} else if (
|
|
3131
|
+
recommended &&
|
|
3132
|
+
recommended !== activeModel &&
|
|
3133
|
+
!hasUsableKey(provider, { apiKey })
|
|
3134
|
+
) {
|
|
3135
|
+
logger.info(
|
|
3136
|
+
chalk.gray(
|
|
3137
|
+
`[auto] ${taskDetection.name}: keeping ${activeModel} — no usable key for "${provider}" (skipping ${recommended})`,
|
|
3138
|
+
),
|
|
3139
|
+
);
|
|
3093
3140
|
}
|
|
3094
3141
|
}
|
|
3095
3142
|
|
|
@@ -3122,7 +3169,8 @@ export async function startAgentRepl(options = {}) {
|
|
|
3122
3169
|
? {
|
|
3123
3170
|
onToken: (t) => {
|
|
3124
3171
|
// Separate the answer from the dimmed reasoning above it (once).
|
|
3125
|
-
if (!_liveStreamed && _liveThinkStarted)
|
|
3172
|
+
if (!_liveStreamed && _liveThinkStarted)
|
|
3173
|
+
process.stdout.write("\n");
|
|
3126
3174
|
_liveStreamed = true;
|
|
3127
3175
|
process.stdout.write(t);
|
|
3128
3176
|
},
|
|
@@ -3143,6 +3191,16 @@ export async function startAgentRepl(options = {}) {
|
|
|
3143
3191
|
thinking: reasoning,
|
|
3144
3192
|
} = await agentLoop(messages, {
|
|
3145
3193
|
...liveOpts,
|
|
3194
|
+
// Visible auto-retry feedback (Claude-Code 2.1.181): when the model's
|
|
3195
|
+
// streaming call hits a transient connection drop and retries, tell the
|
|
3196
|
+
// user instead of leaving them staring at a silent pause. To stderr so
|
|
3197
|
+
// it never corrupts the streamed answer on stdout.
|
|
3198
|
+
onStreamRetry: (attempt) =>
|
|
3199
|
+
process.stderr.write(
|
|
3200
|
+
chalk.dim(
|
|
3201
|
+
` ⟳ connection dropped — retrying (attempt ${attempt})…\n`,
|
|
3202
|
+
),
|
|
3203
|
+
),
|
|
3146
3204
|
signal: _turnAbort.signal,
|
|
3147
3205
|
provider,
|
|
3148
3206
|
model: activeModel,
|
|
@@ -3242,7 +3300,8 @@ export async function startAgentRepl(options = {}) {
|
|
|
3242
3300
|
} else {
|
|
3243
3301
|
// Phase G #2 — route through StreamRouter so REPL / WS / future
|
|
3244
3302
|
// streaming providers share one StreamEvent protocol.
|
|
3245
|
-
const { streamAgentResponse } =
|
|
3303
|
+
const { streamAgentResponse } =
|
|
3304
|
+
await import("../lib/agent-stream.js");
|
|
3246
3305
|
process.stdout.write("\n");
|
|
3247
3306
|
const noStream = options.noStream === true;
|
|
3248
3307
|
const streamResult = await streamAgentResponse(effectiveResponse, {
|
|
@@ -17,6 +17,72 @@ export function maskSecret(v) {
|
|
|
17
17
|
return s.length <= 4 ? "set (hidden)" : `set (…${s.slice(-4)})`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A config key whose value must never be echoed in plaintext in the
|
|
22
|
+
* (shoulder-surfable) interactive REPL — apiKey / secret / token / password.
|
|
23
|
+
* Matched on the last dotted segment (e.g. `llm.apiKey`, `webSearch.apiKey`).
|
|
24
|
+
*/
|
|
25
|
+
const SECRET_KEY_RE = /(api[_-]?key|secret|token|password|passwd)$/i;
|
|
26
|
+
export function isSecretConfigKey(key) {
|
|
27
|
+
if (!key) return false;
|
|
28
|
+
const last = String(key).split(".").pop();
|
|
29
|
+
return SECRET_KEY_RE.test(last);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse the argument string after `/config` into an action.
|
|
34
|
+
* Mirrors Claude Code's `/config key=value` syntax, and also accepts the
|
|
35
|
+
* `/config key value` form. Returns one of:
|
|
36
|
+
* { action: "show" } // `/config`
|
|
37
|
+
* { action: "get", key } // `/config llm.model`
|
|
38
|
+
* { action: "set", key, value } // `/config llm.model=opus` | `/config llm.model opus`
|
|
39
|
+
* { action: "error", message }
|
|
40
|
+
* `value` is the raw string; the caller coerces (true/false/null/number) via
|
|
41
|
+
* config-manager's setConfigValue.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} argStr everything after the literal `/config`
|
|
44
|
+
*/
|
|
45
|
+
export function parseConfigCommand(argStr) {
|
|
46
|
+
const s = (argStr || "").trim();
|
|
47
|
+
if (s === "") return { action: "show" };
|
|
48
|
+
|
|
49
|
+
const eq = s.indexOf("=");
|
|
50
|
+
if (eq !== -1) {
|
|
51
|
+
const key = s.slice(0, eq).trim();
|
|
52
|
+
const value = s.slice(eq + 1).trim();
|
|
53
|
+
if (!key) return { action: "error", message: "missing key before '='" };
|
|
54
|
+
return { action: "set", key, value };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// `key value` — split on the first run of whitespace.
|
|
58
|
+
const m = s.match(/^(\S+)\s+([\s\S]+)$/);
|
|
59
|
+
if (m) return { action: "set", key: m[1], value: m[2].trim() };
|
|
60
|
+
|
|
61
|
+
// Bare token → read that key.
|
|
62
|
+
return { action: "get", key: s };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Render a single config value for `/config <key>`, masking secrets. */
|
|
66
|
+
export function renderConfigGet(key, value) {
|
|
67
|
+
if (value === undefined) return `${key} = (unset)`;
|
|
68
|
+
if (isSecretConfigKey(key)) return `${key} = ${maskSecret(value)}`;
|
|
69
|
+
const shown =
|
|
70
|
+
value !== null && typeof value === "object"
|
|
71
|
+
? JSON.stringify(value)
|
|
72
|
+
: String(value);
|
|
73
|
+
return `${key} = ${shown}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Render the confirmation line after `/config <key>=<value>`. */
|
|
77
|
+
export function renderConfigSet(key, storedValue) {
|
|
78
|
+
if (isSecretConfigKey(key)) return `set ${key} = ${maskSecret(storedValue)}`;
|
|
79
|
+
const shown =
|
|
80
|
+
storedValue !== null && typeof storedValue === "object"
|
|
81
|
+
? JSON.stringify(storedValue)
|
|
82
|
+
: String(storedValue);
|
|
83
|
+
return `set ${key} = ${shown}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
20
86
|
/**
|
|
21
87
|
* @param {object|null} config loaded config.json
|
|
22
88
|
* @param {object} [opts] { path, activeProvider, activeModel }
|
|
@@ -1207,6 +1207,37 @@ export async function executeTool(name, args, context = {}) {
|
|
|
1207
1207
|
return toolResult;
|
|
1208
1208
|
}
|
|
1209
1209
|
|
|
1210
|
+
/**
|
|
1211
|
+
* Write a file then verify the on-disk byte count matches the intended
|
|
1212
|
+
* content. Network drives and cloud-synced folders (OneDrive / Dropbox /
|
|
1213
|
+
* Google Drive) can silently truncate a write or leave a 0-byte file; without
|
|
1214
|
+
* this check the agent reports `success` on a corrupted write and moves on.
|
|
1215
|
+
* Parity with Claude-Code 2.1.181 ("Fixed Write/Edit producing 0-byte or
|
|
1216
|
+
* truncated files on network drives and cloud-synced folders").
|
|
1217
|
+
*
|
|
1218
|
+
* Returns `{ size }` (actual on-disk bytes) on success, or `{ error }`
|
|
1219
|
+
* describing the truncation so the caller surfaces it as a tool error instead
|
|
1220
|
+
* of a false success. `fsImpl` is injectable for unit tests.
|
|
1221
|
+
*/
|
|
1222
|
+
export function writeFileVerified(filePath, content, fsImpl = fs) {
|
|
1223
|
+
const expected = Buffer.byteLength(content, "utf8");
|
|
1224
|
+
fsImpl.writeFileSync(filePath, content, "utf8");
|
|
1225
|
+
let actual;
|
|
1226
|
+
try {
|
|
1227
|
+
actual = fsImpl.statSync(filePath).size;
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
return {
|
|
1230
|
+
error: `Write verification failed: cannot stat ${filePath} after writing (${err.message}). The file may be on an unreliable network or cloud-synced drive.`,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
if (actual !== expected) {
|
|
1234
|
+
return {
|
|
1235
|
+
error: `Write truncated: expected ${expected} bytes but only ${actual} reached disk for ${filePath}. A network drive or cloud-sync folder (OneDrive/Dropbox/Google Drive) may have interrupted the write — retry, or write to a local path.`,
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
return { size: actual };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1210
1241
|
/**
|
|
1211
1242
|
* Inner tool execution — no hooks, no plan-mode checks.
|
|
1212
1243
|
*/
|
|
@@ -1297,11 +1328,12 @@ async function executeToolInner(
|
|
|
1297
1328
|
if (!fs.existsSync(dir)) {
|
|
1298
1329
|
fs.mkdirSync(dir, { recursive: true });
|
|
1299
1330
|
}
|
|
1300
|
-
|
|
1331
|
+
const wrote = writeFileVerified(filePath, args.content);
|
|
1332
|
+
if (wrote.error) return attachDescriptor({ error: wrote.error });
|
|
1301
1333
|
return attachDescriptor({
|
|
1302
1334
|
success: true,
|
|
1303
1335
|
path: filePath,
|
|
1304
|
-
size:
|
|
1336
|
+
size: wrote.size,
|
|
1305
1337
|
});
|
|
1306
1338
|
}
|
|
1307
1339
|
|
|
@@ -1315,8 +1347,13 @@ async function executeToolInner(
|
|
|
1315
1347
|
return attachDescriptor({ error: "old_string not found in file" });
|
|
1316
1348
|
}
|
|
1317
1349
|
const newContent = content.replace(args.old_string, args.new_string);
|
|
1318
|
-
|
|
1319
|
-
return attachDescriptor({
|
|
1350
|
+
const wrote = writeFileVerified(filePath, newContent);
|
|
1351
|
+
if (wrote.error) return attachDescriptor({ error: wrote.error });
|
|
1352
|
+
return attachDescriptor({
|
|
1353
|
+
success: true,
|
|
1354
|
+
path: filePath,
|
|
1355
|
+
size: wrote.size,
|
|
1356
|
+
});
|
|
1320
1357
|
}
|
|
1321
1358
|
|
|
1322
1359
|
case "edit_file_hashed": {
|
|
@@ -1358,10 +1395,12 @@ async function executeToolInner(
|
|
|
1358
1395
|
...(snippet && { current_snippet: snippet }),
|
|
1359
1396
|
});
|
|
1360
1397
|
}
|
|
1361
|
-
|
|
1398
|
+
const wrote = writeFileVerified(filePath, result.content);
|
|
1399
|
+
if (wrote.error) return attachDescriptor({ error: wrote.error });
|
|
1362
1400
|
return attachDescriptor({
|
|
1363
1401
|
success: true,
|
|
1364
1402
|
path: filePath,
|
|
1403
|
+
size: wrote.size,
|
|
1365
1404
|
lineNumber: result.lineNumber,
|
|
1366
1405
|
previousContent: result.previousContent,
|
|
1367
1406
|
});
|
|
@@ -2566,11 +2605,15 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2566
2605
|
// same {message, usage} shape is returned, so the agent loop is unchanged.
|
|
2567
2606
|
// Without onToken we keep the cheaper single-shot non-streaming request.
|
|
2568
2607
|
if (typeof options.onToken === "function") {
|
|
2569
|
-
return await
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2608
|
+
return await _retryStreamingChat(
|
|
2609
|
+
() =>
|
|
2610
|
+
_chatOllamaStreaming(
|
|
2611
|
+
apiUrl,
|
|
2612
|
+
{ model, messages: ollamaMessages, tools },
|
|
2613
|
+
options.onToken,
|
|
2614
|
+
signal,
|
|
2615
|
+
),
|
|
2616
|
+
{ signal, onRetry: options.onStreamRetry },
|
|
2574
2617
|
);
|
|
2575
2618
|
}
|
|
2576
2619
|
const response = await fetch(apiUrl, {
|
|
@@ -2585,7 +2628,7 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2585
2628
|
}),
|
|
2586
2629
|
});
|
|
2587
2630
|
if (!response.ok) {
|
|
2588
|
-
throw new Error(
|
|
2631
|
+
throw new Error(formatProviderHttpError("ollama", response.status));
|
|
2589
2632
|
}
|
|
2590
2633
|
const data = await response.json();
|
|
2591
2634
|
if (data.prompt_eval_count || data.eval_count) {
|
|
@@ -2652,13 +2695,17 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2652
2695
|
// and forward text deltas live, assembling tool_use blocks back into the
|
|
2653
2696
|
// same {message, usage} shape the non-streaming path returns.
|
|
2654
2697
|
if (typeof options.onToken === "function") {
|
|
2655
|
-
return await
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2698
|
+
return await _retryStreamingChat(
|
|
2699
|
+
() =>
|
|
2700
|
+
_chatAnthropicStreaming(
|
|
2701
|
+
`${url}/messages`,
|
|
2702
|
+
{ ...body, stream: true },
|
|
2703
|
+
{ "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
2704
|
+
options.onToken,
|
|
2705
|
+
signal,
|
|
2706
|
+
options.onThinking,
|
|
2707
|
+
),
|
|
2708
|
+
{ signal, onRetry: options.onStreamRetry },
|
|
2662
2709
|
);
|
|
2663
2710
|
}
|
|
2664
2711
|
|
|
@@ -2674,7 +2721,7 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2674
2721
|
});
|
|
2675
2722
|
|
|
2676
2723
|
if (!response.ok) {
|
|
2677
|
-
throw new Error(
|
|
2724
|
+
throw new Error(formatProviderHttpError("anthropic", response.status));
|
|
2678
2725
|
}
|
|
2679
2726
|
|
|
2680
2727
|
const data = await response.json();
|
|
@@ -2736,19 +2783,23 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2736
2783
|
// stream the SSE response, forward content deltas live, and reassemble the
|
|
2737
2784
|
// delta-fragmented tool_calls into the standard {message, usage} shape.
|
|
2738
2785
|
if (typeof options.onToken === "function") {
|
|
2739
|
-
return await
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2786
|
+
return await _retryStreamingChat(
|
|
2787
|
+
() =>
|
|
2788
|
+
_chatOpenAIStreaming(
|
|
2789
|
+
`${url}/chat/completions`,
|
|
2790
|
+
{
|
|
2791
|
+
model: model || defaultModels[provider] || "gpt-4o-mini",
|
|
2792
|
+
messages,
|
|
2793
|
+
tools,
|
|
2794
|
+
stream: true,
|
|
2795
|
+
stream_options: { include_usage: true },
|
|
2796
|
+
},
|
|
2797
|
+
key,
|
|
2798
|
+
options.onToken,
|
|
2799
|
+
signal,
|
|
2800
|
+
provider,
|
|
2801
|
+
),
|
|
2802
|
+
{ signal, onRetry: options.onStreamRetry },
|
|
2752
2803
|
);
|
|
2753
2804
|
}
|
|
2754
2805
|
|
|
@@ -2767,7 +2818,7 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
2767
2818
|
});
|
|
2768
2819
|
|
|
2769
2820
|
if (!response.ok) {
|
|
2770
|
-
throw new Error(
|
|
2821
|
+
throw new Error(formatProviderHttpError(provider, response.status));
|
|
2771
2822
|
}
|
|
2772
2823
|
|
|
2773
2824
|
const data = await response.json();
|
|
@@ -2880,6 +2931,108 @@ export function _streamErrorDisposition(err, signal, partialText) {
|
|
|
2880
2931
|
return "rethrow";
|
|
2881
2932
|
}
|
|
2882
2933
|
|
|
2934
|
+
/**
|
|
2935
|
+
* Format a provider HTTP error with an actionable hint. 401/403 almost always
|
|
2936
|
+
* means a missing/invalid API key for the ACTIVE provider — and because the
|
|
2937
|
+
* provider is resolved from config, a surprise "anthropic 401" usually means
|
|
2938
|
+
* the effective provider differs from what the user configured. Name the
|
|
2939
|
+
* provider and point at the fix instead of dumping a bare status code. Pure +
|
|
2940
|
+
* exported for tests.
|
|
2941
|
+
*/
|
|
2942
|
+
export function formatProviderHttpError(provider, status) {
|
|
2943
|
+
const base = `${provider} API error: HTTP ${status}`;
|
|
2944
|
+
if (status === 401 || status === 403) {
|
|
2945
|
+
return (
|
|
2946
|
+
`${base} — authentication failed: the API key for provider "${provider}" ` +
|
|
2947
|
+
`is missing or invalid. Check "cc config get llm.provider" and ` +
|
|
2948
|
+
`"cc config get llm.apiKey" (or run Configure LLM). A surprise "${provider}" ` +
|
|
2949
|
+
`here usually means the effective provider differs from the one you configured.`
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
if (status === 429) return `${base} — rate limited; please retry shortly.`;
|
|
2953
|
+
return base;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
/**
|
|
2957
|
+
* Is this error from a streaming chat request a transient API CONNECTION drop
|
|
2958
|
+
* that is safe to retry? True only for genuine network failures (reset /
|
|
2959
|
+
* timeout / DNS / refused / socket hangup / undici "terminated" / "fetch
|
|
2960
|
+
* failed"). False for user aborts and for HTTP/status errors (a 4xx/auth/5xx is
|
|
2961
|
+
* the server's verdict carried in the message, not a dropped pipe — retrying a
|
|
2962
|
+
* connection that never dropped won't help and could double-bill).
|
|
2963
|
+
*
|
|
2964
|
+
* Safe to act on at the dispatch seam because any error that propagates OUT of
|
|
2965
|
+
* `_chat*Streaming` is either an abort or a drop with ZERO output already
|
|
2966
|
+
* streamed (partial-output drops are preserved internally and never throw) — so
|
|
2967
|
+
* a retry can never duplicate visible text. Pure + exported for tests.
|
|
2968
|
+
*/
|
|
2969
|
+
export function _isRetryableStreamError(err, signal) {
|
|
2970
|
+
if (isAbortError(err)) return false;
|
|
2971
|
+
if (signal && signal.aborted) return false;
|
|
2972
|
+
if (!err) return false;
|
|
2973
|
+
const code = String(err.code || err.cause?.code || "").toUpperCase();
|
|
2974
|
+
if (
|
|
2975
|
+
[
|
|
2976
|
+
"ECONNRESET",
|
|
2977
|
+
"ETIMEDOUT",
|
|
2978
|
+
"ECONNREFUSED",
|
|
2979
|
+
"EAI_AGAIN",
|
|
2980
|
+
"EPIPE",
|
|
2981
|
+
"ENETUNREACH",
|
|
2982
|
+
"ENOTFOUND",
|
|
2983
|
+
"UND_ERR_SOCKET",
|
|
2984
|
+
].includes(code)
|
|
2985
|
+
) {
|
|
2986
|
+
return true;
|
|
2987
|
+
}
|
|
2988
|
+
const msg = String(err.message || err).toLowerCase();
|
|
2989
|
+
return /econnreset|etimedout|econnrefused|eai_again|socket hang ?up|terminated|fetch failed|network error|timed?\s*out|premature close/.test(
|
|
2990
|
+
msg,
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
/** Bounded auto-retry for streaming connection drops (Claude-Code 2.1.181). */
|
|
2995
|
+
const STREAM_RETRY_MAX = 2;
|
|
2996
|
+
const STREAM_RETRY_BASE_MS = 400;
|
|
2997
|
+
|
|
2998
|
+
/**
|
|
2999
|
+
* Run a streaming chat attempt with bounded auto-retry on transient API
|
|
3000
|
+
* connection drops (Claude-Code 2.1.181: "auto-retry for API connection drops
|
|
3001
|
+
* during thinking"). Only connection-level failures are retried (see
|
|
3002
|
+
* `_isRetryableStreamError`); user aborts and HTTP/status errors surface
|
|
3003
|
+
* immediately. Backoff is exponential and abort-aware. Transparent to the
|
|
3004
|
+
* caller: on success returns the attempt's result; on exhaustion rethrows the
|
|
3005
|
+
* last error — strictly better than today (one drop → instant error).
|
|
3006
|
+
*
|
|
3007
|
+
* @param {() => Promise<any>} streamFn invokes one `_chat*Streaming` attempt
|
|
3008
|
+
* @param {object} opts { signal?, retries?, baseDelayMs?, onRetry?, sleep? }
|
|
3009
|
+
*/
|
|
3010
|
+
export async function _retryStreamingChat(streamFn, opts = {}) {
|
|
3011
|
+
const retries = opts.retries ?? STREAM_RETRY_MAX;
|
|
3012
|
+
const base = opts.baseDelayMs ?? STREAM_RETRY_BASE_MS;
|
|
3013
|
+
const signal = opts.signal;
|
|
3014
|
+
const sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
3015
|
+
let attempt = 0;
|
|
3016
|
+
for (;;) {
|
|
3017
|
+
try {
|
|
3018
|
+
return await streamFn();
|
|
3019
|
+
} catch (err) {
|
|
3020
|
+
if (attempt >= retries || !_isRetryableStreamError(err, signal))
|
|
3021
|
+
throw err;
|
|
3022
|
+
attempt++;
|
|
3023
|
+
if (typeof opts.onRetry === "function") {
|
|
3024
|
+
try {
|
|
3025
|
+
opts.onRetry(attempt, err);
|
|
3026
|
+
} catch {
|
|
3027
|
+
/* the retry notice is best-effort; never let it mask the retry */
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
await sleep(base * Math.pow(2, attempt - 1));
|
|
3031
|
+
if (signal && signal.aborted) throw err; // user bailed during backoff
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
|
|
2883
3036
|
/**
|
|
2884
3037
|
* Finalize a partial stream into the standard {message, usage} shape after a
|
|
2885
3038
|
* mid-stream connection drop: marks the message truncated and drops any
|
|
@@ -2902,7 +3055,7 @@ async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
|
|
|
2902
3055
|
body: JSON.stringify({ ...body, stream: true }),
|
|
2903
3056
|
});
|
|
2904
3057
|
if (!response.ok) {
|
|
2905
|
-
throw new Error(
|
|
3058
|
+
throw new Error(formatProviderHttpError("ollama", response.status));
|
|
2906
3059
|
}
|
|
2907
3060
|
const state = _ollamaInitState();
|
|
2908
3061
|
const reader = response.body.getReader();
|
|
@@ -3064,7 +3217,7 @@ async function _chatAnthropicStreaming(
|
|
|
3064
3217
|
body: JSON.stringify(body),
|
|
3065
3218
|
});
|
|
3066
3219
|
if (!response.ok) {
|
|
3067
|
-
throw new Error(
|
|
3220
|
+
throw new Error(formatProviderHttpError("anthropic", response.status));
|
|
3068
3221
|
}
|
|
3069
3222
|
const state = _anthropicInitState();
|
|
3070
3223
|
const reader = response.body.getReader();
|
|
@@ -3184,7 +3337,7 @@ async function _chatOpenAIStreaming(
|
|
|
3184
3337
|
body: JSON.stringify(body),
|
|
3185
3338
|
});
|
|
3186
3339
|
if (!response.ok) {
|
|
3187
|
-
throw new Error(
|
|
3340
|
+
throw new Error(formatProviderHttpError(provider, response.status));
|
|
3188
3341
|
}
|
|
3189
3342
|
const state = _openaiInitState();
|
|
3190
3343
|
const reader = response.body.getReader();
|