@magpie-community/opencode-zen-free-auth 0.1.14 → 0.1.16

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.
Files changed (3) hide show
  1. package/README.md +19 -2
  2. package/package.json +1 -1
  3. package/provider.mjs +71 -13
package/README.md CHANGED
@@ -6,8 +6,15 @@ OpenCode Zen's free models in magpie and OpenCode. Provider id:
6
6
  Since 2026-10-13 Zen's free tier answers 403 FreeTierError ("can only be
7
7
  used from within OpenCode") to a request that isn't OpenCode's. From
8
8
  0.1.6 the plugin sends a free-model request with exactly the headers
9
- OpenCode 1.19.0 sends to Zen, and a session id of OpenCode's own form.
10
- Older versions (0.1.1 on npm before it) get the 403: update the plugin.
9
+ OpenCode sends to Zen, and a session id of OpenCode's own form. Older
10
+ versions (0.1.1 on npm before it) get the 403: update the plugin.
11
+
12
+ Since 2026-10-26 Zen's free tier answers `429 FreeUsageLimitError`
13
+ ("Free models work in OpenCode") to the User-Agent OpenCode 1.19.0 sent,
14
+ which 0.1.6 to 0.1.15 sent on every request. From 0.1.16 the plugin
15
+ sends OpenCode 2.0.25's headers (`opencode/latest/2.0.25/cli`, its
16
+ session id under `x-session-affinity` and `x-session-id` too): update
17
+ the plugin.
11
18
 
12
19
  ## Install
13
20
 
@@ -74,6 +81,16 @@ itself (`bash`, `edit`, `glob`, `grep`, `read`), described as not
74
81
  available. A call of one of them the agent doesn't have reaches the agent
75
82
  like any other.
76
83
 
84
+ Zen's free tier also tells OpenCode's own requests by the environment
85
+ block every OpenCode prompt ends with (`<env>`, a `Working directory:`
86
+ line, `</env>`) in a system message. Another agent's request without one
87
+ gets a small allowance a day and then `429 FreeUsageLimitError` ("Free
88
+ models work in OpenCode") until 00:00 UTC, while OpenCode's go through.
89
+ So a request whose system messages have no such block gets one added
90
+ after the agent's own prompt (Anthropic `system` and Responses
91
+ `instructions` likewise), its working directory said as unknown; a
92
+ prompt that has one, as OpenCode's does, goes as it is.
93
+
77
94
  Now and then Zen closes a streamed Chat Completions reply before its
78
95
  `[DONE]`. When the reply has said something and no tool call of it is
79
96
  unfinished, it ends there as an ordinary answer, and its text says it was
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-zen-free-auth",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "OpenCode Zen free models through the public credential, with native streaming protocols",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
package/provider.mjs CHANGED
@@ -33,13 +33,17 @@ const CORE_PARAMETER = {
33
33
  grep: "pattern",
34
34
  read: "filePath",
35
35
  };
36
- const OPENCODE_VERSION = "1.19.0";
37
- // What OpenCode 1.19.0 sends as its User-Agent, ai-sdk and Bun included.
38
- // Each protocol's ai-sdk package bundles its own provider-utils, so the
39
- // version differs by protocol (captured from the release binary's requests).
40
- const PROVIDER_UTILS = { chat: "4.0.23", responses: "4.0.40", anthropic: "4.0.46" };
41
- export function userAgent(protocol) {
42
- return `opencode/${OPENCODE_VERSION} ai-sdk/provider-utils/${PROVIDER_UTILS[protocol]} runtime/bun/1.3.14`;
36
+ // What OpenCode 2.0.25 sends as its User-Agent on every protocol:
37
+ // `opencode/${channel}/${version}/${name}` (its App service, read from the
38
+ // release binary and captured from its requests on 2026-10-25). Since
39
+ // 2026-10-26 Zen's free tier answers 429 FreeUsageLimitError ("Free models
40
+ // work in OpenCode") to the exact User-Agent 0.1.6 to 0.1.15 sent, OpenCode
41
+ // 1.19.0's `opencode/1.19.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14`,
42
+ // whatever the body says; the same request under any other string, OpenCode
43
+ // 2.0.25's included, is served.
44
+ const OPENCODE_VERSION = "2.0.25";
45
+ export function userAgent() {
46
+ return `opencode/latest/${OPENCODE_VERSION}/cli`;
43
47
  }
44
48
  // How long a request for a model the last list didn't have waits before it
45
49
  // asks Zen's list again: a model Zen made free since is served at once,
@@ -110,6 +114,59 @@ function normalizeTools(body, protocol) {
110
114
  return { declared };
111
115
  }
112
116
 
117
+ // Zen's free tier serves a request as OpenCode's own only when one of its
118
+ // system messages carries OpenCode's environment block: `<env>`, a
119
+ // `Working directory:` line and `</env>`, as every OpenCode prompt ends.
120
+ // A request without one (another agent's own prompt: Kimi Work's,
121
+ // WorkBuddy's) gets a small allowance a day, then 429 FreeUsageLimitError
122
+ // ("Free models work in OpenCode") until 00:00 UTC, whatever its headers
123
+ // say; the same request with the block added is served (tried against
124
+ // opencode.ai on 2026-10-25, deepseek-v4.1-flash-free and
125
+ // glm-5.3-flash-free). In a user message or a tool description it doesn't
126
+ // count, and `<ENV>` or `working directory:` don't either. The directory
127
+ // isn't known here, and the agent's own prompt says it, so it is told as
128
+ // unknown rather than as a path that would mislead the model.
129
+ const ENVIRONMENT = "<env>\n Working directory: unknown\n</env>";
130
+ const HAS_ENVIRONMENT = /<env>[\s\S]*Working directory:[\s\S]*<\/env>/;
131
+
132
+ function textOf(content) {
133
+ if (typeof content === "string") return content;
134
+ if (!Array.isArray(content)) return "";
135
+ return content.map((part) => (typeof part?.text === "string" ? part.text : "")).join("\n");
136
+ }
137
+
138
+ function withEnvironment(content) {
139
+ if (typeof content === "string") return content ? `${content}\n\n${ENVIRONMENT}` : ENVIRONMENT;
140
+ if (Array.isArray(content)) return [...content, { type: "text", text: ENVIRONMENT }];
141
+ return ENVIRONMENT;
142
+ }
143
+
144
+ const SYSTEM_ROLES = ["system", "developer"];
145
+
146
+ export function addEnvironment(body, protocol) {
147
+ if (protocol === "anthropic") {
148
+ if (!HAS_ENVIRONMENT.test(textOf(body.system))) body.system = withEnvironment(body.system);
149
+ return;
150
+ }
151
+ const messages = protocol === "chat" ? body.messages : body.input;
152
+ const system = Array.isArray(messages)
153
+ ? messages.filter((message) => SYSTEM_ROLES.includes(message?.role))
154
+ : [];
155
+ if (system.some((message) => HAS_ENVIRONMENT.test(textOf(message.content)))) return;
156
+ if (protocol === "responses") {
157
+ if (!HAS_ENVIRONMENT.test(textOf(body.instructions)))
158
+ body.instructions = withEnvironment(body.instructions);
159
+ return;
160
+ }
161
+ // Added to the agent's own system message, so the prompt stays one
162
+ // system message, the same on every turn (its cache holds); one is put
163
+ // first when the agent sent none.
164
+ const first = system.find((message) => message.role === "system");
165
+ if (first) first.content = withEnvironment(first.content);
166
+ else if (Array.isArray(messages)) messages.unshift({ role: "system", content: ENVIRONMENT });
167
+ else body.messages = [{ role: "system", content: ENVIRONMENT }];
168
+ }
169
+
113
170
  const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
114
171
  let lastStamp = 0;
115
172
  let counter = 0;
@@ -319,26 +376,27 @@ async function server(_input, options = {}) {
319
376
  const streaming = body.stream === true;
320
377
  body.stream = true;
321
378
  const toolPolicy = normalizeTools(body, protocol);
379
+ addEnvironment(body, protocol);
322
380
  if (protocol === "chat")
323
381
  body.stream_options = { ...body.stream_options, include_usage: true };
324
- // Only what OpenCode 1.19.0 itself sends to its gateway, so a
325
- // caller's own headers (an agent's User-Agent, x-stainless-*,
326
- // x-session-affinity, ...) never mark the request as not
327
- // OpenCode's (packages/opencode/src/session/llm/request.ts).
382
+ // Only what OpenCode 2.0.25 itself sends to its gateway, so a
383
+ // caller's own headers (an agent's User-Agent, x-stainless-*, ...)
384
+ // never mark the request as not OpenCode's.
328
385
  const given = request.headers;
329
386
  const session = sessionID(given, body);
330
387
  const project = _input?.project;
331
388
  const headers = new Headers({
332
389
  "content-type": "application/json",
333
390
  accept: "*/*",
334
- "user-agent": userAgent(protocol),
391
+ "user-agent": userAgent(),
335
392
  "x-opencode-project":
336
393
  given.get("x-opencode-project") ||
337
394
  (project?.vcs === "git" && project.id ? project.id : "global"),
338
395
  "x-opencode-session": session,
339
- "x-opencode-request": opencodeID("msg", false),
340
396
  "x-opencode-client": "cli",
341
397
  "x-opencode-session-id": session,
398
+ "x-session-affinity": session,
399
+ "x-session-id": session,
342
400
  });
343
401
  if (protocol === "anthropic") {
344
402
  headers.set("x-api-key", "public");