@amir83nasr/10router 0.1.16 → 0.1.18

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 (240) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +5 -5
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page.js +1 -1
  6. package/.next/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page_client-reference-manifest.js +1 -1
  7. package/.next/server/app/(dashboard)/dashboard/cli-tools/page.js +1 -1
  8. package/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/(dashboard)/dashboard/combos/page.js +1 -1
  10. package/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/(dashboard)/dashboard/console-log/page.js +1 -1
  12. package/.next/server/app/(dashboard)/dashboard/console-log/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/(dashboard)/dashboard/endpoint/page.js +1 -1
  14. package/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/[id]/page.js +1 -1
  16. package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/[id]/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/page.js +1 -1
  18. package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/page_client-reference-manifest.js +1 -1
  19. package/.next/server/app/(dashboard)/dashboard/page.js +1 -1
  20. package/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
  21. package/.next/server/app/(dashboard)/dashboard/profile/page.js +1 -1
  22. package/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/(dashboard)/dashboard/providers/[id]/page.js +1 -1
  24. package/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
  25. package/.next/server/app/(dashboard)/dashboard/providers/new/page.js +1 -1
  26. package/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
  27. package/.next/server/app/(dashboard)/dashboard/providers/page.js +1 -1
  28. package/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
  29. package/.next/server/app/(dashboard)/dashboard/proxy-pools/page.js +1 -1
  30. package/.next/server/app/(dashboard)/dashboard/proxy-pools/page_client-reference-manifest.js +1 -1
  31. package/.next/server/app/(dashboard)/dashboard/pxpipe/page.js +1 -1
  32. package/.next/server/app/(dashboard)/dashboard/pxpipe/page_client-reference-manifest.js +1 -1
  33. package/.next/server/app/(dashboard)/dashboard/quota/page.js +1 -1
  34. package/.next/server/app/(dashboard)/dashboard/quota/page_client-reference-manifest.js +1 -1
  35. package/.next/server/app/(dashboard)/dashboard/token-saver/page.js +1 -1
  36. package/.next/server/app/(dashboard)/dashboard/token-saver/page_client-reference-manifest.js +1 -1
  37. package/.next/server/app/(dashboard)/dashboard/usage/page.js +1 -1
  38. package/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  40. package/.next/server/app/_global-error.html +1 -1
  41. package/.next/server/app/_global-error.rsc +1 -1
  42. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  43. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  44. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/.next/server/app/_not-found/page.js +1 -1
  46. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  47. package/.next/server/app/_not-found.html +1 -1
  48. package/.next/server/app/_not-found.rsc +5 -5
  49. package/.next/server/app/_not-found.segments/_full.segment.rsc +5 -5
  50. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -5
  51. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  52. package/.next/server/app/api/cli-tools/all-statuses/route.js +1 -1
  53. package/.next/server/app/api/cli-tools/claude-settings/route.js +1 -1
  54. package/.next/server/app/api/cli-tools/codex-settings/route.js +1 -1
  55. package/.next/server/app/api/cli-tools/cowork-mcp-registry/route.js +1 -1
  56. package/.next/server/app/api/cli-tools/cowork-mcp-tools/route.js +1 -1
  57. package/.next/server/app/api/cli-tools/cowork-settings/route.js +1 -1
  58. package/.next/server/app/api/cli-tools/dsh-settings/route.js +1 -1
  59. package/.next/server/app/api/cli-tools/opencode-settings/route.js +1 -1
  60. package/.next/server/app/api/models/route.js +1 -1
  61. package/.next/server/app/api/oauth/[provider]/[action]/route.js +1 -1
  62. package/.next/server/app/api/oauth/codex/bulk-import/route.js +1 -1
  63. package/.next/server/app/api/oauth/codex/import-token/route.js +1 -1
  64. package/.next/server/app/api/providers/[id]/models/route.js +1 -1
  65. package/.next/server/app/api/providers/[id]/test/route.js +1 -1
  66. package/.next/server/app/api/providers/client/route.js +1 -1
  67. package/.next/server/app/api/providers/route.js +1 -1
  68. package/.next/server/app/api/providers/test-batch/route.js +1 -1
  69. package/.next/server/app/api/providers/validate/route.js +1 -1
  70. package/.next/server/app/api/settings/route.js +1 -1
  71. package/.next/server/app/api/translator/console-logs/route.js +1 -1
  72. package/.next/server/app/api/translator/console-logs/stream/route.js +1 -1
  73. package/.next/server/app/api/translator/send/route.js +1 -1
  74. package/.next/server/app/api/translator/translate/route.js +1 -1
  75. package/.next/server/app/api/tunnel/disable/route.js +1 -1
  76. package/.next/server/app/api/tunnel/enable/route.js +1 -1
  77. package/.next/server/app/api/tunnel/tailscale-disable/route.js +1 -1
  78. package/.next/server/app/api/tunnel/tailscale-enable/route.js +1 -1
  79. package/.next/server/app/api/tunnel/tailscale-install/route.js +2 -2
  80. package/.next/server/app/api/usage/[connectionId]/codex-reset-credits/route.js +1 -1
  81. package/.next/server/app/api/usage/[connectionId]/route.js +1 -1
  82. package/.next/server/app/api/v1/api/chat/route.js +1 -1
  83. package/.next/server/app/api/v1/audio/speech/route.js +1 -1
  84. package/.next/server/app/api/v1/audio/transcriptions/route.js +1 -1
  85. package/.next/server/app/api/v1/chat/completions/route.js +1 -1
  86. package/.next/server/app/api/v1/embeddings/route.js +1 -1
  87. package/.next/server/app/api/v1/images/generations/route.js +1 -1
  88. package/.next/server/app/api/v1/messages/route.js +1 -1
  89. package/.next/server/app/api/v1/models/[...model]/route.js +1 -1
  90. package/.next/server/app/api/v1/models/route.js +1 -1
  91. package/.next/server/app/api/v1/responses/compact/route.js +1 -1
  92. package/.next/server/app/api/v1/responses/route.js +1 -1
  93. package/.next/server/app/api/v1/route.js +1 -1
  94. package/.next/server/app/api/v1/search/route.js +1 -1
  95. package/.next/server/app/api/v1/systemone/route.js +1 -1
  96. package/.next/server/app/api/v1/videos/[id]/route.js +1 -1
  97. package/.next/server/app/api/v1/videos/edits/route.js +1 -1
  98. package/.next/server/app/api/v1/videos/extensions/route.js +1 -1
  99. package/.next/server/app/api/v1/videos/generations/route.js +1 -1
  100. package/.next/server/app/api/v1/web/fetch/route.js +1 -1
  101. package/.next/server/app/api/v1beta/models/[...path]/route.js +1 -1
  102. package/.next/server/app/api/version/route.js +1 -1
  103. package/.next/server/app/callback/page.js +1 -1
  104. package/.next/server/app/callback/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/callback.html +1 -1
  106. package/.next/server/app/callback.rsc +5 -5
  107. package/.next/server/app/callback.segments/_full.segment.rsc +5 -5
  108. package/.next/server/app/callback.segments/_tree.segment.rsc +1 -1
  109. package/.next/server/app/callback.segments/callback/__PAGE__.segment.rsc +5 -5
  110. package/.next/server/app/dashboard/cli-tools.html +1 -1
  111. package/.next/server/app/dashboard/cli-tools.rsc +8 -8
  112. package/.next/server/app/dashboard/cli-tools.segments/!KGRhc2hib2FyZCk/dashboard/cli-tools/__PAGE__.segment.rsc +7 -7
  113. package/.next/server/app/dashboard/cli-tools.segments/_full.segment.rsc +8 -8
  114. package/.next/server/app/dashboard/cli-tools.segments/_tree.segment.rsc +1 -1
  115. package/.next/server/app/dashboard/combos.html +1 -1
  116. package/.next/server/app/dashboard/combos.rsc +7 -7
  117. package/.next/server/app/dashboard/combos.segments/!KGRhc2hib2FyZCk/dashboard/combos/__PAGE__.segment.rsc +7 -7
  118. package/.next/server/app/dashboard/combos.segments/_full.segment.rsc +7 -7
  119. package/.next/server/app/dashboard/combos.segments/_tree.segment.rsc +1 -1
  120. package/.next/server/app/dashboard/endpoint.html +1 -1
  121. package/.next/server/app/dashboard/endpoint.rsc +8 -8
  122. package/.next/server/app/dashboard/endpoint.segments/!KGRhc2hib2FyZCk/dashboard/endpoint/__PAGE__.segment.rsc +7 -7
  123. package/.next/server/app/dashboard/endpoint.segments/_full.segment.rsc +8 -8
  124. package/.next/server/app/dashboard/endpoint.segments/_tree.segment.rsc +1 -1
  125. package/.next/server/app/dashboard/media-providers/systemone.html +1 -1
  126. package/.next/server/app/dashboard/media-providers/systemone.rsc +7 -7
  127. package/.next/server/app/dashboard/media-providers/systemone.segments/!KGRhc2hib2FyZCk/dashboard/media-providers/systemone/__PAGE__.segment.rsc +7 -7
  128. package/.next/server/app/dashboard/media-providers/systemone.segments/_full.segment.rsc +7 -7
  129. package/.next/server/app/dashboard/media-providers/systemone.segments/_tree.segment.rsc +1 -1
  130. package/.next/server/app/dashboard/profile.html +1 -1
  131. package/.next/server/app/dashboard/profile.rsc +7 -7
  132. package/.next/server/app/dashboard/profile.segments/!KGRhc2hib2FyZCk/dashboard/profile/__PAGE__.segment.rsc +7 -7
  133. package/.next/server/app/dashboard/profile.segments/_full.segment.rsc +7 -7
  134. package/.next/server/app/dashboard/profile.segments/_tree.segment.rsc +1 -1
  135. package/.next/server/app/dashboard/providers/new.html +1 -1
  136. package/.next/server/app/dashboard/providers/new.rsc +7 -7
  137. package/.next/server/app/dashboard/providers/new.segments/!KGRhc2hib2FyZCk/dashboard/providers/new/__PAGE__.segment.rsc +7 -7
  138. package/.next/server/app/dashboard/providers/new.segments/_full.segment.rsc +7 -7
  139. package/.next/server/app/dashboard/providers/new.segments/_tree.segment.rsc +1 -1
  140. package/.next/server/app/dashboard/providers.html +1 -1
  141. package/.next/server/app/dashboard/providers.rsc +7 -7
  142. package/.next/server/app/dashboard/providers.segments/!KGRhc2hib2FyZCk/dashboard/providers/__PAGE__.segment.rsc +7 -7
  143. package/.next/server/app/dashboard/providers.segments/_full.segment.rsc +7 -7
  144. package/.next/server/app/dashboard/providers.segments/_tree.segment.rsc +1 -1
  145. package/.next/server/app/dashboard/proxy-pools.html +1 -1
  146. package/.next/server/app/dashboard/proxy-pools.rsc +7 -7
  147. package/.next/server/app/dashboard/proxy-pools.segments/!KGRhc2hib2FyZCk/dashboard/proxy-pools/__PAGE__.segment.rsc +7 -7
  148. package/.next/server/app/dashboard/proxy-pools.segments/_full.segment.rsc +7 -7
  149. package/.next/server/app/dashboard/proxy-pools.segments/_tree.segment.rsc +1 -1
  150. package/.next/server/app/dashboard/pxpipe.html +1 -1
  151. package/.next/server/app/dashboard/pxpipe.rsc +6 -6
  152. package/.next/server/app/dashboard/pxpipe.segments/!KGRhc2hib2FyZCk/dashboard/pxpipe/__PAGE__.segment.rsc +6 -6
  153. package/.next/server/app/dashboard/pxpipe.segments/_full.segment.rsc +6 -6
  154. package/.next/server/app/dashboard/pxpipe.segments/_tree.segment.rsc +1 -1
  155. package/.next/server/app/dashboard/quota.html +2 -2
  156. package/.next/server/app/dashboard/quota.rsc +8 -8
  157. package/.next/server/app/dashboard/quota.segments/!KGRhc2hib2FyZCk/dashboard/quota/__PAGE__.segment.rsc +8 -8
  158. package/.next/server/app/dashboard/quota.segments/_full.segment.rsc +8 -8
  159. package/.next/server/app/dashboard/quota.segments/_tree.segment.rsc +1 -1
  160. package/.next/server/app/dashboard/settings/pricing/page.js +1 -1
  161. package/.next/server/app/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
  162. package/.next/server/app/dashboard/settings/pricing.html +1 -1
  163. package/.next/server/app/dashboard/settings/pricing.rsc +5 -5
  164. package/.next/server/app/dashboard/settings/pricing.segments/_full.segment.rsc +5 -5
  165. package/.next/server/app/dashboard/settings/pricing.segments/_tree.segment.rsc +1 -1
  166. package/.next/server/app/dashboard/settings/pricing.segments/dashboard/settings/pricing/__PAGE__.segment.rsc +5 -5
  167. package/.next/server/app/dashboard/token-saver.html +1 -1
  168. package/.next/server/app/dashboard/token-saver.rsc +6 -6
  169. package/.next/server/app/dashboard/token-saver.segments/!KGRhc2hib2FyZCk/dashboard/token-saver/__PAGE__.segment.rsc +6 -6
  170. package/.next/server/app/dashboard/token-saver.segments/_full.segment.rsc +6 -6
  171. package/.next/server/app/dashboard/token-saver.segments/_tree.segment.rsc +1 -1
  172. package/.next/server/app/dashboard/usage.html +2 -2
  173. package/.next/server/app/dashboard/usage.rsc +7 -7
  174. package/.next/server/app/dashboard/usage.segments/!KGRhc2hib2FyZCk/dashboard/usage/__PAGE__.segment.rsc +7 -7
  175. package/.next/server/app/dashboard/usage.segments/_full.segment.rsc +7 -7
  176. package/.next/server/app/dashboard/usage.segments/_tree.segment.rsc +1 -1
  177. package/.next/server/app/dashboard.html +1 -1
  178. package/.next/server/app/dashboard.rsc +8 -8
  179. package/.next/server/app/dashboard.segments/!KGRhc2hib2FyZCk/dashboard/__PAGE__.segment.rsc +7 -7
  180. package/.next/server/app/dashboard.segments/_full.segment.rsc +8 -8
  181. package/.next/server/app/dashboard.segments/_tree.segment.rsc +1 -1
  182. package/.next/server/app/index.html +1 -1
  183. package/.next/server/app/index.rsc +5 -5
  184. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  185. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  186. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  187. package/.next/server/app/landing/page.js +1 -1
  188. package/.next/server/app/landing/page_client-reference-manifest.js +1 -1
  189. package/.next/server/app/landing.html +1 -1
  190. package/.next/server/app/landing.rsc +5 -5
  191. package/.next/server/app/landing.segments/_full.segment.rsc +5 -5
  192. package/.next/server/app/landing.segments/_tree.segment.rsc +1 -1
  193. package/.next/server/app/landing.segments/landing/__PAGE__.segment.rsc +5 -5
  194. package/.next/server/app/login/page.js +1 -1
  195. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  196. package/.next/server/app/login.html +1 -1
  197. package/.next/server/app/login.rsc +5 -5
  198. package/.next/server/app/login.segments/_full.segment.rsc +5 -5
  199. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  200. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +5 -5
  201. package/.next/server/app/page.js +1 -1
  202. package/.next/server/app/page_client-reference-manifest.js +1 -1
  203. package/.next/server/app-paths-manifest.json +5 -5
  204. package/.next/server/chunks/1003.js +1 -1
  205. package/.next/server/chunks/1649.js +1 -1
  206. package/.next/server/chunks/1751.js +1 -1
  207. package/.next/server/chunks/2143.js +1 -1
  208. package/.next/server/chunks/2151.js +1 -1
  209. package/.next/server/chunks/2282.js +3 -3
  210. package/.next/server/chunks/2531.js +1 -1
  211. package/.next/server/chunks/2801.js +1 -1
  212. package/.next/server/chunks/3671.js +1 -1
  213. package/.next/server/chunks/6343.js +3 -3
  214. package/.next/server/chunks/6617.js +1 -1
  215. package/.next/server/chunks/7591.js +1 -1
  216. package/.next/server/chunks/7904.js +1 -1
  217. package/.next/server/chunks/9429.js +1 -1
  218. package/.next/server/middleware-build-manifest.js +1 -1
  219. package/.next/server/pages/404.html +1 -1
  220. package/.next/server/pages/500.html +1 -1
  221. package/.next/server/server-reference-manifest.js +1 -1
  222. package/.next/server/server-reference-manifest.json +1 -1
  223. package/.next/static/chunks/1660-084df607ff4f889d.js +1 -0
  224. package/.next/static/chunks/{6770-e2921b2ce695ed57.js → 6770-9e948c92e87956c4.js} +1 -1
  225. package/CHANGELOG.md +11 -0
  226. package/open-sse/config/opencodeGo.js +3 -2
  227. package/open-sse/executors/opencode-go.js +7 -0
  228. package/open-sse/executors/opencode.js +20 -0
  229. package/open-sse/handlers/chatCore.js +7 -14
  230. package/open-sse/providers/capabilities.js +30 -0
  231. package/open-sse/providers/models/helpers.js +14 -0
  232. package/open-sse/providers/registry/opencode-go.js +116 -31
  233. package/open-sse/services/lazyRuntime.js +110 -0
  234. package/open-sse/services/opencodeTlsClient.js +91 -0
  235. package/open-sse/services/provider.js +27 -0
  236. package/open-sse/utils/writableDataDir.js +32 -0
  237. package/package.json +1 -1
  238. package/.next/static/chunks/1660-c4c1cfa3cb7c9b2e.js +0 -1
  239. /package/.next/static/{EsP8l_VAPRRfBiiJPJqQe → PyHCF87rmgiD0pV5fDUOP}/_buildManifest.js +0 -0
  240. /package/.next/static/{EsP8l_VAPRRfBiiJPJqQe → PyHCF87rmgiD0pV5fDUOP}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to 10Router will be documented in this file.
4
4
 
5
+ ## [0.1.18] — 2026-10-30
6
+
7
+ - Synced OpenCode free-tier and OpenCode Go improvements from 9router: TLS-impersonating client (wreq-js) for the free tier, opencode free-tier capability entries (glm-5.3-flash-free on openai format, mimo reasoning, deepseek-v4.1-flash-free 330k context cap), and `isOpencodeFreeTierModel`/`isDeepSeekModel` helpers.
8
+
9
+ ## [0.1.17] — 2026-10-26
10
+
11
+ - Fix: OpenCode Go routed each model to the provider's default endpoint instead of one it actually serves. An OpenAI-format client asking for a claude-only model (`claude-haiku-5-5`, `minimax-m2.7`) was sent to `/chat/completions` and rejected upstream with `ModelProtocolUnsupported`; `chatCore` and the tests now share `resolveModelTransport` (`open-sse/services/provider.js`), which prefers the sourceFormat-matched endpoint (no translation) and otherwise a model-declared one.
12
+ - OpenCode Go transports are ordered `claude → openai → openai-responses → gemini`, so the fallback prefers Anthropic Messages, which is what Claude Code traffic uses.
13
+ - OpenCode Go `supportedFormats` re-verified against the live endpoints (the published table is stale): `kimi-k3`, `kimi-k3.1-pro`, `minimax-m3` and `deepseek-v4.1-flash` also answer on `/messages`, most chat models also answer `/responses`, and `minimax-m2.7` is Messages-only. Models disabled in an account's console read as unavailable, so only `ModelProtocolUnsupported` narrows a model's formats.
14
+ - Added the documented `step-5-preview` and `beam-free` models, plus `gemini-4-argon` on the Google wire: the executor builds the model-scoped `/{model}:generateContent` URL and authenticates with `x-goog-api-key`.
15
+
5
16
  ## [0.1.16] — 2026-10-26
6
17
 
7
18
  - Removed the fish-audio, glm-cn, groq, huggingface, minimax, minimax-cn, siliconflow, vercel-ai-gateway, xiaomi-tokenplan and xquik providers: registry entries, executors, usage, TTS and image handlers, logos and their tests are gone.
@@ -2,8 +2,9 @@
2
2
  // Limits and model access are enforced server-side; nothing here gates requests.
3
3
  export const OPENCODE_GO_DOCS_URL = "https://opencode.ai/v2/docs/console/go";
4
4
 
5
- // Wire formats Go serves, matching the registry transport `format` values. A Go key
6
- // must be accepted by every endpoint here, so the connection probe checks all of them.
5
+ // Wire formats the connection probe covers, matching registry transport `format`
6
+ // values. The Google wire (`/google/v1beta/models/{model}`) is served for a single
7
+ // model and is not probed, because its URL depends on that model's availability.
7
8
  export const OPENCODE_GO_ENDPOINTS = [
8
9
  {
9
10
  format: "openai",
@@ -50,6 +50,13 @@ export class OpenCodeGoExecutor extends DefaultExecutor {
50
50
  buildUrl(model, stream, urlIndex = 0, credentials = null) {
51
51
  // Muse Spark lives on /responses even when a stale runtimeTransport leaks in.
52
52
  if (isResponsesModel(model)) return RESPONSES_BASE_URL;
53
+ // Google wire carries the model id in the path, unlike the OpenAI/Anthropic
54
+ // endpoints whose transport URL is the full target.
55
+ const transport = credentials?.runtimeTransport;
56
+ if (transport?.format === "gemini") {
57
+ const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
58
+ return `${transport.baseUrl}/${baseModelId(model)}:${action}`;
59
+ }
53
60
  return super.buildUrl(model, stream, urlIndex, credentials);
54
61
  }
55
62
 
@@ -6,6 +6,7 @@ import { getThinkingLevels } from "../providers/thinkingLevels.js";
6
6
  import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
7
7
  import { resolveSessionId } from "../utils/sessionManager.js";
8
8
  import { isMuseSparkModel } from "../providers/models/helpers.js";
9
+ import { opencodeTlsFetch, OpencodeTlsUnavailableError } from "../services/opencodeTlsClient.js";
9
10
  import {
10
11
  applyFingerprintTools,
11
12
  detectToolShape,
@@ -357,6 +358,25 @@ export class OpenCodeExecutor extends BaseExecutor {
357
358
  const result = await super.execute({
358
359
  ...args,
359
360
  credentials: this.prepareRequestCredentials(args),
361
+ directFetch: async (url, options) => {
362
+ try {
363
+ return await opencodeTlsFetch(
364
+ url,
365
+ {
366
+ method: options.method,
367
+ headers: options.headers,
368
+ body: options.body,
369
+ },
370
+ { signal: options.signal },
371
+ );
372
+ } catch (error) {
373
+ if (error instanceof OpencodeTlsUnavailableError) {
374
+ // Fail-open: wreq-js not installed; use native fetch.
375
+ return fetch(url, options);
376
+ }
377
+ throw error;
378
+ }
379
+ },
360
380
  });
361
381
  // Only a temporary quota window earns the breaker. Entitlement 429s
362
382
  // ("free models work in OpenCode") are permanent plan gates: cooling the
@@ -1,4 +1,4 @@
1
- import { detectFormat, getTargetFormat, resolveTransport } from "../services/provider.js";
1
+ import { detectFormat, getTargetFormat, resolveModelTransport } from "../services/provider.js";
2
2
  import { translateRequest } from "../translator/index.js";
3
3
  import {
4
4
  applyThinking,
@@ -138,20 +138,13 @@ export async function handleChatCore({
138
138
  const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
139
139
  const modelTargetFormat = getModelTargetFormat(alias, model);
140
140
  // Multi-endpoint providers: pick transport matching sourceFormat → zero translation.
141
- // Per-model guard: only use the transport when the model declares support for that
142
- // sourceFormat — opencode-go models differ in endpoint support (kimi/glm only do
143
- // /chat/completions), so without this guard a claude-format request would wrongly
144
- // route kimi to /messages.
141
+ // Guarded per model: opencode-go models differ in endpoint support, so a
142
+ // claude-format request must not go to /messages for kimi/glm and an OpenAI one
143
+ // must not go to /chat/completions for claude-haiku-5-5 (upstream answers
144
+ // ModelProtocolUnsupported). When the client format has no endpoint, the model's
145
+ // preferred declared endpoint is used and translation handles the wire change.
145
146
  const modelSupportedFormats = getModelSupportedFormats(alias, model);
146
- const runtimeTransport = resolveTransport(provider, sourceFormat);
147
- // Per-model guard: when a model declares supportedFormats, only use the
148
- // sourceFormat-matched transport if that format is declared (opencode-go models
149
- // differ — kimi/glm only do /chat/completions). Undeclared models keep the
150
- // upstream default (use the transport), preserving behavior for glm/deepseek/...
151
- const useTransport =
152
- !modelSupportedFormats || modelSupportedFormats.includes(sourceFormat)
153
- ? runtimeTransport
154
- : null;
147
+ const useTransport = resolveModelTransport(provider, sourceFormat, modelSupportedFormats);
155
148
  // A source-format-matched endpoint keeps the request lossless. Prefer it
156
149
  // over a model-level targetFormat, which is only the fallback for clients
157
150
  // whose wire format has no supported transport (for example MiniMax-M3:
@@ -253,6 +253,16 @@ export const MODEL_CAPABILITIES = {
253
253
  contextWindow: 1000000,
254
254
  maxOutput: 131072,
255
255
  },
256
+ "glm-5.3-flash-free": {
257
+ vision: true,
258
+ videoInput: true,
259
+ pdf: true,
260
+ reasoning: true,
261
+ thinkingFormat: "openai",
262
+ thinkingCanDisable: false,
263
+ contextWindow: 1000000,
264
+ maxOutput: 128000,
265
+ },
256
266
  "glm-4.6v": {
257
267
  vision: true,
258
268
  videoInput: true,
@@ -621,6 +631,26 @@ export const PROVIDER_CAPABILITIES = {
621
631
  // /api/v2/image/upload and leaves image_urls/chat_context.imageUrls null
622
632
  // (same as qodercli). reasoning:true on all of them — every model can
623
633
  // reason; the upstream is_reasoning flag only drives model_config selection.
634
+ // OpenCode Free (oc)
635
+ opencode: {
636
+ "mimo-v2.6-flash-free": {
637
+ vision: true,
638
+ audioInput: true,
639
+ videoInput: true,
640
+ reasoning: true,
641
+ thinkingFormat: "openai",
642
+ thinkingCanDisable: true,
643
+ contextWindow: 1048576,
644
+ maxOutput: 131072,
645
+ },
646
+ "deepseek-v4.1-flash-free": {
647
+ vision: true,
648
+ reasoning: true,
649
+ thinkingFormat: "deepseek",
650
+ contextWindow: 330000,
651
+ maxOutput: 128000,
652
+ },
653
+ },
624
654
  // thinkingFormat keeps the true-model family for documentation/UI, but
625
655
  // thinkingCanDisable:false everywhere: the executor only forwards
626
656
  // messages/tools/max_tokens, and thinking is fixed upstream via
@@ -25,3 +25,17 @@ export function isMuseSparkModel(modelId) {
25
25
  const base = clean.includes("/") ? clean.split("/").pop() : clean;
26
26
  return /^muse[-_]?spark(?:$|[-_:.\s])/i.test(base);
27
27
  }
28
+
29
+ export function isDeepSeekModel(modelId) {
30
+ if (!modelId || typeof modelId !== "string") return false;
31
+ const clean = modelId.replace(/\([^()]+\)\s*$/, "").trim();
32
+ const base = clean.includes("/") ? clean.split("/").pop() : clean;
33
+ return /^deepseek-/i.test(base);
34
+ }
35
+
36
+ export function isOpencodeFreeTierModel(modelId) {
37
+ if (!modelId || typeof modelId !== "string") return false;
38
+ const clean = modelId.replace(/\([^()]+\)\s*$/, "").trim();
39
+ const base = clean.includes("/") ? clean.split("/").pop() : clean;
40
+ return /-free$/i.test(base);
41
+ }
@@ -25,8 +25,15 @@ export default {
25
25
  },
26
26
  // Multi-endpoint: pick the transport matching the client sourceFormat to skip
27
27
  // translation. Guarded per-model by `supportedFormats` (see chatCore) because
28
- // opencode-go models differ in endpoint support.
28
+ // opencode-go models differ in endpoint support. Order is the fallback
29
+ // preference when the client format has no endpoint on the model: Anthropic
30
+ // first (Claude Code is the main client), then Chat Completions, then Responses.
29
31
  transports: [
32
+ {
33
+ format: "claude",
34
+ baseUrl: "https://opencode.ai/inference/go/anthropic/v1/messages",
35
+ auth: { combined: true, header: "x-api-key", scheme: "raw", anthropicVersion: true },
36
+ },
30
37
  {
31
38
  format: "openai",
32
39
  baseUrl: "https://opencode.ai/inference/go/openai/v1/chat/completions",
@@ -35,58 +42,122 @@ export default {
35
42
  // (400 unknown field "thinking"), so normalize reasoning to reasoning_effort.
36
43
  thinkingFormat: "openai",
37
44
  },
38
- {
39
- format: "claude",
40
- baseUrl: "https://opencode.ai/inference/go/anthropic/v1/messages",
41
- auth: { combined: true, header: "x-api-key", scheme: "raw", anthropicVersion: true },
42
- },
43
45
  {
44
46
  format: "openai-responses",
45
47
  baseUrl: "https://opencode.ai/inference/go/openai/v1/responses",
46
48
  auth: { combined: true, header: "Authorization", scheme: "bearer" },
47
49
  thinkingFormat: "openai",
48
50
  },
51
+ {
52
+ // Google protocol: the model id is a path segment, so the executor appends
53
+ // `/{model}:generateContent` instead of using baseUrl as the full endpoint.
54
+ format: "gemini",
55
+ baseUrl: "https://opencode.ai/inference/go/google/v1beta/models",
56
+ auth: { combined: true, header: "x-goog-api-key", scheme: "raw" },
57
+ },
49
58
  ],
50
- // supportedFormats follow the endpoint table in https://opencode.ai/v2/docs/console/go
59
+ // supportedFormats are the endpoints each model actually answers on. The docs table
60
+ // is incomplete (it omits models a Go key can reach and lists protocols the gateway
61
+ // rejects); these values were verified against the live Go endpoints. claude comes
62
+ // first on purpose: most 10Router traffic is Claude Code, which then needs no
63
+ // translation. Probes ran on one account, so "model unavailable" can mean the model
64
+ // is disabled in that account's console — do not narrow a model's formats for it;
65
+ // only ModelProtocolUnsupported proves an endpoint is unsupported.
51
66
  models: [
52
- { id: "deepseek-v4.1-flash", name: "DeepSeek V4.1 Flash", supportedFormats: ["openai"] },
53
- { id: "glm-5.3-flash", name: "GLM 5.3 Flash (Vision)", supportedFormats: ["openai"] },
54
- { id: "glm-5.3", name: "GLM 5.3", supportedFormats: ["openai"] },
55
- { id: "glm-5.2", name: "GLM 5.2", supportedFormats: ["openai"] },
56
- { id: "glm-5.1", name: "GLM 5.1", supportedFormats: ["openai"] },
57
- { id: "kimi-k2.7-code", name: "Kimi K2.7 Code", supportedFormats: ["openai"] },
58
- { id: "kimi-k2.6", name: "Kimi K2.6", supportedFormats: ["openai"] },
59
- { id: "kimi-k3", name: "Kimi K3", supportedFormats: ["openai"] },
60
- { id: "kimi-k3.1-pro", name: "Kimi K3.1 Pro", supportedFormats: ["openai"] },
67
+ {
68
+ id: "deepseek-v4.1-flash",
69
+ name: "DeepSeek V4.1 Flash",
70
+ supportedFormats: ["claude", "openai", "openai-responses"],
71
+ },
72
+ {
73
+ id: "glm-5.3-flash",
74
+ name: "GLM 5.3 Flash (Vision)",
75
+ supportedFormats: ["openai", "openai-responses"],
76
+ },
77
+ { id: "glm-5.3", name: "GLM 5.3", supportedFormats: ["openai", "openai-responses"] },
78
+ { id: "glm-5.2", name: "GLM 5.2", supportedFormats: ["openai", "openai-responses"] },
79
+ { id: "glm-5.1", name: "GLM 5.1", supportedFormats: ["openai", "openai-responses"] },
80
+ {
81
+ id: "kimi-k2.7-code",
82
+ name: "Kimi K2.7 Code",
83
+ supportedFormats: ["openai", "openai-responses"],
84
+ },
85
+ { id: "kimi-k2.6", name: "Kimi K2.6", supportedFormats: ["openai", "openai-responses"] },
86
+ {
87
+ id: "kimi-k3",
88
+ name: "Kimi K3",
89
+ supportedFormats: ["claude", "openai", "openai-responses"],
90
+ },
91
+ {
92
+ id: "kimi-k3.1-pro",
93
+ name: "Kimi K3.1 Pro",
94
+ supportedFormats: ["claude", "openai", "openai-responses"],
95
+ },
61
96
  {
62
97
  id: "deepseek-v4-pro",
63
98
  name: "DeepSeek V4 Pro",
64
- supportedFormats: ["openai", "claude", "openai-responses"],
99
+ supportedFormats: ["claude", "openai", "openai-responses"],
65
100
  },
66
101
  {
67
102
  id: "deepseek-v4-flash",
68
103
  name: "DeepSeek V4 Flash",
69
- supportedFormats: ["openai", "claude", "openai-responses"],
104
+ supportedFormats: ["claude", "openai", "openai-responses"],
70
105
  },
71
106
  {
72
107
  id: "deepseek-v4-flash-vision-exp",
73
108
  name: "DeepSeek V4 Flash Vision (Exp)",
74
- supportedFormats: ["openai", "claude", "openai-responses"],
109
+ supportedFormats: ["claude", "openai", "openai-responses"],
110
+ },
111
+ { id: "longcat-2.0", name: "LongCat 2.0", supportedFormats: ["openai", "openai-responses"] },
112
+ {
113
+ id: "mimo-v2.6-flash",
114
+ name: "MiMo V2.6 Flash",
115
+ supportedFormats: ["openai", "openai-responses"],
75
116
  },
76
- { id: "longcat-2.0", name: "LongCat 2.0", supportedFormats: ["openai"] },
77
- { id: "mimo-v2.6-flash", name: "MiMo V2.6 Flash", supportedFormats: ["openai"] },
78
- { id: "mimo-v2.6-pro", name: "MiMo V2.6 Pro", supportedFormats: ["openai"] },
79
- { id: "mimo-v2.5", name: "MiMo V2.5", supportedFormats: ["openai"] },
117
+ {
118
+ id: "mimo-v2.6-pro",
119
+ name: "MiMo V2.6 Pro",
120
+ supportedFormats: ["openai", "openai-responses"],
121
+ },
122
+ { id: "mimo-v2.5", name: "MiMo V2.5", supportedFormats: ["openai", "openai-responses"] },
123
+ // "Unsupported model" on every wire: either retired or disabled in the account's
124
+ // console, which the probe cannot tell apart. Keep the documented Chat Completions
125
+ // route instead of narrowing what other accounts can use.
80
126
  { id: "mimo-v2.5-pro", name: "MiMo V2.5 Pro", supportedFormats: ["openai"] },
81
- { id: "minimax-m3", name: "MiniMax M3", supportedFormats: ["openai", "claude"] },
82
- { id: "minimax-m3.1-flash", name: "MiniMax M3.1 Flash", supportedFormats: ["openai"] },
83
- { id: "minimax-m2.7", name: "MiniMax M2.7", supportedFormats: ["openai", "claude"] },
127
+ {
128
+ id: "minimax-m3",
129
+ name: "MiniMax M3",
130
+ supportedFormats: ["claude", "openai", "openai-responses"],
131
+ },
132
+ {
133
+ id: "minimax-m3.1-flash",
134
+ name: "MiniMax M3.1 Flash",
135
+ supportedFormats: ["claude", "openai"],
136
+ },
137
+ { id: "minimax-m2.7", name: "MiniMax M2.7", supportedFormats: ["claude"] },
84
138
  { id: "claude-haiku-5-5", name: "Claude Haiku 5.5", supportedFormats: ["claude"] },
85
- { id: "qwen3.8-max", name: "Qwen 3.8 Max", supportedFormats: ["openai", "claude"] },
86
- { id: "qwen3.8-flash", name: "Qwen 3.8 Flash", supportedFormats: ["openai", "claude"] },
87
- { id: "qwen3.7-plus", name: "Qwen 3.7 Plus", supportedFormats: ["openai", "claude"] },
88
- { id: "hy4-preview", name: "Hy4 Preview", supportedFormats: ["openai"] },
89
- { id: "hy3", name: "Hy3", supportedFormats: ["openai"] },
139
+ {
140
+ id: "qwen3.8-max",
141
+ name: "Qwen 3.8 Max",
142
+ supportedFormats: ["claude", "openai", "openai-responses"],
143
+ },
144
+ {
145
+ id: "qwen3.8-flash",
146
+ name: "Qwen 3.8 Flash",
147
+ supportedFormats: ["claude", "openai", "openai-responses"],
148
+ },
149
+ {
150
+ id: "qwen3.7-plus",
151
+ name: "Qwen 3.7 Plus",
152
+ supportedFormats: ["claude", "openai", "openai-responses"],
153
+ },
154
+ { id: "hy4-preview", name: "Hy4 Preview", supportedFormats: ["openai", "openai-responses"] },
155
+ { id: "hy3", name: "Hy3", supportedFormats: ["openai", "openai-responses"] },
156
+ {
157
+ id: "step-5-preview",
158
+ name: "Step 5 Preview",
159
+ supportedFormats: ["openai", "openai-responses"],
160
+ },
90
161
  {
91
162
  id: "space-bunny-free",
92
163
  name: "Space Bunny Free",
@@ -99,6 +170,20 @@ export default {
99
170
  supportedFormats: ["openai"],
100
171
  tier: "free",
101
172
  },
173
+ {
174
+ id: "beam-free",
175
+ name: "Beam Free",
176
+ supportedFormats: ["openai", "openai-responses"],
177
+ tier: "free",
178
+ },
179
+ // Google wire only (documented). Every client format pivots through OpenAI to
180
+ // gemini, and the executor builds `/{model}:generateContent` from the transport.
181
+ {
182
+ id: "gemini-4-argon",
183
+ name: "Gemini 4 Argon",
184
+ targetFormat: "gemini",
185
+ supportedFormats: ["gemini"],
186
+ },
102
187
  // Served by /inference/go/openai/v1/responses only — the responses-only entry forces chatCore
103
188
  // past the sourceFormat-matched transports into translation (see chatCore guard).
104
189
  {
@@ -0,0 +1,110 @@
1
+ // Lazy loader for heavy optional runtime deps (playwright-core, wreq-js).
2
+ //
3
+ // They are devDependencies only, so end-user installs stay small. On first
4
+ // use the package is downloaded into <DATA_DIR>/runtime and imported from there.
5
+
6
+ import { spawn } from "node:child_process";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+ import { getWritableDataDir } from "../utils/writableDataDir.js";
11
+
12
+ const LAZY_DEP_VERSIONS = {
13
+ "playwright-core": "^1.62.1",
14
+ "wreq-js": "^3.2.0",
15
+ };
16
+
17
+ function getRuntimeDir() {
18
+ return path.join(getWritableDataDir(), "runtime");
19
+ }
20
+
21
+ let createRequirePromise;
22
+ function getCreateRequire() {
23
+ createRequirePromise ??= import(/* webpackIgnore: true */ "node:module").then(
24
+ (mod) => mod.createRequire,
25
+ );
26
+ return createRequirePromise;
27
+ }
28
+
29
+ function isModuleMissing(error) {
30
+ const code = String(error?.code || "");
31
+ return code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
32
+ }
33
+
34
+ function npmInstall(pkg, version) {
35
+ const cwd = getRuntimeDir();
36
+ fs.mkdirSync(cwd, { recursive: true });
37
+ const manifest = path.join(cwd, "package.json");
38
+ if (!fs.existsSync(manifest)) {
39
+ fs.writeFileSync(
40
+ manifest,
41
+ JSON.stringify({ name: "10router-runtime", private: true }, null, 2),
42
+ );
43
+ }
44
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
45
+ const args = ["install", "--no-audit", "--no-fund", "--prefer-online", `${pkg}@${version}`];
46
+ return new Promise((resolve) => {
47
+ const child = spawn(npm, args, {
48
+ cwd,
49
+ stdio: ["ignore", "pipe", "pipe"],
50
+ shell: process.platform === "win32",
51
+ });
52
+ let stderr = "";
53
+ child.stderr?.on("data", (chunk) => {
54
+ stderr += String(chunk);
55
+ });
56
+ child.on("error", (error) => resolve({ ok: false, stderr: String(error.message || error) }));
57
+ child.on("close", (status) => resolve({ ok: status === 0, stderr }));
58
+ });
59
+ }
60
+
61
+ async function installIntoRuntime(pkg, version) {
62
+ const res = await npmInstall(pkg, version);
63
+ if (!res.ok) {
64
+ const reason =
65
+ (res.stderr.match(/npm ERR! ?(.+)/) || [])[1] ||
66
+ res.stderr.split("\n").filter(Boolean).pop() ||
67
+ "unknown npm error";
68
+ throw new Error(
69
+ `${pkg} is missing and the on-demand install failed (${String(reason).slice(0, 160)}) — ` +
70
+ `install it manually: cd "${getRuntimeDir()}" && npm install ${pkg}@${version}`,
71
+ );
72
+ }
73
+ }
74
+
75
+ const inflight = new Map();
76
+
77
+ async function load(pkg) {
78
+ try {
79
+ return await import(/* webpackIgnore: true */ pkg);
80
+ } catch (error) {
81
+ if (!isModuleMissing(error)) throw error;
82
+ }
83
+ const createRequire = await getCreateRequire();
84
+ const runtimeRequire = createRequire(path.join(getRuntimeDir(), "package.json"));
85
+ try {
86
+ return await import(/* webpackIgnore: true */ pathToFileURL(runtimeRequire.resolve(pkg)).href);
87
+ } catch (error) {
88
+ if (!isModuleMissing(error)) throw error;
89
+ }
90
+ const version = LAZY_DEP_VERSIONS[pkg] || "latest";
91
+ await installIntoRuntime(pkg, version);
92
+ try {
93
+ return await import(/* webpackIgnore: true */ pathToFileURL(runtimeRequire.resolve(pkg)).href);
94
+ } catch (error) {
95
+ if (!isModuleMissing(error)) throw error;
96
+ fs.rmSync(path.join(getRuntimeDir(), "node_modules", pkg), { recursive: true, force: true });
97
+ await installIntoRuntime(pkg, version);
98
+ return import(/* webpackIgnore: true */ pathToFileURL(runtimeRequire.resolve(pkg)).href);
99
+ }
100
+ }
101
+
102
+ export function loadRuntimeDep(pkg) {
103
+ if (!inflight.has(pkg)) {
104
+ inflight.set(
105
+ pkg,
106
+ load(pkg).finally(() => inflight.delete(pkg)),
107
+ );
108
+ }
109
+ return inflight.get(pkg);
110
+ }
@@ -0,0 +1,91 @@
1
+ // TLS-impersonating HTTP client for opencode.ai's free tier.
2
+
3
+ import { loadRuntimeDep } from "./lazyRuntime.js";
4
+
5
+ const OPENCODE_CIPHER_LIST = [
6
+ "TLS_AES_128_GCM_SHA256",
7
+ "TLS_AES_256_GCM_SHA384",
8
+ "TLS_CHACHA20_POLY1305_SHA256",
9
+ "ECDHE-ECDSA-AES128-GCM-SHA256",
10
+ "ECDHE-RSA-AES128-GCM-SHA256",
11
+ "ECDHE-ECDSA-AES256-GCM-SHA384",
12
+ "ECDHE-RSA-AES256-GCM-SHA384",
13
+ "ECDHE-ECDSA-CHACHA20-POLY1305",
14
+ "ECDHE-RSA-CHACHA20-POLY1305",
15
+ "ECDHE-ECDSA-AES128-SHA",
16
+ "ECDHE-RSA-AES128-SHA",
17
+ "ECDHE-ECDSA-AES256-SHA",
18
+ "ECDHE-RSA-AES256-SHA",
19
+ "AES128-GCM-SHA256",
20
+ "AES256-GCM-SHA384",
21
+ "AES128-SHA",
22
+ "AES256-SHA",
23
+ ].join(":");
24
+
25
+ const DEFAULT_TIMEOUT_MS = 120000;
26
+
27
+ let sessionPromise = null;
28
+
29
+ export class OpencodeTlsUnavailableError extends Error {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = "OpencodeTlsUnavailableError";
33
+ }
34
+ }
35
+
36
+ async function loadSession() {
37
+ if (!sessionPromise) {
38
+ sessionPromise = (async () => {
39
+ const wreq = await loadRuntimeDep("wreq-js");
40
+ if (typeof wreq.createSession !== "function") {
41
+ throw new Error("wreq-js runtime is missing createSession");
42
+ }
43
+ return wreq.createSession({
44
+ emulation: { tlsOptions: { cipherList: OPENCODE_CIPHER_LIST } },
45
+ });
46
+ })().catch((error) => {
47
+ sessionPromise = null;
48
+ throw error;
49
+ });
50
+ }
51
+ return sessionPromise;
52
+ }
53
+
54
+ export function resetOpencodeTlsSession() {
55
+ const pending = sessionPromise;
56
+ sessionPromise = null;
57
+ pending?.then((session) => session.close?.()).catch(() => {});
58
+ }
59
+
60
+ export async function opencodeTlsFetch(
61
+ url,
62
+ init = {},
63
+ { signal, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
64
+ ) {
65
+ let session;
66
+ try {
67
+ session = await loadSession();
68
+ } catch (error) {
69
+ throw new OpencodeTlsUnavailableError(
70
+ `wreq-js unavailable (${error instanceof Error ? error.message : "unknown"}) — install the optional wreq-js dependency to reach the OpenCode free tier`,
71
+ );
72
+ }
73
+
74
+ const options = {
75
+ method: init.method || "GET",
76
+ headers: init.headers,
77
+ redirect: "follow",
78
+ timeout: timeoutMs,
79
+ };
80
+ if (init.body !== undefined) options.body = init.body;
81
+ if (signal) options.signal = signal;
82
+
83
+ const res = await session.fetch(url, options);
84
+ const headers = {};
85
+ res.headers?.forEach?.((value, key) => {
86
+ headers[key] = value;
87
+ });
88
+ return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
89
+ }
90
+
91
+ export const __test__ = { OPENCODE_CIPHER_LIST };
@@ -158,6 +158,33 @@ export function resolveTransport(provider, sourceFormat) {
158
158
  return transports.find((t) => t.format === sourceFormat) || null;
159
159
  }
160
160
 
161
+ // Fallback transport for a multi-endpoint provider when the client's wire format
162
+ // has no endpoint on this model (per-model `supportedFormats`). Without it the
163
+ // request would go to the provider's default URL and upstream would answer
164
+ // ModelProtocolUnsupported (opencode-go: claude-haiku-5-5 has no /chat/completions).
165
+ // The registry's transport order is the preference order, so providers can put the
166
+ // endpoint real clients use most first.
167
+ export function resolveFallbackTransport(provider, supportedFormats) {
168
+ if (!Array.isArray(supportedFormats) || !supportedFormats.length) return null;
169
+ const config = PROVIDERS[provider];
170
+ const transports = config?.transports;
171
+ if (!Array.isArray(transports) || !transports.length) return null;
172
+ return transports.find((t) => supportedFormats.includes(t.format || "openai")) || null;
173
+ }
174
+
175
+ // Transport for one model: the sourceFormat-matched endpoint when the model
176
+ // declares that format (zero translation), otherwise the model's preferred
177
+ // declared endpoint. Undeclared models keep the old behavior — the matched
178
+ // transport or the provider default.
179
+ export function resolveModelTransport(provider, sourceFormat, supportedFormats) {
180
+ const matchesSource = !supportedFormats || supportedFormats.includes(sourceFormat);
181
+ if (matchesSource) {
182
+ const matched = resolveTransport(provider, sourceFormat);
183
+ if (matched) return matched;
184
+ }
185
+ return resolveFallbackTransport(provider, supportedFormats);
186
+ }
187
+
161
188
  // Check if last message is from user
162
189
  export function isLastMessageFromUser(body) {
163
190
  const messages = body.messages || body.contents;
@@ -0,0 +1,32 @@
1
+ // Resolve a writable data directory: DATA_DIR first, then ~/.10router, then
2
+ // legacy ~/.9router, then tmpdir. Some installs point DATA_DIR at root-owned
3
+ // paths that the server process cannot write browser profiles or runtime
4
+ // installs into.
5
+
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+
10
+ let cached = null;
11
+
12
+ export function getWritableDataDir() {
13
+ if (cached) return cached;
14
+ const candidates = [
15
+ process.env.DATA_DIR,
16
+ path.join(os.homedir(), ".10router"),
17
+ path.join(os.homedir(), ".9router"),
18
+ path.join(os.tmpdir(), "10router"),
19
+ ].filter(Boolean);
20
+ for (const base of candidates) {
21
+ try {
22
+ fs.mkdirSync(base, { recursive: true });
23
+ fs.accessSync(base, fs.constants.W_OK);
24
+ cached = base;
25
+ return base;
26
+ } catch {
27
+ // Not writable — try the next candidate.
28
+ }
29
+ }
30
+ cached = process.cwd();
31
+ return cached;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amir83nasr/10router",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Amir83Nasr/10Router.git"