opencode-zen-freex 0.2.0 → 0.2.2

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 ADDED
@@ -0,0 +1,171 @@
1
+ # Maintaining this plugin
2
+
3
+ The plugin is one file of logic and one piece of data, and the data is the part
4
+ that goes out of date. This is how to look after it.
5
+
6
+ ## What is actually fragile
7
+
8
+ `fingerprint.json` holds a capture of a real OpenCode: the wording of the tools
9
+ its free tier knows by name, and the User-Agent that identifies it. Zen accepts a
10
+ request only when the whole of it matches one generation of OpenCode, so when
11
+ OpenCode changes any of that, the copy here stops being accepted.
12
+
13
+ Nothing about a stale copy looks different from a working one. The symptom is a
14
+ `403 FreeTierError` on every request, which reads as "you are not OpenCode" and
15
+ sends people off to change headers.
16
+
17
+ ## The loop
18
+
19
+ ```sh
20
+ bun test # the unit tests; these never touch the network
21
+ bun run check:gate # asks Zen which generations are still accepted
22
+ bun run capture -- --dry # takes a fresh capture and reports the difference
23
+ bun run capture # writes it
24
+ bun run check:gate # did that fix it
25
+ ```
26
+
27
+ `check:gate` is the one that matters. It asks each generation twice, because the
28
+ plugin sends two shapes and Zen does not answer them alike:
29
+
30
+ | shape | tools sent | what it is |
31
+ | --- | --- | --- |
32
+ | `floor` | the names in `required` | what the plugin stands in for a host that is not OpenCode and has none of these tools by name |
33
+ | `full` | every tool in the profile | what a host that is OpenCode itself sends |
34
+
35
+ It reports each request as:
36
+
37
+ | verdict | meaning |
38
+ | --- | --- |
39
+ | `answered` | Zen served it; this shape is current |
40
+ | `refused` | Zen did not recognise it (403) |
41
+ | `superseded` | Zen wants a newer OpenCode than the profile claims (426) |
42
+ | `unreachable` | Zen could not be asked at all |
43
+
44
+ The `floor` decides the exit code, because it is the shape this plugin exists to
45
+ send. The `full` is reported for what it says about a wording that has gone
46
+ stale. Asking only the full shape left a real gap: `read` alone is `answered` and
47
+ everything but `read` is `refused`, so a Zen that began insisting on one more
48
+ name would keep `full` passing while the shape actually sent was refused.
49
+
50
+ `capture` needs `opencode` on PATH. It points a real OpenCode at a server running
51
+ locally and reads the request it makes, so nothing is sent to Zen and no key is
52
+ involved.
53
+
54
+ ## Reading a failure
55
+
56
+ **Every `floor` `refused`.** OpenCode reworded its tools, or moved the endpoint.
57
+ Run `bun run capture`, then `check:gate` again. If the endpoint moved,
58
+ `DEFAULT_BASE` in `provider.mjs` is what has to change.
59
+
60
+ **One `floor` `refused`, the other `answered`.** OpenCode has released and this
61
+ generation is retired. Capture from a current OpenCode: it will be detected as
62
+ the other generation, so what is captured goes into that profile.
63
+
64
+ **A `floor` `refused` while its `full` is `answered`.** Zen has begun insisting
65
+ on a tool this plugin does not stand in, which the gate names in a line of its
66
+ own. What gets stood in is `required` in `fingerprint.json`, and the wording it
67
+ is stood in with is that profile's `tools`, so the name has to reach both, from a
68
+ capture taken of an OpenCode that has it. A host that brings its own tool by that
69
+ name is served either way, which is why only the `floor` shows this.
70
+
71
+ **`superseded` (426).** Zen raised the minimum version this profile claims. Raise
72
+ `agent.opencode` in `fingerprint.json` above what it asks for. 426 is Zen saying
73
+ "your client is too old", which is a different problem from 403 and is not fixed
74
+ by changing tools.
75
+
76
+ **`unreachable`.** Not a fingerprint problem. Check the network, and whether
77
+ repeated refusals from this address have put it in a cooldown — they can, and
78
+ the cooldown answers like a refusal.
79
+
80
+ ## Things that are easy to break
81
+
82
+ **Adding a header.** The set sent has one owner: `outboundHeaders` in
83
+ `provider.mjs`, which the plugin builds its request with and which the gate asks
84
+ Zen with, so neither can drift from the other on its own. `index.test.mjs` pins
85
+ the names on the wire. `x-opencode-request`, `x-session-affinity` and
86
+ `x-session-id` are all in OpenCode's own header set upstream and none is sent;
87
+ measured 2026-10-15, each of the three answers on its own while
88
+ x-opencode-request with x-session-affinity is refused. A header cannot be judged
89
+ by adding it on its own, so add it in `outboundHeaders` and let the pinned test
90
+ and a live `check:gate` say what it did.
91
+
92
+ **Rewriting the tool wording.** The wording sent upstream is OpenCode's, because
93
+ Zen reads it. What replaces it is only the wording: the host's tool and its
94
+ parameters are kept, so the call the model makes still fits the tool that runs
95
+ it. Rewriting the parameters as well looks tidier and breaks the agent.
96
+
97
+ **Replacing the wording with something of your own.** That is what 0.1.5 and
98
+ earlier did, and it is a guaranteed refusal.
99
+
100
+ **Trusting one observation.** The gate is not steady: it moved more than once in
101
+ a day while this was being worked out, and identical requests were answered both
102
+ ways minutes apart. Anything measured once is not a fact. Measure it three times,
103
+ interleaved, before believing it. When two sources disagree, prefer the shape
104
+ that passes under the stricter of them, since the looser one will not last.
105
+
106
+ ## Layout
107
+
108
+ | file | what it is |
109
+ | --- | --- |
110
+ | `fingerprint.json` | the capture. The only thing that goes out of date |
111
+ | `core-tools.mjs` | reads the capture; says which generation a request is |
112
+ | `provider.mjs` | the plugin: discovery, headers, tools, the diagnostic |
113
+ | `models.mjs` | which models are free, and what protocol each speaks |
114
+ | `stream.mjs`, `tool-response.mjs` | replies Zen cuts short, and tool calls |
115
+ | `capture.mjs` | takes a new capture |
116
+ | `tools-smoke.mjs` | asks Zen what it accepts (`check:gate`) |
117
+
118
+ ## Publishing
119
+
120
+ The package is `opencode-zen-freex`; the provider it registers is
121
+ `opencode-zen-free`, which is the same one magpie-community/plugins' version
122
+ registers. That is deliberate: a model is written `opencode-zen-free/<model>`
123
+ either way, so one stays in place whichever is installed. It does mean the two
124
+ are mutually exclusive — installing both leaves two plugins claiming one
125
+ provider id, and which one wins is down to load order. Pick one.
126
+
127
+ To publish: bump `version`, `npm publish`, and tag. `magpie plugin add
128
+ opencode-zen-freex` installs it by that name.
129
+
130
+ Publishing is the one step here that cannot be run unattended, because it is
131
+ the one step npm asks a person about. Run it as yourself.
132
+
133
+ **With 2FA on the account.** `npm publish` fails with `EOTP` and prints a
134
+ `https://www.npmjs.com/auth/cli/...` address, redacted in the error and
135
+ different every time. Open it, finish, and the same command carries on. There
136
+ is nothing to pass to a script: the address is tied to the browser session that
137
+ prints it.
138
+
139
+ **With a granular token.** A token with publish permission authorizes the
140
+ publish on its own and never asks for the second factor. Write it to an npmrc
141
+ outside the repository and point npm at it for that one command, so it does
142
+ not land in a tracked file:
143
+
144
+ ```sh
145
+ printf '//registry.npmjs.org/:_authToken=npm_...\n' > "$TMP/npmrc"
146
+ npm publish --userconfig "$TMP/npmrc"; rm -f "$TMP/npmrc"
147
+ ```
148
+
149
+ Whatever carries a token — a shell, a chat, a CI log under
150
+ `%LOCALAPPDATA%\npm-cache\_logs` — is now a place the token has been, and
151
+ npm's debug log records the whole command line. Rotate it once the publish is
152
+ done. It is not needed to install the package afterwards.
153
+
154
+ Two things that read as failures and are not. A version already published
155
+ cannot be published again, so a republished README means a new `version`, not
156
+ a retry — `README.md` is in `files`, so it ships and npmjs.com renders it.
157
+ And for a minute or two after a successful publish, `npm view` still 404s
158
+ while the registry replicates; wait rather than publishing under a second
159
+ number.
160
+
161
+ One trap worth knowing: a token that is missing, truncated or malformed is
162
+ reported by npm as
163
+
164
+ ```
165
+ 404 The requested resource 'opencode-zen-freex@0.2.1' could not be found or
166
+ you do not have permission to access it.
167
+ ```
168
+
169
+ which reads exactly like the package not existing. It is the same status npm
170
+ gives a genuine permission failure, so a 404 from a publish says nothing at
171
+ all until the token has been checked.
package/README.md CHANGED
@@ -1,125 +1,139 @@
1
- # OpenCode Zen Free
2
-
3
- OpenCode Zen's free models for a host that is not OpenCode. Provider id:
4
- `opencode-zen-free`.
5
-
6
- Zen's free tier answers 403 FreeTierError ("can only be used from within
7
- OpenCode") to a request that isn't OpenCode's, and it reads the request body
8
- as well as its headers: the tools it knows by name have to be described the
9
- way OpenCode describes them. This plugin asks the endpoint that serves the
10
- free tier, and presents the tools the way OpenCode presents them while
11
- keeping the host's own tools, so its calls still run.
12
-
13
- This is a maintained fork of the plugin from `magpie-community/plugins`,
14
- which is MIT licensed. It is forked because that fingerprint moves faster
15
- than that repository follows it: its last release is still built for an
16
- endpoint that no longer serves the free tier. See [MAINTAINING.md](MAINTAINING.md)
17
- for how to keep it working, and `bun run check:gate` for whether it is
18
- working now.
19
-
20
- ## Install
21
-
22
- ```sh
23
- magpie plugin add opencode-zen-freex
24
- magpie plugin login opencode-zen-free
25
- ```
26
-
27
- ## Sign in
28
-
29
- Enter `public` in the key field. No personal API key is needed.
30
- The credential is kept in OpenCode's `auth.json` or magpie's
31
- `plugin-auth.json`.
32
-
33
- ## Models
34
-
35
- The plugin discovers Zen's current free models, including their context
36
- windows and reasoning levels. Use `opencode-zen-free/<model>`, for example
37
- `opencode-zen-free/big-pickle`.
38
-
39
- Retired models and SystemOne models are excluded. The list is asked
40
- again each time the host lists models (magpie does this on start and every
41
- hour): a model Zen no longer gives free leaves it, and a new free one joins
42
- it. When Zen's list can't be read, the last list is kept. A request for a
43
- model that isn't free now gets a 404 naming the free ones; one made free
44
- since the last list is served after the list is asked again (at most once a
45
- minute). Availability and rate limits are set by Zen; the plugin has no
46
- remaining-quota data.
47
-
48
- ## When the free tier stops answering
49
-
50
- A stale fingerprint does not look different from a working one until
51
- somebody is refused. Ask Zen which generations the plugin still speaks:
52
-
53
- ```sh
54
- bun run check:gate
55
- ```
56
-
57
- Each profile is reported as `answered`, `refused` (403), `superseded`
58
- (426 — Zen wants a newer OpenCode than the profile claims) or
59
- `unreachable`. Run it before a release and whenever a 403 is reported.
60
-
61
- If every profile is refused, capture the wording again from a current
62
- OpenCode: point a release at a local server and read the tools out of the
63
- request it makes. A refusal from the plugin says which wording went out
64
- and where to look, rather than leaving it to be guessed at.
65
-
66
- Note that the gate is not steady: it moved more than once in a day while
67
- this was being worked out, and repeated refusals from one address can
68
- themselves be answered for a while. Read a verdict here as "at this
69
- moment", and run it again before believing a change.
70
-
71
- ## Requests
72
-
73
- Requests go to `https://opencode.ai/inference/v1` using each model's
74
- native Chat Completions, Responses or Anthropic Messages API. Streaming and
75
- ordinary JSON replies are supported.
76
-
77
- The agent's tools are preserved. Zen's models often call OpenCode's own
78
- tool names; such a call is given the agent's tool whose name differs only
79
- in case (`bash` to `Bash`), or else only in case and `_`/`-` (`todowrite`
80
- to `todo_write`, `webfetch` to `web_fetch`), when exactly one does. A
81
- call with no such tool (or with no name) is left out of the reply, and
82
- the reply's text says which name was left out; a turn whose every call
83
- was left out ends as an ordinary answer. Tools run in the agent.
84
-
85
- Zen's free tier reads the body as well as the headers, and answers a
86
- request only when the whole of it belongs to one generation of OpenCode:
87
- the User-Agent, the headers and the tools have to agree. Two generations
88
- are current, and the tools a request carries say which: `shell` and `read`
89
- are 2.x, `bash`, `glob`, `grep` and `read` are 1.x. A host that is OpenCode
90
- itself says which it is in its own User-Agent, and that is passed on rather
91
- than pinned, so it is never behind. Where nothing says, the tools decide
92
- and the User-Agent follows them.
93
-
94
- Within a generation, the tools Zen knows by name — `bash`, `glob`, `grep`,
95
- `read`, `write`, `edit` — have to be there and described the way OpenCode
96
- describes them. The first four are added when the host lacks them; the
97
- other two are only read when the host has them. A tool is never
98
- discarded: the host's own tool is kept whole, and only the wording sent
99
- upstream is replaced, so the call the model makes still fits the tool that
100
- runs it. A tool of any other name is not looked at.
101
-
102
- A tool that was *added* has nothing behind it: the agent has no such tool
103
- and a call to it is dropped. Describing it as OpenCode describes it is what
104
- Zen asks for, and it also makes the model want to call it, so a note saying
105
- so is placed after the opening Zen reads and before the closing it does
106
- not. That note is phrased about the one tool on purpose: a blanket "these
107
- tools are not available" was measured to stop the model using the host's
108
- own tools as well.
109
-
110
- The headers sent are exactly those of one generation and no more, and that
111
- set is a deliberate subset of everything Zen has been seen to accept.
112
- `x-opencode-request` was measured refused on 2026-10-14 and has never been
113
- measured to be needed, so it is not sent. `x-session-affinity` and
114
- `x-session-id` are in OpenCode's own header set upstream and have been
115
- measured refused, so they are not sent either. A test pins the set by name,
116
- because adding a header is the easiest way to break this plugin.
117
-
118
- Now and then Zen closes a streamed Chat Completions reply before its
119
- `[DONE]`. When the reply has said something and no tool call of it is
120
- unfinished, it ends there as an ordinary answer, and its text says it was
121
- cut short; asking again goes on. A reply cut short with a tool call
122
- unfinished, with nothing said, or collapsed for a client that doesn't
123
- stream fails with an error naming the reply's id and how much of it came.
124
- An Anthropic Messages or Responses reply cut short fails too: magpie
125
- tells it from a whole one and, where the model can, goes on with it.
1
+ # OpenCode Zen FreeX
2
+
3
+ [magpie](https://github.com/yetone/magpie)的拓展插件,为其提供OpenCode Zen的免费模型
4
+
5
+ ## 为什么有这个分支
6
+
7
+ 上游 [zen-free](https://github.com/magpie-community/plugins/tree/main/packages/zen-free) 是这个插件的前身。它一直在修,修得也不慢,但「免费模型还能不能用」这件事在上游那里翻过一次,而往哪边翻不掌握在使用它的人手里:
8
+
9
+ | UTC | 提交 | 那一下说了什么 |
10
+ | --- | --- | --- |
11
+ | 2026-10-13 23:53 | [`643ef50`](https://github.com/magpie-community/plugins/commit/643ef5018e63be78c4aa7554696831d4220ac9c9) | README 加上说明:免费层「只在 OpenCode 本体里能用」,`this plugin doesn't work around that`,请改用 Zen 付费 key 或 OpenCode Go;提交说明里写着 `No version bump … The code on main is left as it is` |
12
+ | 2026-10-14 10:57 | [`a810ee7`](https://github.com/magpie-community/plugins/commit/a810ee7e882012ab106e2deb742176cd43db781a) | 0.1.6 把那段说明**删掉**,改为发 OpenCode 1.19.0 原样的请求头——距加上去 11 小时 |
13
+ | 2026-10-14 17:59 | [`5fc0a75`](https://github.com/magpie-community/plugins/commit/5fc0a75292f0e5c0c10cf65a188bff5545d6b8e9) | 0.1.7:`free models answer again` |
14
+
15
+ 本分支切于 2026-10-15 00:14 UTC,就在那段声明被删掉之后、从 0.1.6 派生出来。所以这里不去赌下一次会往哪边倒:**指纹在这个仓库里可以随时重新采集**(`bun run capture`),不必等上游发版,也不必先有人被 403 挡住。维护步骤(重新采集、读闸门结果、发版)见 [MAINTAINING.md](https://github.com/tkhs101/OpenCode-Zen-FreeX/blob/main/MAINTAINING.md)。
16
+
17
+ 上游的修复是跟着报告走的——0.1.1 到 0.1.8 的每一个版本提交都挂着一位具名的报告者。没人遇到、没人报告,就没有那一次修复。这里的 `bun run check:gate` 把「去问 Zen 现在还认哪一代」变成一条随时能跑的命令。
18
+
19
+ 上游修好的东西会照常跟进。**版本号与上游不可比**:0.2.x 长在上游 0.1.6 上,之后按需合并上游的修复,同时保留自己的取舍(比如工具措辞按代从真实 OpenCode 采集,而不是钉死某一个版本)。升级看这个仓库的提交,别拿版本号大小跟上游排。
20
+
21
+ ## 目录
22
+
23
+ - [为什么有这个分支](#为什么有这个分支)
24
+ - [安装](#安装)
25
+ - [登录](#登录)
26
+ - [模型](#模型)
27
+ - [免费层不再应答时](#免费层不再应答时)
28
+ - [请求](#请求)
29
+ - [排查](#排查)
30
+
31
+ ## 安装
32
+
33
+ ```sh
34
+ magpie plugin add opencode-zen-freex
35
+ magpie plugin login opencode-zen-free
36
+ ```
37
+
38
+ ## 登录
39
+
40
+ 在密钥一栏填 `public`。不需要个人的 API key。
41
+
42
+ 凭据保存在 OpenCode 的 `auth.json` 或 magpie 的 `plugin-auth.json` 里。
43
+
44
+ ## 模型
45
+
46
+ 插件会去发现 Zen 当前的免费模型,包括它们的上下文窗口和思考(reasoning)等级。用 `opencode-zen-free/<模型名>`,例如 `opencode-zen-free/big-pickle`。
47
+
48
+ 已下线的模型和 SystemOne 模型会被排除。这份列表在宿主每次列举模型时重新获取(magpie 在启动时做一次,之后每小时一次):Zen 不再免费开放的模型会退出列表,新转为免费的模型会加入列表。如果读不到 Zen 的列表,就沿用上一次的结果;第一次还没读到之前,用的是打包在插件里的那份,所以这个 provider 不会空着。请求一个当前并不免费的模型,会得到一个 404 并列出当时免费的模型;若某个模型是在上次列举之后才转免费的,请求会在重新获取列表后照常服务(最多每分钟一次)。可用性和限流由 Zen 决定,插件这边拿不到剩余额度。
49
+
50
+ ## 免费层不再应答时
51
+
52
+ 过期的指纹和好用的指纹看起来一模一样,直到有人被拒绝。要问 Zen 它还认哪几代 OpenCode:
53
+
54
+ ```sh
55
+ bun run check:gate
56
+ ```
57
+
58
+ 每一代问两次,因为插件发出的形状和 OpenCode 本体发出的形状并不是同一个:
59
+
60
+ | 形状 | 发出去的工具 | 是什么 |
61
+ | --- | --- | --- |
62
+ | `floor` | `required` 里的名字 | 宿主一个都没有时,插件替它补上的那几个。**这才是插件真实发出的形状,退出码由它决定** |
63
+ | `full` | 这一代携带的全部工具 | 宿主本来就是 OpenCode 时发出来的形状。用来看措辞有没有过期,不决定退出码 |
64
+
65
+ 每一代的结论是 `answered`(正常应答)、`refused`(被拒,403)、`superseded`(426——Zen 要求的 OpenCode 比这份指纹声称的更新)或 `unreachable`(根本联系不上)。只要还有一代的 `floor` 被应答,退出码就是 0。某个 `floor` 被拒、而它自己的 `full` 被应答时会单独点出来:那意味着带着自己那套 OpenCode 工具来的宿主能用,而插件真正要服务的那种宿主会被拒。全部联系不上时它不会说指纹过期,只会说问不到——这两件事得分开。
66
+
67
+ 闸门问的时候用的就是插件自己那份请求:请求头的名字来自 `provider.mjs` 里的 `outboundHeaders`,工具来自插件所顶替的同一份 `required`。所以它绿了是在说「插件真发的那个请求」被应答了,而不是在说「照抄的一份」被应答了——照抄的那一份曾经少发一个 `accept`,而且没有任何东西因此报错。
68
+
69
+ 发版之前、以及有人报告 403 的时候,都该跑一次。`ZEN_CHECK_MODEL` 可以指定探测用的模型,`ZEN_BASE_URL` 可以指定端点,默认值不合适时就改这两个。
70
+
71
+ 如果每一代的 `floor` 都被拒,就从当前版本的 OpenCode 重新采集措辞:把一个 release 指向本地服务器,从它发出的请求里把工具读出来。`bun run capture -- --dry` 取一份新采集并报告差异,`bun run capture` 才真正写入。插件在拒绝时会直接说出发出去的措辞是哪个版本、去哪里找,而不是让人自己猜。
72
+
73
+ 要注意这个闸门并不稳定:摸索它的过程中它一天之内挪动过不止一次,而且同一个地址反复被拒之后,接下来一段时间反而会被应答。这里的结论只能读成"此时此刻",信之前再跑一遍。
74
+
75
+ `bun test` 是这个循环的另一半。它从不联网,所以它回答不了指纹是否还有效——它只能说明这个包的行为和以前是否一致。
76
+
77
+ ## 请求
78
+
79
+ 请求发往 `https://opencode.ai/inference/v1`,按每个模型原生的 Chat Completions、Responses 或 Anthropic Messages 接口来走。流式和普通 JSON 两种回复都支持。
80
+
81
+ 宿主自己的工具会被完整保留。Zen 的模型经常去调 OpenCode 的工具名;这种调用会被对上宿主那个只差大小写的工具(`bash` 对 `Bash`),或者只差大小写和 `_`/`-` 的那个(`todowrite` 对 `todo_write`、`webfetch` 对 `web_fetch`),前提是恰好只有一个对得上。找不到对应工具(或者调用根本没有名字)的,不放进回复里,回复的正文会说明哪个名字被略去了;如果这一轮所有调用都被略去,这一轮就以一段普通回答结束。工具仍然在宿主里执行。
82
+
83
+ Zen 的免费层读请求体,也读请求头,只有当整个请求都属于 OpenCode 的同一代时才作答:User-Agent、请求头和工具三者必须一致。当前一共有两代,请求带的工具说明是哪一代:`shell` 和 `read` 属于 2.x,`bash`、`glob`、`grep` 和 `read` 属于 1.x。如果宿主本身就是 OpenCode,它自己的 User-Agent 已经说明是哪一代,这个值会被原样传出去,而不是被钉死,所以永远不会落后。没有任何一方说明时,以工具为准,User-Agent 跟着工具走。
84
+
85
+ 在同一代之内,插件把这一代的工具名都带上——`bash`、`glob`、`grep`、`read`、`write`、`edit`——并且要按 OpenCode 的说法描述。前四个(`required`)在宿主缺了的时候会补上;后两个只在宿主本来就有的时候才去读。实测只有 `read` 是 Zen 咬住不放的:只发 `read` 会被应答,发齐其余五个却不带 `read` 会被拒。那为什么四个都补?因为超集是被接受的,而多出来的名字只在宿主本来没有它时才有代价——一段不会被调用的工具的描述——却能在 Zen 哪天收紧时先挡住一次。这份清单因此刻意留得比实测更宽。
86
+
87
+ 工具永远不会被丢弃:宿主自己的工具原封不动地保留,只有发往上游的措辞被替换掉,所以模型发出的调用仍然适配真正执行它的那个工具。别的名字的工具一概不看。
88
+
89
+ 被*补上*的工具背后什么都没有:宿主没有这个工具,调它也会被丢弃。但 Zen 要求按 OpenCode 的样子描述它,而这么描述又会让模型想去调它,所以在 Zen 会读的正文开头之后、它不读的那段收尾之前,插一句说明。那句话是只针对这一个工具写的:实测过一句笼统的"这些工具不可用",会连宿主自己的工具也一起不用。
90
+
91
+ 发出去的请求头就是下面这些,一个不多,而这份清单是刻意从 Zen 曾经接受过的全部头里挑出来的子集:
92
+
93
+ | 请求头 | 值 |
94
+ | --- | --- |
95
+ | `content-type` | `application/json` |
96
+ | `accept` | `*/*` |
97
+ | `user-agent` | 上面说的那一代的值 |
98
+ | `x-opencode-project` | 宿主的 project id,或 `global` |
99
+ | `x-opencode-session` | 整个会话共用的一个 id |
100
+ | `x-opencode-session-id` | 同一个 id |
101
+ | `x-opencode-client` | `cli` |
102
+ | `authorization` | `Bearer public`;走 Messages 时是 `x-api-key: public` |
103
+
104
+ `anthropic-version` 和 `anthropic-beta` 也会发出去,但只在走 Messages 时,而且只在宿主自己设了它们的时候。
105
+
106
+ `x-opencode-request`、`x-session-affinity` 和 `x-session-id` 都在 OpenCode 自己的请求头集合里,但没有一个是必需的,所以都不发——不过进来的同名请求头仍然会被读,用来判断这次请求属于哪个会话。这不是「一个个试过不行」得出的结论,2026-10-15 实测(1.x,其余不动):
107
+
108
+ | 加进基准请求的 | 结果 |
109
+ | --- | --- |
110
+ | 任意一个,单独 | 应答 |
111
+ | `x-opencode-request` + `x-session-id` | 应答 |
112
+ | `x-session-affinity` + `x-session-id` | 应答 |
113
+ | **`x-opencode-request` + `x-session-affinity`** | **被拒**(复现三次;同一次运行里被拒与通过交替出现,所以不是冷却期) |
114
+ | 三个一起 | 被拒(复现四次) |
115
+
116
+ 两个各自无害的头放在一起会被拒,所以「某个头能不能加」不能靠单个试出来。测试按名字钉住了发出的这组请求头,因为多加一个请求头是弄坏这个插件最容易的方式。
117
+
118
+ Zen 的免费层会检查 session id 是否是 OpenCode 能生成的那种:时间戳加每毫秒计数器截成十二位十六进制,后面接十四个随机的 base62 字符;session 的那一种还会把低位取反。宿主给的 id 如果本来就是这个形状,就原样传出去;否则按会话的首条消息生成一个并记住,这样同一会话的各轮请求会共用一个 session id。
119
+
120
+ 偶尔 Zen 会在发出 `[DONE]` 之前就结束一个流式的 Chat Completions 回复。如果回复已经说了内容、且其中没有未完成的工具调用,就以普通回答结束,正文里会说明它被截断了;再问一次就会接着往下走。如果是带着未完成的工具调用被截断、或者什么都没说就断了、或者被折叠给一个不支持流式的客户端,请求就会失败,错误信息里写明这个回复的 id 和收到了多少。走 Anthropic Messages 或 Responses 的回复被截断同样会失败:magpie 能把它和完整的回复区分开,并且在该模型支持的情况下继续往下走。
121
+
122
+ ## 排查
123
+
124
+ | 看到的现象 | 这是什么 | 怎么办 |
125
+ | --- | --- | --- |
126
+ | 每个请求都是 `403 FreeTierError` | `fingerprint.json` 里的措辞过期了 | 跑 `bun run check:gate`;如果每一代的 `floor` 都被拒,跑 `bun run capture` |
127
+ | `426 Upgrade Required` | 这个包自称的 OpenCode 版本比 Zen 要的旧 | 升级插件,或者在更新的 OpenCode 里运行 |
128
+ | `404 … not available as an OpenCode Zen free model now` | Zen 不再免费开放这个模型 | 从错误信息列出的模型里换一个 |
129
+ | `Model is not an available free model for this protocol` | 请求发来时列表变了 | 让宿主刷新模型列表再试 |
130
+ | `uses the public credential` | 存的密钥不是 `public` | 用 `public` 重新登录 |
131
+ | 不带 `FreeTierError` 的 403 | 是别的 403,原样透传 | 按 Zen 自己的错误来读,别当成这个插件的 |
132
+
133
+ 这个插件给出的 403 会写明发出去的措辞来自哪个 OpenCode 版本。那正是 `check:gate` 在问的那份指纹,所以从它开始查,而不是从请求头开始查。
134
+
135
+ 模型的可用性和限流归 Zen 管,插件拿不到剩余额度可显示。同一个地址反复被拒,也可能被放进一个冷却期,表现得和被拒很像。
136
+
137
+ ## 许可
138
+
139
+ MIT,分叉自 [zen-free](https://github.com/magpie-community/plugins/tree/main/packages/zen-free)。版权署名见 [LICENSE](https://github.com/tkhs101/OpenCode-Zen-FreeX/blob/main/LICENSE):`Copyright (c) 2026 xiaozhou26` 是上游原作者,本分支的贡献者另有署名。