opencode-zen-freex 0.2.8 → 0.2.10
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 +55 -3
- package/NOTICE +18 -0
- package/README.md +6 -4
- package/core-tools.mjs +85 -7
- package/fingerprint.json +32 -32
- 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/limits.mjs
CHANGED
|
@@ -131,93 +131,236 @@ const NAMED_OUTPUT = [
|
|
|
131
131
|
const OBSERVED = new Map();
|
|
132
132
|
const OBSERVED_OUTPUT = new Map();
|
|
133
133
|
|
|
134
|
+
// Two decisions this file makes leave no trace of themselves: a live value
|
|
135
|
+
// taken from a refusal is applied past the staleness guard and past the
|
|
136
|
+
// raise-only rule, and an entry the catalogue has moved past is void in a way
|
|
137
|
+
// that reads exactly like an axis nobody ever measured. Neither is worth a line
|
|
138
|
+
// while the plugin is working — an error path that chats is its own kind of
|
|
139
|
+
// failure — so both are only reported when they are asked for: a host hands in
|
|
140
|
+
// a sink with `limitDiagnostics`, or sets this variable and reads stderr.
|
|
141
|
+
// Nothing a note does may reach a request: not the sink, not what it prints.
|
|
142
|
+
export const LIMITS_DEBUG_ENV = "OPENCODE_ZEN_FREE_LIMITS_DEBUG";
|
|
143
|
+
let diagnostic = null;
|
|
144
|
+
|
|
145
|
+
// Install the sink these notes are handed to, and return the one it replaced
|
|
146
|
+
// (null when there was none) so a caller can put back what it found.
|
|
147
|
+
export function limitDiagnostics(sink) {
|
|
148
|
+
const previous = diagnostic;
|
|
149
|
+
diagnostic = typeof sink === "function" ? sink : null;
|
|
150
|
+
return previous;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function note(entry) {
|
|
154
|
+
try {
|
|
155
|
+
if (diagnostic) diagnostic(entry);
|
|
156
|
+
else if (globalThis.process?.env?.[LIMITS_DEBUG_ENV])
|
|
157
|
+
console.warn(`[opencode-zen-freex] ${describe(entry)}`);
|
|
158
|
+
} catch {
|
|
159
|
+
// A note is not worth a failed request.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function describe({ kind, id, axis, value, measuredAgainst, declared }) {
|
|
164
|
+
if (kind === "route-statement")
|
|
165
|
+
return `${id} ${axis}=${value} — named by a refusal this process saw, so it is applied as it stands (the catalogue declares ${declared === undefined ? "nothing for this axis" : declared})`;
|
|
166
|
+
return `${id} ${axis}=${value} was measured against a catalogue that declared ${measuredAgainst}, and this one declares ${declared === undefined ? "nothing for this axis" : declared}: not applied`;
|
|
167
|
+
}
|
|
168
|
+
|
|
134
169
|
// The window a refusal names, or undefined when the body says nothing
|
|
135
170
|
// attributable. Exported for the tests and for anyone reading a capture.
|
|
136
171
|
export function namedContextWindow(text) {
|
|
137
|
-
return
|
|
172
|
+
return namedFrom([NAMED_CONTEXT], text)?.value;
|
|
138
173
|
}
|
|
139
174
|
|
|
140
175
|
// The completion ceiling a refusal names, or undefined. Same rule as the window:
|
|
141
|
-
// the body has to name the parameter and a number, or it says nothing.
|
|
176
|
+
// the body has to name the parameter and a number, or it says nothing. Two
|
|
177
|
+
// wordings state it, and one that matches but names no usable number — "supports
|
|
178
|
+
// at most 0 completion tokens" — is not an answer; the next wording may be.
|
|
142
179
|
export function namedOutputCap(text) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
180
|
+
return namedFrom(NAMED_OUTPUT, text)?.value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The first of `patterns` that names a usable number, with the match it was read
|
|
184
|
+
// from. The match and the number have to be the same one: a caller that guards a
|
|
185
|
+
// match and then reads its number elsewhere is judging one sentence and
|
|
186
|
+
// returning another.
|
|
187
|
+
function namedFrom(patterns, text) {
|
|
188
|
+
const subject = typeof text === "string" ? text : "";
|
|
189
|
+
for (const pattern of patterns) {
|
|
190
|
+
const match = pattern.exec(subject);
|
|
191
|
+
const value = numberFrom(match);
|
|
192
|
+
if (value !== undefined) return { match, value };
|
|
146
193
|
}
|
|
147
194
|
return undefined;
|
|
148
195
|
}
|
|
149
196
|
|
|
150
|
-
|
|
151
|
-
|
|
197
|
+
// The number a match names, or undefined when it names nothing usable: a
|
|
198
|
+
// sentence about a parameter that happens to mention zero names no ceiling.
|
|
199
|
+
function numberFrom(match) {
|
|
152
200
|
if (!match) return undefined;
|
|
153
201
|
const tokens = Number.parseInt(match[1], 10);
|
|
154
202
|
return Number.isSafeInteger(tokens) && tokens > 0 ? tokens : undefined;
|
|
155
203
|
}
|
|
156
204
|
|
|
157
|
-
// Read
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
205
|
+
// Read at most `limit` characters of a response without buffering the rest:
|
|
206
|
+
// this is an error path on a request whose caller is waiting, so a body that is
|
|
207
|
+
// not small is dropped as soon as the part that is read is in hand. Returns the
|
|
208
|
+
// text, and whether the read stopped because the body ended (`ended`) or because
|
|
209
|
+
// the budget was reached (`over`).
|
|
210
|
+
//
|
|
211
|
+
// It reads the response's own body, not a copy of it. `response.clone()` tees a
|
|
212
|
+
// body, and cancelling one branch of a tee settles only once the other branch is
|
|
213
|
+
// drained or cancelled — which for the caller's path is never, because the
|
|
214
|
+
// sibling branch is read later, after this returns. A clone here hangs the whole
|
|
215
|
+
// request on any refusal larger than the budget that arrives in more than one
|
|
216
|
+
// read (measured: a 7 KB 400 in 1400-character chunks never settles, and an
|
|
217
|
+
// abort does not unwedge it). The body is read once and the bytes go back to
|
|
218
|
+
// the caller, so nothing needs a second copy.
|
|
219
|
+
//
|
|
220
|
+
// The read is bounded in time as well as in size, because a body can stall as
|
|
221
|
+
// well as be large. Measured: a 403 whose `content-length` promised 999 bytes
|
|
222
|
+
// and delivered 120 left `read()` pending for good, which hung the request just
|
|
223
|
+
// as surely as the clone did — an upstream that dies mid-body, or a proxy that
|
|
224
|
+
// pads a length, is the same shape. `signal` is the caller's own cancellation;
|
|
225
|
+
// without one the budget below is all the wait this evidence is worth, since a
|
|
226
|
+
// number out of a refusal that is not arriving is not worth a caller's turn.
|
|
227
|
+
//
|
|
228
|
+
// A body that fails halfway is still worth what was read of it, and a read that
|
|
229
|
+
// throws is not an error about the request: the callers of this are error paths
|
|
230
|
+
// themselves.
|
|
231
|
+
const READ_DEADLINE_MS = 3000;
|
|
232
|
+
|
|
233
|
+
export async function readBoundedText(response, limit = 4096, { signal } = {}) {
|
|
234
|
+
let reader;
|
|
165
235
|
try {
|
|
166
|
-
|
|
167
|
-
const text = await readPrefix(response, limit);
|
|
168
|
-
// `readPrefix` stops at `limit`, so the last character read may be the first
|
|
169
|
-
// half of a longer number. Both words this reads are numbers inside prose —
|
|
170
|
-
// the window's ends in the literal "tokens", but the second ceiling wording
|
|
171
|
-
// ends with the digits it captured, so `/max_tokens: 1000000 is not less or
|
|
172
|
-
// equal to 2621` carved out of 262144 reads as a ceiling Zen stated. Live
|
|
173
|
-
// values are taken as they stand, past the staleness check and the guard
|
|
174
|
-
// below, so that half a number would become the applied max_tokens. A match
|
|
175
|
-
// that runs up to the cut is therefore not evidence, on either axis: asking
|
|
176
|
-
// again over one character less only finds the same number one digit
|
|
177
|
-
// shorter, and reading the rest of the body is what this path must not do.
|
|
178
|
-
const { context, output } = namedAxes(text, limit, text.length > limit);
|
|
179
|
-
if (context === undefined && output === undefined) return undefined;
|
|
180
|
-
if (context !== undefined) OBSERVED.set(id, context);
|
|
181
|
-
if (output !== undefined) OBSERVED_OUTPUT.set(id, output);
|
|
182
|
-
return { context, output };
|
|
236
|
+
reader = response?.body?.getReader?.();
|
|
183
237
|
} catch {
|
|
184
|
-
|
|
238
|
+
// A body somebody else is holding cannot be read, and nothing of it was
|
|
239
|
+
// read: what came back is not the whole of a body, and says so.
|
|
240
|
+
return { text: "", ended: false, over: true };
|
|
185
241
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
242
|
+
// A response with no stream has no body to read and none to buffer — which is
|
|
243
|
+
// why the fallback that read one whole is gone: the only responses that
|
|
244
|
+
// reached it were body-less, and a body-less response's text is empty either
|
|
245
|
+
// way.
|
|
246
|
+
if (!reader) return { text: "", ended: true, over: false };
|
|
247
|
+
const deadline = AbortSignal.timeout(READ_DEADLINE_MS);
|
|
248
|
+
const bound = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
249
|
+
// The wait ending is this read's to remember, not the reader's: cancelling a
|
|
250
|
+
// stream resolves its pending `read()` as done, so a body cut short by the
|
|
251
|
+
// deadline would come back looking like one that ended — and a number at the
|
|
252
|
+
// end of it like a whole one. Measured before this flag existed: a stalled
|
|
253
|
+
// refusal whose last characters were `...is not less or equal to 2621`, out of
|
|
254
|
+
// `262144`, was stored and applied as a completion ceiling.
|
|
255
|
+
let cut = false;
|
|
256
|
+
const abort = () => {
|
|
257
|
+
// The wait is over and the bytes already read are all this evidence is
|
|
258
|
+
// going to be; stop pulling on a body that is not answering.
|
|
259
|
+
cut = true;
|
|
260
|
+
reader.cancel().catch(() => {});
|
|
261
|
+
};
|
|
262
|
+
if (bound.aborted) abort();
|
|
263
|
+
else bound.addEventListener("abort", abort, { once: true });
|
|
195
264
|
const decoder = new TextDecoder();
|
|
196
265
|
let text = "";
|
|
266
|
+
let ended = false;
|
|
197
267
|
try {
|
|
198
|
-
while
|
|
268
|
+
// The loop runs while the budget has room for one more character, so a body
|
|
269
|
+
// that lands exactly on the budget still gets the read that says it ended,
|
|
270
|
+
// and one that goes on is caught by the buffer stopping a character past it.
|
|
271
|
+
while (text.length <= limit) {
|
|
199
272
|
const { value, done } = await reader.read();
|
|
200
|
-
if (done)
|
|
201
|
-
|
|
273
|
+
if (done) {
|
|
274
|
+
ended = true;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
// At most one character past the budget: enough to know the body went on,
|
|
278
|
+
// and the rest of the chunk is dropped rather than materialised.
|
|
279
|
+
text += decoder.decode(value, { stream: true }).slice(0, limit + 1 - text.length);
|
|
202
280
|
}
|
|
203
281
|
} catch {
|
|
204
|
-
//
|
|
282
|
+
// Keep what was read; a failed read names nothing new. A read that was
|
|
283
|
+
// cancelled by the deadline lands here too, which is the point of it.
|
|
205
284
|
}
|
|
206
|
-
|
|
207
|
-
|
|
285
|
+
bound.removeEventListener("abort", abort);
|
|
286
|
+
// A read the wait cut short saw no end of body, whatever the cancelled reader
|
|
287
|
+
// said about it.
|
|
288
|
+
if (cut) ended = false;
|
|
289
|
+
// The reader is this function's to release, and the body is abandoned rather
|
|
290
|
+
// than left open: the caller has what it needs and will not read on.
|
|
291
|
+
//
|
|
292
|
+
// The cancel is deliberately not awaited. It stops the upstream either way,
|
|
293
|
+
// but the promise it returns settles only once the source reacts — and this
|
|
294
|
+
// path is an error body on a request whose caller is waiting. A body that
|
|
295
|
+
// never ends (a stalled upstream, or headers promising more bytes than the
|
|
296
|
+
// response delivers: measured, a 403 with `content-length: 999` over a 120-byte
|
|
297
|
+
// body) would leave the await pending for good, which is the same hang this
|
|
298
|
+
// function was rewritten to remove. Nothing downstream reads this body again,
|
|
299
|
+
// so its release is nobody's to wait for.
|
|
300
|
+
reader.cancel().catch(() => {});
|
|
301
|
+
// `over` is the read having stopped before the body did — the budget reached,
|
|
302
|
+
// or the read failed, or the deadline passing — which is the only honest
|
|
303
|
+
// source for "this may be cut".
|
|
304
|
+
return { text, ended, over: !ended };
|
|
208
305
|
}
|
|
209
306
|
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
307
|
+
// Read a refusal for the names it makes about this model's limits. Only non-2xx
|
|
308
|
+
// responses are read, and only the first `limit` characters of one.
|
|
309
|
+
//
|
|
310
|
+
// A response body can only be read once, and the response handed in is treated
|
|
311
|
+
// as read once this returns: the bytes come back as `text` so that a caller
|
|
312
|
+
// which has to keep them (provider.mjs rebuilds the message a user reads) can
|
|
313
|
+
// do so without a second read of a body that no longer exists.
|
|
314
|
+
//
|
|
315
|
+
// `cut` says the read stopped at the budget rather than at the end of the body.
|
|
316
|
+
// It is the read that says so, not the length of what was read: a body whose
|
|
317
|
+
// first read lands exactly on the budget leaves `text.length === limit` while
|
|
318
|
+
// the stream is still going, and a number sliced there (`262144` read as
|
|
319
|
+
// `2621`) must not be taken for a limit the route stated.
|
|
320
|
+
//
|
|
321
|
+
// Returns undefined only when nothing was read — a response that is not a
|
|
322
|
+
// refusal, or one with no body to read — and otherwise whichever axes the text
|
|
323
|
+
// named, with that text alongside them. An axis the body did not name is
|
|
324
|
+
// undefined; a body that named nothing still comes back, because a caller that
|
|
325
|
+
// has to hand it on has nowhere else to get it from.
|
|
326
|
+
export async function observeRouteStatement(id, response, limit = 4096, { signal } = {}) {
|
|
327
|
+
try {
|
|
328
|
+
if (typeof id !== "string" || !id || response?.ok !== false) return undefined;
|
|
329
|
+
// A response with no body was not read and is not consumed: the caller
|
|
330
|
+
// still holds the one it had, and undefined says so.
|
|
331
|
+
if (response.body == null) return undefined;
|
|
332
|
+
const { text, over } = await readBoundedText(response, limit, { signal });
|
|
333
|
+
const { context, output } = namedAxes(text, limit, over);
|
|
334
|
+
if (context !== undefined) OBSERVED.set(id, context);
|
|
335
|
+
if (output !== undefined) OBSERVED_OUTPUT.set(id, output);
|
|
336
|
+
return { context, output, text };
|
|
337
|
+
} catch {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// How close to the end of what was read a match has to end before a truncated
|
|
343
|
+
// body makes it unusable. A number ending inside this margin of the last
|
|
344
|
+
// character read may have had digits past it — `2621` where the body said
|
|
345
|
+
// `262144` — and a smaller limit that looks like a real one is the one failure
|
|
346
|
+
// this guard exists to prevent. Numbers further back are inside the buffer
|
|
347
|
+
// entirely and are read as they stand.
|
|
348
|
+
const CUT_MARGIN = 8;
|
|
349
|
+
|
|
350
|
+
// The axes the first `limit` characters of `text` name. `cut` says the read
|
|
351
|
+
// stopped at the budget with the body going on, so a match ending at the margin
|
|
352
|
+
// may have been a longer number: that axis is left unread rather than reported a
|
|
353
|
+
// digit short. Each axis is read from the match its number came from, so the
|
|
354
|
+
// match the guard judges and the number that is returned cannot be two answers.
|
|
213
355
|
function namedAxes(text, limit, cut) {
|
|
214
356
|
const head = text.slice(0, limit);
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
const onCut = (
|
|
357
|
+
const context = namedFrom([NAMED_CONTEXT], head);
|
|
358
|
+
const output = namedFrom(NAMED_OUTPUT, head);
|
|
359
|
+
const onCut = (found) =>
|
|
360
|
+
cut && Boolean(found) && found.match.index + found.match[0].length >= head.length - CUT_MARGIN;
|
|
218
361
|
return {
|
|
219
|
-
context: onCut(
|
|
220
|
-
output: onCut(
|
|
362
|
+
context: onCut(context) ? undefined : context?.value,
|
|
363
|
+
output: onCut(output) ? undefined : output?.value,
|
|
221
364
|
};
|
|
222
365
|
}
|
|
223
366
|
|
|
@@ -235,7 +378,11 @@ export function limitOverrides(
|
|
|
235
378
|
observedOutput = OBSERVED_OUTPUT,
|
|
236
379
|
) {
|
|
237
380
|
const overrides = {};
|
|
238
|
-
|
|
381
|
+
// A record has to be this table's own. `table[id]` otherwise answers out of
|
|
382
|
+
// Object.prototype for `toString` and the rest, and out of whatever prototype
|
|
383
|
+
// an object handed in as `table` carries — evidence for a model nobody
|
|
384
|
+
// declared, from a table that never named it.
|
|
385
|
+
const record = table && Object.hasOwn(table, id) ? table[id] : undefined;
|
|
239
386
|
for (const axis of ["context", "output"]) {
|
|
240
387
|
const declaredValue = declared?.[axis];
|
|
241
388
|
const entry = record?.[axis];
|
|
@@ -250,6 +397,19 @@ export function limitOverrides(
|
|
|
250
397
|
value = live;
|
|
251
398
|
} else if (entry && declaredValue !== undefined && entry.declaredAt === declaredValue) {
|
|
252
399
|
value = entry.value;
|
|
400
|
+
} else if (entry) {
|
|
401
|
+
// Measured against a catalogue that has moved, or against an axis the
|
|
402
|
+
// catalogue no longer declares. Its value is void, and saying so is the
|
|
403
|
+
// whole point of the note: otherwise this is indistinguishable from a
|
|
404
|
+
// model nothing was ever measured for.
|
|
405
|
+
note({
|
|
406
|
+
kind: "catalogue-drift",
|
|
407
|
+
id,
|
|
408
|
+
axis,
|
|
409
|
+
value: entry.value,
|
|
410
|
+
measuredAgainst: entry.declaredAt,
|
|
411
|
+
declared: declaredValue,
|
|
412
|
+
});
|
|
253
413
|
}
|
|
254
414
|
if (value === undefined || !Number.isSafeInteger(value) || value <= 0) continue;
|
|
255
415
|
if (value === declaredValue) continue;
|
|
@@ -264,6 +424,10 @@ export function limitOverrides(
|
|
|
264
424
|
)
|
|
265
425
|
continue;
|
|
266
426
|
overrides[axis] = value;
|
|
427
|
+
// A live value is the one override taken past the staleness guard and past
|
|
428
|
+
// the raise-only rule, so it is the one nobody can account for afterwards:
|
|
429
|
+
// say what was applied and what it stands in for.
|
|
430
|
+
if (live !== undefined) note({ kind: "route-statement", id, axis, value, declared: declaredValue });
|
|
267
431
|
}
|
|
268
432
|
return overrides;
|
|
269
433
|
}
|
package/models.mjs
CHANGED
|
@@ -36,10 +36,18 @@ function sdkProtocol(npm) {
|
|
|
36
36
|
// explicit null, and neither retires a model.
|
|
37
37
|
const hasValue = (value) => value !== null && value !== undefined;
|
|
38
38
|
|
|
39
|
+
// A status has to say something to be one. The catalogue writes an empty string
|
|
40
|
+
// as well as a null, and either one used to stand in front of the `lifecycle`
|
|
41
|
+
// that does retire the model, so a model retired through `lifecycle` kept being
|
|
42
|
+
// offered. A status that is set — "active" included — is still read as itself.
|
|
43
|
+
const isSet = (value) => hasValue(value) && String(value).trim() !== "";
|
|
44
|
+
|
|
39
45
|
function isRetired(model) {
|
|
40
46
|
return (
|
|
41
47
|
model.deprecated === true ||
|
|
42
|
-
["deprecated", "retired", "disabled"].includes(
|
|
48
|
+
["deprecated", "retired", "disabled"].includes(
|
|
49
|
+
isSet(model.status) ? model.status : model.lifecycle,
|
|
50
|
+
) ||
|
|
43
51
|
hasValue(model.deprecated_at) ||
|
|
44
52
|
hasValue(model.retirement_date)
|
|
45
53
|
);
|
|
@@ -72,7 +80,12 @@ function listedFree(id) {
|
|
|
72
80
|
// sending it somewhere it is not served: `systemone` and a model the catalogue
|
|
73
81
|
// drops do not appear.
|
|
74
82
|
function protocolFor(id, model, raw, provider, protocols) {
|
|
75
|
-
|
|
83
|
+
// Own properties only. `protocols` is a plain object, so `protocols["toString"]`
|
|
84
|
+
// is a truthy function — the model would be dropped for naming a protocol
|
|
85
|
+
// outside `SDK` instead of being resolved from the catalogue or its `-free`
|
|
86
|
+
// name, and `protocols["__proto__"]` would be an inherited protocol nobody
|
|
87
|
+
// wrote. What the docs actually say about an id is an own property or nothing.
|
|
88
|
+
if (Object.hasOwn(protocols, id)) return protocols[id];
|
|
76
89
|
if (raw) return sdkProtocol(model.provider?.npm ?? provider.npm);
|
|
77
90
|
if (listedFree(id)) return sdkProtocol(provider.npm) ?? "chat";
|
|
78
91
|
return undefined;
|
|
@@ -89,8 +102,19 @@ const modalities = (values) =>
|
|
|
89
102
|
export function buildModels(ids, provider, protocols, baseURL) {
|
|
90
103
|
const result = {};
|
|
91
104
|
for (const id of ids) {
|
|
105
|
+
// `__proto__` is skipped where the id is first known to be a string: it can
|
|
106
|
+
// never be an entry in the catalogue's own map, since on a plain object the
|
|
107
|
+
// write side of it is the prototype. The own-property read below leaves it
|
|
108
|
+
// unresolved for that same reason, so this is the redundant half of a pair —
|
|
109
|
+
// kept because it states the rule at the point the rule is about.
|
|
92
110
|
if (typeof id !== "string" || !id || id === "__proto__") continue;
|
|
93
|
-
|
|
111
|
+
// The catalogue's own entry for the id, not whatever Object.prototype has
|
|
112
|
+
// under that name: `models?.["toString"]` is a function, and reading it as a
|
|
113
|
+
// description of the model would invent a protocol for an id nothing
|
|
114
|
+
// describes. An id with no entry here is one the catalogue has not caught up
|
|
115
|
+
// with, which the `-free` rule below is for.
|
|
116
|
+
const catalogue = provider.models ?? {};
|
|
117
|
+
const raw = Object.hasOwn(catalogue, id) ? catalogue[id] : undefined;
|
|
94
118
|
const model = raw ?? {};
|
|
95
119
|
const protocol = protocolFor(id, model, raw, provider, protocols);
|
|
96
120
|
if (!isFree(id, model) || !Object.hasOwn(SDK, protocol)) continue;
|
package/package.json
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-zen-freex",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "OpenCode Zen's free models, for hosts that are not OpenCode: the public credential, automatic discovery and native streaming protocols",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
|
-
"exports": {
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.mjs",
|
|
9
|
+
"./index.mjs": "./index.mjs",
|
|
10
|
+
"./provider.mjs": "./provider.mjs",
|
|
11
|
+
"./core-tools.mjs": "./core-tools.mjs",
|
|
12
|
+
"./models.mjs": "./models.mjs",
|
|
13
|
+
"./limits.mjs": "./limits.mjs",
|
|
14
|
+
"./stream.mjs": "./stream.mjs",
|
|
15
|
+
"./tool-response.mjs": "./tool-response.mjs"
|
|
16
|
+
},
|
|
8
17
|
"files": [
|
|
9
18
|
"index.mjs",
|
|
10
19
|
"provider.mjs",
|
|
11
20
|
"core-tools.mjs",
|
|
12
21
|
"fingerprint.json",
|
|
13
22
|
"limits.mjs",
|
|
14
|
-
"limits-audit.mjs",
|
|
15
23
|
"models.mjs",
|
|
16
24
|
"stream.mjs",
|
|
17
25
|
"tool-response.mjs",
|
|
18
|
-
"tools-smoke.mjs",
|
|
19
|
-
"capture.mjs",
|
|
20
26
|
"README.md",
|
|
21
27
|
"MAINTAINING.md",
|
|
22
|
-
"LICENSE"
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"NOTICE"
|
|
23
30
|
],
|
|
24
31
|
"keywords": ["opencode", "opencode-plugin", "zen", "free", "magpie"],
|
|
25
32
|
"license": "MIT",
|
|
@@ -32,5 +39,6 @@
|
|
|
32
39
|
"limits:audit": "bun limits-audit.mjs",
|
|
33
40
|
"capture": "bun capture.mjs"
|
|
34
41
|
},
|
|
42
|
+
"packageManager": "bun@1.4.3",
|
|
35
43
|
"magpie": { "icon": "https://opencode.ai/favicon-v3.ico", "maxConcurrency": 2 }
|
|
36
44
|
}
|