open-multi-agent-kit 0.99.0 → 1.0.0

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 (271) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +3 -3
  3. package/dist/bun/cli.d.ts.map +1 -1
  4. package/dist/bun/cli.js +1 -0
  5. package/dist/bun/cli.js.map +1 -1
  6. package/dist/bun/register-bundled-coding-agent.d.ts +10 -0
  7. package/dist/bun/register-bundled-coding-agent.d.ts.map +1 -0
  8. package/dist/bun/register-bundled-coding-agent.js +12 -0
  9. package/dist/bun/register-bundled-coding-agent.js.map +1 -0
  10. package/dist/cli/args.d.ts +1 -1
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +1 -1
  13. package/dist/cli/args.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +1 -1
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +14 -2
  19. package/dist/cli.js.map +1 -1
  20. package/dist/commands/neo-cli.d.ts +9 -0
  21. package/dist/commands/neo-cli.d.ts.map +1 -0
  22. package/dist/commands/neo-cli.js +61 -0
  23. package/dist/commands/neo-cli.js.map +1 -0
  24. package/dist/core/agent-session.d.ts +29 -2
  25. package/dist/core/agent-session.d.ts.map +1 -1
  26. package/dist/core/agent-session.js +107 -30
  27. package/dist/core/agent-session.js.map +1 -1
  28. package/dist/core/bundled-skills.d.ts +4 -0
  29. package/dist/core/bundled-skills.d.ts.map +1 -0
  30. package/dist/core/bundled-skills.js +32 -0
  31. package/dist/core/bundled-skills.js.map +1 -0
  32. package/dist/core/cli-diagnostics.d.ts +6 -0
  33. package/dist/core/cli-diagnostics.d.ts.map +1 -0
  34. package/dist/core/cli-diagnostics.js +20 -0
  35. package/dist/core/cli-diagnostics.js.map +1 -0
  36. package/dist/core/context-budget-headroom.d.ts +12 -0
  37. package/dist/core/context-budget-headroom.d.ts.map +1 -1
  38. package/dist/core/context-budget-headroom.js +35 -0
  39. package/dist/core/context-budget-headroom.js.map +1 -1
  40. package/dist/core/context-budget-v2-input-validation.d.ts +4 -0
  41. package/dist/core/context-budget-v2-input-validation.d.ts.map +1 -0
  42. package/dist/core/context-budget-v2-input-validation.js +79 -0
  43. package/dist/core/context-budget-v2-input-validation.js.map +1 -0
  44. package/dist/core/context-budget-v2-observability.d.ts +15 -0
  45. package/dist/core/context-budget-v2-observability.d.ts.map +1 -0
  46. package/dist/core/context-budget-v2-observability.js +35 -0
  47. package/dist/core/context-budget-v2-observability.js.map +1 -0
  48. package/dist/core/context-budget-v2-planned-items.d.ts +5 -0
  49. package/dist/core/context-budget-v2-planned-items.d.ts.map +1 -0
  50. package/dist/core/context-budget-v2-planned-items.js +40 -0
  51. package/dist/core/context-budget-v2-planned-items.js.map +1 -0
  52. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  53. package/dist/core/context-budget-v2-planner.js +57 -38
  54. package/dist/core/context-budget-v2-planner.js.map +1 -1
  55. package/dist/core/context-budget-v2-selection.d.ts +19 -3
  56. package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
  57. package/dist/core/context-budget-v2-selection.js +35 -57
  58. package/dist/core/context-budget-v2-selection.js.map +1 -1
  59. package/dist/core/context-budget-v2-tiers.d.ts.map +1 -1
  60. package/dist/core/context-budget-v2-tiers.js +9 -42
  61. package/dist/core/context-budget-v2-tiers.js.map +1 -1
  62. package/dist/core/context-budget-v2-types.d.ts +8 -1
  63. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  64. package/dist/core/context-budget-v2-types.js.map +1 -1
  65. package/dist/core/extensions/bundled-virtual-modules.d.ts +15 -0
  66. package/dist/core/extensions/bundled-virtual-modules.d.ts.map +1 -0
  67. package/dist/core/extensions/bundled-virtual-modules.js +63 -0
  68. package/dist/core/extensions/bundled-virtual-modules.js.map +1 -0
  69. package/dist/core/extensions/loader.d.ts.map +1 -1
  70. package/dist/core/extensions/loader.js +2 -42
  71. package/dist/core/extensions/loader.js.map +1 -1
  72. package/dist/core/extensions/runner.d.ts +1 -0
  73. package/dist/core/extensions/runner.d.ts.map +1 -1
  74. package/dist/core/extensions/runner.js +6 -0
  75. package/dist/core/extensions/runner.js.map +1 -1
  76. package/dist/core/extensions/types.d.ts +10 -0
  77. package/dist/core/extensions/types.d.ts.map +1 -1
  78. package/dist/core/extensions/types.js.map +1 -1
  79. package/dist/core/index.d.ts +2 -1
  80. package/dist/core/index.d.ts.map +1 -1
  81. package/dist/core/index.js +2 -1
  82. package/dist/core/index.js.map +1 -1
  83. package/dist/core/loadout-runtime.d.ts +2 -8
  84. package/dist/core/loadout-runtime.d.ts.map +1 -1
  85. package/dist/core/loadout-runtime.js.map +1 -1
  86. package/dist/core/mcp/client.d.ts +27 -6
  87. package/dist/core/mcp/client.d.ts.map +1 -1
  88. package/dist/core/mcp/client.js +80 -20
  89. package/dist/core/mcp/client.js.map +1 -1
  90. package/dist/core/mcp/manager.d.ts +10 -1
  91. package/dist/core/mcp/manager.d.ts.map +1 -1
  92. package/dist/core/mcp/manager.js +68 -9
  93. package/dist/core/mcp/manager.js.map +1 -1
  94. package/dist/core/mcp/protocol.d.ts +9 -3
  95. package/dist/core/mcp/protocol.d.ts.map +1 -1
  96. package/dist/core/mcp/protocol.js +61 -15
  97. package/dist/core/mcp/protocol.js.map +1 -1
  98. package/dist/core/mcp/stdio-transport.d.ts +12 -1
  99. package/dist/core/mcp/stdio-transport.d.ts.map +1 -1
  100. package/dist/core/mcp/stdio-transport.js +30 -2
  101. package/dist/core/mcp/stdio-transport.js.map +1 -1
  102. package/dist/core/mcp-descriptor-injection.d.ts +26 -0
  103. package/dist/core/mcp-descriptor-injection.d.ts.map +1 -0
  104. package/dist/core/mcp-descriptor-injection.js +25 -0
  105. package/dist/core/mcp-descriptor-injection.js.map +1 -0
  106. package/dist/core/mcp-public-presets.d.ts +1 -3
  107. package/dist/core/mcp-public-presets.d.ts.map +1 -1
  108. package/dist/core/mcp-public-presets.js +3 -15
  109. package/dist/core/mcp-public-presets.js.map +1 -1
  110. package/dist/core/model-registry.d.ts.map +1 -1
  111. package/dist/core/model-registry.js +24 -3
  112. package/dist/core/model-registry.js.map +1 -1
  113. package/dist/core/neo/catalog.d.ts +20 -0
  114. package/dist/core/neo/catalog.d.ts.map +1 -0
  115. package/dist/core/neo/catalog.js +50 -0
  116. package/dist/core/neo/catalog.js.map +1 -0
  117. package/dist/core/neo/setup.d.ts +3 -0
  118. package/dist/core/neo/setup.d.ts.map +1 -0
  119. package/dist/core/neo/setup.js +47 -0
  120. package/dist/core/neo/setup.js.map +1 -0
  121. package/dist/core/provider-default-models.d.ts +1 -0
  122. package/dist/core/provider-default-models.d.ts.map +1 -1
  123. package/dist/core/provider-default-models.js +1 -0
  124. package/dist/core/provider-default-models.js.map +1 -1
  125. package/dist/core/provider-display-names.d.ts.map +1 -1
  126. package/dist/core/provider-display-names.js +1 -0
  127. package/dist/core/provider-display-names.js.map +1 -1
  128. package/dist/core/provider-error-classification.d.ts +57 -0
  129. package/dist/core/provider-error-classification.d.ts.map +1 -0
  130. package/dist/core/provider-error-classification.js +103 -0
  131. package/dist/core/provider-error-classification.js.map +1 -0
  132. package/dist/core/provider-resilience.d.ts +10 -0
  133. package/dist/core/provider-resilience.d.ts.map +1 -1
  134. package/dist/core/provider-resilience.js +36 -3
  135. package/dist/core/provider-resilience.js.map +1 -1
  136. package/dist/core/provider-usage-commandcode.d.ts +9 -0
  137. package/dist/core/provider-usage-commandcode.d.ts.map +1 -0
  138. package/dist/core/provider-usage-commandcode.js +198 -0
  139. package/dist/core/provider-usage-commandcode.js.map +1 -0
  140. package/dist/core/provider-usage-devin.d.ts +5 -3
  141. package/dist/core/provider-usage-devin.d.ts.map +1 -1
  142. package/dist/core/provider-usage-devin.js +15 -5
  143. package/dist/core/provider-usage-devin.js.map +1 -1
  144. package/dist/core/provider-usage-types.d.ts +1 -1
  145. package/dist/core/provider-usage-types.d.ts.map +1 -1
  146. package/dist/core/provider-usage-types.js.map +1 -1
  147. package/dist/core/provider-usage.d.ts.map +1 -1
  148. package/dist/core/provider-usage.js +6 -0
  149. package/dist/core/provider-usage.js.map +1 -1
  150. package/dist/core/resource-admission.d.ts +36 -0
  151. package/dist/core/resource-admission.d.ts.map +1 -1
  152. package/dist/core/resource-admission.js +59 -0
  153. package/dist/core/resource-admission.js.map +1 -1
  154. package/dist/core/resource-loader.d.ts.map +1 -1
  155. package/dist/core/resource-loader.js +2 -2
  156. package/dist/core/resource-loader.js.map +1 -1
  157. package/dist/core/run-usage-ledger.d.ts +47 -0
  158. package/dist/core/run-usage-ledger.d.ts.map +1 -0
  159. package/dist/core/run-usage-ledger.js +162 -0
  160. package/dist/core/run-usage-ledger.js.map +1 -0
  161. package/dist/core/run-usage-operation.d.ts +8 -0
  162. package/dist/core/run-usage-operation.d.ts.map +1 -0
  163. package/dist/core/run-usage-operation.js +12 -0
  164. package/dist/core/run-usage-operation.js.map +1 -0
  165. package/dist/core/sdk.d.ts.map +1 -1
  166. package/dist/core/sdk.js +5 -6
  167. package/dist/core/sdk.js.map +1 -1
  168. package/dist/core/session-failure-cause.d.ts.map +1 -1
  169. package/dist/core/session-failure-cause.js +14 -5
  170. package/dist/core/session-failure-cause.js.map +1 -1
  171. package/dist/core/session-prompt-lifecycle.d.ts +6 -0
  172. package/dist/core/session-prompt-lifecycle.d.ts.map +1 -1
  173. package/dist/core/session-prompt-lifecycle.js +47 -2
  174. package/dist/core/session-prompt-lifecycle.js.map +1 -1
  175. package/dist/core/session-termination.d.ts.map +1 -1
  176. package/dist/core/session-termination.js +4 -2
  177. package/dist/core/session-termination.js.map +1 -1
  178. package/dist/core/subagent-lane-authority.d.ts +28 -0
  179. package/dist/core/subagent-lane-authority.d.ts.map +1 -0
  180. package/dist/core/subagent-lane-authority.js +119 -0
  181. package/dist/core/subagent-lane-authority.js.map +1 -0
  182. package/dist/core/subagent-lane-contract.d.ts +114 -0
  183. package/dist/core/subagent-lane-contract.d.ts.map +1 -0
  184. package/dist/core/subagent-lane-contract.js +18 -0
  185. package/dist/core/subagent-lane-contract.js.map +1 -0
  186. package/dist/core/subagent-lane-launcher.d.ts +3 -6
  187. package/dist/core/subagent-lane-launcher.d.ts.map +1 -1
  188. package/dist/core/subagent-lane-launcher.js +53 -12
  189. package/dist/core/subagent-lane-launcher.js.map +1 -1
  190. package/dist/core/subagent-orchestration.d.ts +4 -17
  191. package/dist/core/subagent-orchestration.d.ts.map +1 -1
  192. package/dist/core/subagent-orchestration.js.map +1 -1
  193. package/dist/core/verified-run/broker.d.ts.map +1 -1
  194. package/dist/core/verified-run/broker.js +4 -1
  195. package/dist/core/verified-run/broker.js.map +1 -1
  196. package/dist/core/workload-permit-pool.d.ts +1 -1
  197. package/dist/core/workload-permit-pool.d.ts.map +1 -1
  198. package/dist/core/workload-permit-pool.js +3 -0
  199. package/dist/core/workload-permit-pool.js.map +1 -1
  200. package/dist/guardrails/strict-evidence-approval-adapter.d.ts +34 -0
  201. package/dist/guardrails/strict-evidence-approval-adapter.d.ts.map +1 -0
  202. package/dist/guardrails/strict-evidence-approval-adapter.js +70 -0
  203. package/dist/guardrails/strict-evidence-approval-adapter.js.map +1 -0
  204. package/dist/index.d.ts +3 -0
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +1 -0
  207. package/dist/index.js.map +1 -1
  208. package/dist/main.d.ts.map +1 -1
  209. package/dist/main.js +11 -18
  210. package/dist/main.js.map +1 -1
  211. package/dist/modes/acp/acp-agent.d.ts +24 -0
  212. package/dist/modes/acp/acp-agent.d.ts.map +1 -0
  213. package/dist/modes/acp/acp-agent.js +133 -0
  214. package/dist/modes/acp/acp-agent.js.map +1 -0
  215. package/dist/modes/acp/acp-mode.d.ts +4 -0
  216. package/dist/modes/acp/acp-mode.d.ts.map +1 -0
  217. package/dist/modes/acp/acp-mode.js +34 -0
  218. package/dist/modes/acp/acp-mode.js.map +1 -0
  219. package/dist/modes/acp/acp-session.d.ts +4 -0
  220. package/dist/modes/acp/acp-session.d.ts.map +1 -0
  221. package/dist/modes/acp/acp-session.js +76 -0
  222. package/dist/modes/acp/acp-session.js.map +1 -0
  223. package/dist/modes/acp/acp-transport.d.ts +5 -0
  224. package/dist/modes/acp/acp-transport.d.ts.map +1 -0
  225. package/dist/modes/acp/acp-transport.js +90 -0
  226. package/dist/modes/acp/acp-transport.js.map +1 -0
  227. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  228. package/dist/modes/interactive/interactive-mode.js +1 -0
  229. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  230. package/docs/audit-revalidation-2026-09-17.md +77 -0
  231. package/docs/devin-harness.md +33 -4
  232. package/docs/ecraf-normalization.md +83 -0
  233. package/docs/model-catalog-refresh.md +51 -1
  234. package/docs/models.md +1 -1
  235. package/docs/neo.md +134 -0
  236. package/docs/providers.md +72 -6
  237. package/docs/run-usage-ledger.md +56 -0
  238. package/docs/runtime-algorithms.md +44 -0
  239. package/docs/skills.md +1 -1
  240. package/docs/usage.md +2 -1
  241. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  242. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  243. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  244. package/examples/extensions/gondolin/package-lock.json +2 -2
  245. package/examples/extensions/gondolin/package.json +1 -1
  246. package/examples/extensions/sandbox/package-lock.json +2 -2
  247. package/examples/extensions/sandbox/package.json +1 -1
  248. package/examples/extensions/subagent/adaptive-agent-runtime.ts +14 -1
  249. package/examples/extensions/subagent/graph-result.ts +48 -0
  250. package/examples/extensions/subagent/index.ts +246 -111
  251. package/examples/extensions/subagent/managed-process-tree.ts +42 -0
  252. package/examples/extensions/subagent/managed-process.test.ts +24 -0
  253. package/examples/extensions/subagent/managed-process.ts +93 -112
  254. package/examples/extensions/subagent/subagent-runtime-types.ts +17 -1
  255. package/examples/extensions/subagent/subagent-stream.ts +161 -0
  256. package/examples/extensions/terminal-browser/README.md +54 -0
  257. package/examples/extensions/terminal-browser/bridge-protocol.ts +29 -0
  258. package/examples/extensions/terminal-browser/bridge.ts +219 -0
  259. package/examples/extensions/terminal-browser/browser-surface.ts +288 -0
  260. package/examples/extensions/terminal-browser/index.ts +215 -0
  261. package/examples/extensions/terminal-browser/placeholders.ts +49 -0
  262. package/examples/extensions/with-deps/package-lock.json +2 -2
  263. package/examples/extensions/with-deps/package.json +1 -1
  264. package/npm-shrinkwrap.json +18 -18
  265. package/package.json +8 -7
  266. package/resources/neo/skills/omk-browser/SKILL.md +32 -0
  267. package/resources/neo/skills/omk-code-review/SKILL.md +24 -0
  268. package/resources/neo/skills/omk-computeruse/SKILL.md +34 -0
  269. package/resources/neo/skills/omk-mcp-setup/SKILL.md +48 -0
  270. package/resources/neo/skills/omk-research/SKILL.md +24 -0
  271. package/resources/neo/skills/omk-site/SKILL.md +28 -0
@@ -0,0 +1,50 @@
1
+ /** Public distribution catalog. Entries are offers, not connection or capability attestations. */
2
+ export const NEO_SKILL_NAMES = [
3
+ "omk-computeruse",
4
+ "omk-browser",
5
+ "omk-site",
6
+ "omk-research",
7
+ "omk-code-review",
8
+ "omk-mcp-setup",
9
+ ];
10
+ export const NEO_MCP_PRESETS = [
11
+ {
12
+ id: "playwright",
13
+ purpose: "Browser navigation, accessibility snapshots, interaction and visual verification",
14
+ packageSpec: "@playwright/mcp@0.0.81",
15
+ args: ["--headless", "--isolated", "--sandbox", "--timeout-action", "10000", "--timeout-navigation", "30000"],
16
+ notice: "Opt-in executable download and website access. A compatible browser is required. Profile isolation is not OS isolation. Sandbox failure must not trigger --no-sandbox fallback. No personal browser attachment.",
17
+ },
18
+ {
19
+ id: "context7",
20
+ purpose: "Current library documentation retrieval",
21
+ packageSpec: "@upstash/context7-mcp@4.1.1",
22
+ args: ["--transport", "stdio"],
23
+ notice: "Opt-in executable download and external documentation queries. Do not send private code or secrets. Authentication and limits are user-specific; no credential is bundled.",
24
+ },
25
+ ];
26
+ export function selectNeoMcpPresets(ids) {
27
+ if (ids.length === 0)
28
+ throw new Error("Select at least one MCP preset: playwright, context7");
29
+ if (new Set(ids).size !== ids.length)
30
+ throw new Error("Duplicate MCP preset");
31
+ return ids.map((id) => {
32
+ const preset = NEO_MCP_PRESETS.find((entry) => entry.id === id);
33
+ if (!preset)
34
+ throw new Error("Unknown MCP preset; supported: playwright, context7");
35
+ return preset;
36
+ });
37
+ }
38
+ export function neoMcpConfig(ids, enabled) {
39
+ const mcpServers = {};
40
+ for (const preset of selectNeoMcpPresets(ids)) {
41
+ mcpServers[`neo-${preset.id}`] = {
42
+ command: "npx",
43
+ args: ["--yes", preset.packageSpec, ...preset.args],
44
+ disabled: !enabled,
45
+ startup_timeout_sec: 60,
46
+ };
47
+ }
48
+ return { mcpServers };
49
+ }
50
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../src/core/neo/catalog.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,MAAM,CAAC,MAAM,eAAe,GAAG;IAC9B,iBAAiB;IACjB,aAAa;IACb,UAAU;IACV,cAAc;IACd,iBAAiB;IACjB,eAAe;CACN,CAAC;AAUX,MAAM,CAAC,MAAM,eAAe,GAA4B;IACvD;QACC,EAAE,EAAE,YAAY;QAChB,OAAO,EAAE,kFAAkF;QAC3F,WAAW,EAAE,wBAAwB;QACrC,IAAI,EAAE,CAAC,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,OAAO,EAAE,sBAAsB,EAAE,OAAO,CAAC;QAC7G,MAAM,EACL,iNAAiN;KAClN;IACD;QACC,EAAE,EAAE,UAAU;QACd,OAAO,EAAE,yCAAyC;QAClD,WAAW,EAAE,6BAA6B;QAC1C,IAAI,EAAE,CAAC,aAAa,EAAE,OAAO,CAAC;QAC9B,MAAM,EACL,4KAA4K;KAC7K;CACD,CAAC;AAEF,MAAM,UAAU,mBAAmB,CAAC,GAAsB,EAA2B;IACpF,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC9F,IAAI,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;IAC9E,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;QACpF,OAAO,MAAM,CAAC;IAAA,CACd,CAAC,CAAC;AAAA,CACH;AAED,MAAM,UAAU,YAAY,CAC3B,GAAsB,EACtB,OAAgB,EAGf;IACD,MAAM,UAAU,GAAkD,EAAE,CAAC;IACrE,KAAK,MAAM,MAAM,IAAI,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/C,UAAU,CAAC,OAAO,MAAM,CAAC,EAAE,EAAE,CAAC,GAAG;YAChC,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC;YACnD,QAAQ,EAAE,CAAC,OAAO;YAClB,mBAAmB,EAAE,EAAE;SACvB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,CAAC;AAAA,CACtB","sourcesContent":["/** Public distribution catalog. Entries are offers, not connection or capability attestations. */\nexport const NEO_SKILL_NAMES = [\n\t\"omk-computeruse\",\n\t\"omk-browser\",\n\t\"omk-site\",\n\t\"omk-research\",\n\t\"omk-code-review\",\n\t\"omk-mcp-setup\",\n] as const;\n\nexport interface NeoMcpPreset {\n\treadonly id: string;\n\treadonly purpose: string;\n\treadonly packageSpec: string;\n\treadonly args: readonly string[];\n\treadonly notice: string;\n}\n\nexport const NEO_MCP_PRESETS: readonly NeoMcpPreset[] = [\n\t{\n\t\tid: \"playwright\",\n\t\tpurpose: \"Browser navigation, accessibility snapshots, interaction and visual verification\",\n\t\tpackageSpec: \"@playwright/mcp@0.0.81\",\n\t\targs: [\"--headless\", \"--isolated\", \"--sandbox\", \"--timeout-action\", \"10000\", \"--timeout-navigation\", \"30000\"],\n\t\tnotice:\n\t\t\t\"Opt-in executable download and website access. A compatible browser is required. Profile isolation is not OS isolation. Sandbox failure must not trigger --no-sandbox fallback. No personal browser attachment.\",\n\t},\n\t{\n\t\tid: \"context7\",\n\t\tpurpose: \"Current library documentation retrieval\",\n\t\tpackageSpec: \"@upstash/context7-mcp@4.1.1\",\n\t\targs: [\"--transport\", \"stdio\"],\n\t\tnotice:\n\t\t\t\"Opt-in executable download and external documentation queries. Do not send private code or secrets. Authentication and limits are user-specific; no credential is bundled.\",\n\t},\n];\n\nexport function selectNeoMcpPresets(ids: readonly string[]): readonly NeoMcpPreset[] {\n\tif (ids.length === 0) throw new Error(\"Select at least one MCP preset: playwright, context7\");\n\tif (new Set(ids).size !== ids.length) throw new Error(\"Duplicate MCP preset\");\n\treturn ids.map((id) => {\n\t\tconst preset = NEO_MCP_PRESETS.find((entry) => entry.id === id);\n\t\tif (!preset) throw new Error(\"Unknown MCP preset; supported: playwright, context7\");\n\t\treturn preset;\n\t});\n}\n\nexport function neoMcpConfig(\n\tids: readonly string[],\n\tenabled: boolean,\n): {\n\tmcpServers: Record<string, { command: string; args: string[]; disabled: boolean; startup_timeout_sec: number }>;\n} {\n\tconst mcpServers: ReturnType<typeof neoMcpConfig>[\"mcpServers\"] = {};\n\tfor (const preset of selectNeoMcpPresets(ids)) {\n\t\tmcpServers[`neo-${preset.id}`] = {\n\t\t\tcommand: \"npx\",\n\t\t\targs: [\"--yes\", preset.packageSpec, ...preset.args],\n\t\t\tdisabled: !enabled,\n\t\t\tstartup_timeout_sec: 60,\n\t\t};\n\t}\n\treturn { mcpServers };\n}\n"]}
@@ -0,0 +1,3 @@
1
+ /** Create a new configuration only. Never merge, replace, or print an existing credential-bearing file. */
2
+ export declare function createNeoMcpConfig(root: string, ids: readonly string[]): string;
3
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../src/core/neo/setup.ts"],"names":[],"mappings":"AAeA,2GAA2G;AAC3G,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAuC/E","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n\tcloseSync,\n\tconstants,\n\tfsyncSync,\n\tlinkSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { neoMcpConfig } from \"./catalog.ts\";\n\n/** Create a new configuration only. Never merge, replace, or print an existing credential-bearing file. */\nexport function createNeoMcpConfig(root: string, ids: readonly string[]): string {\n\tconst content = `${JSON.stringify(neoMcpConfig(ids, true), null, 2)}\\n`;\n\tconst directory = join(resolve(root), \".omk\");\n\tmkdirSync(directory, { recursive: true, mode: 0o700 });\n\tconst info = lstatSync(directory);\n\tif (!info.isDirectory() || info.isSymbolicLink())\n\t\tthrow new Error(\"Neo setup refuses a symlinked or non-directory .omk\");\n\tconst target = join(directory, \"mcp.json\");\n\tconst temporary = join(directory, `.neo-mcp-${randomUUID()}.tmp`);\n\tlet fd: number | undefined;\n\tlet published = false;\n\ttry {\n\t\tfd = openSync(\n\t\t\ttemporary,\n\t\t\tconstants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0),\n\t\t\t0o600,\n\t\t);\n\t\twriteFileSync(fd, content, \"utf8\");\n\t\tfsyncSync(fd);\n\t\tcloseSync(fd);\n\t\tfd = undefined;\n\t\t// Atomic no-replace publication: an existing mcp.json (including a symlink) wins.\n\t\tlinkSync(temporary, target);\n\t\tpublished = true;\n\t} catch (error) {\n\t\tconst code = typeof error === \"object\" && error !== null && \"code\" in error ? error.code : undefined;\n\t\tif (code === \"EEXIST\")\n\t\t\tthrow new Error(\"mcp.json already exists; no changes made. Use neo mcp-config and review a manual merge.\");\n\t\tthrow new Error(\"Could not create MCP configuration; no existing configuration was overwritten.\");\n\t} finally {\n\t\tif (fd !== undefined) closeSync(fd);\n\t\ttry {\n\t\t\tunlinkSync(temporary);\n\t\t} catch {\n\t\t\t/* No credential data is logged. */\n\t\t}\n\t}\n\tif (!published) throw new Error(\"MCP configuration was not published\");\n\treturn target;\n}\n"]}
@@ -0,0 +1,47 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, constants, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { neoMcpConfig } from "./catalog.js";
5
+ /** Create a new configuration only. Never merge, replace, or print an existing credential-bearing file. */
6
+ export function createNeoMcpConfig(root, ids) {
7
+ const content = `${JSON.stringify(neoMcpConfig(ids, true), null, 2)}\n`;
8
+ const directory = join(resolve(root), ".omk");
9
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
10
+ const info = lstatSync(directory);
11
+ if (!info.isDirectory() || info.isSymbolicLink())
12
+ throw new Error("Neo setup refuses a symlinked or non-directory .omk");
13
+ const target = join(directory, "mcp.json");
14
+ const temporary = join(directory, `.neo-mcp-${randomUUID()}.tmp`);
15
+ let fd;
16
+ let published = false;
17
+ try {
18
+ fd = openSync(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), 0o600);
19
+ writeFileSync(fd, content, "utf8");
20
+ fsyncSync(fd);
21
+ closeSync(fd);
22
+ fd = undefined;
23
+ // Atomic no-replace publication: an existing mcp.json (including a symlink) wins.
24
+ linkSync(temporary, target);
25
+ published = true;
26
+ }
27
+ catch (error) {
28
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
29
+ if (code === "EEXIST")
30
+ throw new Error("mcp.json already exists; no changes made. Use neo mcp-config and review a manual merge.");
31
+ throw new Error("Could not create MCP configuration; no existing configuration was overwritten.");
32
+ }
33
+ finally {
34
+ if (fd !== undefined)
35
+ closeSync(fd);
36
+ try {
37
+ unlinkSync(temporary);
38
+ }
39
+ catch {
40
+ /* No credential data is logged. */
41
+ }
42
+ }
43
+ if (!published)
44
+ throw new Error("MCP configuration was not published");
45
+ return target;
46
+ }
47
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/core/neo/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACN,SAAS,EACT,SAAS,EACT,SAAS,EACT,QAAQ,EACR,SAAS,EACT,SAAS,EACT,QAAQ,EACR,UAAU,EACV,aAAa,GACb,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,2GAA2G;AAC3G,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,GAAsB,EAAU;IAChF,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IACxE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;IAC9C,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,cAAc,EAAE;QAC/C,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,YAAY,UAAU,EAAE,MAAM,CAAC,CAAC;IAClE,IAAI,EAAsB,CAAC;IAC3B,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,CAAC;QACJ,EAAE,GAAG,QAAQ,CACZ,SAAS,EACT,SAAS,CAAC,OAAO,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,QAAQ,GAAG,CAAC,SAAS,CAAC,UAAU,IAAI,CAAC,CAAC,EACvF,KAAK,CACL,CAAC;QACF,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACnC,SAAS,CAAC,EAAE,CAAC,CAAC;QACd,SAAS,CAAC,EAAE,CAAC,CAAC;QACd,EAAE,GAAG,SAAS,CAAC;QACf,kFAAkF;QAClF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAC5B,SAAS,GAAG,IAAI,CAAC;IAClB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QACrG,IAAI,IAAI,KAAK,QAAQ;YACpB,MAAM,IAAI,KAAK,CAAC,yFAAyF,CAAC,CAAC;QAC5G,MAAM,IAAI,KAAK,CAAC,gFAAgF,CAAC,CAAC;IACnG,CAAC;YAAS,CAAC;QACV,IAAI,EAAE,KAAK,SAAS;YAAE,SAAS,CAAC,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC;YACJ,UAAU,CAAC,SAAS,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACR,mCAAmC;QACpC,CAAC;IACF,CAAC;IACD,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACvE,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n\tcloseSync,\n\tconstants,\n\tfsyncSync,\n\tlinkSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { neoMcpConfig } from \"./catalog.ts\";\n\n/** Create a new configuration only. Never merge, replace, or print an existing credential-bearing file. */\nexport function createNeoMcpConfig(root: string, ids: readonly string[]): string {\n\tconst content = `${JSON.stringify(neoMcpConfig(ids, true), null, 2)}\\n`;\n\tconst directory = join(resolve(root), \".omk\");\n\tmkdirSync(directory, { recursive: true, mode: 0o700 });\n\tconst info = lstatSync(directory);\n\tif (!info.isDirectory() || info.isSymbolicLink())\n\t\tthrow new Error(\"Neo setup refuses a symlinked or non-directory .omk\");\n\tconst target = join(directory, \"mcp.json\");\n\tconst temporary = join(directory, `.neo-mcp-${randomUUID()}.tmp`);\n\tlet fd: number | undefined;\n\tlet published = false;\n\ttry {\n\t\tfd = openSync(\n\t\t\ttemporary,\n\t\t\tconstants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0),\n\t\t\t0o600,\n\t\t);\n\t\twriteFileSync(fd, content, \"utf8\");\n\t\tfsyncSync(fd);\n\t\tcloseSync(fd);\n\t\tfd = undefined;\n\t\t// Atomic no-replace publication: an existing mcp.json (including a symlink) wins.\n\t\tlinkSync(temporary, target);\n\t\tpublished = true;\n\t} catch (error) {\n\t\tconst code = typeof error === \"object\" && error !== null && \"code\" in error ? error.code : undefined;\n\t\tif (code === \"EEXIST\")\n\t\t\tthrow new Error(\"mcp.json already exists; no changes made. Use neo mcp-config and review a manual merge.\");\n\t\tthrow new Error(\"Could not create MCP configuration; no existing configuration was overwritten.\");\n\t} finally {\n\t\tif (fd !== undefined) closeSync(fd);\n\t\ttry {\n\t\t\tunlinkSync(temporary);\n\t\t} catch {\n\t\t\t/* No credential data is logged. */\n\t\t}\n\t}\n\tif (!published) throw new Error(\"MCP configuration was not published\");\n\treturn target;\n}\n"]}
@@ -37,6 +37,7 @@ export declare const defaultModelPerProvider: {
37
37
  readonly "xiaomi-token-plan-ams": string;
38
38
  readonly "xiaomi-token-plan-sgp": string;
39
39
  readonly zyloo: string;
40
+ readonly cursor: string;
40
41
  readonly "modelstudio-maas": "qwen3.8-max";
41
42
  };
42
43
  //# sourceMappingURL=provider-default-models.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider-default-models.d.ts","sourceRoot":"","sources":["../../src/core/provider-default-models.ts"],"names":[],"mappings":"AAiDA,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAG1B,CAAC","sourcesContent":["import type { KnownProvider } from \"omk-ai\";\n\n/** Default model IDs for every built-in provider. */\nconst builtInDefaultModelPerProvider = {\n\t\"amazon-bedrock\": \"us.anthropic.claude-opus-4-6-v1\",\n\t\"ant-ling\": \"Ring-2.6-1T\",\n\tanthropic: \"claude-opus-4-8\",\n\topenai: \"gpt-5.4\",\n\t\"azure-openai-responses\": \"gpt-5.4\",\n\t\"openai-codex\": \"gpt-5.5\",\n\tnvidia: \"nvidia/nemotron-3-super-120b-a12b\",\n\tdeepseek: \"deepseek-v4-pro\",\n\tdevin: \"swe-2\",\n\tmeta: \"muse-spark-1.3\",\n\tgoogle: \"gemini-3.1-pro-preview\",\n\t\"google-vertex\": \"gemini-3.1-pro-preview\",\n\t\"github-copilot\": \"gpt-5.4\",\n\topenrouter: \"moonshotai/kimi-k2.6\",\n\t\"vercel-ai-gateway\": \"zai/glm-5.1\",\n\txai: \"grok-4.20-0309-reasoning\",\n\tgroq: \"openai/gpt-oss-120b\",\n\tcerebras: \"zai-glm-4.7\",\n\tzai: \"glm-5.1\",\n\t\"zai-coding-cn\": \"glm-5.1\",\n\tmistral: \"devstral-medium-latest\",\n\tminimax: \"MiniMax-M2.7\",\n\t\"minimax-cn\": \"MiniMax-M2.7\",\n\tmoonshotai: \"kimi-k2.6\",\n\t\"moonshotai-cn\": \"kimi-k2.6\",\n\thuggingface: \"moonshotai/Kimi-K2.6\",\n\tfireworks: \"accounts/fireworks/models/kimi-k2p6\",\n\ttogether: \"moonshotai/Kimi-K2.6\",\n\topencode: \"kimi-k2.6\",\n\t\"opencode-go\": \"kimi-k2.6\",\n\t\"kimi-coding\": \"k3\",\n\t\"cloudflare-workers-ai\": \"@cf/moonshotai/kimi-k2.6\",\n\t\"cloudflare-ai-gateway\": \"workers-ai/@cf/moonshotai/kimi-k2.6\",\n\txiaomi: \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-cn\": \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-ams\": \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-sgp\": \"mimo-v2.5-pro\",\n\tzyloo: \"claude-opus-4-7\",\n} satisfies Record<KnownProvider, string>;\n\n/** Recommended defaults for providers supplied through models.json or extensions. */\nconst customDefaultModelPerProvider = {\n\t\"modelstudio-maas\": \"qwen3.8-max\",\n} as const satisfies Readonly<Record<string, string>>;\n\nexport const defaultModelPerProvider = {\n\t...builtInDefaultModelPerProvider,\n\t...customDefaultModelPerProvider,\n} as const;\n"]}
1
+ {"version":3,"file":"provider-default-models.d.ts","sourceRoot":"","sources":["../../src/core/provider-default-models.ts"],"names":[],"mappings":"AAkDA,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAG1B,CAAC","sourcesContent":["import type { KnownProvider } from \"omk-ai\";\n\n/** Default model IDs for every built-in provider. */\nconst builtInDefaultModelPerProvider = {\n\t\"amazon-bedrock\": \"us.anthropic.claude-opus-4-6-v1\",\n\t\"ant-ling\": \"Ring-2.6-1T\",\n\tanthropic: \"claude-opus-4-8\",\n\topenai: \"gpt-5.4\",\n\t\"azure-openai-responses\": \"gpt-5.4\",\n\t\"openai-codex\": \"gpt-5.5\",\n\tnvidia: \"nvidia/nemotron-3-super-120b-a12b\",\n\tdeepseek: \"deepseek-v4-pro\",\n\tdevin: \"swe-2\",\n\tmeta: \"muse-spark-1.3\",\n\tgoogle: \"gemini-3.1-pro-preview\",\n\t\"google-vertex\": \"gemini-3.1-pro-preview\",\n\t\"github-copilot\": \"gpt-5.4\",\n\topenrouter: \"moonshotai/kimi-k2.6\",\n\t\"vercel-ai-gateway\": \"zai/glm-5.1\",\n\txai: \"grok-4.20-0309-reasoning\",\n\tgroq: \"openai/gpt-oss-120b\",\n\tcerebras: \"zai-glm-4.7\",\n\tzai: \"glm-5.1\",\n\t\"zai-coding-cn\": \"glm-5.1\",\n\tmistral: \"devstral-medium-latest\",\n\tminimax: \"MiniMax-M2.7\",\n\t\"minimax-cn\": \"MiniMax-M2.7\",\n\tmoonshotai: \"kimi-k2.6\",\n\t\"moonshotai-cn\": \"kimi-k2.6\",\n\thuggingface: \"moonshotai/Kimi-K2.6\",\n\tfireworks: \"accounts/fireworks/models/kimi-k2p6\",\n\ttogether: \"moonshotai/Kimi-K2.6\",\n\topencode: \"kimi-k2.6\",\n\t\"opencode-go\": \"kimi-k2.6\",\n\t\"kimi-coding\": \"k3\",\n\t\"cloudflare-workers-ai\": \"@cf/moonshotai/kimi-k2.6\",\n\t\"cloudflare-ai-gateway\": \"workers-ai/@cf/moonshotai/kimi-k2.6\",\n\txiaomi: \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-cn\": \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-ams\": \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-sgp\": \"mimo-v2.5-pro\",\n\tzyloo: \"claude-opus-4-7\",\n\tcursor: \"default\",\n} satisfies Record<KnownProvider, string>;\n\n/** Recommended defaults for providers supplied through models.json or extensions. */\nconst customDefaultModelPerProvider = {\n\t\"modelstudio-maas\": \"qwen3.8-max\",\n} as const satisfies Readonly<Record<string, string>>;\n\nexport const defaultModelPerProvider = {\n\t...builtInDefaultModelPerProvider,\n\t...customDefaultModelPerProvider,\n} as const;\n"]}
@@ -38,6 +38,7 @@ const builtInDefaultModelPerProvider = {
38
38
  "xiaomi-token-plan-ams": "mimo-v2.5-pro",
39
39
  "xiaomi-token-plan-sgp": "mimo-v2.5-pro",
40
40
  zyloo: "claude-opus-4-7",
41
+ cursor: "default",
41
42
  };
42
43
  /** Recommended defaults for providers supplied through models.json or extensions. */
43
44
  const customDefaultModelPerProvider = {
@@ -1 +1 @@
1
- {"version":3,"file":"provider-default-models.js","sourceRoot":"","sources":["../../src/core/provider-default-models.ts"],"names":[],"mappings":"AAEA,qDAAqD;AACrD,MAAM,8BAA8B,GAAG;IACtC,gBAAgB,EAAE,iCAAiC;IACnD,UAAU,EAAE,aAAa;IACzB,SAAS,EAAE,iBAAiB;IAC5B,MAAM,EAAE,SAAS;IACjB,wBAAwB,EAAE,SAAS;IACnC,cAAc,EAAE,SAAS;IACzB,MAAM,EAAE,mCAAmC;IAC3C,QAAQ,EAAE,iBAAiB;IAC3B,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,gBAAgB;IACtB,MAAM,EAAE,wBAAwB;IAChC,eAAe,EAAE,wBAAwB;IACzC,gBAAgB,EAAE,SAAS;IAC3B,UAAU,EAAE,sBAAsB;IAClC,mBAAmB,EAAE,aAAa;IAClC,GAAG,EAAE,0BAA0B;IAC/B,IAAI,EAAE,qBAAqB;IAC3B,QAAQ,EAAE,aAAa;IACvB,GAAG,EAAE,SAAS;IACd,eAAe,EAAE,SAAS;IAC1B,OAAO,EAAE,wBAAwB;IACjC,OAAO,EAAE,cAAc;IACvB,YAAY,EAAE,cAAc;IAC5B,UAAU,EAAE,WAAW;IACvB,eAAe,EAAE,WAAW;IAC5B,WAAW,EAAE,sBAAsB;IACnC,SAAS,EAAE,qCAAqC;IAChD,QAAQ,EAAE,sBAAsB;IAChC,QAAQ,EAAE,WAAW;IACrB,aAAa,EAAE,WAAW;IAC1B,aAAa,EAAE,IAAI;IACnB,uBAAuB,EAAE,0BAA0B;IACnD,uBAAuB,EAAE,qCAAqC;IAC9D,MAAM,EAAE,eAAe;IACvB,sBAAsB,EAAE,eAAe;IACvC,uBAAuB,EAAE,eAAe;IACxC,uBAAuB,EAAE,eAAe;IACxC,KAAK,EAAE,iBAAiB;CACgB,CAAC;AAE1C,qFAAqF;AACrF,MAAM,6BAA6B,GAAG;IACrC,kBAAkB,EAAE,aAAa;CACmB,CAAC;AAEtD,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACtC,GAAG,8BAA8B;IACjC,GAAG,6BAA6B;CACvB,CAAC","sourcesContent":["import type { KnownProvider } from \"omk-ai\";\n\n/** Default model IDs for every built-in provider. */\nconst builtInDefaultModelPerProvider = {\n\t\"amazon-bedrock\": \"us.anthropic.claude-opus-4-6-v1\",\n\t\"ant-ling\": \"Ring-2.6-1T\",\n\tanthropic: \"claude-opus-4-8\",\n\topenai: \"gpt-5.4\",\n\t\"azure-openai-responses\": \"gpt-5.4\",\n\t\"openai-codex\": \"gpt-5.5\",\n\tnvidia: \"nvidia/nemotron-3-super-120b-a12b\",\n\tdeepseek: \"deepseek-v4-pro\",\n\tdevin: \"swe-2\",\n\tmeta: \"muse-spark-1.3\",\n\tgoogle: \"gemini-3.1-pro-preview\",\n\t\"google-vertex\": \"gemini-3.1-pro-preview\",\n\t\"github-copilot\": \"gpt-5.4\",\n\topenrouter: \"moonshotai/kimi-k2.6\",\n\t\"vercel-ai-gateway\": \"zai/glm-5.1\",\n\txai: \"grok-4.20-0309-reasoning\",\n\tgroq: \"openai/gpt-oss-120b\",\n\tcerebras: \"zai-glm-4.7\",\n\tzai: \"glm-5.1\",\n\t\"zai-coding-cn\": \"glm-5.1\",\n\tmistral: \"devstral-medium-latest\",\n\tminimax: \"MiniMax-M2.7\",\n\t\"minimax-cn\": \"MiniMax-M2.7\",\n\tmoonshotai: \"kimi-k2.6\",\n\t\"moonshotai-cn\": \"kimi-k2.6\",\n\thuggingface: \"moonshotai/Kimi-K2.6\",\n\tfireworks: \"accounts/fireworks/models/kimi-k2p6\",\n\ttogether: \"moonshotai/Kimi-K2.6\",\n\topencode: \"kimi-k2.6\",\n\t\"opencode-go\": \"kimi-k2.6\",\n\t\"kimi-coding\": \"k3\",\n\t\"cloudflare-workers-ai\": \"@cf/moonshotai/kimi-k2.6\",\n\t\"cloudflare-ai-gateway\": \"workers-ai/@cf/moonshotai/kimi-k2.6\",\n\txiaomi: \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-cn\": \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-ams\": \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-sgp\": \"mimo-v2.5-pro\",\n\tzyloo: \"claude-opus-4-7\",\n} satisfies Record<KnownProvider, string>;\n\n/** Recommended defaults for providers supplied through models.json or extensions. */\nconst customDefaultModelPerProvider = {\n\t\"modelstudio-maas\": \"qwen3.8-max\",\n} as const satisfies Readonly<Record<string, string>>;\n\nexport const defaultModelPerProvider = {\n\t...builtInDefaultModelPerProvider,\n\t...customDefaultModelPerProvider,\n} as const;\n"]}
1
+ {"version":3,"file":"provider-default-models.js","sourceRoot":"","sources":["../../src/core/provider-default-models.ts"],"names":[],"mappings":"AAEA,qDAAqD;AACrD,MAAM,8BAA8B,GAAG;IACtC,gBAAgB,EAAE,iCAAiC;IACnD,UAAU,EAAE,aAAa;IACzB,SAAS,EAAE,iBAAiB;IAC5B,MAAM,EAAE,SAAS;IACjB,wBAAwB,EAAE,SAAS;IACnC,cAAc,EAAE,SAAS;IACzB,MAAM,EAAE,mCAAmC;IAC3C,QAAQ,EAAE,iBAAiB;IAC3B,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,gBAAgB;IACtB,MAAM,EAAE,wBAAwB;IAChC,eAAe,EAAE,wBAAwB;IACzC,gBAAgB,EAAE,SAAS;IAC3B,UAAU,EAAE,sBAAsB;IAClC,mBAAmB,EAAE,aAAa;IAClC,GAAG,EAAE,0BAA0B;IAC/B,IAAI,EAAE,qBAAqB;IAC3B,QAAQ,EAAE,aAAa;IACvB,GAAG,EAAE,SAAS;IACd,eAAe,EAAE,SAAS;IAC1B,OAAO,EAAE,wBAAwB;IACjC,OAAO,EAAE,cAAc;IACvB,YAAY,EAAE,cAAc;IAC5B,UAAU,EAAE,WAAW;IACvB,eAAe,EAAE,WAAW;IAC5B,WAAW,EAAE,sBAAsB;IACnC,SAAS,EAAE,qCAAqC;IAChD,QAAQ,EAAE,sBAAsB;IAChC,QAAQ,EAAE,WAAW;IACrB,aAAa,EAAE,WAAW;IAC1B,aAAa,EAAE,IAAI;IACnB,uBAAuB,EAAE,0BAA0B;IACnD,uBAAuB,EAAE,qCAAqC;IAC9D,MAAM,EAAE,eAAe;IACvB,sBAAsB,EAAE,eAAe;IACvC,uBAAuB,EAAE,eAAe;IACxC,uBAAuB,EAAE,eAAe;IACxC,KAAK,EAAE,iBAAiB;IACxB,MAAM,EAAE,SAAS;CACuB,CAAC;AAE1C,qFAAqF;AACrF,MAAM,6BAA6B,GAAG;IACrC,kBAAkB,EAAE,aAAa;CACmB,CAAC;AAEtD,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACtC,GAAG,8BAA8B;IACjC,GAAG,6BAA6B;CACvB,CAAC","sourcesContent":["import type { KnownProvider } from \"omk-ai\";\n\n/** Default model IDs for every built-in provider. */\nconst builtInDefaultModelPerProvider = {\n\t\"amazon-bedrock\": \"us.anthropic.claude-opus-4-6-v1\",\n\t\"ant-ling\": \"Ring-2.6-1T\",\n\tanthropic: \"claude-opus-4-8\",\n\topenai: \"gpt-5.4\",\n\t\"azure-openai-responses\": \"gpt-5.4\",\n\t\"openai-codex\": \"gpt-5.5\",\n\tnvidia: \"nvidia/nemotron-3-super-120b-a12b\",\n\tdeepseek: \"deepseek-v4-pro\",\n\tdevin: \"swe-2\",\n\tmeta: \"muse-spark-1.3\",\n\tgoogle: \"gemini-3.1-pro-preview\",\n\t\"google-vertex\": \"gemini-3.1-pro-preview\",\n\t\"github-copilot\": \"gpt-5.4\",\n\topenrouter: \"moonshotai/kimi-k2.6\",\n\t\"vercel-ai-gateway\": \"zai/glm-5.1\",\n\txai: \"grok-4.20-0309-reasoning\",\n\tgroq: \"openai/gpt-oss-120b\",\n\tcerebras: \"zai-glm-4.7\",\n\tzai: \"glm-5.1\",\n\t\"zai-coding-cn\": \"glm-5.1\",\n\tmistral: \"devstral-medium-latest\",\n\tminimax: \"MiniMax-M2.7\",\n\t\"minimax-cn\": \"MiniMax-M2.7\",\n\tmoonshotai: \"kimi-k2.6\",\n\t\"moonshotai-cn\": \"kimi-k2.6\",\n\thuggingface: \"moonshotai/Kimi-K2.6\",\n\tfireworks: \"accounts/fireworks/models/kimi-k2p6\",\n\ttogether: \"moonshotai/Kimi-K2.6\",\n\topencode: \"kimi-k2.6\",\n\t\"opencode-go\": \"kimi-k2.6\",\n\t\"kimi-coding\": \"k3\",\n\t\"cloudflare-workers-ai\": \"@cf/moonshotai/kimi-k2.6\",\n\t\"cloudflare-ai-gateway\": \"workers-ai/@cf/moonshotai/kimi-k2.6\",\n\txiaomi: \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-cn\": \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-ams\": \"mimo-v2.5-pro\",\n\t\"xiaomi-token-plan-sgp\": \"mimo-v2.5-pro\",\n\tzyloo: \"claude-opus-4-7\",\n\tcursor: \"default\",\n} satisfies Record<KnownProvider, string>;\n\n/** Recommended defaults for providers supplied through models.json or extensions. */\nconst customDefaultModelPerProvider = {\n\t\"modelstudio-maas\": \"qwen3.8-max\",\n} as const satisfies Readonly<Record<string, string>>;\n\nexport const defaultModelPerProvider = {\n\t...builtInDefaultModelPerProvider,\n\t...customDefaultModelPerProvider,\n} as const;\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"provider-display-names.d.ts","sourceRoot":"","sources":["../../src/core/provider-display-names.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAqClE,CAAC","sourcesContent":["export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {\n\tanthropic: \"Anthropic\",\n\t\"amazon-bedrock\": \"Amazon Bedrock\",\n\t\"ant-ling\": \"Ant Ling\",\n\t\"azure-openai-responses\": \"Azure OpenAI Responses\",\n\tcerebras: \"Cerebras\",\n\t\"cloudflare-ai-gateway\": \"Cloudflare AI Gateway\",\n\t\"cloudflare-workers-ai\": \"Cloudflare Workers AI\",\n\tdeepseek: \"DeepSeek\",\n\tdevin: \"Devin CLI session token\",\n\tfireworks: \"Fireworks\",\n\tgoogle: \"Google Gemini\",\n\t\"google-vertex\": \"Google Vertex AI\",\n\tgroq: \"Groq\",\n\thuggingface: \"Hugging Face\",\n\t\"kimi-coding\": \"Kimi For Coding\",\n\tmeta: \"Meta Model API\",\n\tmistral: \"Mistral\",\n\tminimax: \"MiniMax\",\n\t\"minimax-cn\": \"MiniMax (China)\",\n\tmoonshotai: \"Moonshot AI\",\n\t\"moonshotai-cn\": \"Moonshot AI (China)\",\n\tnvidia: \"NVIDIA NIM\",\n\topencode: \"OpenCode Zen\",\n\t\"opencode-go\": \"OpenCode Go\",\n\topenai: \"OpenAI\",\n\topenrouter: \"OpenRouter\",\n\ttogether: \"Together AI\",\n\t\"vercel-ai-gateway\": \"Vercel AI Gateway\",\n\txai: \"xAI\",\n\tzai: \"ZAI\",\n\t\"zai-coding-cn\": \"ZAI Coding Plan (China)\",\n\txiaomi: \"Xiaomi MiMo\",\n\t\"xiaomi-token-plan-cn\": \"Xiaomi MiMo Token Plan (China)\",\n\t\"xiaomi-token-plan-ams\": \"Xiaomi MiMo Token Plan (Amsterdam)\",\n\t\"xiaomi-token-plan-sgp\": \"Xiaomi MiMo Token Plan (Singapore)\",\n\tzyloo: \"Zyloo\",\n};\n"]}
1
+ {"version":3,"file":"provider-display-names.d.ts","sourceRoot":"","sources":["../../src/core/provider-display-names.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAsClE,CAAC","sourcesContent":["export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {\n\tanthropic: \"Anthropic\",\n\t\"amazon-bedrock\": \"Amazon Bedrock\",\n\t\"ant-ling\": \"Ant Ling\",\n\t\"azure-openai-responses\": \"Azure OpenAI Responses\",\n\tcerebras: \"Cerebras\",\n\t\"cloudflare-ai-gateway\": \"Cloudflare AI Gateway\",\n\t\"cloudflare-workers-ai\": \"Cloudflare Workers AI\",\n\tdeepseek: \"DeepSeek\",\n\tdevin: \"Devin CLI session token\",\n\tcursor: \"Cursor subscription\",\n\tfireworks: \"Fireworks\",\n\tgoogle: \"Google Gemini\",\n\t\"google-vertex\": \"Google Vertex AI\",\n\tgroq: \"Groq\",\n\thuggingface: \"Hugging Face\",\n\t\"kimi-coding\": \"Kimi For Coding\",\n\tmeta: \"Meta Model API\",\n\tmistral: \"Mistral\",\n\tminimax: \"MiniMax\",\n\t\"minimax-cn\": \"MiniMax (China)\",\n\tmoonshotai: \"Moonshot AI\",\n\t\"moonshotai-cn\": \"Moonshot AI (China)\",\n\tnvidia: \"NVIDIA NIM\",\n\topencode: \"OpenCode Zen\",\n\t\"opencode-go\": \"OpenCode Go\",\n\topenai: \"OpenAI\",\n\topenrouter: \"OpenRouter\",\n\ttogether: \"Together AI\",\n\t\"vercel-ai-gateway\": \"Vercel AI Gateway\",\n\txai: \"xAI\",\n\tzai: \"ZAI\",\n\t\"zai-coding-cn\": \"ZAI Coding Plan (China)\",\n\txiaomi: \"Xiaomi MiMo\",\n\t\"xiaomi-token-plan-cn\": \"Xiaomi MiMo Token Plan (China)\",\n\t\"xiaomi-token-plan-ams\": \"Xiaomi MiMo Token Plan (Amsterdam)\",\n\t\"xiaomi-token-plan-sgp\": \"Xiaomi MiMo Token Plan (Singapore)\",\n\tzyloo: \"Zyloo\",\n};\n"]}
@@ -8,6 +8,7 @@ export const BUILT_IN_PROVIDER_DISPLAY_NAMES = {
8
8
  "cloudflare-workers-ai": "Cloudflare Workers AI",
9
9
  deepseek: "DeepSeek",
10
10
  devin: "Devin CLI session token",
11
+ cursor: "Cursor subscription",
11
12
  fireworks: "Fireworks",
12
13
  google: "Google Gemini",
13
14
  "google-vertex": "Google Vertex AI",
@@ -1 +1 @@
1
- {"version":3,"file":"provider-display-names.js","sourceRoot":"","sources":["../../src/core/provider-display-names.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,+BAA+B,GAA2B;IACtE,SAAS,EAAE,WAAW;IACtB,gBAAgB,EAAE,gBAAgB;IAClC,UAAU,EAAE,UAAU;IACtB,wBAAwB,EAAE,wBAAwB;IAClD,QAAQ,EAAE,UAAU;IACpB,uBAAuB,EAAE,uBAAuB;IAChD,uBAAuB,EAAE,uBAAuB;IAChD,QAAQ,EAAE,UAAU;IACpB,KAAK,EAAE,yBAAyB;IAChC,SAAS,EAAE,WAAW;IACtB,MAAM,EAAE,eAAe;IACvB,eAAe,EAAE,kBAAkB;IACnC,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,cAAc;IAC3B,aAAa,EAAE,iBAAiB;IAChC,IAAI,EAAE,gBAAgB;IACtB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,iBAAiB;IAC/B,UAAU,EAAE,aAAa;IACzB,eAAe,EAAE,qBAAqB;IACtC,MAAM,EAAE,YAAY;IACpB,QAAQ,EAAE,cAAc;IACxB,aAAa,EAAE,aAAa;IAC5B,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,YAAY;IACxB,QAAQ,EAAE,aAAa;IACvB,mBAAmB,EAAE,mBAAmB;IACxC,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,KAAK;IACV,eAAe,EAAE,yBAAyB;IAC1C,MAAM,EAAE,aAAa;IACrB,sBAAsB,EAAE,gCAAgC;IACxD,uBAAuB,EAAE,oCAAoC;IAC7D,uBAAuB,EAAE,oCAAoC;IAC7D,KAAK,EAAE,OAAO;CACd,CAAC","sourcesContent":["export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {\n\tanthropic: \"Anthropic\",\n\t\"amazon-bedrock\": \"Amazon Bedrock\",\n\t\"ant-ling\": \"Ant Ling\",\n\t\"azure-openai-responses\": \"Azure OpenAI Responses\",\n\tcerebras: \"Cerebras\",\n\t\"cloudflare-ai-gateway\": \"Cloudflare AI Gateway\",\n\t\"cloudflare-workers-ai\": \"Cloudflare Workers AI\",\n\tdeepseek: \"DeepSeek\",\n\tdevin: \"Devin CLI session token\",\n\tfireworks: \"Fireworks\",\n\tgoogle: \"Google Gemini\",\n\t\"google-vertex\": \"Google Vertex AI\",\n\tgroq: \"Groq\",\n\thuggingface: \"Hugging Face\",\n\t\"kimi-coding\": \"Kimi For Coding\",\n\tmeta: \"Meta Model API\",\n\tmistral: \"Mistral\",\n\tminimax: \"MiniMax\",\n\t\"minimax-cn\": \"MiniMax (China)\",\n\tmoonshotai: \"Moonshot AI\",\n\t\"moonshotai-cn\": \"Moonshot AI (China)\",\n\tnvidia: \"NVIDIA NIM\",\n\topencode: \"OpenCode Zen\",\n\t\"opencode-go\": \"OpenCode Go\",\n\topenai: \"OpenAI\",\n\topenrouter: \"OpenRouter\",\n\ttogether: \"Together AI\",\n\t\"vercel-ai-gateway\": \"Vercel AI Gateway\",\n\txai: \"xAI\",\n\tzai: \"ZAI\",\n\t\"zai-coding-cn\": \"ZAI Coding Plan (China)\",\n\txiaomi: \"Xiaomi MiMo\",\n\t\"xiaomi-token-plan-cn\": \"Xiaomi MiMo Token Plan (China)\",\n\t\"xiaomi-token-plan-ams\": \"Xiaomi MiMo Token Plan (Amsterdam)\",\n\t\"xiaomi-token-plan-sgp\": \"Xiaomi MiMo Token Plan (Singapore)\",\n\tzyloo: \"Zyloo\",\n};\n"]}
1
+ {"version":3,"file":"provider-display-names.js","sourceRoot":"","sources":["../../src/core/provider-display-names.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,+BAA+B,GAA2B;IACtE,SAAS,EAAE,WAAW;IACtB,gBAAgB,EAAE,gBAAgB;IAClC,UAAU,EAAE,UAAU;IACtB,wBAAwB,EAAE,wBAAwB;IAClD,QAAQ,EAAE,UAAU;IACpB,uBAAuB,EAAE,uBAAuB;IAChD,uBAAuB,EAAE,uBAAuB;IAChD,QAAQ,EAAE,UAAU;IACpB,KAAK,EAAE,yBAAyB;IAChC,MAAM,EAAE,qBAAqB;IAC7B,SAAS,EAAE,WAAW;IACtB,MAAM,EAAE,eAAe;IACvB,eAAe,EAAE,kBAAkB;IACnC,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,cAAc;IAC3B,aAAa,EAAE,iBAAiB;IAChC,IAAI,EAAE,gBAAgB;IACtB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,iBAAiB;IAC/B,UAAU,EAAE,aAAa;IACzB,eAAe,EAAE,qBAAqB;IACtC,MAAM,EAAE,YAAY;IACpB,QAAQ,EAAE,cAAc;IACxB,aAAa,EAAE,aAAa;IAC5B,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,YAAY;IACxB,QAAQ,EAAE,aAAa;IACvB,mBAAmB,EAAE,mBAAmB;IACxC,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,KAAK;IACV,eAAe,EAAE,yBAAyB;IAC1C,MAAM,EAAE,aAAa;IACrB,sBAAsB,EAAE,gCAAgC;IACxD,uBAAuB,EAAE,oCAAoC;IAC7D,uBAAuB,EAAE,oCAAoC;IAC7D,KAAK,EAAE,OAAO;CACd,CAAC","sourcesContent":["export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {\n\tanthropic: \"Anthropic\",\n\t\"amazon-bedrock\": \"Amazon Bedrock\",\n\t\"ant-ling\": \"Ant Ling\",\n\t\"azure-openai-responses\": \"Azure OpenAI Responses\",\n\tcerebras: \"Cerebras\",\n\t\"cloudflare-ai-gateway\": \"Cloudflare AI Gateway\",\n\t\"cloudflare-workers-ai\": \"Cloudflare Workers AI\",\n\tdeepseek: \"DeepSeek\",\n\tdevin: \"Devin CLI session token\",\n\tcursor: \"Cursor subscription\",\n\tfireworks: \"Fireworks\",\n\tgoogle: \"Google Gemini\",\n\t\"google-vertex\": \"Google Vertex AI\",\n\tgroq: \"Groq\",\n\thuggingface: \"Hugging Face\",\n\t\"kimi-coding\": \"Kimi For Coding\",\n\tmeta: \"Meta Model API\",\n\tmistral: \"Mistral\",\n\tminimax: \"MiniMax\",\n\t\"minimax-cn\": \"MiniMax (China)\",\n\tmoonshotai: \"Moonshot AI\",\n\t\"moonshotai-cn\": \"Moonshot AI (China)\",\n\tnvidia: \"NVIDIA NIM\",\n\topencode: \"OpenCode Zen\",\n\t\"opencode-go\": \"OpenCode Go\",\n\topenai: \"OpenAI\",\n\topenrouter: \"OpenRouter\",\n\ttogether: \"Together AI\",\n\t\"vercel-ai-gateway\": \"Vercel AI Gateway\",\n\txai: \"xAI\",\n\tzai: \"ZAI\",\n\t\"zai-coding-cn\": \"ZAI Coding Plan (China)\",\n\txiaomi: \"Xiaomi MiMo\",\n\t\"xiaomi-token-plan-cn\": \"Xiaomi MiMo Token Plan (China)\",\n\t\"xiaomi-token-plan-ams\": \"Xiaomi MiMo Token Plan (Amsterdam)\",\n\t\"xiaomi-token-plan-sgp\": \"Xiaomi MiMo Token Plan (Singapore)\",\n\tzyloo: \"Zyloo\",\n};\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Typed provider-error classification (audit §17): structured signals win over
3
+ * free text. The precedence is
4
+ *
5
+ * transport/status/providerCode → typed category → conservative text
6
+ * fallback
7
+ *
8
+ * A bare string regex cannot tell a retryable "400 tool_call_id is not found"
9
+ * from a permanent "400 invalid_request_error", and a stray "500" inside a
10
+ * message body is not a status code. Callers that only have a message keep the
11
+ * {@link classifyProviderError} text fallback; callers that can also pass
12
+ * transport fields should prefer {@link classifyProviderError} over
13
+ * {@link isTransientProviderErrorMessage}.
14
+ */
15
+ export type ProviderErrorKind =
16
+ /** Network cut, upstream 5xx, dropped stream, overload — heals by backoff/retry/failover. */
17
+ "transient_transport"
18
+ /** Provider-directed throttling; honor Retry-After inside the run deadline. */
19
+ | "rate_limit"
20
+ /** Quota/billing exhaustion — same-model retry is useless until reset; failover only. */
21
+ | "quota_exhausted"
22
+ /** Permanent auth/permission failure — never auto-repeat on the same credential. */
23
+ | "auth"
24
+ /** Permanent request/model/config fault — retrying resends the same rejection. */
25
+ | "permanent_request"
26
+ /** Provider refusal/policy stop — a separate state, not a transport failure. */
27
+ | "refusal"
28
+ /** Transcript-shape fault that heals after sanitize+retry (e.g. orphan tool_call_id). */
29
+ | "transcript_shape"
30
+ /** No category assigned — caller treats as non-retryable. */
31
+ | "unclassified";
32
+ export interface ProviderErrorClassification {
33
+ readonly kind: ProviderErrorKind;
34
+ /** Retry the same route inside the shared run budget. */
35
+ readonly retryable: boolean;
36
+ /** Failing over to another authorized route may save the turn. */
37
+ readonly failoverEligible: boolean;
38
+ /** Which signal decided the classification. */
39
+ readonly basis: "status" | "provider_code" | "known_permanent" | "text_fallback" | "none";
40
+ }
41
+ export interface ProviderErrorInput {
42
+ /** Free-text error message (may be the only signal available). */
43
+ readonly text?: string;
44
+ /** HTTP status code when the caller has one. */
45
+ readonly status?: number;
46
+ /** Structured provider error type, e.g. `invalid_request_error`. */
47
+ readonly errorType?: string;
48
+ /** Structured provider error code, e.g. `claude_code_version_too_old`. */
49
+ readonly errorCode?: string;
50
+ }
51
+ /**
52
+ * Classify a provider failure. Structured fields are consulted first; a text
53
+ * fallback keeps the observed-provider patterns (the same precedence
54
+ * {@link providerFailureCause} uses) when nothing else is available.
55
+ */
56
+ export declare function classifyProviderError(input: ProviderErrorInput): ProviderErrorClassification;
57
+ //# sourceMappingURL=provider-error-classification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-error-classification.d.ts","sourceRoot":"","sources":["../../src/core/provider-error-classification.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,MAAM,MAAM,iBAAiB;AAC5B,+FAA6F;AAC3F,qBAAqB;AACvB,+EAA+E;GAC7E,YAAY;AACd,2FAAyF;GACvF,iBAAiB;AACnB,sFAAoF;GAClF,MAAM;AACR,oFAAkF;GAChF,mBAAmB;AACrB,kFAAgF;GAC9E,SAAS;AACX,yFAAyF;GACvF,kBAAkB;AACpB,+DAA6D;GAC3D,cAAc,CAAC;AAElB,MAAM,WAAW,2BAA2B;IAC3C,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,eAAe,GAAG,iBAAiB,GAAG,eAAe,GAAG,MAAM,CAAC;CAC1F;AAED,MAAM,WAAW,kBAAkB;IAClC,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAwBD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG,2BAA2B,CAkE5F","sourcesContent":["/**\n * Typed provider-error classification (audit §17): structured signals win over\n * free text. The precedence is\n *\n * transport/status/providerCode → typed category → conservative text\n * fallback\n *\n * A bare string regex cannot tell a retryable \"400 tool_call_id is not found\"\n * from a permanent \"400 invalid_request_error\", and a stray \"500\" inside a\n * message body is not a status code. Callers that only have a message keep the\n * {@link classifyProviderError} text fallback; callers that can also pass\n * transport fields should prefer {@link classifyProviderError} over\n * {@link isTransientProviderErrorMessage}.\n */\n\nimport {\n\tisClaudeCodeVersionTooOldMessage,\n\tisCodexChatgptAccountUnsupportedModelMessage,\n\tisContentSafetyStopMessage,\n\tisOrphanToolCallIdError,\n\tisQuotaExhaustionMessage,\n\tisTransientProviderErrorMessage,\n\tisUpstreamUnavailableMessage,\n} from \"./provider-resilience.ts\";\n\nexport type ProviderErrorKind =\n\t/** Network cut, upstream 5xx, dropped stream, overload — heals by backoff/retry/failover. */\n\t| \"transient_transport\"\n\t/** Provider-directed throttling; honor Retry-After inside the run deadline. */\n\t| \"rate_limit\"\n\t/** Quota/billing exhaustion — same-model retry is useless until reset; failover only. */\n\t| \"quota_exhausted\"\n\t/** Permanent auth/permission failure — never auto-repeat on the same credential. */\n\t| \"auth\"\n\t/** Permanent request/model/config fault — retrying resends the same rejection. */\n\t| \"permanent_request\"\n\t/** Provider refusal/policy stop — a separate state, not a transport failure. */\n\t| \"refusal\"\n\t/** Transcript-shape fault that heals after sanitize+retry (e.g. orphan tool_call_id). */\n\t| \"transcript_shape\"\n\t/** No category assigned — caller treats as non-retryable. */\n\t| \"unclassified\";\n\nexport interface ProviderErrorClassification {\n\treadonly kind: ProviderErrorKind;\n\t/** Retry the same route inside the shared run budget. */\n\treadonly retryable: boolean;\n\t/** Failing over to another authorized route may save the turn. */\n\treadonly failoverEligible: boolean;\n\t/** Which signal decided the classification. */\n\treadonly basis: \"status\" | \"provider_code\" | \"known_permanent\" | \"text_fallback\" | \"none\";\n}\n\nexport interface ProviderErrorInput {\n\t/** Free-text error message (may be the only signal available). */\n\treadonly text?: string;\n\t/** HTTP status code when the caller has one. */\n\treadonly status?: number;\n\t/** Structured provider error type, e.g. `invalid_request_error`. */\n\treadonly errorType?: string;\n\t/** Structured provider error code, e.g. `claude_code_version_too_old`. */\n\treadonly errorCode?: string;\n}\n\nconst PERMANENT_PROVIDER_CODES = new Set([\n\t\"claude_code_version_too_old\",\n\t\"unsupported_model\",\n\t\"model_not_supported\",\n\t\"invalid_api_key\",\n\t\"authentication_error\",\n\t\"permission_error\",\n]);\n\nconst TRANSIENT_PROVIDER_CODES = new Set([\n\t\"overloaded_error\",\n\t\"rate_limit_error\",\n\t\"timeout\",\n\t\"server_error\",\n\t\"service_unavailable\",\n\t\"upstream_error\",\n]);\n\nfunction isAuthText(text: string): boolean {\n\treturn /auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\\/login/i.test(text);\n}\n\n/**\n * Classify a provider failure. Structured fields are consulted first; a text\n * fallback keeps the observed-provider patterns (the same precedence\n * {@link providerFailureCause} uses) when nothing else is available.\n */\nexport function classifyProviderError(input: ProviderErrorInput): ProviderErrorClassification {\n\tconst text = input.text ?? \"\";\n\n\t// Known permanent bodies beat every other signal: they carry\n\t// `invalid_request_error` or a 400, but no retry fixes a stale client\n\t// version or an account that lacks the model.\n\tif (isClaudeCodeVersionTooOldMessage(text) || isCodexChatgptAccountUnsupportedModelMessage(text)) {\n\t\treturn { kind: \"permanent_request\", retryable: false, failoverEligible: false, basis: \"known_permanent\" };\n\t}\n\n\tif (input.errorCode !== undefined) {\n\t\tconst code = input.errorCode.toLowerCase();\n\t\tif (PERMANENT_PROVIDER_CODES.has(code)) {\n\t\t\treturn { kind: \"permanent_request\", retryable: false, failoverEligible: false, basis: \"provider_code\" };\n\t\t}\n\t\tif (TRANSIENT_PROVIDER_CODES.has(code)) {\n\t\t\treturn { kind: \"transient_transport\", retryable: true, failoverEligible: true, basis: \"provider_code\" };\n\t\t}\n\t}\n\n\tif (input.status !== undefined) {\n\t\tif (input.status === 429) {\n\t\t\treturn { kind: \"rate_limit\", retryable: true, failoverEligible: true, basis: \"status\" };\n\t\t}\n\t\tif (input.status >= 500 && input.status <= 599) {\n\t\t\treturn { kind: \"transient_transport\", retryable: true, failoverEligible: true, basis: \"status\" };\n\t\t}\n\t\tif (input.status === 401 || input.status === 403) {\n\t\t\t// A 403 quota body still wins over the auth default.\n\t\t\tif (isQuotaExhaustionMessage(text)) {\n\t\t\t\treturn { kind: \"quota_exhausted\", retryable: false, failoverEligible: true, basis: \"text_fallback\" };\n\t\t\t}\n\t\t\treturn { kind: \"auth\", retryable: false, failoverEligible: false, basis: \"status\" };\n\t\t}\n\t\tif (input.status === 402) {\n\t\t\treturn { kind: \"quota_exhausted\", retryable: false, failoverEligible: true, basis: \"status\" };\n\t\t}\n\t\tif (input.status === 400 || input.status === 404 || input.status === 422) {\n\t\t\t// A 4xx is permanent unless a known-healing transcript shape explains\n\t\t\t// it — `tool_call_id is not found` is the only observed exception.\n\t\t\tif (isOrphanToolCallIdError(text)) {\n\t\t\t\treturn { kind: \"transcript_shape\", retryable: true, failoverEligible: true, basis: \"text_fallback\" };\n\t\t\t}\n\t\t\treturn { kind: \"permanent_request\", retryable: false, failoverEligible: false, basis: \"status\" };\n\t\t}\n\t}\n\n\tif (isContentSafetyStopMessage(text)) {\n\t\treturn { kind: \"refusal\", retryable: false, failoverEligible: true, basis: \"text_fallback\" };\n\t}\n\tif (isQuotaExhaustionMessage(text)) {\n\t\treturn { kind: \"quota_exhausted\", retryable: false, failoverEligible: true, basis: \"text_fallback\" };\n\t}\n\tif (isUpstreamUnavailableMessage(text)) {\n\t\treturn { kind: \"transient_transport\", retryable: true, failoverEligible: true, basis: \"text_fallback\" };\n\t}\n\tif (isOrphanToolCallIdError(text)) {\n\t\treturn { kind: \"transcript_shape\", retryable: true, failoverEligible: true, basis: \"text_fallback\" };\n\t}\n\tif (isAuthText(text)) {\n\t\treturn { kind: \"auth\", retryable: false, failoverEligible: false, basis: \"text_fallback\" };\n\t}\n\tif (isTransientProviderErrorMessage(text)) {\n\t\treturn { kind: \"transient_transport\", retryable: true, failoverEligible: true, basis: \"text_fallback\" };\n\t}\n\treturn { kind: \"unclassified\", retryable: false, failoverEligible: false, basis: \"none\" };\n}\n"]}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Typed provider-error classification (audit §17): structured signals win over
3
+ * free text. The precedence is
4
+ *
5
+ * transport/status/providerCode → typed category → conservative text
6
+ * fallback
7
+ *
8
+ * A bare string regex cannot tell a retryable "400 tool_call_id is not found"
9
+ * from a permanent "400 invalid_request_error", and a stray "500" inside a
10
+ * message body is not a status code. Callers that only have a message keep the
11
+ * {@link classifyProviderError} text fallback; callers that can also pass
12
+ * transport fields should prefer {@link classifyProviderError} over
13
+ * {@link isTransientProviderErrorMessage}.
14
+ */
15
+ import { isClaudeCodeVersionTooOldMessage, isCodexChatgptAccountUnsupportedModelMessage, isContentSafetyStopMessage, isOrphanToolCallIdError, isQuotaExhaustionMessage, isTransientProviderErrorMessage, isUpstreamUnavailableMessage, } from "./provider-resilience.js";
16
+ const PERMANENT_PROVIDER_CODES = new Set([
17
+ "claude_code_version_too_old",
18
+ "unsupported_model",
19
+ "model_not_supported",
20
+ "invalid_api_key",
21
+ "authentication_error",
22
+ "permission_error",
23
+ ]);
24
+ const TRANSIENT_PROVIDER_CODES = new Set([
25
+ "overloaded_error",
26
+ "rate_limit_error",
27
+ "timeout",
28
+ "server_error",
29
+ "service_unavailable",
30
+ "upstream_error",
31
+ ]);
32
+ function isAuthText(text) {
33
+ return /auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\/login/i.test(text);
34
+ }
35
+ /**
36
+ * Classify a provider failure. Structured fields are consulted first; a text
37
+ * fallback keeps the observed-provider patterns (the same precedence
38
+ * {@link providerFailureCause} uses) when nothing else is available.
39
+ */
40
+ export function classifyProviderError(input) {
41
+ const text = input.text ?? "";
42
+ // Known permanent bodies beat every other signal: they carry
43
+ // `invalid_request_error` or a 400, but no retry fixes a stale client
44
+ // version or an account that lacks the model.
45
+ if (isClaudeCodeVersionTooOldMessage(text) || isCodexChatgptAccountUnsupportedModelMessage(text)) {
46
+ return { kind: "permanent_request", retryable: false, failoverEligible: false, basis: "known_permanent" };
47
+ }
48
+ if (input.errorCode !== undefined) {
49
+ const code = input.errorCode.toLowerCase();
50
+ if (PERMANENT_PROVIDER_CODES.has(code)) {
51
+ return { kind: "permanent_request", retryable: false, failoverEligible: false, basis: "provider_code" };
52
+ }
53
+ if (TRANSIENT_PROVIDER_CODES.has(code)) {
54
+ return { kind: "transient_transport", retryable: true, failoverEligible: true, basis: "provider_code" };
55
+ }
56
+ }
57
+ if (input.status !== undefined) {
58
+ if (input.status === 429) {
59
+ return { kind: "rate_limit", retryable: true, failoverEligible: true, basis: "status" };
60
+ }
61
+ if (input.status >= 500 && input.status <= 599) {
62
+ return { kind: "transient_transport", retryable: true, failoverEligible: true, basis: "status" };
63
+ }
64
+ if (input.status === 401 || input.status === 403) {
65
+ // A 403 quota body still wins over the auth default.
66
+ if (isQuotaExhaustionMessage(text)) {
67
+ return { kind: "quota_exhausted", retryable: false, failoverEligible: true, basis: "text_fallback" };
68
+ }
69
+ return { kind: "auth", retryable: false, failoverEligible: false, basis: "status" };
70
+ }
71
+ if (input.status === 402) {
72
+ return { kind: "quota_exhausted", retryable: false, failoverEligible: true, basis: "status" };
73
+ }
74
+ if (input.status === 400 || input.status === 404 || input.status === 422) {
75
+ // A 4xx is permanent unless a known-healing transcript shape explains
76
+ // it — `tool_call_id is not found` is the only observed exception.
77
+ if (isOrphanToolCallIdError(text)) {
78
+ return { kind: "transcript_shape", retryable: true, failoverEligible: true, basis: "text_fallback" };
79
+ }
80
+ return { kind: "permanent_request", retryable: false, failoverEligible: false, basis: "status" };
81
+ }
82
+ }
83
+ if (isContentSafetyStopMessage(text)) {
84
+ return { kind: "refusal", retryable: false, failoverEligible: true, basis: "text_fallback" };
85
+ }
86
+ if (isQuotaExhaustionMessage(text)) {
87
+ return { kind: "quota_exhausted", retryable: false, failoverEligible: true, basis: "text_fallback" };
88
+ }
89
+ if (isUpstreamUnavailableMessage(text)) {
90
+ return { kind: "transient_transport", retryable: true, failoverEligible: true, basis: "text_fallback" };
91
+ }
92
+ if (isOrphanToolCallIdError(text)) {
93
+ return { kind: "transcript_shape", retryable: true, failoverEligible: true, basis: "text_fallback" };
94
+ }
95
+ if (isAuthText(text)) {
96
+ return { kind: "auth", retryable: false, failoverEligible: false, basis: "text_fallback" };
97
+ }
98
+ if (isTransientProviderErrorMessage(text)) {
99
+ return { kind: "transient_transport", retryable: true, failoverEligible: true, basis: "text_fallback" };
100
+ }
101
+ return { kind: "unclassified", retryable: false, failoverEligible: false, basis: "none" };
102
+ }
103
+ //# sourceMappingURL=provider-error-classification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-error-classification.js","sourceRoot":"","sources":["../../src/core/provider-error-classification.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Typed provider-error classification (audit §17): structured signals win over\n * free text. The precedence is\n *\n * transport/status/providerCode → typed category → conservative text\n * fallback\n *\n * A bare string regex cannot tell a retryable \"400 tool_call_id is not found\"\n * from a permanent \"400 invalid_request_error\", and a stray \"500\" inside a\n * message body is not a status code. Callers that only have a message keep the\n * {@link classifyProviderError} text fallback; callers that can also pass\n * transport fields should prefer {@link classifyProviderError} over\n * {@link isTransientProviderErrorMessage}.\n */\n\nimport {\n\tisClaudeCodeVersionTooOldMessage,\n\tisCodexChatgptAccountUnsupportedModelMessage,\n\tisContentSafetyStopMessage,\n\tisOrphanToolCallIdError,\n\tisQuotaExhaustionMessage,\n\tisTransientProviderErrorMessage,\n\tisUpstreamUnavailableMessage,\n} from \"./provider-resilience.ts\";\n\nexport type ProviderErrorKind =\n\t/** Network cut, upstream 5xx, dropped stream, overload — heals by backoff/retry/failover. */\n\t| \"transient_transport\"\n\t/** Provider-directed throttling; honor Retry-After inside the run deadline. */\n\t| \"rate_limit\"\n\t/** Quota/billing exhaustion — same-model retry is useless until reset; failover only. */\n\t| \"quota_exhausted\"\n\t/** Permanent auth/permission failure — never auto-repeat on the same credential. */\n\t| \"auth\"\n\t/** Permanent request/model/config fault — retrying resends the same rejection. */\n\t| \"permanent_request\"\n\t/** Provider refusal/policy stop — a separate state, not a transport failure. */\n\t| \"refusal\"\n\t/** Transcript-shape fault that heals after sanitize+retry (e.g. orphan tool_call_id). */\n\t| \"transcript_shape\"\n\t/** No category assigned — caller treats as non-retryable. */\n\t| \"unclassified\";\n\nexport interface ProviderErrorClassification {\n\treadonly kind: ProviderErrorKind;\n\t/** Retry the same route inside the shared run budget. */\n\treadonly retryable: boolean;\n\t/** Failing over to another authorized route may save the turn. */\n\treadonly failoverEligible: boolean;\n\t/** Which signal decided the classification. */\n\treadonly basis: \"status\" | \"provider_code\" | \"known_permanent\" | \"text_fallback\" | \"none\";\n}\n\nexport interface ProviderErrorInput {\n\t/** Free-text error message (may be the only signal available). */\n\treadonly text?: string;\n\t/** HTTP status code when the caller has one. */\n\treadonly status?: number;\n\t/** Structured provider error type, e.g. `invalid_request_error`. */\n\treadonly errorType?: string;\n\t/** Structured provider error code, e.g. `claude_code_version_too_old`. */\n\treadonly errorCode?: string;\n}\n\nconst PERMANENT_PROVIDER_CODES = new Set([\n\t\"claude_code_version_too_old\",\n\t\"unsupported_model\",\n\t\"model_not_supported\",\n\t\"invalid_api_key\",\n\t\"authentication_error\",\n\t\"permission_error\",\n]);\n\nconst TRANSIENT_PROVIDER_CODES = new Set([\n\t\"overloaded_error\",\n\t\"rate_limit_error\",\n\t\"timeout\",\n\t\"server_error\",\n\t\"service_unavailable\",\n\t\"upstream_error\",\n]);\n\nfunction isAuthText(text: string): boolean {\n\treturn /auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\\/login/i.test(text);\n}\n\n/**\n * Classify a provider failure. Structured fields are consulted first; a text\n * fallback keeps the observed-provider patterns (the same precedence\n * {@link providerFailureCause} uses) when nothing else is available.\n */\nexport function classifyProviderError(input: ProviderErrorInput): ProviderErrorClassification {\n\tconst text = input.text ?? \"\";\n\n\t// Known permanent bodies beat every other signal: they carry\n\t// `invalid_request_error` or a 400, but no retry fixes a stale client\n\t// version or an account that lacks the model.\n\tif (isClaudeCodeVersionTooOldMessage(text) || isCodexChatgptAccountUnsupportedModelMessage(text)) {\n\t\treturn { kind: \"permanent_request\", retryable: false, failoverEligible: false, basis: \"known_permanent\" };\n\t}\n\n\tif (input.errorCode !== undefined) {\n\t\tconst code = input.errorCode.toLowerCase();\n\t\tif (PERMANENT_PROVIDER_CODES.has(code)) {\n\t\t\treturn { kind: \"permanent_request\", retryable: false, failoverEligible: false, basis: \"provider_code\" };\n\t\t}\n\t\tif (TRANSIENT_PROVIDER_CODES.has(code)) {\n\t\t\treturn { kind: \"transient_transport\", retryable: true, failoverEligible: true, basis: \"provider_code\" };\n\t\t}\n\t}\n\n\tif (input.status !== undefined) {\n\t\tif (input.status === 429) {\n\t\t\treturn { kind: \"rate_limit\", retryable: true, failoverEligible: true, basis: \"status\" };\n\t\t}\n\t\tif (input.status >= 500 && input.status <= 599) {\n\t\t\treturn { kind: \"transient_transport\", retryable: true, failoverEligible: true, basis: \"status\" };\n\t\t}\n\t\tif (input.status === 401 || input.status === 403) {\n\t\t\t// A 403 quota body still wins over the auth default.\n\t\t\tif (isQuotaExhaustionMessage(text)) {\n\t\t\t\treturn { kind: \"quota_exhausted\", retryable: false, failoverEligible: true, basis: \"text_fallback\" };\n\t\t\t}\n\t\t\treturn { kind: \"auth\", retryable: false, failoverEligible: false, basis: \"status\" };\n\t\t}\n\t\tif (input.status === 402) {\n\t\t\treturn { kind: \"quota_exhausted\", retryable: false, failoverEligible: true, basis: \"status\" };\n\t\t}\n\t\tif (input.status === 400 || input.status === 404 || input.status === 422) {\n\t\t\t// A 4xx is permanent unless a known-healing transcript shape explains\n\t\t\t// it — `tool_call_id is not found` is the only observed exception.\n\t\t\tif (isOrphanToolCallIdError(text)) {\n\t\t\t\treturn { kind: \"transcript_shape\", retryable: true, failoverEligible: true, basis: \"text_fallback\" };\n\t\t\t}\n\t\t\treturn { kind: \"permanent_request\", retryable: false, failoverEligible: false, basis: \"status\" };\n\t\t}\n\t}\n\n\tif (isContentSafetyStopMessage(text)) {\n\t\treturn { kind: \"refusal\", retryable: false, failoverEligible: true, basis: \"text_fallback\" };\n\t}\n\tif (isQuotaExhaustionMessage(text)) {\n\t\treturn { kind: \"quota_exhausted\", retryable: false, failoverEligible: true, basis: \"text_fallback\" };\n\t}\n\tif (isUpstreamUnavailableMessage(text)) {\n\t\treturn { kind: \"transient_transport\", retryable: true, failoverEligible: true, basis: \"text_fallback\" };\n\t}\n\tif (isOrphanToolCallIdError(text)) {\n\t\treturn { kind: \"transcript_shape\", retryable: true, failoverEligible: true, basis: \"text_fallback\" };\n\t}\n\tif (isAuthText(text)) {\n\t\treturn { kind: \"auth\", retryable: false, failoverEligible: false, basis: \"text_fallback\" };\n\t}\n\tif (isTransientProviderErrorMessage(text)) {\n\t\treturn { kind: \"transient_transport\", retryable: true, failoverEligible: true, basis: \"text_fallback\" };\n\t}\n\treturn { kind: \"unclassified\", retryable: false, failoverEligible: false, basis: \"none\" };\n}\n"]}
@@ -55,6 +55,16 @@ export declare function isClaudeCodeVersionTooOldMessage(text: string | undefine
55
55
  export declare function isCodexChatgptAccountUnsupportedModelMessage(text: string | undefined): boolean;
56
56
  /** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */
57
57
  export declare function isOrphanToolCallIdError(text: string | undefined): boolean;
58
+ /**
59
+ * Status-code-context match: a bare `500` inside a message body ("saved 500
60
+ * records") is not a status code, so numeric matches require an adjacent
61
+ * transport word — either a code-context word before the digits ("HTTP 429",
62
+ * "error 503", "status 401") or a status-context word after them ("503
63
+ * Upstream request failed", "429 Too Many Requests", "500 Internal Server
64
+ * Error"). Structured `status`/`errorCode` fields on the caller's side still
65
+ * outrank this text heuristic entirely.
66
+ */
67
+ export declare function hasStatusCodeLike(text: string, codes: readonly number[]): boolean;
58
68
  /**
59
69
  * Transient upstream availability failures: gateway 5xx passthroughs
60
70
  * ("503 Upstream request failed", "Endpoint is unavailable") and dropped
@@ -1 +1 @@
1
- {"version":3,"file":"provider-resilience.d.ts","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Claude lines that must stay on-model when they refuse. No DeepSeek hop. */\nexport const NO_SAFETY_FAILOVER_MODEL_RE = /(?:claude-)?(?:fable|opus|sonnet)/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max\" },\n\t{ provider: \"xai\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\n/** Fable / Opus / Sonnet stay on the requested id after a safety stop. */\nexport function isNoSafetyFailoverModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (NO_SAFETY_FAILOVER_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && NO_SAFETY_FAILOVER_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Billing/quota exhaustion (\"usage limit for this billing cycle\",\n * \"insufficient_quota\", zero balance, ...). Same-model retry is useless until\n * the cycle resets, but the turn can still be saved by failing over to another\n * candidate — so these classify as retryable-with-failover, not auth-fatal.\n */\nexport function isQuotaExhaustionMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /usage limit|GoUsageLimitError|FreeUsageLimitError|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Anthropic rejecting the spoofed Claude Code client version because the target\n * model is gated on a newer one (`claude_code_version_too_old`, HTTP 400).\n * Permanent for this build: every retry and every failover re-sends the same\n * user-agent, so the fix is bumping `CLAUDE_CODE_VERSION` in omk-ai (or\n * selecting an ungated model).\n */\nexport function isClaudeCodeVersionTooOldMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /claude_code_version_too_old|does not support this model;\\s*version\\s+[\\d.]+\\s+or newer is required/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Codex HTTP 400 when a ChatGPT-account OAuth session asks for a model the\n * account catalog does not serve (`gpt-6-astra` during the 2026-09-04 rollout).\n * Permanent for this login: same-model retry, transcript sanitize, and /new\n * session all re-send the same slug and get the same 400. Switch model or use\n * an API-key route that has the model.\n */\nexport function isCodexChatgptAccountUnsupportedModelMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /model is not supported when using Codex with a ChatGPT account/i.test(text);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Transient upstream availability failures: gateway 5xx passthroughs\n * (\"503 Upstream request failed\", \"Endpoint is unavailable\") and dropped\n * streams that never carried a finish reason (\"Stream ended without\n * finish_reason\"). Transport problems, not transcript-shape problems — they\n * heal by backoff/retry or switching model, never by transcript sanitization.\n */\nexport function isUpstreamUnavailableMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /upstream(?: request)? failed|upstream connect|endpoint (?:is )?unavailable|stream ended without finish_reason|\\b50[0234]\\b/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Route families: the same underlying model served by several provider\n * routes. Matched on the model id OR display name so alias routes like\n * `openrouter/stealth/ox-alpha`, `opencode-go/ox-alpha-free`, and\n * `opencode/x-preview-f-free` (\"Ox Alpha Free (Unlimited)\") rotate as one\n * ox-alpha family.\n */\nconst MODEL_ROUTE_FAMILIES: readonly { readonly pattern: RegExp; readonly family: string }[] = [\n\t{ pattern: /(?:^|[-_/])ox[-_ ]?alpha(?:[-_/\\s(]|$)/i, family: \"ox-alpha\" },\n];\n\n/** Family key for a model route, or undefined when it has no known alias group. */\nexport function modelRouteFamily(model: {\n\treadonly provider?: string | undefined;\n\treadonly id: string;\n\treadonly name?: string | undefined;\n}): string | undefined {\n\tfor (const { pattern, family } of MODEL_ROUTE_FAMILIES) {\n\t\tif (pattern.test(model.id)) return family;\n\t\tif (model.name && pattern.test(model.name)) return family;\n\t}\n\treturn undefined;\n}\n\n/**\n * Alternative provider routes serving the same model family as\n * {@link failed}, in registry order, excluding the failed route itself.\n * Callers filter by authentication and per-turn failure bookkeeping.\n */\nexport function sameModelRouteCandidates<\n\tT extends { readonly provider: string; readonly id: string; readonly name?: string | undefined },\n>(\n\tfailed: { readonly provider: string; readonly id: string; readonly name?: string | undefined },\n\tavailable: readonly T[],\n): T[] {\n\tconst family = modelRouteFamily(failed);\n\tif (!family) return [];\n\tconst candidates: T[] = [];\n\tfor (const model of available) {\n\t\tif (model.provider === failed.provider && model.id === failed.id) continue;\n\t\tif (modelRouteFamily(model) === family) candidates.push(model);\n\t}\n\treturn candidates;\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\t// Checked first: the payload carries `invalid_request_error`, which the\n\t// pattern below treats as transient, but a stale client version never heals\n\t// by retrying.\n\tif (isClaudeCodeVersionTooOldMessage(text)) return false;\n\tif (isCodexChatgptAccountUnsupportedModelMessage(text)) return false;\n\t// `at capacity|high demand`: xAI serves overload as HTTP 429 whose body carries\n\t// no status or limit token, so the message-level classifier misses it and the\n\t// turn never auto-retries (observed 2026-09-03, xai/grok-4.6).\n\treturn /overloaded|at capacity|high demand|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error|json error injected into sse stream|injected into sse/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\n/** Explicit `--model` / `--provider` pins stay on the requested model. */\nexport function shouldHonorSafetyFailover(options: {\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly modelPinned: boolean;\n\treadonly modelId?: string;\n\treadonly provider?: string;\n}): boolean {\n\tif (!options.autoFailoverOnSafetyStop || options.modelPinned) return false;\n\treturn !isNoSafetyFailoverModel(options.modelId, options.provider);\n}\n\n/** Sticky-model eject still applies unless the user pinned that model. */\nexport function shouldEjectStickySafetyModel(options: {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly modelPinned: boolean;\n}): boolean {\n\treturn options.blockStickySafetyModels && !options.modelPinned;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
1
+ {"version":3,"file":"provider-resilience.d.ts","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Claude lines that must stay on-model when they refuse. No DeepSeek hop. */\nexport const NO_SAFETY_FAILOVER_MODEL_RE = /(?:claude-)?(?:fable|opus|sonnet)/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max\" },\n\t{ provider: \"xai\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\n/** Fable / Opus / Sonnet stay on the requested id after a safety stop. */\nexport function isNoSafetyFailoverModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (NO_SAFETY_FAILOVER_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && NO_SAFETY_FAILOVER_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Billing/quota exhaustion (\"usage limit for this billing cycle\",\n * \"insufficient_quota\", zero balance, ...). Same-model retry is useless until\n * the cycle resets, but the turn can still be saved by failing over to another\n * candidate — so these classify as retryable-with-failover, not auth-fatal.\n */\nexport function isQuotaExhaustionMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /usage limit|GoUsageLimitError|FreeUsageLimitError|available balance|insufficient_quota|insufficient balance|insufficient credits|out of budget|quota exceeded|quota\\s+(?:has been\\s+|was\\s+|is\\s+)?exhaust|billing/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Anthropic rejecting the spoofed Claude Code client version because the target\n * model is gated on a newer one (`claude_code_version_too_old`, HTTP 400).\n * Permanent for this build: every retry and every failover re-sends the same\n * user-agent, so the fix is bumping `CLAUDE_CODE_VERSION` in omk-ai (or\n * selecting an ungated model).\n */\nexport function isClaudeCodeVersionTooOldMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /claude_code_version_too_old|does not support this model;\\s*version\\s+[\\d.]+\\s+or newer is required/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Codex HTTP 400 when a ChatGPT-account OAuth session asks for a model the\n * account catalog does not serve (`gpt-6-astra` during the 2026-09-04 rollout).\n * Permanent for this login: same-model retry, transcript sanitize, and /new\n * session all re-send the same slug and get the same 400. Switch model or use\n * an API-key route that has the model.\n */\nexport function isCodexChatgptAccountUnsupportedModelMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /model is not supported when using Codex with a ChatGPT account/i.test(text);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Status-code-context match: a bare `500` inside a message body (\"saved 500\n * records\") is not a status code, so numeric matches require an adjacent\n * transport word — either a code-context word before the digits (\"HTTP 429\",\n * \"error 503\", \"status 401\") or a status-context word after them (\"503\n * Upstream request failed\", \"429 Too Many Requests\", \"500 Internal Server\n * Error\"). Structured `status`/`errorCode` fields on the caller's side still\n * outrank this text heuristic entirely.\n */\nexport function hasStatusCodeLike(text: string, codes: readonly number[]): boolean {\n\tconst left = /(?:https?|status(?:\\s*code)?|error(?:\\s*code)?|code)\\s*:?\\s*(\\d{3})\\b/gi;\n\tconst right =\n\t\t/\\b(\\d{3})\\s+(?:too many|upstream|bad gateway|service unavailable|internal server|gateway|error|status|request failed|timed? ?out|unauthori[sz]ed|forbidden)/gi;\n\tfor (const re of [left, right]) {\n\t\tre.lastIndex = 0;\n\t\tfor (;;) {\n\t\t\tconst match = re.exec(text);\n\t\t\tif (match === null) break;\n\t\t\tif (codes.includes(Number.parseInt(match[1], 10))) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Transient upstream availability failures: gateway 5xx passthroughs\n * (\"503 Upstream request failed\", \"Endpoint is unavailable\") and dropped\n * streams that never carried a finish reason (\"Stream ended without\n * finish_reason\"). Transport problems, not transcript-shape problems — they\n * heal by backoff/retry or switching model, never by transcript sanitization.\n */\nexport function isUpstreamUnavailableMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn (\n\t\t/upstream(?: request)? failed|upstream connect|endpoint (?:is )?unavailable|stream ended without finish_reason/i.test(\n\t\t\ttext,\n\t\t) || hasStatusCodeLike(text, [500, 502, 503, 504])\n\t);\n}\n\n/**\n * Route families: the same underlying model served by several provider\n * routes. Matched on the model id OR display name so alias routes like\n * `openrouter/stealth/ox-alpha`, `opencode-go/ox-alpha-free`, and\n * `opencode/x-preview-f-free` (\"Ox Alpha Free (Unlimited)\") rotate as one\n * ox-alpha family.\n */\nconst MODEL_ROUTE_FAMILIES: readonly { readonly pattern: RegExp; readonly family: string }[] = [\n\t{ pattern: /(?:^|[-_/])ox[-_ ]?alpha(?:[-_/\\s(]|$)/i, family: \"ox-alpha\" },\n\t// union-alpha is served by opencode-go/union-alpha (anthropic-messages) and\n\t// openrouter/stealth/union-alpha (openai-completions); both map to one family\n\t// so an upstream-unavailable 503/empty stream rotates to the live route.\n\t{ pattern: /(?:^|[-_/])union[-_ ]?alpha(?:[-_/\\s(]|$)/i, family: \"union-alpha\" },\n];\n\n/** Family key for a model route, or undefined when it has no known alias group. */\nexport function modelRouteFamily(model: {\n\treadonly provider?: string | undefined;\n\treadonly id: string;\n\treadonly name?: string | undefined;\n}): string | undefined {\n\tfor (const { pattern, family } of MODEL_ROUTE_FAMILIES) {\n\t\tif (pattern.test(model.id)) return family;\n\t\tif (model.name && pattern.test(model.name)) return family;\n\t}\n\treturn undefined;\n}\n\n/**\n * Alternative provider routes serving the same model family as\n * {@link failed}, in registry order, excluding the failed route itself.\n * Callers filter by authentication and per-turn failure bookkeeping.\n */\nexport function sameModelRouteCandidates<\n\tT extends { readonly provider: string; readonly id: string; readonly name?: string | undefined },\n>(\n\tfailed: { readonly provider: string; readonly id: string; readonly name?: string | undefined },\n\tavailable: readonly T[],\n): T[] {\n\tconst family = modelRouteFamily(failed);\n\tif (!family) return [];\n\tconst candidates: T[] = [];\n\tfor (const model of available) {\n\t\tif (model.provider === failed.provider && model.id === failed.id) continue;\n\t\tif (modelRouteFamily(model) === family) candidates.push(model);\n\t}\n\treturn candidates;\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\t// Checked first: the payload carries `invalid_request_error`, which the\n\t// pattern below treats as transient, but a stale client version never heals\n\t// by retrying.\n\tif (isClaudeCodeVersionTooOldMessage(text)) return false;\n\tif (isCodexChatgptAccountUnsupportedModelMessage(text)) return false;\n\t// `at capacity|high demand`: xAI serves overload as HTTP 429 whose body carries\n\t// no status or limit token, so the message-level classifier misses it and the\n\t// turn never auto-retries (observed 2026-09-03, xai/grok-4.6).\n\tif (\n\t\t/overloaded|at capacity|high demand|provider.?returned.?error|rate.?limit|too many requests|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|stream ended before terminal|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error|json error injected into sse stream|injected into sse/i.test(\n\t\t\ttext,\n\t\t)\n\t) {\n\t\treturn true;\n\t}\n\t// Status-looking numbers only count with adjacent transport context — a\n\t// bare `500` in a message body (\"saved 500 records\") is not a status code.\n\treturn hasStatusCodeLike(text, [429, 500, 502, 503, 504]);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\n/** Explicit `--model` / `--provider` pins stay on the requested model. */\nexport function shouldHonorSafetyFailover(options: {\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly modelPinned: boolean;\n\treadonly modelId?: string;\n\treadonly provider?: string;\n}): boolean {\n\tif (!options.autoFailoverOnSafetyStop || options.modelPinned) return false;\n\treturn !isNoSafetyFailoverModel(options.modelId, options.provider);\n}\n\n/** Sticky-model eject still applies unless the user pinned that model. */\nexport function shouldEjectStickySafetyModel(options: {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly modelPinned: boolean;\n}): boolean {\n\treturn options.blockStickySafetyModels && !options.modelPinned;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}