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/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
- }