@amir83nasr/10router 0.1.5 → 0.1.7

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 (204) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +10 -10
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/required-server-files.json +5 -0
  6. package/.next/server/app/(dashboard)/dashboard/basic-chat/page_client-reference-manifest.js +1 -1
  7. package/.next/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page_client-reference-manifest.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_client-reference-manifest.js +1 -1
  10. package/.next/server/app/(dashboard)/dashboard/console-log/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
  12. package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/[id]/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
  19. package/.next/server/app/(dashboard)/dashboard/proxy-pools/page_client-reference-manifest.js +1 -1
  20. package/.next/server/app/(dashboard)/dashboard/pxpipe/page_client-reference-manifest.js +1 -1
  21. package/.next/server/app/(dashboard)/dashboard/quota/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/(dashboard)/dashboard/token-saver/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
  24. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  25. package/.next/server/app/_global-error.html +1 -1
  26. package/.next/server/app/_global-error.rsc +1 -1
  27. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  28. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  29. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  30. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  31. package/.next/server/app/_not-found.html +1 -1
  32. package/.next/server/app/_not-found.rsc +5 -5
  33. package/.next/server/app/_not-found.segments/_full.segment.rsc +5 -5
  34. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -5
  35. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  36. package/.next/server/app/api/changelog/route.js +1 -1
  37. package/.next/server/app/api/cli-tools/all-statuses/route.js +1 -1
  38. package/.next/server/app/api/cli-tools/claude-settings/route.js +1 -1
  39. package/.next/server/app/api/cli-tools/codex-settings/route.js +1 -1
  40. package/.next/server/app/api/cli-tools/cowork-mcp-registry/route.js +1 -1
  41. package/.next/server/app/api/cli-tools/cowork-mcp-tools/route.js +1 -1
  42. package/.next/server/app/api/cli-tools/cowork-settings/route.js +1 -1
  43. package/.next/server/app/api/cli-tools/dsh-settings/route.js +1 -1
  44. package/.next/server/app/api/cli-tools/opencode-settings/route.js +1 -1
  45. package/.next/server/app/api/oauth/[provider]/[action]/route.js +1 -1
  46. package/.next/server/app/api/oauth/codex/bulk-import/route.js +1 -1
  47. package/.next/server/app/api/oauth/codex/import-token/route.js +1 -1
  48. package/.next/server/app/api/providers/client/route.js +1 -1
  49. package/.next/server/app/api/providers/validate/route.js +1 -1
  50. package/.next/server/app/api/translator/send/route.js +1 -1
  51. package/.next/server/app/api/translator/translate/route.js +1 -1
  52. package/.next/server/app/api/tunnel/tailscale-install/route.js +2 -2
  53. package/.next/server/app/api/usage/[connectionId]/codex-reset-credits/route.js +1 -1
  54. package/.next/server/app/api/usage/[connectionId]/route.js +1 -1
  55. package/.next/server/app/api/version/route.js +1 -1
  56. package/.next/server/app/callback/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/callback.html +1 -1
  58. package/.next/server/app/callback.rsc +5 -5
  59. package/.next/server/app/callback.segments/_full.segment.rsc +5 -5
  60. package/.next/server/app/callback.segments/_tree.segment.rsc +1 -1
  61. package/.next/server/app/callback.segments/callback/__PAGE__.segment.rsc +5 -5
  62. package/.next/server/app/dashboard/basic-chat.html +1 -1
  63. package/.next/server/app/dashboard/basic-chat.rsc +7 -7
  64. package/.next/server/app/dashboard/basic-chat.segments/!KGRhc2hib2FyZCk/dashboard/basic-chat/__PAGE__.segment.rsc +7 -7
  65. package/.next/server/app/dashboard/basic-chat.segments/_full.segment.rsc +7 -7
  66. package/.next/server/app/dashboard/basic-chat.segments/_tree.segment.rsc +1 -1
  67. package/.next/server/app/dashboard/cli-tools.html +1 -1
  68. package/.next/server/app/dashboard/cli-tools.rsc +7 -7
  69. package/.next/server/app/dashboard/cli-tools.segments/!KGRhc2hib2FyZCk/dashboard/cli-tools/__PAGE__.segment.rsc +7 -7
  70. package/.next/server/app/dashboard/cli-tools.segments/_full.segment.rsc +7 -7
  71. package/.next/server/app/dashboard/cli-tools.segments/_tree.segment.rsc +1 -1
  72. package/.next/server/app/dashboard/combos.html +1 -1
  73. package/.next/server/app/dashboard/combos.rsc +7 -7
  74. package/.next/server/app/dashboard/combos.segments/!KGRhc2hib2FyZCk/dashboard/combos/__PAGE__.segment.rsc +7 -7
  75. package/.next/server/app/dashboard/combos.segments/_full.segment.rsc +7 -7
  76. package/.next/server/app/dashboard/combos.segments/_tree.segment.rsc +1 -1
  77. package/.next/server/app/dashboard/endpoint.html +1 -1
  78. package/.next/server/app/dashboard/endpoint.rsc +7 -7
  79. package/.next/server/app/dashboard/endpoint.segments/!KGRhc2hib2FyZCk/dashboard/endpoint/__PAGE__.segment.rsc +7 -7
  80. package/.next/server/app/dashboard/endpoint.segments/_full.segment.rsc +7 -7
  81. package/.next/server/app/dashboard/endpoint.segments/_tree.segment.rsc +1 -1
  82. package/.next/server/app/dashboard/media-providers/systemone.html +1 -1
  83. package/.next/server/app/dashboard/media-providers/systemone.rsc +7 -7
  84. package/.next/server/app/dashboard/media-providers/systemone.segments/!KGRhc2hib2FyZCk/dashboard/media-providers/systemone/__PAGE__.segment.rsc +7 -7
  85. package/.next/server/app/dashboard/media-providers/systemone.segments/_full.segment.rsc +7 -7
  86. package/.next/server/app/dashboard/media-providers/systemone.segments/_tree.segment.rsc +1 -1
  87. package/.next/server/app/dashboard/profile.html +1 -1
  88. package/.next/server/app/dashboard/profile.rsc +7 -7
  89. package/.next/server/app/dashboard/profile.segments/!KGRhc2hib2FyZCk/dashboard/profile/__PAGE__.segment.rsc +7 -7
  90. package/.next/server/app/dashboard/profile.segments/_full.segment.rsc +7 -7
  91. package/.next/server/app/dashboard/profile.segments/_tree.segment.rsc +1 -1
  92. package/.next/server/app/dashboard/providers/new.html +1 -1
  93. package/.next/server/app/dashboard/providers/new.rsc +7 -7
  94. package/.next/server/app/dashboard/providers/new.segments/!KGRhc2hib2FyZCk/dashboard/providers/new/__PAGE__.segment.rsc +7 -7
  95. package/.next/server/app/dashboard/providers/new.segments/_full.segment.rsc +7 -7
  96. package/.next/server/app/dashboard/providers/new.segments/_tree.segment.rsc +1 -1
  97. package/.next/server/app/dashboard/providers.html +1 -1
  98. package/.next/server/app/dashboard/providers.rsc +7 -7
  99. package/.next/server/app/dashboard/providers.segments/!KGRhc2hib2FyZCk/dashboard/providers/__PAGE__.segment.rsc +7 -7
  100. package/.next/server/app/dashboard/providers.segments/_full.segment.rsc +7 -7
  101. package/.next/server/app/dashboard/providers.segments/_tree.segment.rsc +1 -1
  102. package/.next/server/app/dashboard/proxy-pools.html +1 -1
  103. package/.next/server/app/dashboard/proxy-pools.rsc +7 -7
  104. package/.next/server/app/dashboard/proxy-pools.segments/!KGRhc2hib2FyZCk/dashboard/proxy-pools/__PAGE__.segment.rsc +7 -7
  105. package/.next/server/app/dashboard/proxy-pools.segments/_full.segment.rsc +7 -7
  106. package/.next/server/app/dashboard/proxy-pools.segments/_tree.segment.rsc +1 -1
  107. package/.next/server/app/dashboard/pxpipe.html +1 -1
  108. package/.next/server/app/dashboard/pxpipe.rsc +6 -6
  109. package/.next/server/app/dashboard/pxpipe.segments/!KGRhc2hib2FyZCk/dashboard/pxpipe/__PAGE__.segment.rsc +6 -6
  110. package/.next/server/app/dashboard/pxpipe.segments/_full.segment.rsc +6 -6
  111. package/.next/server/app/dashboard/pxpipe.segments/_tree.segment.rsc +1 -1
  112. package/.next/server/app/dashboard/quota.html +2 -2
  113. package/.next/server/app/dashboard/quota.rsc +8 -8
  114. package/.next/server/app/dashboard/quota.segments/!KGRhc2hib2FyZCk/dashboard/quota/__PAGE__.segment.rsc +8 -8
  115. package/.next/server/app/dashboard/quota.segments/_full.segment.rsc +8 -8
  116. package/.next/server/app/dashboard/quota.segments/_tree.segment.rsc +1 -1
  117. package/.next/server/app/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
  118. package/.next/server/app/dashboard/settings/pricing.html +1 -1
  119. package/.next/server/app/dashboard/settings/pricing.rsc +5 -5
  120. package/.next/server/app/dashboard/settings/pricing.segments/_full.segment.rsc +5 -5
  121. package/.next/server/app/dashboard/settings/pricing.segments/_tree.segment.rsc +1 -1
  122. package/.next/server/app/dashboard/settings/pricing.segments/dashboard/settings/pricing/__PAGE__.segment.rsc +5 -5
  123. package/.next/server/app/dashboard/token-saver.html +1 -1
  124. package/.next/server/app/dashboard/token-saver.rsc +6 -6
  125. package/.next/server/app/dashboard/token-saver.segments/!KGRhc2hib2FyZCk/dashboard/token-saver/__PAGE__.segment.rsc +6 -6
  126. package/.next/server/app/dashboard/token-saver.segments/_full.segment.rsc +6 -6
  127. package/.next/server/app/dashboard/token-saver.segments/_tree.segment.rsc +1 -1
  128. package/.next/server/app/dashboard/usage.html +2 -2
  129. package/.next/server/app/dashboard/usage.rsc +7 -7
  130. package/.next/server/app/dashboard/usage.segments/!KGRhc2hib2FyZCk/dashboard/usage/__PAGE__.segment.rsc +7 -7
  131. package/.next/server/app/dashboard/usage.segments/_full.segment.rsc +7 -7
  132. package/.next/server/app/dashboard/usage.segments/_tree.segment.rsc +1 -1
  133. package/.next/server/app/dashboard.html +1 -1
  134. package/.next/server/app/dashboard.rsc +7 -7
  135. package/.next/server/app/dashboard.segments/!KGRhc2hib2FyZCk/dashboard/__PAGE__.segment.rsc +7 -7
  136. package/.next/server/app/dashboard.segments/_full.segment.rsc +7 -7
  137. package/.next/server/app/dashboard.segments/_tree.segment.rsc +1 -1
  138. package/.next/server/app/index.html +1 -1
  139. package/.next/server/app/index.rsc +5 -5
  140. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  141. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  142. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  143. package/.next/server/app/landing/page_client-reference-manifest.js +1 -1
  144. package/.next/server/app/landing.html +1 -1
  145. package/.next/server/app/landing.rsc +5 -5
  146. package/.next/server/app/landing.segments/_full.segment.rsc +5 -5
  147. package/.next/server/app/landing.segments/_tree.segment.rsc +1 -1
  148. package/.next/server/app/landing.segments/landing/__PAGE__.segment.rsc +5 -5
  149. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  150. package/.next/server/app/login.html +1 -1
  151. package/.next/server/app/login.rsc +5 -5
  152. package/.next/server/app/login.segments/_full.segment.rsc +5 -5
  153. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  154. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +5 -5
  155. package/.next/server/app/page_client-reference-manifest.js +1 -1
  156. package/.next/server/app-paths-manifest.json +10 -10
  157. package/.next/server/chunks/1011.js +1 -1
  158. package/.next/server/chunks/1194.js +1 -1
  159. package/.next/server/chunks/1289.js +1 -1
  160. package/.next/server/chunks/1633.js +1 -1
  161. package/.next/server/chunks/2282.js +8 -8
  162. package/.next/server/chunks/2339.js +1 -1
  163. package/.next/server/chunks/2801.js +1 -1
  164. package/.next/server/chunks/3425.js +1 -1
  165. package/.next/server/chunks/4129.js +1 -1
  166. package/.next/server/chunks/537.js +5 -1
  167. package/.next/server/chunks/6056.js +1 -1
  168. package/.next/server/chunks/6343.js +6 -6
  169. package/.next/server/chunks/7816.js +1 -1
  170. package/.next/server/chunks/9429.js +1 -1
  171. package/.next/server/middleware-build-manifest.js +1 -1
  172. package/.next/server/pages/404.html +1 -1
  173. package/.next/server/pages/500.html +1 -1
  174. package/.next/server/server-reference-manifest.js +1 -1
  175. package/.next/server/server-reference-manifest.json +1 -1
  176. package/.next/static/chunks/1278-db66df071a072f90.js +1 -0
  177. package/.next/static/chunks/{159-39356ce437096d44.js → 159-ec207a220d7d5c16.js} +1 -1
  178. package/.next/static/chunks/app/(dashboard)/dashboard/console-log/{page-bf251f657ba991f0.js → page-168534817f7aecd8.js} +1 -1
  179. package/.next/static/chunks/app/(dashboard)/dashboard/providers/new/{page-bedeed81041d8f79.js → page-81f2d7e490f1c69c.js} +1 -1
  180. package/.next/static/chunks/app/{layout-6cb42d6935f83cc9.js → layout-d5f5e92e2a8102f1.js} +1 -1
  181. package/CHANGELOG.md +72 -0
  182. package/next.config.mjs +5 -0
  183. package/open-sse/executors/opencode-go.js +1 -1
  184. package/open-sse/executors/opencode-zen.js +39 -99
  185. package/open-sse/executors/opencode.js +42 -198
  186. package/open-sse/handlers/chatCore/nonStreamingHandler.js +2 -1
  187. package/open-sse/handlers/chatCore/sseToJsonHandler.js +5 -3
  188. package/open-sse/handlers/chatCore.js +6 -0
  189. package/open-sse/providers/registry/opencode-go.js +6 -6
  190. package/open-sse/providers/registry/opencode-zen.js +7 -7
  191. package/open-sse/providers/registry/opencode.js +2 -2
  192. package/open-sse/services/usage/opencode-go.js +1 -1
  193. package/open-sse/translator/index.js +6 -2
  194. package/open-sse/utils/opencodeFingerprint.js +385 -0
  195. package/package.json +2 -1
  196. package/src/app/api/changelog/{route.js → route.ts} +2 -1
  197. package/src/app/api/providers/[id]/test/testUtils.js +3 -3
  198. package/src/app/api/providers/validate/route.js +1 -1
  199. package/src/app/api/version/route.js +1 -1
  200. package/src/lib/versionCheck.js +18 -16
  201. package/src/shared/components/UpdateDialog.tsx +1 -1
  202. package/.next/static/chunks/1278-846112734a0f5e55.js +0 -1
  203. /package/.next/static/{lLaVjv9m3OfeP2eGEVKC1 → P0IikrdjxpmTneD33YPWQ}/_buildManifest.js +0 -0
  204. /package/.next/static/{lLaVjv9m3OfeP2eGEVKC1 → P0IikrdjxpmTneD33YPWQ}/_ssgManifest.js +0 -0
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Helpers for the OpenCode free-tier client fingerprint.
3
+ *
4
+ * Live upstream probes (OpenCode v2, /inference/openai|anthropic/v1) show that free-tier
5
+ * requests must declare OpenCode's own `read` and `shell` tools with their exact
6
+ * descriptions and parameter schemas; stubbed descriptions, renamed schemas or
7
+ * tool_choice "none" are answered with 403 FreeTierError. Caller tools are sent
8
+ * untouched, except one that already uses an exact fingerprint name with a
9
+ * different definition: it travels under an alias and the response side
10
+ * restores the caller's original name.
11
+ */
12
+
13
+ /** Names required by the upstream free-tier gate. */
14
+ export const OPENCODE_FINGERPRINT_TOOLS = ["read", "shell"];
15
+
16
+ // Verbatim from the OpenCode v2.0.26 CLI. Upstream compares description and
17
+ // parameters, so do not reword or reformat these.
18
+ const FINGERPRINT_TOOL_DEFINITIONS = {
19
+ read: {
20
+ description:
21
+ "Read the contents of a file or directory. Supports text files, images, and PDFs. Images and PDFs are presented directly to the model. Each text line is prefixed by its 1-based line number as <line>: <content>. The prefix is for reference and is not part of the file content. Directory entries are returned one per line. Use offset and limit to read large files or directories in sections. Prefer one larger read over many small slices, and use grep to find specific content in large files.",
22
+ parameters: {
23
+ type: "object",
24
+ properties: {
25
+ path: { type: "string", description: "File or directory to read" },
26
+ offset: {
27
+ type: "integer",
28
+ minimum: 0,
29
+ description: "The line or directory entry to start reading from (1-based)",
30
+ },
31
+ limit: {
32
+ type: "integer",
33
+ minimum: 0,
34
+ description:
35
+ "The maximum number of lines or directory entries to read (defaults to and capped at 2000)",
36
+ },
37
+ },
38
+ required: ["path"],
39
+ additionalProperties: false,
40
+ },
41
+ },
42
+ shell: {
43
+ description:
44
+ "Execute a shell command and return its output. Commands run on Linux using bash. Quote file paths containing spaces or special characters. Prefer dedicated tools over shell commands when possible. When output is large, the full result is saved to a file and a truncated preview is returned. Rely on automatic truncation unless filtering the output is more useful. Commands accept an optional timeout, background commands have no timeout by default. Background commands return immediately, and you will be notified when they complete.",
45
+ parameters: {
46
+ type: "object",
47
+ properties: {
48
+ command: { type: "string", description: "Shell command string to execute" },
49
+ workdir: {
50
+ type: "string",
51
+ description:
52
+ "Working directory to execute the command in. Defaults to the current working directory. When possible, avoid changing directories in the command and set the working directory here instead.",
53
+ },
54
+ timeout: {
55
+ type: "integer",
56
+ minimum: 0,
57
+ description:
58
+ "Timeout in milliseconds. Set to 0 to disable the timeout. Defaults to 120000 for foreground commands. Background commands have no timeout by default.",
59
+ },
60
+ background: {
61
+ type: "boolean",
62
+ description:
63
+ "Run the command in the background and return immediately (useful for dev servers and long-running builds). You do not need to use '&' at the end of the command when using this parameter. You will be notified when it completes. DO NOT poll for completion.",
64
+ },
65
+ },
66
+ required: ["command"],
67
+ additionalProperties: false,
68
+ },
69
+ },
70
+ };
71
+
72
+ // Request body -> names renamed for that request. transformRequest() mutates the
73
+ // same body object that chatCore passed into the executor, so a WeakMap keeps the
74
+ // mapping request-local without putting transport metadata on the wire.
75
+ const renamedToolNames = new WeakMap();
76
+
77
+ /** The fingerprint name when `name` is exactly one; "" otherwise. */
78
+ export function fingerprintToolKey(name) {
79
+ const trimmed = String(name ?? "").trim();
80
+ return OPENCODE_FINGERPRINT_TOOLS.includes(trimmed) ? trimmed : "";
81
+ }
82
+
83
+ /** Read a tool name from either flat ({name}) or chat ({function:{name}}) shape. */
84
+ function toolNameOf(tool) {
85
+ if (!tool || typeof tool !== "object" || Array.isArray(tool)) return "";
86
+ if (typeof tool.name === "string" && tool.name.trim()) return tool.name.trim();
87
+ const fn = tool.function;
88
+ if (fn && typeof fn === "object" && !Array.isArray(fn) && typeof fn.name === "string") {
89
+ return fn.name.trim();
90
+ }
91
+ return "";
92
+ }
93
+
94
+ function chatFunctionOf(tool) {
95
+ const fn = tool?.function;
96
+ return fn && typeof fn === "object" && !Array.isArray(fn) ? fn : null;
97
+ }
98
+
99
+ /** True when `tool` already carries OpenCode's exact definition for `key`. */
100
+ function isFingerprintDefinition(tool, key) {
101
+ const def = tool && (chatFunctionOf(tool) || tool);
102
+ const want = FINGERPRINT_TOOL_DEFINITIONS[key];
103
+ return (
104
+ !!def &&
105
+ def.description === want.description &&
106
+ JSON.stringify(def.parameters) === JSON.stringify(want.parameters)
107
+ );
108
+ }
109
+
110
+ function withToolName(tool, name) {
111
+ const fn = chatFunctionOf(tool);
112
+ return fn ? { ...tool, function: { ...fn, name } } : { ...tool, name };
113
+ }
114
+
115
+ /**
116
+ * Move caller tools off the fingerprint names when their definition differs
117
+ * from OpenCode's, so the exact definitions can be appended without a
118
+ * duplicate name. Everything else, including case variants such as `Read`, is
119
+ * preserved verbatim; upstream accepts them alongside `read`.
120
+ *
121
+ * @param {Array} tools
122
+ * @returns {{ tools: Array, map: Map<string,string> }} map: sent name -> original name
123
+ */
124
+ export function concealFingerprintToolNames(tools) {
125
+ const map = new Map();
126
+ if (!Array.isArray(tools) || tools.length === 0) return { tools, map };
127
+
128
+ const taken = new Set(tools.map(toolNameOf));
129
+ const seen = new Set();
130
+ const out = [];
131
+ for (const tool of tools) {
132
+ if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
133
+ out.push(tool);
134
+ continue;
135
+ }
136
+
137
+ const key = fingerprintToolKey(toolNameOf(tool));
138
+ if (!key) {
139
+ out.push(tool);
140
+ continue;
141
+ }
142
+
143
+ // Duplicate declarations of one name are rejected upstream; keep the first.
144
+ if (seen.has(key)) continue;
145
+ seen.add(key);
146
+
147
+ if (isFingerprintDefinition(tool, key)) {
148
+ out.push(tool);
149
+ continue;
150
+ }
151
+
152
+ let alias = `${key}_client`;
153
+ for (let i = 2; taken.has(alias); i++) alias = `${key}_client${i}`;
154
+ taken.add(alias);
155
+ map.set(alias, key);
156
+ out.push(withToolName(tool, alias));
157
+ }
158
+ return { tools: out, map };
159
+ }
160
+
161
+ /**
162
+ * Append OpenCode's definition for each fingerprint tool not already declared.
163
+ *
164
+ * @returns {string[]} names that were appended
165
+ */
166
+ export function appendMissingFingerprintTools(tools, flat) {
167
+ const appended = [];
168
+ if (!Array.isArray(tools)) return appended;
169
+ for (const name of OPENCODE_FINGERPRINT_TOOLS) {
170
+ if (tools.some((tool) => fingerprintToolKey(toolNameOf(tool)) === name)) continue;
171
+ const { description, parameters } = FINGERPRINT_TOOL_DEFINITIONS[name];
172
+ tools.push(
173
+ flat
174
+ ? { type: "function", name, description, parameters, strict: false }
175
+ : { type: "function", function: { name, description, parameters } },
176
+ );
177
+ appended.push(name);
178
+ }
179
+ return appended;
180
+ }
181
+
182
+ /** Invert a sent -> original rename map. */
183
+ function aliasesByOriginal(map) {
184
+ return new Map([...map].map(([alias, original]) => [original, alias]));
185
+ }
186
+
187
+ /** Point an explicit tool_choice at the alias of a concealed caller tool. */
188
+ export function retargetToolChoice(body, map) {
189
+ if (!body || typeof body !== "object" || !map?.size) return;
190
+ const choice = body.tool_choice;
191
+ if (!choice || typeof choice !== "object" || Array.isArray(choice)) return;
192
+ const aliases = aliasesByOriginal(map);
193
+
194
+ if (typeof choice.name === "string") {
195
+ const alias = aliases.get(choice.name.trim());
196
+ if (alias) body.tool_choice = { ...choice, name: alias };
197
+ return;
198
+ }
199
+
200
+ const fn = choice.function;
201
+ if (fn && typeof fn === "object" && !Array.isArray(fn) && typeof fn.name === "string") {
202
+ const alias = aliases.get(fn.name.trim());
203
+ if (alias) body.tool_choice = { ...choice, function: { ...fn, name: alias } };
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Rename prior-turn calls to concealed caller tools as well; otherwise the model
209
+ * reads its own earlier calls as calls to OpenCode's tool of the same name.
210
+ */
211
+ export function aliasHistoryToolCalls(body, map) {
212
+ if (!body || typeof body !== "object" || !map?.size) return;
213
+ const aliases = aliasesByOriginal(map);
214
+
215
+ if (Array.isArray(body.input)) {
216
+ body.input = body.input.map((item) =>
217
+ item?.type === "function_call" && aliases.has(item.name)
218
+ ? { ...item, name: aliases.get(item.name) }
219
+ : item,
220
+ );
221
+ }
222
+
223
+ if (Array.isArray(body.messages)) {
224
+ body.messages = body.messages.map((msg) => {
225
+ if (!Array.isArray(msg?.tool_calls)) return msg;
226
+ return {
227
+ ...msg,
228
+ tool_calls: msg.tool_calls.map((call) =>
229
+ aliases.has(call?.function?.name)
230
+ ? { ...call, function: { ...call.function, name: aliases.get(call.function.name) } }
231
+ : call,
232
+ ),
233
+ };
234
+ });
235
+ }
236
+ }
237
+
238
+ // Without the old "must not be used" descriptions the model does call the
239
+ // appended tools (live probe: `read` on "what files are in /etc?"), so tell it
240
+ // in the system prompt instead. Upstream does not gate on the prompt text.
241
+ // Wording matters: "not available" made the model also refuse the caller's own
242
+ // Read/Bash; "placeholders ... use the other tools" kept those working.
243
+ function noteUnavailableTools(body, flat, names) {
244
+ const list = names.map((name) => `\`${name}\``).join(" or ");
245
+ const note =
246
+ names.length > 1
247
+ ? `Do not call the ${list} tools; they are placeholders that do nothing. Use the other tools you have been given instead.`
248
+ : `Do not call the ${list} tool; it is a placeholder that does nothing. Use the other tools you have been given instead.`;
249
+ if (flat) {
250
+ body.instructions =
251
+ typeof body.instructions === "string" && body.instructions.trim()
252
+ ? `${body.instructions}\n\n${note}`
253
+ : note;
254
+ return;
255
+ }
256
+ // OpenAI chat shape only; Claude-shaped bodies carry `system` instead.
257
+ if (!Array.isArray(body.messages) || body.system !== undefined) return;
258
+ const first = body.messages[0];
259
+ if (first?.role === "system" && typeof first.content === "string") {
260
+ body.messages = [
261
+ { ...first, content: `${first.content}\n\n${note}` },
262
+ ...body.messages.slice(1),
263
+ ];
264
+ } else {
265
+ body.messages = [{ role: "system", content: note }, ...body.messages];
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Full request-side pass: alias conflicting caller tools, append OpenCode's
271
+ * fingerprint definitions, and tell the model the appended ones are off-limits.
272
+ *
273
+ * @param {object} body
274
+ * @param {boolean} flat - true for Responses tools ({name}), false for chat tools
275
+ * @returns {Map<string,string>} map: sent name -> original name
276
+ */
277
+ export function applyFingerprintTools(body, flat) {
278
+ if (!body || typeof body !== "object") return new Map();
279
+
280
+ const { tools, map } = concealFingerprintToolNames(body.tools);
281
+ body.tools = Array.isArray(tools) ? tools : [];
282
+ const appended = appendMissingFingerprintTools(body.tools, flat);
283
+ retargetToolChoice(body, map);
284
+ aliasHistoryToolCalls(body, map);
285
+ if (appended.length) noteUnavailableTools(body, flat, appended);
286
+
287
+ // Responses keeps its explicit auto default. Never default to "none": upstream
288
+ // answers tool_choice "none" with 403 FreeTierError.
289
+ if (!body.tool_choice && flat) body.tool_choice = "auto";
290
+
291
+ recordRenamedToolNames(body, map);
292
+ return map;
293
+ }
294
+
295
+ /** Store the rename map for `body`. */
296
+ export function recordRenamedToolNames(body, map) {
297
+ if (!body || typeof body !== "object" || !map?.size) return;
298
+ renamedToolNames.set(body, map);
299
+ }
300
+
301
+ /** Retrieve the rename map for `body`. */
302
+ export function takeRenamedToolNames(body) {
303
+ if (!body || typeof body !== "object") return null;
304
+ return renamedToolNames.get(body) || null;
305
+ }
306
+
307
+ // Response side -------------------------------------------------------------
308
+
309
+ /** Restore caller tool spellings in supported response/event shapes. */
310
+ export function restoreToolNames(payload, map) {
311
+ if (!map?.size || !payload) return payload;
312
+ if (Array.isArray(payload)) return payload.map((item) => restoreToolNames(item, map));
313
+ if (typeof payload !== "object") return payload;
314
+
315
+ let out = payload;
316
+ const put = (key, value) => {
317
+ if (out === payload) out = { ...payload };
318
+ out[key] = value;
319
+ };
320
+
321
+ // Claude streaming content_block_start event.
322
+ if (payload.type === "content_block_start") {
323
+ const block = payload.content_block;
324
+ if (block?.type === "tool_use" && typeof block.name === "string" && map.has(block.name)) {
325
+ put("content_block", { ...block, name: map.get(block.name) });
326
+ }
327
+ }
328
+
329
+ // Claude non-streaming message body.
330
+ if (Array.isArray(payload.content)) {
331
+ put(
332
+ "content",
333
+ payload.content.map((block) =>
334
+ block?.type === "tool_use" && typeof block.name === "string" && map.has(block.name)
335
+ ? { ...block, name: map.get(block.name) }
336
+ : block,
337
+ ),
338
+ );
339
+ }
340
+
341
+ // OpenAI Chat Completions, both streaming delta and JSON message shapes.
342
+ if (Array.isArray(payload.choices)) {
343
+ put(
344
+ "choices",
345
+ payload.choices.map((choice) => {
346
+ let changed = false;
347
+ const next = { ...choice };
348
+ for (const holder of ["delta", "message"]) {
349
+ const value = choice?.[holder];
350
+ if (!value || !Array.isArray(value.tool_calls) || value.tool_calls.length === 0) continue;
351
+ const calls = value.tool_calls.map((call) => {
352
+ const name = call?.function?.name;
353
+ if (typeof name === "string" && map.has(name)) {
354
+ changed = true;
355
+ return { ...call, function: { ...call.function, name: map.get(name) } };
356
+ }
357
+ return call;
358
+ });
359
+ next[holder] = { ...value, tool_calls: calls };
360
+ }
361
+ return changed ? next : choice;
362
+ }),
363
+ );
364
+ }
365
+
366
+ // OpenAI Responses final JSON body.
367
+ if (Array.isArray(payload.output)) {
368
+ put(
369
+ "output",
370
+ payload.output.map((item) =>
371
+ item?.type === "function_call" && typeof item.name === "string" && map.has(item.name)
372
+ ? { ...item, name: map.get(item.name) }
373
+ : item,
374
+ ),
375
+ );
376
+ }
377
+
378
+ // OpenAI Responses SSE events such as response.output_item.added/done.
379
+ const item = payload.item;
380
+ if (item?.type === "function_call" && typeof item.name === "string" && map.has(item.name)) {
381
+ put("item", { ...item, name: map.get(item.name) });
382
+ }
383
+
384
+ return out;
385
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amir83nasr/10router",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Amir83Nasr/10Router.git"
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "files": [
14
14
  "bin/",
15
+ "CHANGELOG.md",
15
16
  "custom-server.js",
16
17
  "src/",
17
18
  "open-sse/",
@@ -12,7 +12,8 @@ export async function GET() {
12
12
  let markdown = "";
13
13
  try {
14
14
  markdown = await fs.promises.readFile(path.join(process.cwd(), "CHANGELOG.md"), "utf8");
15
- } catch {
15
+ } catch (error) {
16
+ console.error("GET /api/changelog: failed to read CHANGELOG.md", error);
16
17
  return NextResponse.json(
17
18
  { error: "changelog unavailable" },
18
19
  { status: 500, headers: CORS_HEADERS },
@@ -901,12 +901,12 @@ async function testApiKeyConnection(connection, effectiveProxy = null) {
901
901
  case "opencode": {
902
902
  // Connectivity-only probe: GET /inference/v1/models never exercises the Responses
903
903
  // tool path, so a green dashboard test does NOT rule out 403
904
- // FreeTierError on POST /zen/v1/responses with tools (requires
904
+ // FreeTierError on POST /inference/openai/v1/responses with tools (requires
905
905
  // bash+read decoys + tool_choice auto — see OpenCodeExecutor).
906
906
  const res = await fetchWithConnectionProxy(
907
907
  "https://opencode.ai/inference/v1/models",
908
908
  {
909
- headers: { "User-Agent": "opencode/1.18.35" },
909
+ headers: { "User-Agent": "opencode/latest/2.0.26/cli" },
910
910
  },
911
911
  effectiveProxy,
912
912
  );
@@ -914,7 +914,7 @@ async function testApiKeyConnection(connection, effectiveProxy = null) {
914
914
  }
915
915
  case "opencode-go": {
916
916
  const res = await fetchWithConnectionProxy(
917
- "https://opencode.ai/zen/go/v1/chat/completions",
917
+ "https://opencode.ai/inference/go/v1/chat/completions",
918
918
  {
919
919
  method: "POST",
920
920
  headers: {
@@ -454,7 +454,7 @@ export async function POST(request) {
454
454
  }
455
455
 
456
456
  case "opencode-go": {
457
- const res = await fetch("https://opencode.ai/zen/go/v1/chat/completions", {
457
+ const res = await fetch("https://opencode.ai/inference/go/v1/chat/completions", {
458
458
  method: "POST",
459
459
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
460
460
  body: JSON.stringify({
@@ -3,7 +3,7 @@ import { getLatestVersion, isNewerVersion } from "@/lib/versionCheck";
3
3
 
4
4
  export async function GET() {
5
5
  const currentVersion = pkg.version;
6
- // Fail-open: GitHub unreachable → latestVersion falls back to current.
6
+ // Fail-open: npm registry unreachable → latestVersion falls back to current.
7
7
  const { version, reason } = await getLatestVersion();
8
8
  const latestVersion = version || currentVersion;
9
9
  return Response.json({
@@ -1,7 +1,12 @@
1
- const REPO = "Amir83Nasr/10Router";
2
- const TAGS_URL = `https://api.github.com/repos/${REPO}/tags?per_page=100`;
1
+ // Single source of truth for the dashboard update check (/api/version →
2
+ // sidebar VersionLabel). Uses the npm registry — not GitHub tags — so the
3
+ // offered version always matches `npm i -g <pkg>@latest`.
4
+ import pkg from "../../package.json" with { type: "json" };
5
+
6
+ const NPM_PKG = pkg.name || "@amir83nasr/10router";
7
+ const LATEST_URL = `https://registry.npmjs.org/${NPM_PKG.replace("/", "%2F")}/latest`;
3
8
  const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
4
- // Short negative cache so one GitHub failure (rate limit / no egress on
9
+ // Short negative cache so one npm failure (rate limit / no egress on
5
10
  // docker) doesn't turn every /api/version hit into another upstream call.
6
11
  const FAIL_TTL_MS = 10 * 60 * 1000;
7
12
 
@@ -36,26 +41,23 @@ export async function getLatestVersion({ fetchFn = fetch, now = Date.now } = {})
36
41
  return { version: cache.latest, reason: "cache-fresh" };
37
42
  if (!cache.latest && now() - cache.failAt < FAIL_TTL_MS)
38
43
  return { version: null, reason: "cooldown" };
39
- const headers = { Accept: "application/vnd.github+json", "User-Agent": "10router" };
40
- const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
41
- if (token) headers.Authorization = `Bearer ${token}`;
42
44
  try {
43
- const res = await fetchFn(TAGS_URL, {
44
- headers,
45
+ const res = await fetchFn(LATEST_URL, {
46
+ headers: { Accept: "application/json" },
45
47
  signal: AbortSignal.timeout(8000),
46
48
  });
47
49
  if (!res.ok) {
48
50
  cache.failAt = now();
49
- return { version: cache.latest, reason: `github-${res.status}` };
51
+ return { version: cache.latest, reason: `npm-${res.status}` };
50
52
  }
51
- const tags = await res.json();
52
- let best = null;
53
- for (const t of Array.isArray(tags) ? tags : []) {
54
- const v = normalizeVersion(t?.name);
55
- if (/^\d+\.\d+\.\d+/.test(v) && (!best || compareVersions(v, best) > 0)) best = v;
53
+ const data = await res.json();
54
+ const v = normalizeVersion(data?.version);
55
+ if (!/^\d+\.\d+\.\d+/.test(v)) {
56
+ cache.failAt = now();
57
+ return { version: cache.latest, reason: "no-version" };
56
58
  }
57
- if (best) cache = { at: now(), latest: best, failAt: 0 };
58
- return { version: cache.latest, reason: best ? "github" : "no-tags" };
59
+ cache = { at: now(), latest: v, failAt: 0 };
60
+ return { version: v, reason: "npm" };
59
61
  } catch (err) {
60
62
  cache.failAt = now();
61
63
  return { version: cache.latest, reason: "fetch-error" };
@@ -203,7 +203,7 @@ export default function UpdateDialog({
203
203
  const status = !info ? (
204
204
  <p className="rounded-lg border border-border bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
205
205
  {checkFailed
206
- ? "Could not reach GitHub to check for updates — the manual steps below still work."
206
+ ? "Could not reach npm registry to check for updates — the manual steps below still work."
207
207
  : "Checking for updates…"}
208
208
  </p>
209
209
  ) : info.hasUpdate ? (