@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -38,7 +38,7 @@ const PROVIDER_ALIASES: Record<string, string> = {
38
38
  type Row = readonly [id: string, contextWindow?: number | null, maxTokens?: number | null, input?: string | null, reasoning?: 0 | 1 | null, wireModelId?: string | null, costInput?: number | null, costOutput?: number | null, costCacheRead?: number | null, costCacheWrite?: number | null];
39
39
  const DATA: Record<string, readonly Row[]> = {
40
40
  "amazon-bedrock": [["anthropic.claude-3-5-haiku-20241022-v1:0",200000,8192,"text,image",0,null,0.8,4,0.08,1],["anthropic.claude-3-5-sonnet-20240620-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["anthropic.claude-3-5-sonnet-20241022-v2:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["anthropic.claude-3-haiku-20240307-v1:0",200000,4096,"text,image",0,null,0.25,1.25,0,0],["anthropic.claude-3-opus-20240229-v1:0",200000,4096,"text,image",0,null,15,75,0,0],["anthropic.claude-3-sonnet-20240229-v1:0",200000,4096,"text,image",0,null,3,15,0,0],["anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["au.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["au.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,16.5,82.5,0.5,6.25],["au.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["au.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["au.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["au.anthropic.claude-sonnet-4-6",1000000,128000,"text,image",1,null,3.3,16.5,0.33,4.125],["au.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["cohere.command-r-plus-v1:0",128000,4096,"text",0,null,3,15,0,0],["cohere.command-r-v1:0",128000,4096,"text",0,null,0.5,1.5,0,0],["deepseek.v3-v1:0",163840,81920,"text",1,null,0.58,1.68,0,0],["deepseek.v3.2",163840,81920,"text",1,null,0.62,1.85,0,0],["deepseek.v3.2-v1:0",163840,81920,"text",1,null,0.62,1.85,0,0],["eu.anthropic.claude-3-5-haiku-20241022-v1:0",200000,8192,"text,image",0,null,0.8,4,0.08,1],["eu.anthropic.claude-3-5-sonnet-20240620-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["eu.anthropic.claude-3-5-sonnet-20241022-v2:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["eu.anthropic.claude-3-7-sonnet-20250219-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["eu.anthropic.claude-3-haiku-20240307-v1:0",200000,4096,"text,image",0,null,0.25,1.25,0,0],["eu.anthropic.claude-3-opus-20240229-v1:0",200000,4096,"text,image",0,null,15,75,0,0],["eu.anthropic.claude-3-sonnet-20240229-v1:0",200000,4096,"text,image",0,null,3,15,0,0],["eu.anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["eu.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1.1,5.5,0.11,1.375],["eu.anthropic.claude-opus-4-1-20250805-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["eu.anthropic.claude-opus-4-20250514-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["eu.anthropic.claude-opus-4-5-20251101-v1:0",200000,64000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["eu.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-sonnet-4-20250514-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["eu.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["eu.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["eu.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["global.amazon.nova-2-lite-v1:0",128000,4096,"text,image",1,null,0.33,2.75,0,0],["global.anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["global.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["global.anthropic.claude-opus-4-5-20251101-v1:0",200000,64000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-sonnet-4-20250514-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["global.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["global.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["global.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["google.gemma-3-27b-it",202752,8192,"text,image",0,null,0.12,0.2,0,0],["google.gemma-3-4b-it",128000,4096,"text,image",0,null,0.04,0.08,0,0],["jp.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["jp.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["jp.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["jp.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["jp.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["jp.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["jp.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["meta.llama3-1-405b-instruct-v1:0",128000,4096,"text",0,null,2.4,2.4,0,0],["meta.llama3-1-70b-instruct-v1:0",128000,4096,"text",0,null,0.72,0.72,0,0],["meta.llama3-1-8b-instruct-v1:0",128000,4096,"text",0,null,0.22,0.22,0,0],["minimax.minimax-m2",204608,128000,"text",1,null,0.3,1.2,0,0],["minimax.minimax-m2.1",204800,131072,"text",1,null,0.3,1.2,0,0],["minimax.minimax-m2.5",196608,98304,"text",1,null,0.3,1.2,0,0],["mistral.devstral-2-123b",256000,8192,"text",0,null,0.4,2,0,0],["mistral.magistral-small-2509",128000,40000,"text,image",1,null,0.5,1.5,0,0],["mistral.ministral-3-14b-instruct",128000,4096,"text",0,null,0.2,0.2,0,0],["mistral.ministral-3-3b-instruct",256000,8192,"text,image",0,null,0.1,0.1,0,0],["mistral.ministral-3-8b-instruct",128000,4096,"text",0,null,0.15,0.15,0,0],["mistral.mistral-large-2402-v1:0",128000,4096,"text",0,null,0.5,1.5,0,0],["mistral.mistral-large-3-675b-instruct",256000,8192,"text,image",0,null,0.5,1.5,0,0],["mistral.pixtral-large-2502-v1:0",128000,8192,"text,image",0,null,2,6,0,0],["mistral.voxtral-mini-3b-2507",128000,4096,"text",0,null,0.04,0.04,0,0],["mistral.voxtral-small-24b-2507",32000,8192,"text",0,null,0.15,0.35,0,0],["moonshot.kimi-k2-thinking",262143,16000,"text",1,null,0.6,2.5,0,0],["moonshotai.kimi-k2.5",262143,16000,"text,image",1,null,0.6,3,0,0],["nvidia.nemotron-nano-12b-v2",128000,4096,"text,image",0,null,0.2,0.6,0,0],["nvidia.nemotron-nano-3-30b",128000,4096,"text",1,null,0.06,0.24,0,0],["nvidia.nemotron-nano-9b-v2",128000,4096,"text",0,null,0.06,0.23,0,0],["nvidia.nemotron-super-3-120b",262144,131072,"text",1,null,0.15,0.65,0,0],["openai.gpt-5.4",272000,128000,"text,image",1,null,2.75,16.5,0.275,0],["openai.gpt-5.5",272000,128000,"text,image",1,null,5.5,33,0.55,0],["openai.gpt-5.6-luna",373000,128000,"text,image",1,null,1,6,0.1,1.25],["openai.gpt-5.6-sol",373000,128000,"text,image",1,null,5,30,0.5,6.25],["openai.gpt-5.6-terra",373000,128000,"text,image",1,null,2.5,15,0.25,3.125],["openai.gpt-oss-120b",128000,16384,"text",1,null,0.15,0.6,0,0],["openai.gpt-oss-120b-1:0",128000,16384,"text",1,null,0.15,0.6,0,0],["openai.gpt-oss-20b",128000,16384,"text",1,null,0.07,0.3,0,0],["openai.gpt-oss-20b-1:0",128000,16384,"text",1,null,0.07,0.3,0,0],["openai.gpt-oss-safeguard-120b",128000,16384,"text",0,null,0.15,0.6,0,0],["openai.gpt-oss-safeguard-20b",128000,16384,"text",0,null,0.07,0.2,0,0],["qwen.qwen3-235b-a22b-2507-v1:0",262144,131072,"text",0,null,0.22,0.88,0,0],["qwen.qwen3-32b-v1:0",16384,16384,"text",1,null,0.15,0.6,0,0],["qwen.qwen3-coder-30b-a3b-v1:0",262144,131072,"text",0,null,0.15,0.6,0,0],["qwen.qwen3-coder-480b-a35b-v1:0",131072,65536,"text",0,null,0.22,1.8,0,0],["qwen.qwen3-coder-next",131072,65536,"text",1,null,0.22,1.8,0,0],["qwen.qwen3-next-80b-a3b",262000,262000,"text",0,null,0.14,1.4,0,0],["qwen.qwen3-vl-235b-a22b",262000,262000,"text,image",0,null,0.3,1.5,0,0],["us.amazon.nova-lite-v1:0",300000,8192,"text,image",0,null,0.06,0.24,0.015,0],["us.amazon.nova-micro-v1:0",128000,8192,"text",0,null,0.035,0.14,0.00875,0],["us.amazon.nova-premier-v1:0",1000000,16384,"text,image",1,null,2.5,12.5,0,0],["us.amazon.nova-pro-v1:0",300000,8192,"text,image",0,null,0.8,3.2,0.2,0],["us.anthropic.claude-3-7-sonnet-20250219-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["us.anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["us.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["us.anthropic.claude-opus-4-1-20250805-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["us.anthropic.claude-opus-4-20250514-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["us.anthropic.claude-opus-4-5-20251101-v1:0",200000,64000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-sonnet-4-20250514-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["us.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["us.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["us.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["us.deepseek.r1-v1:0",128000,32768,"text",1,null,1.35,5.4,0,0],["us.meta.llama3-2-11b-instruct-v1:0",128000,4096,"text,image",0,null,0.16,0.16,0,0],["us.meta.llama3-2-1b-instruct-v1:0",131000,4096,"text",0,null,0.1,0.1,0,0],["us.meta.llama3-2-3b-instruct-v1:0",131000,4096,"text",0,null,0.15,0.15,0,0],["us.meta.llama3-2-90b-instruct-v1:0",128000,4096,"text,image",0,null,0.72,0.72,0,0],["us.meta.llama3-3-70b-instruct-v1:0",128000,4096,"text",0,null,0.72,0.72,0,0],["us.meta.llama4-maverick-17b-instruct-v1:0",1000000,16384,"text,image",0,null,0.24,0.97,0,0],["us.meta.llama4-scout-17b-instruct-v1:0",3500000,16384,"text,image",0,null,0.17,0.66,0,0],["writer.palmyra-x4-v1:0",122880,8192,"text",1,null,2.5,10,0,0],["writer.palmyra-x5-v1:0",1040000,8192,"text",1,null,0.6,6,0,0],["xai.grok-4.3",1000000,131072,"text,image",1,null,1.25,2.5,0.2,0],["zai.glm-4.7",204800,131072,"text",1,null,0.6,2.2,0,0],["zai.glm-4.7-flash",200000,131072,"text",1,null,0.07,0.4,0,0],["zai.glm-5",202752,101376,"text",1,null,1,3.2,0,0]],
41
- "anthropic": [["claude-3-5-sonnet-20240620",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-5-sonnet-20241022",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-haiku-20240307",200000,4096,"text,image",0,null,0.25,1.25,0.03,0.3],["claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["claude-haiku-4-5",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-haiku-4-5-20251001",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-opus-4-0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1-20250805",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-20250514",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-5",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-5-20251101",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6[1m]",1000000,128000,"text,image",1,"claude-opus-4-6",5,25,0.5,6.25],["claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-7[1m]",1000000,128000,"text,image",1,"claude-opus-4-7",5,25,0.5,6.25],["claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-8[1m]",1000000,128000,"text,image",1,"claude-opus-4-8",5,25,0.5,6.25],["claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-sonnet-4-0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-20250514",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5-20250929",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6",1000000,128000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6[1m]",1000000,64000,"text,image",1,"claude-sonnet-4-6",3,15,0.3,3.75],["claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5]],
41
+ "anthropic": [["claude-3-5-sonnet-20240620",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-5-sonnet-20241022",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-haiku-20240307",200000,4096,"text,image",0,null,0.25,1.25,0.03,0.3],["claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["claude-fable-5-1",1000000,128000,"text,image",1,null,10,50,0.25,12.5],["claude-haiku-4-5",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-haiku-4-5-20251001",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-opus-4-0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1-20250805",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-20250514",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-5",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-5-20251101",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6[1m]",1000000,128000,"text,image",1,"claude-opus-4-6",5,25,0.5,6.25],["claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-7[1m]",1000000,128000,"text,image",1,"claude-opus-4-7",5,25,0.5,6.25],["claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-8[1m]",1000000,128000,"text,image",1,"claude-opus-4-8",5,25,0.5,6.25],["claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-sonnet-4-0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-20250514",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5-20250929",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6",1000000,128000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6[1m]",1000000,64000,"text,image",1,"claude-sonnet-4-6",3,15,0.3,3.75],["claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5]],
42
42
  "azure-openai": [["gpt-4.1",1047576,32768,"text,image",0,null,2,8,0.5,0],["gpt-4o",128000,16384,"text,image",0,null,2.5,10,1.25,0],["gpt-4o-mini",128000,16384,"text,image",0,null,0.15,0.6,0.075,0],["o3",200000,100000,"text,image",1,null,2,8,0.5,0],["o3-mini",200000,100000,"text",1,null,1.1,4.4,0.55,0]],
43
43
  "cerebras": [["gemma-4-31b",131072,40960,"text,image",1,null,0.99,1.49,0,0],["gpt-oss-120b",131072,40960,"text",1,null,0.35,0.75,0,0],["llama3.1-8b",32000,8000,"text",0,null,0.1,0.1,0,0],["qwen-3-235b-a22b-instruct-2507",131000,32000,"text",0,null,0.6,1.2,0,0],["qwen-3-coder-480b",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.6",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.7",131072,40960,"text",1,null,2.25,2.75,2.25,0]],
44
44
  "deepseek": [["deepseek-v4-flash",1048576,384000,"text",1,null,0.14,0.28,0.0028,0],["deepseek-v4-pro",1048576,384000,"text",1,null,0.435,0.87,0.003625,0]],
@@ -26,7 +26,7 @@ import { isIntegrationClientId, type IntegrationClientId } from "./registry";
26
26
  *
27
27
  * This union is re-declared, not imported, in two other places -- the management
28
28
  * route envelope and the GUI adapter -- because neither imports across that
29
- * boundary. `tests/integrations-journal.test.ts` asserts the three agree, since
29
+ * boundary. `tests/clients/integrations-journal.test.ts` asserts the three agree, since
30
30
  * nothing else can: a kind persisted here and missing there renders as a raw
31
31
  * key with no type error anywhere.
32
32
  */
@@ -64,6 +64,44 @@ export interface JournalEntry {
64
64
 
65
65
  export const SNAPSHOT_RETENTION = 10;
66
66
 
67
+ /**
68
+ * A deletion, expressed as an APPEND.
69
+ *
70
+ * The alternative -- rewriting journal.jsonl without the row -- breaks all three
71
+ * things this file's header promises. `appendOperation` commits and nothing
72
+ * else, so a read-modify-write would race any concurrent append; a torn write
73
+ * would truncate the whole log rather than one trailing line, which
74
+ * `listOperations` is built to tolerate; and no lock covers this file, because
75
+ * append-only never needed one (writer-lock.ts guards `<configPath>.lock`,
76
+ * which is a client config, not this).
77
+ *
78
+ * "journal rows always survive" (pruneSnapshots below) is a promise about
79
+ * RETENTION, not about the user. An operator deleting their own row is not
80
+ * retention, and the physical line does in fact survive -- this record is laid
81
+ * over it.
82
+ */
83
+ export interface JournalTombstone {
84
+ /** opId this row retires. */
85
+ tombstone: string;
86
+ at: string;
87
+ /** Management principal that asked. Never a token, never a path. */
88
+ by: string;
89
+ }
90
+
91
+ function isTombstone(value: unknown): value is JournalTombstone {
92
+ return typeof value === "object" && value !== null
93
+ && typeof (value as { tombstone?: unknown }).tombstone === "string";
94
+ }
95
+
96
+ /** Retire one operation. Append-only, exactly like `appendOperation`. */
97
+ export function appendTombstone(
98
+ record: JournalTombstone,
99
+ dir: string = integrationsDir(),
100
+ ): void {
101
+ ensureDir(journalPath(dir));
102
+ appendFileSync(journalPath(dir), `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
103
+ }
104
+
67
105
  /**
68
106
  * Does the file on disk still hold what this operation left behind?
69
107
  *
@@ -159,16 +197,34 @@ export function listOperations(
159
197
  return [];
160
198
  }
161
199
  const rows: JournalEntry[] = [];
200
+ /*
201
+ * Collected in the SAME pass, before any filtering. A tombstone carries no
202
+ * clientId -- it names an opId -- so a pass that filtered by client first
203
+ * would drop the tombstone and resurrect the row on the per-client route
204
+ * while the global route hid it. The two routes read the same log and must
205
+ * agree.
206
+ */
207
+ const retired = new Set<string>();
162
208
  for (const line of raw.split("\n")) {
163
209
  if (!line.trim()) continue;
164
210
  try {
165
- const parsed = JSON.parse(line) as JournalEntry;
166
- if (!clientId || parsed.clientId === clientId) rows.push(parsed);
211
+ const parsed: unknown = JSON.parse(line);
212
+ if (isTombstone(parsed)) {
213
+ retired.add(parsed.tombstone);
214
+ continue;
215
+ }
216
+ const entry = parsed as JournalEntry;
217
+ if (!clientId || entry.clientId === clientId) rows.push(entry);
167
218
  } catch {
168
219
  // Torn line from an interrupted append; the rest of the log is still good.
169
220
  }
170
221
  }
171
- return rows.reverse().slice(0, limit);
222
+ /*
223
+ * Filter AFTER the whole file is read, never during. A tombstone is always
224
+ * appended after the row it retires, so an in-loop check would miss every one.
225
+ */
226
+ const live = retired.size === 0 ? rows : rows.filter(row => !retired.has(row.opId));
227
+ return live.reverse().slice(0, limit);
172
228
  }
173
229
 
174
230
  export function findOperation(opId: string, dir: string = integrationsDir()): JournalEntry | null {
@@ -211,6 +267,11 @@ export function countSnapshots(
211
267
  * Keep the newest N snapshot files per client; journal rows always survive.
212
268
  * Structured rather than throwing or swallowing: a swallowed failure would let
213
269
  * credential-bearing snapshots pile up while every operation reported success.
270
+ *
271
+ * A user-deleted row no longer occupies a retention slot: `listOperations`
272
+ * hides retired rows, so the keep window below slides down by one for each
273
+ * deletion. The direction is safe -- an older backup is kept rather than
274
+ * collected -- but "ten backups per client" counts LIVE rows, not operations.
214
275
  */
215
276
  export function pruneSnapshots(
216
277
  clientId: IntegrationClientId,
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import {
14
14
  appendOperation,
15
+ appendTombstone,
15
16
  captureSnapshot,
16
17
  clearPruneFailure,
17
18
  countSnapshots,
@@ -22,6 +23,7 @@ import {
22
23
  readMaintenance,
23
24
  readSnapshot,
24
25
  type JournalEntry,
26
+ type JournalTombstone,
25
27
  type MaintenanceState,
26
28
  type SnapshotRef,
27
29
  } from "./journal";
@@ -42,6 +44,8 @@ export interface IntegrationStateStore {
42
44
  putRecord(record: OwnershipRecord): void;
43
45
  dropRecord(clientId: IntegrationClientId): void;
44
46
  appendJournal(entry: JournalEntry): void;
47
+ /** Retire one operation. Append-only; see journal.ts `appendTombstone`. */
48
+ retireOperation(record: JournalTombstone): void;
45
49
  listOperations(clientId?: IntegrationClientId, limit?: number): JournalEntry[];
46
50
  findOperation(opId: string): JournalEntry | null;
47
51
  captureSnapshot(clientId: IntegrationClientId, opId: string, text: string | null): SnapshotRef;
@@ -78,6 +82,7 @@ export function createIntegrationStateStore(root: string = integrationsDir()): I
78
82
  putRecord: record => writeRecord(record, dir),
79
83
  dropRecord: clientId => deleteRecord(clientId, dir),
80
84
  appendJournal: entry => appendOperation(entry, dir),
85
+ retireOperation: record => appendTombstone(record, dir),
81
86
  listOperations: (clientId, limit) => listOperations(clientId, limit, dir),
82
87
  findOperation: opId => findOperation(opId, dir),
83
88
  captureSnapshot: (clientId, opId, text) => captureSnapshot(clientId, opId, text, dir),
@@ -33,6 +33,8 @@ const FORBIDDEN_EXACT_KEYS = new Set([
33
33
 
34
34
  const RAW_POSIX_PATH_RE =
35
35
  /(?:^|[^A-Za-z0-9._~/])\/(?:(?=$|[^A-Za-z0-9._~/])|(?!\/)(?![ \t\r\n])(?:\/|[^/\0\r\n]+)+\/?(?=$|[^A-Za-z0-9._~/]))/u;
36
+ const ASCII_URL_WHITESPACE_RE = /[\t\r\n]/g;
37
+ const FILE_URI_RE = /(?:^|[^A-Za-z0-9+.-])file:/i;
36
38
 
37
39
  function fieldPath(base: string, key: string | number): string {
38
40
  return base ? `${base}.${String(key)}` : String(key);
@@ -80,6 +82,8 @@ export function enforceEventStructureLimits(
80
82
  }
81
83
  if (
82
84
  /^[A-Za-z]:\\/.test(value) ||
85
+ FILE_URI_RE.test(value) ||
86
+ FILE_URI_RE.test(value.replace(ASCII_URL_WHITESPACE_RE, "")) ||
83
87
  RAW_POSIX_PATH_RE.test(value) ||
84
88
  value.includes("\\Users\\")
85
89
  ) {
@@ -145,6 +145,25 @@ function isBenchmarkDnsAnswer(address: string, assessment: DestinationAssessment
145
145
  return embedded.kind === "private" && embedded.detail === "benchmark address";
146
146
  }
147
147
 
148
+ /**
149
+ * Mihomo (Clash.Meta) fake-IP DNS answers IPv6 queries from `fdfe:dcba:9876::/48` — its
150
+ * documented default `fake-ip-range6` (#3462). That prefix sits inside ULA `fc00::/7`, so
151
+ * `classifyIpv6` reports it as a private-network address and, unlike the IPv4 benchmark
152
+ * range, nothing about the address itself marks it synthetic. The exception is therefore
153
+ * narrower than the benchmark one: exact /48 match, DNS answers only (a literal URL still
154
+ * rejects), and only behind the `allowMihomoIpv6FakeIp` opt-in that the outbound caller
155
+ * derives from a scheme-matched proxy it then binds the request to.
156
+ */
157
+ const MIHOMO_IPV6_FAKE_IP_PREFIX = [0xfdfe, 0xdcba, 0x9876] as const;
158
+
159
+ function isMihomoIpv6FakeIpAnswer(address: string, assessment: DestinationAssessment | null): boolean {
160
+ if (assessment?.kind !== "private" || assessment.detail !== "private-network address") return false;
161
+ if (isIP(address) !== 6) return false;
162
+ const hextets = ipv6Hextets(normalizeHostname(address));
163
+ if (!hextets) return false;
164
+ return MIHOMO_IPV6_FAKE_IP_PREFIX.every((group, index) => hextets[index] === group);
165
+ }
166
+
148
167
  function firstIpv6Hextet(hostname: string): number | null {
149
168
  const head = hostname.split(":")[0];
150
169
  if (!head) return 0;
@@ -385,7 +404,13 @@ export function assessUrlDestination(url: string): UrlDestinationAssessment | nu
385
404
  */
386
405
  export async function resolvePublicAddresses(
387
406
  url: string,
388
- options?: string | { context?: string; allowPrivateNetwork?: boolean; allowBenchmarkAddresses?: boolean },
407
+ options?: string | {
408
+ context?: string;
409
+ allowPrivateNetwork?: boolean;
410
+ allowBenchmarkAddresses?: boolean;
411
+ /** Mihomo IPv6 fake-IP (`fdfe:dcba:9876::/48`) DNS answers; see `isMihomoIpv6FakeIpAnswer`. */
412
+ allowMihomoIpv6FakeIp?: boolean;
413
+ },
389
414
  ): Promise<{
390
415
  hostname: string;
391
416
  addresses: { address: string; family: number }[];
@@ -396,6 +421,7 @@ export async function resolvePublicAddresses(
396
421
  : options?.context?.trim() || "image URL";
397
422
  const privateNetworkAllowed = typeof options === "object" && options?.allowPrivateNetwork === true;
398
423
  const benchmarkAllowed = typeof options === "object" && options?.allowBenchmarkAddresses === true;
424
+ const mihomoIpv6Allowed = typeof options === "object" && options?.allowMihomoIpv6FakeIp === true;
399
425
  let hostname: string;
400
426
  try {
401
427
  hostname = normalizeHostname(new URL(url.trim()).hostname);
@@ -440,7 +466,10 @@ export async function resolvePublicAddresses(
440
466
  // fake-IP DNS, not a LAN provider. Accept it without allowPrivateNetwork and
441
467
  // do not mark the destination private, so the caller's HTTP(S)_PROXY path
442
468
  // still applies (credit #1748).
443
- if (benchmarkAllowed && isBenchmarkDnsAnswer(address, assessment)) {
469
+ if (
470
+ (benchmarkAllowed && isBenchmarkDnsAnswer(address, assessment))
471
+ || (mihomoIpv6Allowed && isMihomoIpv6FakeIpAnswer(address, assessment))
472
+ ) {
444
473
  validatedAddresses.push({ address, family: ipKind === 4 || ipKind === 6 ? ipKind : (family || 4) });
445
474
  continue;
446
475
  }
package/src/lib/errors.ts CHANGED
@@ -127,6 +127,28 @@ function isPermissionMessage(text: string): boolean {
127
127
  );
128
128
  }
129
129
 
130
+ /**
131
+ * Geographic / network-location denials. Google's Cloud Code Assist API returns these as
132
+ * HTTP 400 `FAILED_PRECONDITION: User location is not supported for the API use.` — the
133
+ * request is well-formed, the caller's location is refused. Treated as a permission-class
134
+ * rejection, never as an invalid request (#3467).
135
+ */
136
+ const LOCATION_UNSUPPORTED_PATTERNS = [
137
+ "location is not supported",
138
+ "location not supported",
139
+ "unsupported location",
140
+ "region is not supported",
141
+ "unsupported region",
142
+ "country is not supported",
143
+ "not supported in your country",
144
+ "not supported in your region",
145
+ ] as const;
146
+
147
+ export function isLocationUnsupportedMessage(text: string): boolean {
148
+ const lower = text.toLowerCase();
149
+ return LOCATION_UNSUPPORTED_PATTERNS.some(needle => lower.includes(needle));
150
+ }
151
+
130
152
  /**
131
153
  * Client cancelled / closed the turn. Matches ONLY abort phrases this codebase
132
154
  * produces — "client closed request during web-search" (src/web-search/loop.ts),
@@ -244,6 +266,15 @@ export function classifyError(status: number, type: string, message: string): Oc
244
266
  ) {
245
267
  return { message, type: "authentication_error", code: "invalid_api_key" };
246
268
  }
269
+ // An explicit permission enum must not acquire a more specific inferred reason.
270
+ if (type === "PERMISSION_DENIED" || text.includes("permission_denied")) {
271
+ return { message, type: "permission_error", code: "permission_denied" };
272
+ }
273
+ // Location denials outrank generic permission / subscription wording, but never an
274
+ // authoritative 5xx. Message-only adapter terminals arrive here with inferred 403.
275
+ if (status < 500 && (type === "location_not_supported" || isLocationUnsupportedMessage(text))) {
276
+ return { message, type: "permission_error", code: "location_not_supported" };
277
+ }
247
278
  // Subscription labels are valid only in a known permission context.
248
279
  if (
249
280
  (status === 403 || type === "permission_error") &&
@@ -352,6 +383,9 @@ export function inferHttpStatusFromAdapterMessage(message: string): number {
352
383
  // Strong authentication signals win when a message contains mixed auth and
353
384
  // subscription/permission wording.
354
385
  if (isAuthenticationMessage(lower)) return 401;
386
+ // A location denial is a permission-class rejection; keep it aligned with the
387
+ // `permission_error` envelope status so message-only and classified paths agree.
388
+ if (isLocationUnsupportedMessage(lower)) return 403;
355
389
  if (isSubscriptionGateMessage(lower) || isPermissionMessage(lower)) return 403;
356
390
  // Same precedence rule as classifyCursorError: an explicit gRPC FAILED_PRECONDITION is a
357
391
  // structured, deterministic rejection, so it outranks the overload keywords that routinely
@@ -359,6 +393,11 @@ export function inferHttpStatusFromAdapterMessage(message: string): number {
359
393
  // the message matched "unavailable" and returned a retryable 503, so clients kept retrying
360
394
  // a rejection that can never succeed.
361
395
  if (lower.includes("failed_precondition") || lower.includes("failed precondition")) return 400;
396
+ // Bytes the upstream itself produced and then mangled are a provider protocol failure, not a
397
+ // malformed client request. This must sit ahead of the generic "malformed" -> 400 branch so a
398
+ // combo can fail over instead of returning a terminal 4xx the caller cannot act on. Scoped to
399
+ // the "malformed upstream" phrase our adapters emit; plain "malformed" keeps its 400 verdict.
400
+ if (lower.includes("malformed upstream")) return 502;
362
401
  if (
363
402
  lower.includes("unavailable") ||
364
403
  lower.includes("overloaded") ||
@@ -7,7 +7,7 @@ import {
7
7
  resolvePublicAddresses,
8
8
  } from "./destination-policy";
9
9
  import { pinnedHttpGet, pinnedHttpPost } from "./pinned-http";
10
- import { outboundProxyConfigured } from "./proxy-env";
10
+ import { effectiveProxyFor, outboundProxyConfigured } from "./proxy-env";
11
11
  import { publicProviderBaseUrl } from "./provider-url";
12
12
 
13
13
  type ProviderGetInit = Omit<RequestInit, "body" | "method" | "redirect">;
@@ -19,6 +19,14 @@ export interface ProviderOutboundDependencies {
19
19
  resolveAddresses?: typeof resolvePublicAddresses;
20
20
  pinnedGet?: typeof pinnedHttpGet;
21
21
  pinnedPost?: typeof pinnedHttpPost;
22
+ /**
23
+ * Canonical-URL proof for the transparent fake-IP exception (Clash TUN mode
24
+ * without proxy env). Injected so this transport core stays decoupled from
25
+ * the registry module; production compares the final request URL against the
26
+ * registry's own fixed discovery URL. Defaults to "not canonical" so a caller
27
+ * that forgets the seam fails closed, never open.
28
+ */
29
+ isCanonicalUrl?: (name: string, url: string) => boolean;
22
30
  }
23
31
 
24
32
  export class ProviderOutboundPolicyError extends Error {
@@ -33,6 +41,41 @@ function configuredProxyFor(): boolean {
33
41
  return outboundProxyConfigured();
34
42
  }
35
43
 
44
+ /**
45
+ * Registry-owned fake-IP transparency exception (Clash/Surge/Mihomo TUN mode).
46
+ *
47
+ * Under TUN mode the packet path intercepts the fake-IP destination itself, so a
48
+ * canonical registry destination whose local DNS answers include Clash fake-IP
49
+ * space (198.18.0.0/15) is reachable by pin-connecting through the TUN — no
50
+ * outbound HTTP(S) proxy env is required. The exception is deliberately narrow:
51
+ *
52
+ * - hostname-only: a literal 198.18.x.x URL never reaches it (the literal gate
53
+ * in `resolvePublicAddresses` rejects before DNS answers are examined);
54
+ * - canonical-URL-only: `isCanonicalUrl` must prove the FINAL request URL is
55
+ * the registry's own fixed discovery URL for this provider (not merely that
56
+ * the provider NAME matches — OAuth/forward names match any baseUrl by
57
+ * design, and the bearer is pinned to the registry destination independently
58
+ * in `buildModelsRequest`). A retargeted row or a renamed custom row sends
59
+ * its credential to the registry URL anyway, so the proof must be on the URL
60
+ * actually fetched. The check is injected so the transport core stays
61
+ * decoupled from the registry module;
62
+ * - per-answer validation: benchmark and public answers may coexist. The exception
63
+ * does not admit loopback/RFC1918/link-local/metadata companions; those still
64
+ * follow the resolver's private-network policy. Benchmark admission leaves
65
+ * `privateNetwork` false; proxy/NO_PROXY semantics are unchanged.
66
+ *
67
+ * Image/Lab fetch never passes the underlying flag and is unaffected.
68
+ */
69
+ function transparentFakeIpException(
70
+ url: string,
71
+ parsed: URL,
72
+ isCanonicalUrl: (name: string, url: string) => boolean,
73
+ name: string,
74
+ ): boolean {
75
+ if (noProxyMatches(parsed)) return false;
76
+ return isCanonicalUrl(name, url);
77
+ }
78
+
36
79
  function normalizeProxyHostname(hostname: string): string {
37
80
  const normalized = hostname.trim().toLowerCase().replace(/\.+$/, "");
38
81
  return normalized.startsWith("[") && normalized.endsWith("]")
@@ -139,9 +182,15 @@ async function providerOutboundRequest(
139
182
  }
140
183
  const parsed = postUrl ?? new URL(url);
141
184
  const proxyConfigured = configuredProxyFor();
185
+ // Snapshot the scheme-matched proxy once, before the DNS await, so admission and transport
186
+ // below reason about the same value. `null` here means "no proxy fetch would actually use",
187
+ // even if some other proxy variable is set.
188
+ const effectiveProxy = effectiveProxyFor(parsed);
189
+ const allowMihomoIpv6FakeIp = effectiveProxy !== null && !noProxyMatches(parsed);
142
190
  const resolveAddresses = dependencies.resolveAddresses ?? resolvePublicAddresses;
143
191
  const pinnedGet = dependencies.pinnedGet ?? pinnedHttpGet;
144
192
  const pinnedPost = dependencies.pinnedPost ?? pinnedHttpPost;
193
+ const isCanonicalUrl = dependencies.isCanonicalUrl ?? (() => false);
145
194
  const allowPrivate = providerAllowsPrivateNetwork(name, provider);
146
195
  let resolved: Awaited<ReturnType<typeof resolvePublicAddresses>>;
147
196
  try {
@@ -154,7 +203,21 @@ async function providerOutboundRequest(
154
203
  // destination or being pin-connected to the fake-IP (credit #1748). A NO_PROXY
155
204
  // match is a direct route, so it keeps the benchmark answer rejected. Image/Lab
156
205
  // fetch never passes this flag.
157
- allowBenchmarkAddresses: proxyConfigured && !noProxyMatches(parsed),
206
+ //
207
+ // TUN-mode transparency: with no proxy env, Clash/Surge/Mihomo TUN still
208
+ // intercepts the fake-IP destination itself, so the REGISTRY's own fixed
209
+ // discovery URL stays reachable by pin-connecting through the TUN. The
210
+ // proof is on the final request URL — not the provider name — because an
211
+ // OAuth/forward name matches any baseUrl by design while the bearer is
212
+ // pinned to the registry destination independently.
213
+ allowBenchmarkAddresses: (proxyConfigured && !noProxyMatches(parsed))
214
+ || transparentFakeIpException(url, parsed, isCanonicalUrl, name),
215
+ // Mihomo IPv6 fake-IP (fdfe:dcba:9876::/48) answers are admitted on a stricter gate
216
+ // than the benchmark range: the proxy must be the one fetch will use for this URL's
217
+ // scheme, and the request below is then bound to it explicitly (#3462). A ULA answer
218
+ // is otherwise indistinguishable from a real private host, so proxy presence alone
219
+ // is not enough.
220
+ allowMihomoIpv6FakeIp,
158
221
  });
159
222
  } catch (error) {
160
223
  const dnsResolutionFailed = error instanceof DestinationDnsResolutionError
@@ -169,7 +232,10 @@ async function providerOutboundRequest(
169
232
  }
170
233
  if (proxyConfigured && !resolved.privateNetwork) {
171
234
  warnProxyBoundaryOnce();
172
- return globalThis.fetch(url, { ...init, method, redirect: "manual" });
235
+ // When the Mihomo exception could have admitted an answer, pin the transport to the
236
+ // proxy the admission assumed instead of letting fetch re-infer it from the environment.
237
+ const proxy = allowMihomoIpv6FakeIp ? effectiveProxy : undefined;
238
+ return globalThis.fetch(url, { ...init, method, redirect: "manual", ...(proxy ? { proxy } : {}) });
173
239
  }
174
240
  if (proxyConfigured && resolved.privateNetwork && !noProxyMatches(parsed)) {
175
241
  const hostname = normalizeProxyHostname(parsed.hostname);
@@ -16,3 +16,25 @@ export function outboundProxyConfigured(
16
16
  ): boolean {
17
17
  return OUTBOUND_PROXY_ENV_KEYS.some(key => proxyEnvPresent(key, env));
18
18
  }
19
+
20
+ /**
21
+ * The proxy URL that Bun's fetch will actually use for `url`, or null when none applies.
22
+ *
23
+ * Bun selects by scheme: `HTTPS_PROXY` for `https:` targets, `HTTP_PROXY` for `http:`.
24
+ * `ALL_PROXY` is deliberately not consulted here — fetch does not honour it, so a caller
25
+ * that needs "this request will ride the proxy" as a precondition must not count it.
26
+ * Presence of *some* proxy variable (`outboundProxyConfigured`) is not that guarantee.
27
+ */
28
+ export function effectiveProxyFor(
29
+ url: URL,
30
+ env: ProxyEnvMap = process.env,
31
+ ): string | null {
32
+ const key: ProxyEnvKey | null = url.protocol === "https:"
33
+ ? "HTTPS_PROXY"
34
+ : url.protocol === "http:"
35
+ ? "HTTP_PROXY"
36
+ : null;
37
+ if (!key) return null;
38
+ const value = env[key]?.trim() || env[key.toLowerCase()]?.trim();
39
+ return value ? value : null;
40
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Characters that render as a colon separator. Folded to `:` in the matching
3
+ * view so a look-alike cannot hide a header from the label pattern.
4
+ */
5
+ const COLON_CONFUSABLES = new Set([
6
+ "\uFF1A", "\uFE55", "\uFE13", "\uA789", "\u02D0", "\u2236",
7
+ "\u205A", "\u0589", "\u1361", "\u16EC", "\u1803", "\u2982", "\u2AF6", "\uFE30",
8
+ ]);
9
+
10
+ /**
11
+ * Characters dropped from the matching view: anything with no visible width
12
+ * that could split a label into pieces the pattern no longer recognizes.
13
+ * `\p{Default_Ignorable_Code_Point}` is the systematic answer — it covers the
14
+ * zero-width set, the bidi isolates and marks, the Mongolian vowel separator,
15
+ * and the variation selectors in one property instead of a list that review
16
+ * keeps finding another member of. `\p{Cf}` and combining marks are folded too.
17
+ */
18
+ const INVISIBLE_FORMAT = /[\p{Default_Ignorable_Code_Point}\p{Cf}\p{Mn}\p{Me}]/u;
19
+
20
+ /**
21
+ * HTML named character references.
22
+ *
23
+ * A hand-picked list is a coverage promise nobody can keep — review found
24
+ * `&ii;`, `&ee;`, and `&DifferentialD;` decoding to compatibility letters that
25
+ * NFKD already maps onto `i`, `e`, and `d`, and the WHATWG table holds roughly
26
+ * 2200 entries. Neither Bun nor Node exposes that table, and pulling in a
27
+ * dependency to spell a header name is the wrong trade for this path.
28
+ *
29
+ * So names are not resolved at all. A named reference sitting inside a
30
+ * credential label is folded to a single placeholder character of unknown
31
+ * identity, and the label alternation accepts that placeholder wherever a
32
+ * letter may appear. Every named entity is covered, present and future,
33
+ * without claiming to know what any of them mean.
34
+ */
35
+ const NAMED_ENTITY_PLACEHOLDER = "\u0001";
36
+
37
+ /**
38
+ * The handful of named references that spell a SEPARATOR rather than a letter.
39
+ * These have to resolve exactly, because the placeholder stands in for a letter
40
+ * position and a separator is structure, not a character of the name.
41
+ */
42
+ const SEPARATOR_ENTITIES = new Map<string, string>([
43
+ ["colon", ":"], ["semi", ";"], ["equals", "="], ["quot", '"'], ["apos", "'"],
44
+ ["lt", "<"], ["gt", ">"], ["amp", "&"], ["sol", "/"], ["lowbar", "_"],
45
+ ["hyphen", "-"], ["dash", "-"], ["ndash", "-"], ["mdash", "-"], ["minus", "-"],
46
+ ["period", "."], ["comma", ","], ["num", "#"], ["nbsp", " "],
47
+ ]);
48
+
49
+ /**
50
+ * Latin look-alikes for the ASCII letters that appear in credential labels.
51
+ * Cyrillic `а`/`е`, Greek `ο`, fullwidth forms and the mathematical alphabets
52
+ * all render as the label to a human, so the matching view folds them back.
53
+ * NFKD handles the width/font variants; this table covers the cross-script
54
+ * homoglyphs NFKD deliberately leaves alone.
55
+ */
56
+ const LETTER_CONFUSABLES = new Map<string, string>([
57
+ // Cyrillic
58
+ ["\u0430", "a"], ["\u0435", "e"], ["\u043E", "o"], ["\u0440", "p"], ["\u0441", "c"],
59
+ ["\u0445", "x"], ["\u0443", "y"], ["\u04BB", "h"], ["\u0455", "s"], ["\u0456", "i"],
60
+ ["\u0458", "j"], ["\u043A", "k"], ["\u0442", "t"], ["\u0432", "b"], ["\u043C", "m"],
61
+ ["\u043D", "h"], ["\u0501", "d"], ["\u0503", "g"], ["\u051B", "q"], ["\u051D", "w"],
62
+ ["\u04CF", "l"], ["\u0261", "g"], ["\u04AB", "c"], ["\u04BD", "e"], ["\u0459", "k"],
63
+ // Greek
64
+ ["\u03B1", "a"], ["\u03BF", "o"], ["\u03C1", "p"], ["\u03BD", "v"], ["\u03BA", "k"],
65
+ ["\u03B5", "e"], ["\u03C4", "t"], ["\u03B9", "i"], ["\u03C5", "u"], ["\u03C7", "x"],
66
+ ["\u03B7", "n"], ["\u03BC", "u"], ["\u03C3", "o"], ["\u03B2", "b"], ["\u03B3", "y"],
67
+ // Latin extended / other
68
+ ["\u0131", "i"], ["\u0269", "i"], ["\u1D0F", "o"], ["\u0280", "r"], ["\u01BF", "p"],
69
+ ["\u0578", "n"], ["\u057D", "u"], ["\u0585", "o"], ["\u0581", "g"], ["\u2044", "/"],
70
+ ]);
71
+
72
+ /**
73
+ * Build a folded copy plus an index map back to the original string, so the
74
+ * match runs on normalized text while the output keeps every byte the match did
75
+ * not cover.
76
+ */
77
+ export function foldForMatching(value: string, decodeEscapes = true): { folded: string; map: number[] } {
78
+ let folded = "";
79
+ const map: number[] = [];
80
+ // Serialization escapes are ALIASES for the label, not decoration: a JSON
81
+ // `\u0069`, a percent-encoded `%69`, and an XML `&#105;` all spell the same
82
+ // field name to whatever parses the body, while spelling something else to a
83
+ // literal matcher. Decode them into the matching view (one folded character
84
+ // per escape, with the whole escape mapped back to its start) so
85
+ // `author\u0069zation`, `author%69zation`, and `author&#105;zation` are the
86
+ // label they claim to be.
87
+ const decodeEscape = (at: number): { ch: string; width: number } | null => {
88
+ // JSON `\uXXXX`, INCLUDING a surrogate pair. Decoding the halves
89
+ // independently left `\uD835\uDD69` as two lone surrogates, so the
90
+ // mathematical letter they spell was never normalized as one code point.
91
+ const json = /^\\u([0-9a-fA-F]{4})/.exec(value.slice(at, at + 6));
92
+ if (json) {
93
+ const high = parseInt(json[1]!, 16);
94
+ if (high >= 0xd800 && high <= 0xdbff) {
95
+ const low = /^\\u([0-9a-fA-F]{4})/.exec(value.slice(at + 6, at + 12));
96
+ const lowCode = low ? parseInt(low[1]!, 16) : NaN;
97
+ if (lowCode >= 0xdc00 && lowCode <= 0xdfff) {
98
+ return { ch: String.fromCharCode(high, lowCode), width: 12 };
99
+ }
100
+ }
101
+ return { ch: String.fromCharCode(high), width: 6 };
102
+ }
103
+ // Percent encoding is UTF-8: consecutive `%XX` bytes form ONE character.
104
+ // Decoding each byte on its own turned `%D0%B5` into two unrelated
105
+ // Latin-1 characters instead of the Cyrillic `е` the fold would have
106
+ // recognized.
107
+ const pct = /^(?:%[0-9a-fA-F]{2})+/.exec(value.slice(at, at + 24));
108
+ if (pct) {
109
+ try {
110
+ const decoded = decodeURIComponent(pct[0]);
111
+ if (decoded.length >= 1) {
112
+ // Consume only the bytes that produced the FIRST character, so the
113
+ // rest of the sequence is decoded on the next iteration.
114
+ const first = String.fromCodePoint(decoded.codePointAt(0)!);
115
+ const bytes = new TextEncoder().encode(first).length;
116
+ return { ch: first, width: bytes * 3 };
117
+ }
118
+ } catch {
119
+ const single = parseInt(pct[0].slice(1, 3), 16);
120
+ return { ch: String.fromCharCode(single), width: 3 };
121
+ }
122
+ }
123
+ const xml = /^&#(x[0-9a-fA-F]{1,6}|[0-9]{1,7});/.exec(value.slice(at, at + 11));
124
+ if (xml) {
125
+ const raw = xml[1]!;
126
+ const code = raw[0] === "x" || raw[0] === "X"
127
+ ? parseInt(raw.slice(1), 16)
128
+ : parseInt(raw, 10);
129
+ if (Number.isFinite(code) && code > 0 && code <= 0x10ffff) {
130
+ return { ch: String.fromCodePoint(code), width: xml[0].length };
131
+ }
132
+ }
133
+ // HTML named references. `&colon;` and the other separator names are
134
+ // resolved exactly; anything else folds to the opaque placeholder so the
135
+ // label still matches without pretending to know the character.
136
+ const named = /^&([A-Za-z][A-Za-z0-9]{1,31});/.exec(value.slice(at, at + 34));
137
+ if (named) {
138
+ const separator = SEPARATOR_ENTITIES.get(named[1]!.toLowerCase());
139
+ return { ch: separator ?? NAMED_ENTITY_PLACEHOLDER, width: named[0].length };
140
+ }
141
+ return null;
142
+ };
143
+ // Iterate by CODE POINT, not UTF-16 code unit: a supplementary character
144
+ // (mathematical letters, variation selectors above the BMP) is two units, so
145
+ // a per-unit loop hands each half to the property tests separately and
146
+ // neither half matches anything. `𝕩-api-key` and a U+E0100 inside a label
147
+ // both walked straight past the fold that way.
148
+ let i = 0;
149
+ while (i < value.length) {
150
+ const escaped = decodeEscapes ? decodeEscape(i) : null;
151
+ const ch = escaped ? escaped.ch : String.fromCodePoint(value.codePointAt(i)!);
152
+ const width = escaped ? escaped.width : ch.length;
153
+ if (INVISIBLE_FORMAT.test(ch)) {
154
+ i += width;
155
+ continue;
156
+ }
157
+ const mapped = COLON_CONFUSABLES.has(ch)
158
+ ? ":"
159
+ : LETTER_CONFUSABLES.get(ch.toLowerCase())
160
+ // NFKD collapses fullwidth, circled, and mathematical letter variants
161
+ // onto their ASCII base.
162
+ ?? (ch.normalize("NFKD").length === 1 ? ch.normalize("NFKD") : ch);
163
+ // One folded unit per source code point keeps the offset map aligned; a
164
+ // multi-unit fold would desynchronize it, so those keep the original.
165
+ folded += mapped.length === 1 ? mapped : ch;
166
+ // One map entry per EMITTED UTF-16 unit. An escaped supplementary
167
+ // character emits two units, and giving it one entry desynchronized every
168
+ // later offset — the mask then landed mid-token and left part of the
169
+ // credential behind.
170
+ const emittedText = mapped.length === 1 ? mapped : ch;
171
+ for (let k = 0; k < emittedText.length; k += 1) map.push(i);
172
+ i += width;
173
+ }
174
+ map.push(value.length);
175
+ return { folded, map };
176
+ }