opencode-zen-freex 0.2.8 → 0.2.9
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 +2 -9
- package/MAINTAINING.md +44 -3
- package/NOTICE +18 -0
- package/README.md +6 -4
- package/core-tools.mjs +85 -7
- package/index.mjs +14 -4
- package/limits.mjs +221 -57
- package/models.mjs +27 -3
- package/package.json +14 -6
- package/provider.mjs +111 -22
- package/stream.mjs +119 -21
- package/tool-response.mjs +56 -11
- package/capture.mjs +0 -231
- package/limits-audit.mjs +0 -340
- package/tools-smoke.mjs +0 -234
package/tools-smoke.mjs
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
// Asks Zen which of the client generations this package still speaks.
|
|
2
|
-
//
|
|
3
|
-
// The gate moved more than once in a day, and nothing short of asking it says
|
|
4
|
-
// so: a stale fingerprint does not look different from a working one until a
|
|
5
|
-
// user is refused. This is the check to run before a release, and whenever a
|
|
6
|
-
// report comes in that the free tier answers 403.
|
|
7
|
-
//
|
|
8
|
-
// bun run check:gate
|
|
9
|
-
//
|
|
10
|
-
// Each profile is asked twice, because the plugin sends two shapes and Zen
|
|
11
|
-
// does not answer them alike:
|
|
12
|
-
//
|
|
13
|
-
// floor the names in `required`, which is what the plugin stands in for a
|
|
14
|
-
// host that is not OpenCode and has none of these tools by name. This
|
|
15
|
-
// is the shape the plugin exists to send, so it is the one that
|
|
16
|
-
// decides the answer.
|
|
17
|
-
// full every tool the profile carries, which is what a host that is
|
|
18
|
-
// OpenCode itself sends. It is reported for what it says about a
|
|
19
|
-
// wording that has gone stale, and does not decide the exit code.
|
|
20
|
-
//
|
|
21
|
-
// Asking only the full shape hid a real gap: `read` alone is served and
|
|
22
|
-
// everything but `read` is refused, so a Zen that started insisting on another
|
|
23
|
-
// name would leave the full shape passing while the shape the plugin actually
|
|
24
|
-
// sends was refused.
|
|
25
|
-
//
|
|
26
|
-
// Every request here is built by the plugin itself: the header names come from
|
|
27
|
-
// provider.mjs's outboundHeaders and the tools from the same `required` the
|
|
28
|
-
// plugin stands in, so this cannot report on a shape the plugin does not send.
|
|
29
|
-
// A copy of the headers is what it used to ask with, and that copy had drifted -
|
|
30
|
-
// it was missing `accept` - with nothing failing on it.
|
|
31
|
-
//
|
|
32
|
-
// Each request is reported as:
|
|
33
|
-
//
|
|
34
|
-
// answered Zen served the request; this shape is current
|
|
35
|
-
// refused Zen did not recognise this generation (403)
|
|
36
|
-
// superseded Zen wants a newer OpenCode than the one this profile claims (426)
|
|
37
|
-
// failed Zen answered something other than 403: the request was made, and
|
|
38
|
-
// what came back is not a verdict on the fingerprint
|
|
39
|
-
// unreachable the free tier could not be asked at all
|
|
40
|
-
//
|
|
41
|
-
// Exit code is 0 when at least one generation's floor is answered. A generation
|
|
42
|
-
// being refused is not a failure while another still works.
|
|
43
|
-
|
|
44
|
-
import { randomBytes } from "node:crypto";
|
|
45
|
-
import { fileURLToPath } from "node:url";
|
|
46
|
-
import { PROFILES, agentFor } from "./core-tools.mjs";
|
|
47
|
-
import { outboundHeaders } from "./provider.mjs";
|
|
48
|
-
|
|
49
|
-
const BASE = process.env.ZEN_BASE_URL ?? "https://opencode.ai/inference/v1";
|
|
50
|
-
const MODEL = process.env.ZEN_CHECK_MODEL ?? "space-bunny-free";
|
|
51
|
-
const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
52
|
-
|
|
53
|
-
// A session id of the shape OpenCode makes, since that is part of what is asked.
|
|
54
|
-
let stamp = 0;
|
|
55
|
-
let counter = 0;
|
|
56
|
-
function sessionID() {
|
|
57
|
-
const now = Date.now();
|
|
58
|
-
if (now !== stamp) {
|
|
59
|
-
stamp = now;
|
|
60
|
-
counter = 0;
|
|
61
|
-
}
|
|
62
|
-
counter++;
|
|
63
|
-
const value = (~(BigInt(now) * 0x1000n + BigInt(counter))) & 0xffffffffffffn;
|
|
64
|
-
let tail = "";
|
|
65
|
-
for (const byte of randomBytes(14)) tail += ALPHABET[byte % 62];
|
|
66
|
-
return `ses_${value.toString(16).padStart(12, "0")}${tail}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Zen gives the same sentence whichever part of a request failed to look like
|
|
70
|
-
// OpenCode, so it is worth printing: a refusal that arrives with no message at
|
|
71
|
-
// all reads as something other than a stale fingerprint.
|
|
72
|
-
function says(text) {
|
|
73
|
-
const match = text.match(/"message"\s*:\s*"([^"]{0,120})"/);
|
|
74
|
-
return match ? `: ${match[1]}` : "";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// The request a shape is asked with. It is its own function so index.test.mjs
|
|
78
|
-
// can hold it against the request the plugin was observed to send, offline, and
|
|
79
|
-
// so limits-audit.mjs asks with the same shape this gate does instead of a copy
|
|
80
|
-
// of it — a copy is what drifted before (it was missing `accept`).
|
|
81
|
-
export function requestFor(generation, profile, names) {
|
|
82
|
-
return chatRequest({ model: MODEL, generation, profile, names, maxTokens: 40 });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function chatRequest({ model, generation, profile, names, maxTokens = 40, messages }) {
|
|
86
|
-
return {
|
|
87
|
-
headers: outboundHeaders({
|
|
88
|
-
session: sessionID(),
|
|
89
|
-
agent: agentFor(generation),
|
|
90
|
-
project: "global",
|
|
91
|
-
protocol: "chat",
|
|
92
|
-
}),
|
|
93
|
-
body: {
|
|
94
|
-
model,
|
|
95
|
-
messages: messages ?? [{ role: "user", content: "hi" }],
|
|
96
|
-
// A name in `required` that the profile does not carry is a broken
|
|
97
|
-
// fingerprint, not a request to ask Zen with: without this it is sent as
|
|
98
|
-
// `{"type":"function"}`, Zen refuses it as malformed, and the gate blames
|
|
99
|
-
// the wording instead of the file it came from.
|
|
100
|
-
tools: names.map((name) => {
|
|
101
|
-
const tool = profile.tools[name];
|
|
102
|
-
if (!tool) throw new Error(`${generation} has no tool named "${name}" in fingerprint.json`);
|
|
103
|
-
return { type: "function", function: tool };
|
|
104
|
-
}),
|
|
105
|
-
stream: true,
|
|
106
|
-
stream_options: { include_usage: true },
|
|
107
|
-
max_tokens: maxTokens,
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function check(generation, profile, shape, names) {
|
|
113
|
-
const { headers, body } = requestFor(generation, profile, names);
|
|
114
|
-
try {
|
|
115
|
-
const response = await fetch(`${BASE}/chat/completions`, {
|
|
116
|
-
method: "POST",
|
|
117
|
-
headers,
|
|
118
|
-
body: JSON.stringify(body),
|
|
119
|
-
signal: AbortSignal.timeout(30000),
|
|
120
|
-
});
|
|
121
|
-
const text = await response.text();
|
|
122
|
-
if (response.ok)
|
|
123
|
-
return { generation, shape, verdict: "answered", detail: `as ${headers.get("user-agent")}` };
|
|
124
|
-
if (response.status === 426)
|
|
125
|
-
return {
|
|
126
|
-
generation,
|
|
127
|
-
shape,
|
|
128
|
-
verdict: "superseded",
|
|
129
|
-
detail: "Zen wants a newer OpenCode than this profile claims",
|
|
130
|
-
};
|
|
131
|
-
// Only a 403 is the answer this gate reads: Zen quoting the fingerprint
|
|
132
|
-
// back. A 5xx, a 429 or a 401 says something about the service or the way
|
|
133
|
-
// it was asked, and reading any of them as "the fingerprint is stale" sends
|
|
134
|
-
// the reader to `capture` over a problem capturing cannot fix.
|
|
135
|
-
if (response.status === 403)
|
|
136
|
-
return { generation, shape, verdict: "refused", detail: `HTTP 403${says(text)}` };
|
|
137
|
-
return { generation, shape, verdict: "failed", detail: `HTTP ${response.status}${says(text)}` };
|
|
138
|
-
} catch (error) {
|
|
139
|
-
return { generation, shape, verdict: "unreachable", detail: error.message };
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Whether this module is the file that was run rather than one the tests
|
|
144
|
-
// import. `import.meta.main` is the direct answer and the one Bun gives, but it
|
|
145
|
-
// arrived in Node 22.18 and nothing before that has it — and a gate that cannot
|
|
146
|
-
// tell whether it was run is a gate that reports success without asking.
|
|
147
|
-
function invokedDirectly() {
|
|
148
|
-
// `typeof` rather than `??`: a runtime without the property throws on the
|
|
149
|
-
// member access itself, where there is nothing to fall back from.
|
|
150
|
-
if (typeof import.meta.main === "boolean") return import.meta.main;
|
|
151
|
-
try {
|
|
152
|
-
return process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1];
|
|
153
|
-
} catch {
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Only a run asks Zen. index.test.mjs imports this module to hold its request
|
|
159
|
-
// against the plugin's, and an import must not reach the network.
|
|
160
|
-
const entry = invokedDirectly();
|
|
161
|
-
if (entry === undefined) {
|
|
162
|
-
console.error(
|
|
163
|
-
"Could not tell whether this file was run or imported, so nothing was asked: neither import.meta.main nor process.argv[1] named an entry point. Run it with `bun run check:gate`.",
|
|
164
|
-
);
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (entry) {
|
|
169
|
-
// Each request is independent and every result is printed in order, so the
|
|
170
|
-
// two shapes of every profile are asked at once rather than one abort's
|
|
171
|
-
// worth of waiting after another.
|
|
172
|
-
const results = (
|
|
173
|
-
await Promise.all(
|
|
174
|
-
Object.entries(PROFILES).map(async ([generation, profile]) => [
|
|
175
|
-
await check(generation, profile, "floor", [...profile.required]),
|
|
176
|
-
await check(generation, profile, "full", Object.keys(profile.tools)),
|
|
177
|
-
]),
|
|
178
|
-
)
|
|
179
|
-
).flat();
|
|
180
|
-
|
|
181
|
-
const width = Math.max(...results.map((result) => result.generation.length));
|
|
182
|
-
for (const result of results)
|
|
183
|
-
console.log(
|
|
184
|
-
`${result.shape.padEnd(6)}${result.verdict.padEnd(12)} ${result.generation.padEnd(width)} ${result.detail}`,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const floor = results.filter((result) => result.shape === "floor");
|
|
188
|
-
const served = floor.filter((result) => result.verdict === "answered");
|
|
189
|
-
console.log(`\n${served.length}/${floor.length} generations serve a host that is not OpenCode at ${BASE}`);
|
|
190
|
-
|
|
191
|
-
// A generation whose full shape is served while its floor is not is the case
|
|
192
|
-
// this gate was widened to see: the plugin would work for a host bringing its
|
|
193
|
-
// own OpenCode tools and be refused for the host it was written for.
|
|
194
|
-
for (const result of floor) {
|
|
195
|
-
if (result.verdict === "answered") continue;
|
|
196
|
-
const full = results.find(
|
|
197
|
-
(other) => other.generation === result.generation && other.shape === "full",
|
|
198
|
-
);
|
|
199
|
-
if (full?.verdict === "answered")
|
|
200
|
-
console.log(
|
|
201
|
-
`${result.generation} answers the full OpenCode shape but not the tools the plugin stands in: a host that has none of them by name will be refused.`,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (!served.length) {
|
|
206
|
-
// Being unable to ask is not an answer. Saying the fingerprint has gone stale
|
|
207
|
-
// when nothing was reached sends the reader to `capture` over a dead network.
|
|
208
|
-
// Only 403 and 426 are Zen answering about the fingerprint at all: a 5xx or a
|
|
209
|
-
// 429 was answered by something that never judged it, and counting those as a
|
|
210
|
-
// verdict would blame the wording for an outage.
|
|
211
|
-
const answered = (result) => ["refused", "superseded"].includes(result.verdict);
|
|
212
|
-
const unreached = results.filter((result) => result.verdict === "unreachable");
|
|
213
|
-
if (results.every(answered)) {
|
|
214
|
-
console.log(
|
|
215
|
-
"None of them. Zen has moved past every generation this plugin knows; the tool wording in core-tools.mjs needs capturing again from a current OpenCode.",
|
|
216
|
-
);
|
|
217
|
-
} else if (results.some(answered)) {
|
|
218
|
-
console.log(
|
|
219
|
-
"Some of this run was answered about the fingerprint and some of it was not, which is not a fact about the fingerprint either way. Check the network or ZEN_BASE_URL and ask again.",
|
|
220
|
-
);
|
|
221
|
-
} else if (!unreached.length) {
|
|
222
|
-
console.log(
|
|
223
|
-
"Zen answered every request, but none of them with a verdict on the fingerprint, so this says nothing about it. Check the service or ZEN_BASE_URL and ask again.",
|
|
224
|
-
);
|
|
225
|
-
} else {
|
|
226
|
-
console.log(
|
|
227
|
-
"The free tier could not be reached at all, so this says nothing about the fingerprint. Check the network or ZEN_BASE_URL and ask again.",
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
process.exitCode = 1;
|
|
231
|
-
} else if (served.length < floor.length) {
|
|
232
|
-
console.log("Some are refused but at least one works, so the plugin still has a generation Zen will serve.");
|
|
233
|
-
}
|
|
234
|
-
}
|