@magpie-community/opencode-zen-free-auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/index.mjs +3 -0
- package/models.mjs +148 -0
- package/package.json +13 -0
- package/provider.mjs +290 -0
- package/stream.mjs +398 -0
- package/tool-response.mjs +542 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xiaozhou26
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# OpenCode Zen Free
|
|
2
|
+
|
|
3
|
+
OpenCode Zen's free models in magpie and OpenCode. Provider id:
|
|
4
|
+
`opencode-zen-free`.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
magpie plugin add @magpie-community/opencode-zen-free-auth
|
|
10
|
+
magpie plugin login opencode-zen-free
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Sign in
|
|
14
|
+
|
|
15
|
+
Enter `public` in the key field. No personal API key is needed.
|
|
16
|
+
The credential is kept in OpenCode's `auth.json` or magpie's
|
|
17
|
+
`plugin-auth.json`.
|
|
18
|
+
|
|
19
|
+
## Models
|
|
20
|
+
|
|
21
|
+
The plugin discovers Zen's current free models, including their context
|
|
22
|
+
windows and reasoning levels. Use `opencode-zen-free/<model>`, for example
|
|
23
|
+
`opencode-zen-free/big-pickle`.
|
|
24
|
+
|
|
25
|
+
Retired models and SystemOne models are excluded. Availability and rate
|
|
26
|
+
limits are set by Zen; the plugin has no remaining-quota data.
|
|
27
|
+
|
|
28
|
+
## Requests
|
|
29
|
+
|
|
30
|
+
Requests go to `https://opencode.ai/zen/v1` using each model's native
|
|
31
|
+
Chat Completions, Responses or Anthropic Messages API. Streaming and
|
|
32
|
+
ordinary JSON replies are supported.
|
|
33
|
+
|
|
34
|
+
The agent's tools are preserved. If Zen calls an added tool, its name is
|
|
35
|
+
mapped to the agent's unique case-insensitive match (`bash` to `Bash`).
|
|
36
|
+
A call with no matching tool returns an error. Tools run in the agent.
|
package/index.mjs
ADDED
package/models.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export const PROVIDER = "opencode-zen-free";
|
|
2
|
+
export const SDK = {
|
|
3
|
+
chat: "@ai-sdk/openai-compatible",
|
|
4
|
+
responses: "@ai-sdk/openai",
|
|
5
|
+
anthropic: "@ai-sdk/anthropic",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function parseProtocolDocs(text) {
|
|
9
|
+
const protocols = {};
|
|
10
|
+
const pattern =
|
|
11
|
+
/\|[^|]+\|\s*`?([^|`\s]+)`?\s*\|\s*`[^`]+\/v1\/(chat\/completions|responses|messages|systemone)`/g;
|
|
12
|
+
for (const match of text.matchAll(pattern)) {
|
|
13
|
+
protocols[match[1]] = {
|
|
14
|
+
"chat/completions": "chat",
|
|
15
|
+
responses: "responses",
|
|
16
|
+
messages: "anthropic",
|
|
17
|
+
systemone: "systemone",
|
|
18
|
+
}[match[2]];
|
|
19
|
+
}
|
|
20
|
+
return protocols;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sdkProtocol(npm) {
|
|
24
|
+
if (npm?.includes("anthropic")) return "anthropic";
|
|
25
|
+
if (npm === SDK.responses) return "responses";
|
|
26
|
+
if (npm?.includes("openai-compatible")) return "chat";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isRetired(model) {
|
|
30
|
+
return (
|
|
31
|
+
model.deprecated === true ||
|
|
32
|
+
["deprecated", "retired", "disabled"].includes(model.status ?? model.lifecycle) ||
|
|
33
|
+
model.deprecated_at != null ||
|
|
34
|
+
model.retirement_date != null
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isFree(id, model = {}) {
|
|
39
|
+
return (
|
|
40
|
+
!isRetired(model) && (/free/i.test(id) || (model.cost?.input === 0 && model.cost?.output === 0))
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildModels(ids, provider, protocols, baseURL) {
|
|
45
|
+
const result = {};
|
|
46
|
+
for (const id of ids) {
|
|
47
|
+
if (typeof id !== "string" || !id || id === "__proto__") continue;
|
|
48
|
+
const raw = provider.models?.[id];
|
|
49
|
+
const model = raw ?? {};
|
|
50
|
+
const protocol = protocols[id] ?? (raw && sdkProtocol(model.provider?.npm ?? provider.npm));
|
|
51
|
+
if (!isFree(id, model) || !Object.hasOwn(SDK, protocol)) continue;
|
|
52
|
+
const input = model.modalities?.input ?? ["text"];
|
|
53
|
+
const output = model.modalities?.output ?? ["text"];
|
|
54
|
+
const modalities = (values) =>
|
|
55
|
+
Object.fromEntries(
|
|
56
|
+
["text", "image", "audio", "video", "pdf"].map((name) => [name, values.includes(name)]),
|
|
57
|
+
);
|
|
58
|
+
const efforts = (model.reasoning_options ?? [])
|
|
59
|
+
.filter((option) => option.type === "effort")
|
|
60
|
+
.flatMap((option) => option.values ?? []);
|
|
61
|
+
result[id] = {
|
|
62
|
+
id,
|
|
63
|
+
providerID: PROVIDER,
|
|
64
|
+
name: model.name ?? id,
|
|
65
|
+
api: { id, url: baseURL, npm: SDK[protocol] },
|
|
66
|
+
limit: { context: 32768, output: 4096, ...model.limit },
|
|
67
|
+
capabilities: {
|
|
68
|
+
temperature: model.temperature ?? true,
|
|
69
|
+
reasoning: model.reasoning ?? false,
|
|
70
|
+
attachment: model.attachment ?? input.some((name) => name !== "text"),
|
|
71
|
+
toolcall: model.tool_call ?? true,
|
|
72
|
+
input: modalities(input),
|
|
73
|
+
output: modalities(output),
|
|
74
|
+
interleaved: model.interleaved ?? false,
|
|
75
|
+
},
|
|
76
|
+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
77
|
+
variants: Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }])),
|
|
78
|
+
options: {},
|
|
79
|
+
headers: {},
|
|
80
|
+
status: model.status ?? "active",
|
|
81
|
+
release_date: model.release_date ?? "",
|
|
82
|
+
free: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Offline bootstrap metadata from the OpenCode catalog, 2026-10-06.
|
|
89
|
+
export const bootstrapProvider = {
|
|
90
|
+
npm: SDK.chat,
|
|
91
|
+
models: {
|
|
92
|
+
"big-pickle": {
|
|
93
|
+
name: "Big Pickle",
|
|
94
|
+
cost: { input: 0, output: 0 },
|
|
95
|
+
limit: { context: 200000, input: 160000, output: 32000 },
|
|
96
|
+
reasoning: true,
|
|
97
|
+
tool_call: true,
|
|
98
|
+
interleaved: { field: "reasoning_content" },
|
|
99
|
+
},
|
|
100
|
+
"space-bunny-free": {
|
|
101
|
+
name: "Space Bunny Free",
|
|
102
|
+
cost: { input: 0, output: 0 },
|
|
103
|
+
limit: { context: 1048576, input: 524288, output: 524288 },
|
|
104
|
+
reasoning: true,
|
|
105
|
+
tool_call: true,
|
|
106
|
+
},
|
|
107
|
+
"muse-spark-1.3-contributor-free": {
|
|
108
|
+
name: "Muse Spark 1.3 Free",
|
|
109
|
+
cost: { input: 0, output: 0 },
|
|
110
|
+
provider: { npm: SDK.responses },
|
|
111
|
+
limit: { context: 1048576, output: 131072 },
|
|
112
|
+
reasoning: true,
|
|
113
|
+
tool_call: true,
|
|
114
|
+
modalities: { input: ["text", "image", "video", "pdf", "audio"], output: ["text"] },
|
|
115
|
+
reasoning_options: [
|
|
116
|
+
{ type: "effort", values: ["minimal", "low", "medium", "high", "xhigh"] },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export function configModels(models) {
|
|
123
|
+
return Object.fromEntries(
|
|
124
|
+
Object.entries(models).map(([id, model]) => [
|
|
125
|
+
id,
|
|
126
|
+
{
|
|
127
|
+
id,
|
|
128
|
+
name: model.name,
|
|
129
|
+
provider: { npm: model.api.npm, api: model.api.url },
|
|
130
|
+
limit: model.limit,
|
|
131
|
+
reasoning: model.capabilities.reasoning,
|
|
132
|
+
tool_call: model.capabilities.toolcall,
|
|
133
|
+
attachment: model.capabilities.attachment,
|
|
134
|
+
modalities: {
|
|
135
|
+
input: Object.keys(model.capabilities.input).filter(
|
|
136
|
+
(key) => model.capabilities.input[key],
|
|
137
|
+
),
|
|
138
|
+
output: Object.keys(model.capabilities.output).filter(
|
|
139
|
+
(key) => model.capabilities.output[key],
|
|
140
|
+
),
|
|
141
|
+
},
|
|
142
|
+
cost: { input: 0, output: 0 },
|
|
143
|
+
variants: model.variants,
|
|
144
|
+
free: true,
|
|
145
|
+
},
|
|
146
|
+
]),
|
|
147
|
+
);
|
|
148
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@magpie-community/opencode-zen-free-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode Zen free models through the public credential, with native streaming protocols",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.mjs",
|
|
7
|
+
"files": ["index.mjs", "provider.mjs", "models.mjs", "stream.mjs", "tool-response.mjs", "README.md", "LICENSE"],
|
|
8
|
+
"keywords": ["opencode", "opencode-plugin", "zen", "free", "magpie"],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": { "type": "git", "url": "https://github.com/magpie-community/plugins", "directory": "packages/zen-free" },
|
|
11
|
+
"scripts": { "test": "bun test" },
|
|
12
|
+
"magpie": { "icon": "https://opencode.ai/favicon-v3.ico", "maxConcurrency": 2 }
|
|
13
|
+
}
|
package/provider.mjs
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
PROVIDER,
|
|
4
|
+
SDK,
|
|
5
|
+
bootstrapProvider,
|
|
6
|
+
buildModels,
|
|
7
|
+
configModels,
|
|
8
|
+
parseProtocolDocs,
|
|
9
|
+
} from "./models.mjs";
|
|
10
|
+
import { collapseResponse } from "./stream.mjs";
|
|
11
|
+
import { guardToolResponse } from "./tool-response.mjs";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BASE = "https://opencode.ai/zen/v1";
|
|
14
|
+
const CATALOG = "https://models.opencode.ai/api.json";
|
|
15
|
+
const DOCS =
|
|
16
|
+
"https://raw.githubusercontent.com/anomalyco/opencode/dev/packages/web/src/content/docs/zen.mdx";
|
|
17
|
+
const PATHS = { chat: "/chat/completions", responses: "/responses", anthropic: "/messages" };
|
|
18
|
+
const CORE_TOOLS = ["bash", "edit", "glob", "grep", "read"];
|
|
19
|
+
const OPENCODE_VERSION = "1.18.34";
|
|
20
|
+
|
|
21
|
+
function httpURL(value) {
|
|
22
|
+
const url = new URL(value);
|
|
23
|
+
if (
|
|
24
|
+
!["http:", "https:"].includes(url.protocol) ||
|
|
25
|
+
url.username ||
|
|
26
|
+
url.password ||
|
|
27
|
+
url.search ||
|
|
28
|
+
url.hash
|
|
29
|
+
) {
|
|
30
|
+
throw new Error("Expected an HTTP(S) URL without credentials, query or fragment");
|
|
31
|
+
}
|
|
32
|
+
return url.href.replace(/\/+$/, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requirePublic(auth) {
|
|
36
|
+
if (auth?.type !== "api" || auth.key !== "public") {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"OpenCode Zen Free uses the public credential. Enable its free access login method.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readRemote(url, json, headers, callerSignal) {
|
|
44
|
+
const timeout = AbortSignal.timeout(15000);
|
|
45
|
+
const signal = callerSignal ? AbortSignal.any([callerSignal, timeout]) : timeout;
|
|
46
|
+
const response = await fetch(url, { headers, signal });
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
await response.body?.cancel();
|
|
49
|
+
throw new Error(`Model discovery returned HTTP ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
return json ? response.json() : response.text();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeTools(body, protocol) {
|
|
55
|
+
if (body.tools !== undefined && !Array.isArray(body.tools)) {
|
|
56
|
+
throw new Error("tools must be an array");
|
|
57
|
+
}
|
|
58
|
+
body.tools ??= [];
|
|
59
|
+
const present = new Set(
|
|
60
|
+
body.tools.map((tool) => (protocol === "chat" ? tool?.function?.name : tool?.name)),
|
|
61
|
+
);
|
|
62
|
+
const declared = new Set([...present].filter((name) => typeof name === "string"));
|
|
63
|
+
if (protocol === "chat" && Array.isArray(body.functions)) {
|
|
64
|
+
for (const tool of body.functions) if (typeof tool?.name === "string") declared.add(tool.name);
|
|
65
|
+
}
|
|
66
|
+
const injected = new Set();
|
|
67
|
+
for (const name of CORE_TOOLS) {
|
|
68
|
+
if (present.has(name)) continue;
|
|
69
|
+
injected.add(name);
|
|
70
|
+
const parameters = { type: "object", properties: {}, additionalProperties: false };
|
|
71
|
+
const definition = { name, description: "Not available. Never call this tool." };
|
|
72
|
+
body.tools.push(
|
|
73
|
+
protocol === "chat"
|
|
74
|
+
? { type: "function", function: { ...definition, parameters } }
|
|
75
|
+
: protocol === "anthropic"
|
|
76
|
+
? { ...definition, input_schema: parameters }
|
|
77
|
+
: { type: "function", ...definition, parameters },
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return { declared, injected };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sessionID(headers, body) {
|
|
84
|
+
let signal;
|
|
85
|
+
for (const name of [
|
|
86
|
+
"x-opencode-session",
|
|
87
|
+
"x-session-affinity",
|
|
88
|
+
"x-session-id",
|
|
89
|
+
"conversation-id",
|
|
90
|
+
]) {
|
|
91
|
+
if (headers.get(name)) {
|
|
92
|
+
signal = headers.get(name);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!signal && typeof body.metadata?.session_id === "string") signal = body.metadata.session_id;
|
|
97
|
+
if (!signal) {
|
|
98
|
+
const messages = body.messages ?? body.input;
|
|
99
|
+
const first = Array.isArray(messages)
|
|
100
|
+
? messages.find((message) => message.role === "user")?.content
|
|
101
|
+
: messages;
|
|
102
|
+
signal = first ? JSON.stringify(first) : randomUUID();
|
|
103
|
+
}
|
|
104
|
+
if (/^ses_[0-9a-f]{12}[0-9A-Za-z]{14}$/.test(signal)) return signal;
|
|
105
|
+
// Keep conversation affinity using OpenCode's canonical session format.
|
|
106
|
+
const hash = createHash("sha256").update(`ses\0${signal}`).digest("hex");
|
|
107
|
+
let value = BigInt(`0x${hash.slice(12, 32)}`);
|
|
108
|
+
let suffix = "";
|
|
109
|
+
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
110
|
+
for (let i = 0; i < 14; i++) {
|
|
111
|
+
suffix = alphabet[Number(value % 62n)] + suffix;
|
|
112
|
+
value /= 62n;
|
|
113
|
+
}
|
|
114
|
+
return `ses_${hash.slice(0, 12)}${suffix}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function server(_input, options = {}) {
|
|
118
|
+
const baseURL = httpURL(options.baseURL ?? DEFAULT_BASE);
|
|
119
|
+
const catalogURL = httpURL(options.catalogURL ?? CATALOG);
|
|
120
|
+
const docsURL = httpURL(options.docsURL ?? DOCS);
|
|
121
|
+
let known = buildModels(Object.keys(bootstrapProvider.models), bootstrapProvider, {}, baseURL);
|
|
122
|
+
let metadata = bootstrapProvider;
|
|
123
|
+
let protocols = {};
|
|
124
|
+
let listed = false;
|
|
125
|
+
|
|
126
|
+
async function discover(signal) {
|
|
127
|
+
const headers = { authorization: "Bearer public", "x-opencode-client": "cli" };
|
|
128
|
+
const [listing, catalog, docs] = await Promise.allSettled([
|
|
129
|
+
readRemote(`${baseURL}/models`, true, headers, signal),
|
|
130
|
+
readRemote(catalogURL, true, undefined, signal),
|
|
131
|
+
readRemote(docsURL, false, undefined, signal),
|
|
132
|
+
]);
|
|
133
|
+
signal?.throwIfAborted();
|
|
134
|
+
let partial = false;
|
|
135
|
+
if (catalog.status === "fulfilled" && catalog.value?.opencode?.models) {
|
|
136
|
+
metadata = catalog.value.opencode;
|
|
137
|
+
} else {
|
|
138
|
+
partial = true;
|
|
139
|
+
}
|
|
140
|
+
if (docs.status === "fulfilled" && Object.keys(parseProtocolDocs(docs.value)).length) {
|
|
141
|
+
protocols = parseProtocolDocs(docs.value);
|
|
142
|
+
} else {
|
|
143
|
+
partial = true;
|
|
144
|
+
}
|
|
145
|
+
if (listing.status !== "fulfilled" || !Array.isArray(listing.value?.data)) {
|
|
146
|
+
const fallback = { ...known };
|
|
147
|
+
fallback[Symbol.for("magpie.fellBack")] = true;
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
const ids = listing.value.data.map((item) => item.id);
|
|
151
|
+
const discovered = buildModels(ids, metadata, protocols, baseURL);
|
|
152
|
+
if (partial) {
|
|
153
|
+
// Preserve previously verified models only while they remain in the live catalog.
|
|
154
|
+
for (const id of ids) {
|
|
155
|
+
if (!discovered[id] && !metadata.models?.[id] && !protocols[id] && known[id]) {
|
|
156
|
+
discovered[id] = known[id];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
discovered[Symbol.for("magpie.fellBack")] = true;
|
|
160
|
+
}
|
|
161
|
+
known = discovered;
|
|
162
|
+
listed = true;
|
|
163
|
+
return discovered;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
async config(cfg) {
|
|
168
|
+
cfg.provider ??= {};
|
|
169
|
+
cfg.provider[PROVIDER] ??= {
|
|
170
|
+
name: "OpenCode Zen Free",
|
|
171
|
+
npm: SDK.chat,
|
|
172
|
+
api: baseURL,
|
|
173
|
+
models: configModels(known),
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
auth: {
|
|
177
|
+
provider: PROVIDER,
|
|
178
|
+
icon: "https://opencode.ai/favicon-v3.ico",
|
|
179
|
+
maxConcurrency: 2,
|
|
180
|
+
methods: [
|
|
181
|
+
{
|
|
182
|
+
type: "api",
|
|
183
|
+
label: "Zen 免费访问(密钥填写 public)",
|
|
184
|
+
placeholder: "public",
|
|
185
|
+
async authorize() {
|
|
186
|
+
return { type: "success", key: "public" };
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
async loader(getAuth) {
|
|
191
|
+
const auth = await getAuth();
|
|
192
|
+
if (!auth) return {};
|
|
193
|
+
requirePublic(auth);
|
|
194
|
+
return {
|
|
195
|
+
baseURL,
|
|
196
|
+
apiKey: "public",
|
|
197
|
+
async fetch(input, init) {
|
|
198
|
+
requirePublic(await getAuth());
|
|
199
|
+
const request = new Request(input, init);
|
|
200
|
+
const protocol = Object.keys(PATHS).find(
|
|
201
|
+
(key) => request.url === `${baseURL}${PATHS[key]}`,
|
|
202
|
+
);
|
|
203
|
+
if (!protocol || request.method !== "POST")
|
|
204
|
+
throw new Error("Unsupported Zen endpoint or method");
|
|
205
|
+
request.signal.throwIfAborted();
|
|
206
|
+
const body = await request.json();
|
|
207
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
208
|
+
throw new Error("Expected a JSON request object");
|
|
209
|
+
if (!listed && !Object.hasOwn(known, body.model)) {
|
|
210
|
+
await discover(request.signal);
|
|
211
|
+
}
|
|
212
|
+
const model = known[body.model];
|
|
213
|
+
if (!model || model.api.npm !== SDK[protocol]) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"Model is not an available free model for this protocol. Refresh the model list.",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const streaming = body.stream === true;
|
|
219
|
+
body.stream = true;
|
|
220
|
+
const toolPolicy = normalizeTools(body, protocol);
|
|
221
|
+
if (protocol === "chat")
|
|
222
|
+
body.stream_options = { ...body.stream_options, include_usage: true };
|
|
223
|
+
const headers = new Headers(request.headers);
|
|
224
|
+
for (const name of [
|
|
225
|
+
"authorization",
|
|
226
|
+
"x-api-key",
|
|
227
|
+
"x-goog-api-key",
|
|
228
|
+
"content-length",
|
|
229
|
+
"content-encoding",
|
|
230
|
+
"host",
|
|
231
|
+
"cookie",
|
|
232
|
+
]) {
|
|
233
|
+
headers.delete(name);
|
|
234
|
+
}
|
|
235
|
+
headers.set("content-type", "application/json");
|
|
236
|
+
headers.set("accept", "text/event-stream");
|
|
237
|
+
headers.set("user-agent", `opencode/${OPENCODE_VERSION}`);
|
|
238
|
+
headers.set("x-opencode-client", "cli");
|
|
239
|
+
headers.set("x-opencode-request", `req_${randomUUID().replaceAll("-", "")}`);
|
|
240
|
+
if (!headers.has("x-opencode-project")) {
|
|
241
|
+
const project = _input?.project;
|
|
242
|
+
headers.set(
|
|
243
|
+
"x-opencode-project",
|
|
244
|
+
project?.vcs === "git" && project.id ? project.id : "global",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const session = sessionID(headers, body);
|
|
248
|
+
for (const name of ["x-session-id", "x-opencode-session", "x-session-affinity"])
|
|
249
|
+
headers.set(name, session);
|
|
250
|
+
if (protocol === "anthropic") {
|
|
251
|
+
headers.set("x-api-key", "public");
|
|
252
|
+
if (!headers.has("anthropic-version")) headers.set("anthropic-version", "2023-06-01");
|
|
253
|
+
} else {
|
|
254
|
+
headers.set("authorization", "Bearer public");
|
|
255
|
+
}
|
|
256
|
+
const response = await fetch(request.url, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers,
|
|
259
|
+
body: JSON.stringify(body),
|
|
260
|
+
signal: request.signal,
|
|
261
|
+
redirect: "error",
|
|
262
|
+
});
|
|
263
|
+
const guarded = await guardToolResponse(response, protocol, toolPolicy);
|
|
264
|
+
return streaming ? guarded : collapseResponse(guarded, protocol);
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
async usage() {
|
|
269
|
+
return { plan: "OpenCode Zen Free", windows: [] };
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
provider: {
|
|
273
|
+
id: PROVIDER,
|
|
274
|
+
async models(provider, { auth } = {}) {
|
|
275
|
+
if (!auth) return provider.models;
|
|
276
|
+
requirePublic(auth);
|
|
277
|
+
return discover();
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
async "chat.headers"(input, output) {
|
|
281
|
+
if (![PROVIDER, `${PROVIDER}-plugin`].includes(input.model?.providerID) || !input.sessionID)
|
|
282
|
+
return;
|
|
283
|
+
output.headers["x-session-id"] = input.sessionID;
|
|
284
|
+
output.headers["x-opencode-session"] = input.sessionID;
|
|
285
|
+
output.headers["x-session-affinity"] = input.sessionID;
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export default { id: PROVIDER, server };
|