opencode-zen-freex 0.2.1 → 0.2.3
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/MAINTAINING.md +245 -0
- package/README.md +47 -11
- package/capture.mjs +203 -0
- package/index.mjs +3 -3
- package/limits.mjs +123 -0
- package/models.mjs +213 -148
- package/package.json +5 -1
- package/provider.mjs +509 -461
- package/stream.mjs +398 -398
- package/tool-response.mjs +881 -881
- package/tools-smoke.mjs +169 -0
package/limits.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Zen's catalogue is where a model's limits are declared from, and for some
|
|
2
|
+
// free ids it under-reports them. A free id and its paid twin are the same
|
|
3
|
+
// upstream model — x-opencode-upstream-model-id answers
|
|
4
|
+
// "deepseek/deepseek-v4.1-flash" whichever of the two is asked for — and only
|
|
5
|
+
// the paid one is declared at what that model can actually do.
|
|
6
|
+
//
|
|
7
|
+
// A limit the host believes is acted on. The window decides when a
|
|
8
|
+
// conversation is compacted and whether a request is worth sending at all; the
|
|
9
|
+
// output cap is sent as max_tokens on every request. So an under-reported limit
|
|
10
|
+
// is not the conservative choice, it is a wrong one nobody can see: a window
|
|
11
|
+
// that is too small is never refused, it is just quietly never used, and an
|
|
12
|
+
// output cap that is too small cuts replies off with a finish reason that looks
|
|
13
|
+
// like the model's own decision.
|
|
14
|
+
//
|
|
15
|
+
// Only evidence moves a value here, and evidence is sorted:
|
|
16
|
+
//
|
|
17
|
+
// route the route's own refusal, in its own words
|
|
18
|
+
// route-twin what this route declares for the same upstream model
|
|
19
|
+
// vendor the model vendor's published page
|
|
20
|
+
// witnessed a generation somebody watched finish
|
|
21
|
+
//
|
|
22
|
+
// Being accepted is not evidence. Zen answers 200 to max_tokens 2000000 under a
|
|
23
|
+
// short prompt, so a value that is accepted says something about the request
|
|
24
|
+
// and nothing about what the model writes. A route or route-twin value may set
|
|
25
|
+
// any number; a vendor or witnessed one may only raise, because lowering a
|
|
26
|
+
// limit on weaker evidence invents a restriction nobody asked for.
|
|
27
|
+
//
|
|
28
|
+
// Every axis carries the catalogue value it was measured against. A catalogue
|
|
29
|
+
// that moves voids that axis rather than leaving a stale number behind, and a
|
|
30
|
+
// refusal that names a different window while the plugin is running corrects it
|
|
31
|
+
// for the rest of the process (see observeRouteStatement).
|
|
32
|
+
export const LIMITS = {
|
|
33
|
+
"deepseek-v4.1-flash-free": {
|
|
34
|
+
context: {
|
|
35
|
+
value: 1048576,
|
|
36
|
+
source: "route",
|
|
37
|
+
declaredAt: 200000,
|
|
38
|
+
measured: "2026-10-15",
|
|
39
|
+
samples: "1,048,507 tokens served; 1,048,707 and 1,060,000 refused 4/4",
|
|
40
|
+
how: "a prompt sized just over the window; Zen's own 400 answers \"This model's maximum context length is 1048576 tokens. However, you requested 1422278 tokens (1038278 in the messages, 384000 in the completion).\"",
|
|
41
|
+
},
|
|
42
|
+
output: {
|
|
43
|
+
value: 384000,
|
|
44
|
+
source: "route-twin",
|
|
45
|
+
declaredAt: 32000,
|
|
46
|
+
measured: "2026-10-15",
|
|
47
|
+
samples: "one reply of 306,880 completion tokens, finish=stop; nothing above it observed",
|
|
48
|
+
how: "deepseek-v4.1-flash, the paid id serving the same upstream model, is declared 384000 in this same catalogue; DeepSeek's own page says 384K, which is 393216 read as binary K",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// "This model's maximum context length is 1048576 tokens. However, you
|
|
54
|
+
// requested 1422278 tokens (1038278 in the messages, 384000 in the
|
|
55
|
+
// completion)." — the route's own words, and the only sentence seen on this
|
|
56
|
+
// route that states a window. A generic 400 says nothing attributable and is
|
|
57
|
+
// left alone; §7 of the measured notes has a context overflow and an invalid
|
|
58
|
+
// parameter answering with byte-identical 400s on another Zen model.
|
|
59
|
+
const NAMED_CONTEXT = /maximum context length is (\d+) tokens/i;
|
|
60
|
+
|
|
61
|
+
// Windows named by a refusal while this process is running, by model id. Not
|
|
62
|
+
// persisted: it is the same kind of fact as LIMITS, only newer, and a restart
|
|
63
|
+
// falls back to what was measured last.
|
|
64
|
+
const OBSERVED = new Map();
|
|
65
|
+
|
|
66
|
+
// The window a refusal names, or undefined when the body says nothing
|
|
67
|
+
// attributable. Exported for the tests and for anyone reading a capture.
|
|
68
|
+
export function namedContextWindow(text) {
|
|
69
|
+
const match = NAMED_CONTEXT.exec(typeof text === "string" ? text : "");
|
|
70
|
+
if (!match) return undefined;
|
|
71
|
+
const tokens = Number.parseInt(match[1], 10);
|
|
72
|
+
return Number.isSafeInteger(tokens) && tokens > 0 ? tokens : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Read a refusal for a window it names. Only non-2xx responses are read, and
|
|
76
|
+
// only the first `limit` characters of one: this is an error path on the
|
|
77
|
+
// request's way to a caller that is waiting, so it must never be able to fail a
|
|
78
|
+
// request, change a response, or buffer a body that is not small.
|
|
79
|
+
export async function observeRouteStatement(id, response, limit = 4096) {
|
|
80
|
+
try {
|
|
81
|
+
if (typeof id !== "string" || !id || response?.ok !== false) return undefined;
|
|
82
|
+
const text = await response
|
|
83
|
+
.clone()
|
|
84
|
+
.text()
|
|
85
|
+
.catch(() => "");
|
|
86
|
+
const tokens = namedContextWindow(text.slice(0, limit));
|
|
87
|
+
if (tokens !== undefined) OBSERVED.set(id, tokens);
|
|
88
|
+
return tokens;
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const ROUTE_TIERS = new Set(["route", "route-twin"]);
|
|
95
|
+
|
|
96
|
+
// What this plugin declares for a model instead of what the catalogue says, as
|
|
97
|
+
// a plain object to spread over the catalogue's own limit. Empty when there is
|
|
98
|
+
// no evidence, when the evidence was measured against a different catalogue, or
|
|
99
|
+
// when it would change nothing.
|
|
100
|
+
export function limitOverrides(id, declared = {}, table = LIMITS, observed = OBSERVED) {
|
|
101
|
+
const overrides = {};
|
|
102
|
+
const record = table?.[id];
|
|
103
|
+
const live = observed?.get(id);
|
|
104
|
+
for (const axis of ["context", "output"]) {
|
|
105
|
+
const declaredValue = declared?.[axis];
|
|
106
|
+
const entry = record?.[axis];
|
|
107
|
+
// A route statement made while this process was running is the same tier as
|
|
108
|
+
// the table and newer than it, so it is taken as it stands. Everything else
|
|
109
|
+
// has to have been measured against the catalogue in front of us now.
|
|
110
|
+
const value =
|
|
111
|
+
axis === "context" && live !== undefined
|
|
112
|
+
? live
|
|
113
|
+
: entry && entry.declaredAt === declaredValue
|
|
114
|
+
? entry.value
|
|
115
|
+
: undefined;
|
|
116
|
+
if (value === undefined || !Number.isSafeInteger(value) || value <= 0) continue;
|
|
117
|
+
if (value === declaredValue) continue;
|
|
118
|
+
if (entry && !ROUTE_TIERS.has(entry.source) && Number.isSafeInteger(declaredValue) && value < declaredValue)
|
|
119
|
+
continue;
|
|
120
|
+
overrides[axis] = value;
|
|
121
|
+
}
|
|
122
|
+
return overrides;
|
|
123
|
+
}
|
package/models.mjs
CHANGED
|
@@ -1,148 +1,213 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (npm?.includes("
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
model.
|
|
34
|
-
model.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
1
|
+
import { limitOverrides } from "./limits.mjs";
|
|
2
|
+
|
|
3
|
+
export const PROVIDER = "opencode-zen-free";
|
|
4
|
+
export const SDK = {
|
|
5
|
+
chat: "@ai-sdk/openai-compatible",
|
|
6
|
+
responses: "@ai-sdk/openai",
|
|
7
|
+
anthropic: "@ai-sdk/anthropic",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function parseProtocolDocs(text) {
|
|
11
|
+
const protocols = {};
|
|
12
|
+
const pattern =
|
|
13
|
+
/\|[^|]+\|\s*`?([^|`\s]+)`?\s*\|\s*`[^`]+\/v1\/(chat\/completions|responses|messages|systemone)`/g;
|
|
14
|
+
for (const match of text.matchAll(pattern)) {
|
|
15
|
+
protocols[match[1]] = {
|
|
16
|
+
"chat/completions": "chat",
|
|
17
|
+
responses: "responses",
|
|
18
|
+
messages: "anthropic",
|
|
19
|
+
systemone: "systemone",
|
|
20
|
+
}[match[2]];
|
|
21
|
+
}
|
|
22
|
+
return protocols;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sdkProtocol(npm) {
|
|
26
|
+
if (npm?.includes("anthropic")) return "anthropic";
|
|
27
|
+
if (npm === SDK.responses) return "responses";
|
|
28
|
+
if (npm?.includes("openai-compatible")) return "chat";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isRetired(model) {
|
|
32
|
+
return (
|
|
33
|
+
model.deprecated === true ||
|
|
34
|
+
["deprecated", "retired", "disabled"].includes(model.status ?? model.lifecycle) ||
|
|
35
|
+
model.deprecated_at != null ||
|
|
36
|
+
model.retirement_date != null
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isFree(id, model = {}) {
|
|
41
|
+
return (
|
|
42
|
+
!isRetired(model) && (/free/i.test(id) || (model.cost?.input === 0 && model.cost?.output === 0))
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildModels(ids, provider, protocols, baseURL) {
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const id of ids) {
|
|
49
|
+
if (typeof id !== "string" || !id || id === "__proto__") continue;
|
|
50
|
+
const raw = provider.models?.[id];
|
|
51
|
+
const model = raw ?? {};
|
|
52
|
+
const protocol = protocols[id] ?? (raw && sdkProtocol(model.provider?.npm ?? provider.npm));
|
|
53
|
+
if (!isFree(id, model) || !Object.hasOwn(SDK, protocol)) continue;
|
|
54
|
+
const input = model.modalities?.input ?? ["text"];
|
|
55
|
+
const output = model.modalities?.output ?? ["text"];
|
|
56
|
+
const modalities = (values) =>
|
|
57
|
+
Object.fromEntries(
|
|
58
|
+
["text", "image", "audio", "video", "pdf"].map((name) => [name, values.includes(name)]),
|
|
59
|
+
);
|
|
60
|
+
const efforts = (model.reasoning_options ?? [])
|
|
61
|
+
.filter((option) => option.type === "effort")
|
|
62
|
+
.flatMap((option) => option.values ?? []);
|
|
63
|
+
result[id] = {
|
|
64
|
+
id,
|
|
65
|
+
providerID: PROVIDER,
|
|
66
|
+
name: model.name ?? id,
|
|
67
|
+
api: { id, url: baseURL, npm: SDK[protocol] },
|
|
68
|
+
limit: { context: 32768, output: 4096, ...model.limit, ...limitOverrides(id, model.limit) },
|
|
69
|
+
capabilities: {
|
|
70
|
+
temperature: model.temperature ?? true,
|
|
71
|
+
reasoning: model.reasoning ?? false,
|
|
72
|
+
attachment: model.attachment ?? input.some((name) => name !== "text"),
|
|
73
|
+
toolcall: model.tool_call ?? true,
|
|
74
|
+
input: modalities(input),
|
|
75
|
+
output: modalities(output),
|
|
76
|
+
interleaved: model.interleaved ?? false,
|
|
77
|
+
},
|
|
78
|
+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
79
|
+
variants: Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }])),
|
|
80
|
+
options: {},
|
|
81
|
+
headers: {},
|
|
82
|
+
status: model.status ?? "active",
|
|
83
|
+
release_date: model.release_date ?? "",
|
|
84
|
+
free: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// What each free model is asked at, from OpenCode's console docs
|
|
91
|
+
// (services/www/src/docs/content/console/models.mdx) on 2026-10-15: used for
|
|
92
|
+
// a listed model when the docs can't be read, so the catalog or the docs
|
|
93
|
+
// being out of reach never leaves a listed free model out. Jev is a
|
|
94
|
+
// SystemOne decision model, not one to chat with.
|
|
95
|
+
export const bootstrapProtocols = {
|
|
96
|
+
"big-pickle": "chat",
|
|
97
|
+
"space-bunny-free": "chat",
|
|
98
|
+
"cat-jam-free": "chat",
|
|
99
|
+
"longcat-2.5-preview-free": "chat",
|
|
100
|
+
"step-5-preview-free": "chat",
|
|
101
|
+
"mimo-v2.6-flash-free": "chat",
|
|
102
|
+
"nemotron-3.5-lightning-free": "chat",
|
|
103
|
+
"muse-spark-1.3-contributor-free": "responses",
|
|
104
|
+
"jev-1.13-free": "systemone",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Offline bootstrap metadata from the OpenCode catalog, 2026-10-06; the
|
|
108
|
+
// free models listed since added on 2026-10-15.
|
|
109
|
+
export const bootstrapProvider = {
|
|
110
|
+
npm: SDK.chat,
|
|
111
|
+
models: {
|
|
112
|
+
"cat-jam-free": {
|
|
113
|
+
name: "Cat Jam Free",
|
|
114
|
+
cost: { input: 0, output: 0 },
|
|
115
|
+
limit: { context: 262144, output: 131072 },
|
|
116
|
+
reasoning: true,
|
|
117
|
+
tool_call: true,
|
|
118
|
+
interleaved: { field: "reasoning_content" },
|
|
119
|
+
reasoning_options: [{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }],
|
|
120
|
+
},
|
|
121
|
+
"step-5-preview-free": {
|
|
122
|
+
name: "Step 5 Preview Free",
|
|
123
|
+
cost: { input: 0, output: 0 },
|
|
124
|
+
limit: { context: 1000000, input: 1000000, output: 65536 },
|
|
125
|
+
reasoning: true,
|
|
126
|
+
tool_call: true,
|
|
127
|
+
interleaved: { field: "reasoning_content" },
|
|
128
|
+
modalities: { input: ["text", "image", "video"], output: ["text"] },
|
|
129
|
+
reasoning_options: [{ type: "effort", values: ["low", "medium", "high"] }],
|
|
130
|
+
},
|
|
131
|
+
"longcat-2.5-preview-free": {
|
|
132
|
+
name: "LongCat 2.5 Preview Free",
|
|
133
|
+
cost: { input: 0, output: 0 },
|
|
134
|
+
limit: { context: 1000000, output: 131072 },
|
|
135
|
+
reasoning: true,
|
|
136
|
+
tool_call: true,
|
|
137
|
+
interleaved: { field: "reasoning_content" },
|
|
138
|
+
modalities: { input: ["text", "image"], output: ["text"] },
|
|
139
|
+
},
|
|
140
|
+
"mimo-v2.6-flash-free": {
|
|
141
|
+
name: "MiMo-V2.6-Flash Free",
|
|
142
|
+
cost: { input: 0, output: 0 },
|
|
143
|
+
limit: { context: 200000, output: 32000 },
|
|
144
|
+
reasoning: true,
|
|
145
|
+
tool_call: true,
|
|
146
|
+
interleaved: { field: "reasoning_content" },
|
|
147
|
+
modalities: { input: ["text", "image", "audio", "video"], output: ["text"] },
|
|
148
|
+
},
|
|
149
|
+
"nemotron-3.5-lightning-free": {
|
|
150
|
+
name: "Nemotron 3.5 Lightning Free",
|
|
151
|
+
cost: { input: 0, output: 0 },
|
|
152
|
+
limit: { context: 262144, output: 262144 },
|
|
153
|
+
reasoning: true,
|
|
154
|
+
tool_call: true,
|
|
155
|
+
interleaved: { field: "reasoning_content" },
|
|
156
|
+
},
|
|
157
|
+
"big-pickle": {
|
|
158
|
+
name: "Big Pickle",
|
|
159
|
+
cost: { input: 0, output: 0 },
|
|
160
|
+
limit: { context: 200000, input: 160000, output: 32000 },
|
|
161
|
+
reasoning: true,
|
|
162
|
+
tool_call: true,
|
|
163
|
+
interleaved: { field: "reasoning_content" },
|
|
164
|
+
},
|
|
165
|
+
"space-bunny-free": {
|
|
166
|
+
name: "Space Bunny Free",
|
|
167
|
+
cost: { input: 0, output: 0 },
|
|
168
|
+
limit: { context: 1048576, input: 524288, output: 524288 },
|
|
169
|
+
reasoning: true,
|
|
170
|
+
tool_call: true,
|
|
171
|
+
},
|
|
172
|
+
"muse-spark-1.3-contributor-free": {
|
|
173
|
+
name: "Muse Spark 1.3 Free",
|
|
174
|
+
cost: { input: 0, output: 0 },
|
|
175
|
+
provider: { npm: SDK.responses },
|
|
176
|
+
limit: { context: 1048576, output: 131072 },
|
|
177
|
+
reasoning: true,
|
|
178
|
+
tool_call: true,
|
|
179
|
+
modalities: { input: ["text", "image", "video", "pdf", "audio"], output: ["text"] },
|
|
180
|
+
reasoning_options: [
|
|
181
|
+
{ type: "effort", values: ["minimal", "low", "medium", "high", "xhigh"] },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export function configModels(models) {
|
|
188
|
+
return Object.fromEntries(
|
|
189
|
+
Object.entries(models).map(([id, model]) => [
|
|
190
|
+
id,
|
|
191
|
+
{
|
|
192
|
+
id,
|
|
193
|
+
name: model.name,
|
|
194
|
+
provider: { npm: model.api.npm, api: model.api.url },
|
|
195
|
+
limit: model.limit,
|
|
196
|
+
reasoning: model.capabilities.reasoning,
|
|
197
|
+
tool_call: model.capabilities.toolcall,
|
|
198
|
+
attachment: model.capabilities.attachment,
|
|
199
|
+
modalities: {
|
|
200
|
+
input: Object.keys(model.capabilities.input).filter(
|
|
201
|
+
(key) => model.capabilities.input[key],
|
|
202
|
+
),
|
|
203
|
+
output: Object.keys(model.capabilities.output).filter(
|
|
204
|
+
(key) => model.capabilities.output[key],
|
|
205
|
+
),
|
|
206
|
+
},
|
|
207
|
+
cost: { input: 0, output: 0 },
|
|
208
|
+
variants: model.variants,
|
|
209
|
+
free: true,
|
|
210
|
+
},
|
|
211
|
+
]),
|
|
212
|
+
);
|
|
213
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-zen-freex",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
"provider.mjs",
|
|
10
10
|
"core-tools.mjs",
|
|
11
11
|
"fingerprint.json",
|
|
12
|
+
"limits.mjs",
|
|
12
13
|
"models.mjs",
|
|
13
14
|
"stream.mjs",
|
|
14
15
|
"tool-response.mjs",
|
|
16
|
+
"tools-smoke.mjs",
|
|
17
|
+
"capture.mjs",
|
|
15
18
|
"README.md",
|
|
19
|
+
"MAINTAINING.md",
|
|
16
20
|
"LICENSE"
|
|
17
21
|
],
|
|
18
22
|
"keywords": ["opencode", "opencode-plugin", "zen", "free", "magpie"],
|