opencode-zen-freex 0.2.17 → 0.2.18

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 CHANGED
@@ -173,6 +173,12 @@ bun run capture # writes it
173
173
  bun run check:gate # did that fix it
174
174
  ```
175
175
 
176
+ `bun test` reads the tree; a running gateway does not. Magpie's plugin host
177
+ imports the plugin once, when it starts, and no `magpie plugin` subcommand
178
+ reloads it — pointing a gateway at this tree does not make it watch the tree, so
179
+ a change to be measured through a live gateway needs that gateway restarted
180
+ first.
181
+
176
182
  `check:gate` is the one that matters. It asks each generation twice, because the
177
183
  plugin sends two shapes and Zen does not answer them alike:
178
184
 
@@ -304,6 +310,22 @@ it. Rewriting the parameters as well looks tidier and breaks the agent.
304
310
  **Replacing the wording with something of your own.** That is what 0.1.5 and
305
311
  earlier did, and it is a guaranteed refusal.
306
312
 
313
+ **Leaving out a call the agent has no tool for.** A call whose name folded onto
314
+ no declared tool was dropped from the reply with a note in its text, and a reply
315
+ whose every call was dropped ended as a plain `stop`. An autonomous host reads
316
+ that as an answer: DeepSeek Harness ended 16 of the 19 such turns one user
317
+ measured and the model never saw an error it could correct
318
+ (`magpie-community/plugins#209`; upstream made the same change in its 0.1.11).
319
+ The name now goes on as the model wrote it — same name, same id, same arguments,
320
+ a tool-call finish — and the host answers it with its own unknown-tool error,
321
+ which is the correction signal. A name two declared tools fold to is still never
322
+ guessed: it goes on as called. Only a call with **no name** is left out now, and
323
+ `droppedNote` says so. Measured 2026-10-18 through a local magpie pointed at
324
+ this tree, `deepseek-v4.1-flash-free`, declaring `pwsh` and calling `bash`:
325
+ before, `200` + `finish_reason: "stop"` + the note; after, `200` + `tool_calls`
326
+ + `bash` carrying its id. Do not put the drop back to "protect" a host —
327
+ answering is the host's to do, and a silent `stop` is the failure this replaced.
328
+
307
329
  **Merging a tool call's frames over its id.** The route puts `"id": null` on
308
330
  the continuation frames of a call whose first frame named it (measured
309
331
  2026-10-17 on `deepseek-v4.1-flash-free`), and a host that keys a tool result
@@ -315,6 +337,17 @@ invented one. Both live in `tool-response.mjs` — the merge's keep, and
315
337
  `ensureCallIDs` at the two places a call is published — and `stream.mjs`'s
316
338
  collapse reads the same rule.
317
339
 
340
+ Both of those places publish the **chat** shape only. A Responses or Messages
341
+ reply whose call carries no id of its own is handed on as it stands — no id, no
342
+ `call_id` — and both protocols require the field. Measured 2026-10-18, no free
343
+ model is served on either route: the tier's own `/models` listed none,
344
+ `muse-spark-1.3-contributor-free` answered 410 `ModelDeprecated` and the four
345
+ Messages models answered "Model is unavailable". So the gap is real but not
346
+ reachable today, and the sentence above — every published call leaves with a
347
+ non-empty id — is chat's. A free model on either route puts it back in scope,
348
+ and then the id has to be minted once per call across its added, done and
349
+ terminal copies, the way the dropped ones already are.
350
+
318
351
  **Trusting one observation.** The gate is not steady: it moved more than once in
319
352
  a day while this was being worked out, and identical requests were answered both
320
353
  ways minutes apart. Anything measured once is not a fact. Measure it three times,
package/README.md CHANGED
@@ -64,8 +64,8 @@ bun run capture # 确认差异后才写入
64
64
 
65
65
  发往 `https://opencode.ai/inference/v1`,按每个模型原生协议走(Chat Completions / Responses / Anthropic Messages),流式与普通 JSON 都支持。
66
66
 
67
- - **宿主的工具不会被丢**:Zen 调 OpenCode 工具名时,按大小写与 `_`/`-` 对上唯一匹配的宿主工具(`bash`↔`Bash`、`todowrite`↔`todo_write`);对不上就略去并在正文说明。工具仍在宿主执行,调用一律带非空 id。
68
- - **只借 OpenCode 的样子**:请求头、User-Agent、工具必须同属一代(`bash`/`glob`/`grep`/`read` 是 1.x,`shell`/`read` 是 2.x);宿主缺的 `required` 工具由插件补发描述(背后没有实现,正文注明),宿主自带的 User-Agent 原样透传。
67
+ - **对不上的名字照样交给宿主**:Zen 调 OpenCode 工具名时,按大小写与 `_`/`-` 对上唯一匹配的宿主工具(`bash`↔`Bash`、`todowrite`↔`todo_write`);对不上或有歧义时**不猜也不丢**,按模型写的名字、原样的 id 与参数送达宿主,由宿主回它自己的"没有这个工具",模型据此改名重来。只有**没有名字**的调用会被略去,并在正文说明。工具仍在宿主执行,调用一律带非空 id。
68
+ - **只借 OpenCode 的样子**:请求头、User-Agent、工具必须同属一代(`bash`/`glob`/`grep`/`read` 是 1.x,`shell`/`read` 是 2.x);宿主缺的 `required` 工具由插件补发描述(背后没有实现,描述里注明别调它),宿主自带的 User-Agent 原样透传。
69
69
  - **请求头是刻意挑出的最小集合**:`content-type`、`accept`、`user-agent`、`x-opencode-project`、`x-opencode-session(-id)`(同一 id)、`x-opencode-client: cli`、`authorization: Bearer public`(走 Messages 时 `x-api-key: public` + `anthropic-version`;`anthropic-beta` 这个头插件自己从不设置:只在宿主已经发了的时候原样透传)。**不要加别的头**:两个各自无害的组合起来也会被拒——单试一个得不出结论。
70
70
  - **会话 id** 按会话首条消息生成并记住,同一会话各轮共用;宿主的 id 形状合法就原样传。
71
71
  - **提前掐断的流**:有内容且无未完成调用 → 按普通回答收尾并注明截断,再问会续;否则请求失败,错误写明回复 id 与已收字节。Messages / Responses 被截断同样失败。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-zen-freex",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
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",
package/provider.mjs CHANGED
@@ -339,10 +339,12 @@ function normalizeTools(body, protocol) {
339
339
  continue;
340
340
  }
341
341
  filled.push(name);
342
- // A tool added here has nothing behind it: the agent has no such tool and
343
- // a call to it is dropped. Describing it as OpenCode describes it is what
344
- // Zen asks for, and it also makes the model want to call it, so the note
345
- // goes after the opening Zen reads and before the closing it does not.
342
+ // A tool added here has nothing behind it: the agent has no such tool, so a
343
+ // call to it goes to the agent as called and comes back as the agent's own
344
+ // unknown-tool error, which the model corrects. Describing it as OpenCode
345
+ // describes it is what Zen asks for, and that description also makes the
346
+ // model want to call it, so the note goes after the opening Zen reads and
347
+ // before the closing it does not.
346
348
  body.tools.push(shapeOf({ ...spoken, description: `${spoken.description}\n\n${PLACEHOLDER}` }, protocol));
347
349
  }
348
350
  return { declared, filled, generation };
package/tool-response.mjs CHANGED
@@ -19,8 +19,8 @@ function headersFor(response) {
19
19
  }
20
20
 
21
21
  // failure answers the error a client reads. `code` says what went wrong
22
- // where the shape has room for it: only a call of no tool the agent has is
23
- // an undeclared one, and a limit, a truncation or a malformed upstream is
22
+ // where the shape has room for it: only a call the guard cannot name at all
23
+ // is an undeclared one, and a limit, a truncation or a malformed upstream is
24
24
  // not.
25
25
  function failure(protocol, message, stream = false, code = "upstream_error") {
26
26
  if (protocol === "anthropic") return { type: "error", error: { type: "api_error", message } };
@@ -88,8 +88,15 @@ function declaredNames(policy) {
88
88
  return new Set(names);
89
89
  }
90
90
 
91
- // nameGuard answers the agent's name for a call, or null for a call of no
92
- // tool the agent has (a missing name included).
91
+ // nameGuard answers the agent's name for a call: the agent's spelling when one
92
+ // folds to it, else the name as the model wrote it. A call of a tool the agent
93
+ // has no spelling of (bedit, psh, mcp__netcatcatty__unused) reaches the agent
94
+ // as called, as it does on every other provider: the agent answers it with its
95
+ // own unknown-tool error, and the model corrects the name and goes on (#209).
96
+ // Leaving it out ended an agent's turn as a plain answer, which an autonomous
97
+ // host reads as "the task is done" (#76 fixed the failure that came before
98
+ // this one; that reply is not handed a turn's end any more, it is handed a
99
+ // call). Only a call with no name answers null, and is left out.
93
100
  function nameGuard(policy) {
94
101
  const declared = declaredNames(policy);
95
102
  const lower = (name) => name.toLowerCase();
@@ -101,16 +108,16 @@ function nameGuard(policy) {
101
108
  if (declared.has(name)) return name;
102
109
  const match =
103
110
  byCase.get(lower(name)) ?? (byCase.has(lower(name)) ? null : byShape.get(bare(name)));
104
- return match || null;
111
+ return match || name;
105
112
  };
106
113
  }
107
114
 
108
- // The models also call tools no agent has: bedit, bcp, ash, a mangled
109
- // mcp__netcatcatty__unused (#76). Such a call can't be run or named onto
110
- // one the agent has, and failing the reply for it threw away the whole
111
- // turn. It is left out instead, and the reply says so in its text, where
112
- // the agent's user and the model's next turn both read it. A reply left
113
- // with no call ends as a plain stop.
115
+ // A call with no name can't be run, named onto one the agent has, or answered
116
+ // by any agent: nothing keys a result by it and its own id names nothing. It
117
+ // is left out instead of failing the reply — failing threw away the whole turn
118
+ // (#76) — and the reply says so in its text, where the agent's user and the
119
+ // model's next turn both read it. A reply left with no call ends as a plain
120
+ // stop. Every named call, whatever the name, is handed on as called.
114
121
  export function droppedNote(name) {
115
122
  return typeof name === "string" && name
116
123
  ? `(The model called a tool this agent doesn't have: ${JSON.stringify(name.slice(0, 128))}. The call was left out.)`
@@ -785,11 +792,11 @@ function snapshotStream(protocol, checkName) {
785
792
  insideWords.clear();
786
793
  if (opens && nowhere[open]) nowhere[open].settled = true;
787
794
  }
788
- // The item this frame carries, when it is a call of no tool the agent
789
- // has: the walk below replaces it — opens or closes it, the copy is
790
- // the same call — and its words are the ones a note takes the place
791
- // of. Every other item the frame names is one the reply keeps, and the
792
- // place it holds is a fact the suppression below reads before any word.
795
+ // The item this frame carries, when it is a call with no name: the
796
+ // walk below replaces it — opens or closes it, the copy is the same
797
+ // call — and its words are the ones a note takes the place of. Every
798
+ // other item the frame names is one the reply keeps, and the place it
799
+ // holds is a fact the suppression below reads before any word.
793
800
  const leaving =
794
801
  (opens || closes) &&
795
802
  object(item) &&