@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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +10 -10
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.json +5 -0
- package/.next/server/app/(dashboard)/dashboard/basic-chat/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/console-log/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/media-providers/systemone/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/proxy-pools/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/pxpipe/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/quota/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/token-saver/page_client-reference-manifest.js +1 -1
- package/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +5 -5
- package/.next/server/app/_not-found.segments/_full.segment.rsc +5 -5
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/changelog/route.js +1 -1
- package/.next/server/app/api/cli-tools/all-statuses/route.js +1 -1
- package/.next/server/app/api/cli-tools/claude-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/codex-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/cowork-mcp-registry/route.js +1 -1
- package/.next/server/app/api/cli-tools/cowork-mcp-tools/route.js +1 -1
- package/.next/server/app/api/cli-tools/cowork-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/dsh-settings/route.js +1 -1
- package/.next/server/app/api/cli-tools/opencode-settings/route.js +1 -1
- package/.next/server/app/api/oauth/[provider]/[action]/route.js +1 -1
- package/.next/server/app/api/oauth/codex/bulk-import/route.js +1 -1
- package/.next/server/app/api/oauth/codex/import-token/route.js +1 -1
- package/.next/server/app/api/providers/client/route.js +1 -1
- package/.next/server/app/api/providers/validate/route.js +1 -1
- package/.next/server/app/api/translator/send/route.js +1 -1
- package/.next/server/app/api/translator/translate/route.js +1 -1
- package/.next/server/app/api/tunnel/tailscale-install/route.js +2 -2
- package/.next/server/app/api/usage/[connectionId]/codex-reset-credits/route.js +1 -1
- package/.next/server/app/api/usage/[connectionId]/route.js +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/callback/page_client-reference-manifest.js +1 -1
- package/.next/server/app/callback.html +1 -1
- package/.next/server/app/callback.rsc +5 -5
- package/.next/server/app/callback.segments/_full.segment.rsc +5 -5
- package/.next/server/app/callback.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/callback.segments/callback/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/dashboard/basic-chat.html +1 -1
- package/.next/server/app/dashboard/basic-chat.rsc +7 -7
- package/.next/server/app/dashboard/basic-chat.segments/!KGRhc2hib2FyZCk/dashboard/basic-chat/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/basic-chat.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/basic-chat.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/cli-tools.html +1 -1
- package/.next/server/app/dashboard/cli-tools.rsc +7 -7
- package/.next/server/app/dashboard/cli-tools.segments/!KGRhc2hib2FyZCk/dashboard/cli-tools/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/cli-tools.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/cli-tools.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/combos.html +1 -1
- package/.next/server/app/dashboard/combos.rsc +7 -7
- package/.next/server/app/dashboard/combos.segments/!KGRhc2hib2FyZCk/dashboard/combos/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/combos.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/combos.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/endpoint.html +1 -1
- package/.next/server/app/dashboard/endpoint.rsc +7 -7
- package/.next/server/app/dashboard/endpoint.segments/!KGRhc2hib2FyZCk/dashboard/endpoint/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/endpoint.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/endpoint.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/media-providers/systemone.html +1 -1
- package/.next/server/app/dashboard/media-providers/systemone.rsc +7 -7
- package/.next/server/app/dashboard/media-providers/systemone.segments/!KGRhc2hib2FyZCk/dashboard/media-providers/systemone/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/media-providers/systemone.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/media-providers/systemone.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/profile.html +1 -1
- package/.next/server/app/dashboard/profile.rsc +7 -7
- package/.next/server/app/dashboard/profile.segments/!KGRhc2hib2FyZCk/dashboard/profile/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/profile.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/profile.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/providers/new.html +1 -1
- package/.next/server/app/dashboard/providers/new.rsc +7 -7
- package/.next/server/app/dashboard/providers/new.segments/!KGRhc2hib2FyZCk/dashboard/providers/new/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers/new.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers/new.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/providers.html +1 -1
- package/.next/server/app/dashboard/providers.rsc +7 -7
- package/.next/server/app/dashboard/providers.segments/!KGRhc2hib2FyZCk/dashboard/providers/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/providers.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/proxy-pools.html +1 -1
- package/.next/server/app/dashboard/proxy-pools.rsc +7 -7
- package/.next/server/app/dashboard/proxy-pools.segments/!KGRhc2hib2FyZCk/dashboard/proxy-pools/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/proxy-pools.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/proxy-pools.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/pxpipe.html +1 -1
- package/.next/server/app/dashboard/pxpipe.rsc +6 -6
- package/.next/server/app/dashboard/pxpipe.segments/!KGRhc2hib2FyZCk/dashboard/pxpipe/__PAGE__.segment.rsc +6 -6
- package/.next/server/app/dashboard/pxpipe.segments/_full.segment.rsc +6 -6
- package/.next/server/app/dashboard/pxpipe.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/quota.html +2 -2
- package/.next/server/app/dashboard/quota.rsc +8 -8
- package/.next/server/app/dashboard/quota.segments/!KGRhc2hib2FyZCk/dashboard/quota/__PAGE__.segment.rsc +8 -8
- package/.next/server/app/dashboard/quota.segments/_full.segment.rsc +8 -8
- package/.next/server/app/dashboard/quota.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
- package/.next/server/app/dashboard/settings/pricing.html +1 -1
- package/.next/server/app/dashboard/settings/pricing.rsc +5 -5
- package/.next/server/app/dashboard/settings/pricing.segments/_full.segment.rsc +5 -5
- package/.next/server/app/dashboard/settings/pricing.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/settings/pricing.segments/dashboard/settings/pricing/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/dashboard/token-saver.html +1 -1
- package/.next/server/app/dashboard/token-saver.rsc +6 -6
- package/.next/server/app/dashboard/token-saver.segments/!KGRhc2hib2FyZCk/dashboard/token-saver/__PAGE__.segment.rsc +6 -6
- package/.next/server/app/dashboard/token-saver.segments/_full.segment.rsc +6 -6
- package/.next/server/app/dashboard/token-saver.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard/usage.html +2 -2
- package/.next/server/app/dashboard/usage.rsc +7 -7
- package/.next/server/app/dashboard/usage.segments/!KGRhc2hib2FyZCk/dashboard/usage/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard/usage.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard/usage.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/dashboard.html +1 -1
- package/.next/server/app/dashboard.rsc +7 -7
- package/.next/server/app/dashboard.segments/!KGRhc2hib2FyZCk/dashboard/__PAGE__.segment.rsc +7 -7
- package/.next/server/app/dashboard.segments/_full.segment.rsc +7 -7
- package/.next/server/app/dashboard.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/landing/page_client-reference-manifest.js +1 -1
- package/.next/server/app/landing.html +1 -1
- package/.next/server/app/landing.rsc +5 -5
- package/.next/server/app/landing.segments/_full.segment.rsc +5 -5
- package/.next/server/app/landing.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/landing.segments/landing/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +5 -5
- package/.next/server/app/login.segments/_full.segment.rsc +5 -5
- package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +10 -10
- package/.next/server/chunks/1011.js +1 -1
- package/.next/server/chunks/1194.js +1 -1
- package/.next/server/chunks/1289.js +1 -1
- package/.next/server/chunks/1633.js +1 -1
- package/.next/server/chunks/2282.js +8 -8
- package/.next/server/chunks/2339.js +1 -1
- package/.next/server/chunks/2801.js +1 -1
- package/.next/server/chunks/3425.js +1 -1
- package/.next/server/chunks/4129.js +1 -1
- package/.next/server/chunks/537.js +5 -1
- package/.next/server/chunks/6056.js +1 -1
- package/.next/server/chunks/6343.js +6 -6
- package/.next/server/chunks/7816.js +1 -1
- package/.next/server/chunks/9429.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/1278-db66df071a072f90.js +1 -0
- package/.next/static/chunks/{159-39356ce437096d44.js → 159-ec207a220d7d5c16.js} +1 -1
- package/.next/static/chunks/app/(dashboard)/dashboard/console-log/{page-bf251f657ba991f0.js → page-168534817f7aecd8.js} +1 -1
- package/.next/static/chunks/app/(dashboard)/dashboard/providers/new/{page-bedeed81041d8f79.js → page-81f2d7e490f1c69c.js} +1 -1
- package/.next/static/chunks/app/{layout-6cb42d6935f83cc9.js → layout-d5f5e92e2a8102f1.js} +1 -1
- package/CHANGELOG.md +72 -0
- package/next.config.mjs +5 -0
- package/open-sse/executors/opencode-go.js +1 -1
- package/open-sse/executors/opencode-zen.js +39 -99
- package/open-sse/executors/opencode.js +42 -198
- package/open-sse/handlers/chatCore/nonStreamingHandler.js +2 -1
- package/open-sse/handlers/chatCore/sseToJsonHandler.js +5 -3
- package/open-sse/handlers/chatCore.js +6 -0
- package/open-sse/providers/registry/opencode-go.js +6 -6
- package/open-sse/providers/registry/opencode-zen.js +7 -7
- package/open-sse/providers/registry/opencode.js +2 -2
- package/open-sse/services/usage/opencode-go.js +1 -1
- package/open-sse/translator/index.js +6 -2
- package/open-sse/utils/opencodeFingerprint.js +385 -0
- package/package.json +2 -1
- package/src/app/api/changelog/{route.js → route.ts} +2 -1
- package/src/app/api/providers/[id]/test/testUtils.js +3 -3
- package/src/app/api/providers/validate/route.js +1 -1
- package/src/app/api/version/route.js +1 -1
- package/src/lib/versionCheck.js +18 -16
- package/src/shared/components/UpdateDialog.tsx +1 -1
- package/.next/static/chunks/1278-846112734a0f5e55.js +0 -1
- /package/.next/static/{lLaVjv9m3OfeP2eGEVKC1 → P0IikrdjxpmTneD33YPWQ}/_buildManifest.js +0 -0
- /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.
|
|
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 /
|
|
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/
|
|
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/
|
|
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/
|
|
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:
|
|
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({
|
package/src/lib/versionCheck.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
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(
|
|
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: `
|
|
51
|
+
return { version: cache.latest, reason: `npm-${res.status}` };
|
|
50
52
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
return { version:
|
|
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
|
|
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 ? (
|