mslxdff 0.1.133 → 0.1.134

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.133",
3
+ "version": "0.1.134",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,131 @@
1
+ // zen 免费层"agent 形状"门禁(2026-09-18 上线,bisect 实测):
2
+ // 403 FreeTierError ≤ 请求必须同时满足:① stream:true ② tools 含 bash/edit/glob/grep/read 五个核心名。
3
+ // 另 UA 版本需 ≥ opencode/1.18.0(低版本返回 426 UpgradeRequired,见 opencode-identity.js)。
4
+ // 官方客户端自带这五个工具天然通过;-chat 直连(非流式、工具名不同)与裸 API 客户端需补形状。
5
+ // 逃生阀:MSLXDFF_FREE_LANE=0 关闭(注入与强制流式都跳过),上游若撤门禁可回退。
6
+
7
+ export const CORE_AGENT_TOOL_NAMES = ["bash", "edit", "glob", "grep", "read"];
8
+
9
+ function laneDisabled(env = process.env) {
10
+ const raw = env.MSLXDFF_FREE_LANE;
11
+ if (raw === undefined || raw === null || raw === "") return false;
12
+ const s = String(raw).trim().toLowerCase();
13
+ return s === "0" || s === "false" || s === "off" || s === "no";
14
+ }
15
+
16
+ const chatTool = (name) => ({
17
+ type: "function",
18
+ function: { name, description: `The ${name} tool.`, parameters: { type: "object", properties: {} } },
19
+ });
20
+
21
+ const responsesTool = (name) => ({
22
+ type: "function",
23
+ name,
24
+ description: `The ${name} tool.`,
25
+ parameters: { type: "object", properties: {} },
26
+ });
27
+
28
+ function toolName(tool, responses) {
29
+ return responses ? tool?.name : tool?.function?.name;
30
+ }
31
+
32
+ /**
33
+ * 给免费层请求补 agent 形状(幂等,原地改 body)。
34
+ * @returns {{injected: string[], forcedStream: boolean, disabled: boolean}}
35
+ */
36
+ export function ensureFreeLaneShape(body, { responses = false, env = process.env } = {}) {
37
+ if (!body || typeof body !== "object") return { injected: [], forcedStream: false, disabled: true };
38
+ if (laneDisabled(env)) return { injected: [], forcedStream: false, disabled: true };
39
+ const forcedStream = body.stream !== true;
40
+ body.stream = true;
41
+ const mk = responses ? responsesTool : chatTool;
42
+ const list = Array.isArray(body.tools) ? body.tools : [];
43
+ const have = new Set(list.map((t) => toolName(t, responses)).filter(Boolean));
44
+ const injected = CORE_AGENT_TOOL_NAMES.filter((n) => !have.has(n));
45
+ if (injected.length) body.tools = [...list, ...injected.map(mk)];
46
+ return { injected, forcedStream, disabled: false };
47
+ }
48
+
49
+ function mergeToolCall(target, idx, delta) {
50
+ const cur = target[idx] || (target[idx] = { index: idx, id: undefined, type: "function", function: { name: undefined, arguments: "" } });
51
+ if (delta?.id) cur.id = delta.id;
52
+ if (delta?.type) cur.type = delta.type;
53
+ if (delta?.function?.name) cur.function.name = delta.function.name;
54
+ if (delta?.function?.arguments) cur.function.arguments += delta.function.arguments;
55
+ }
56
+
57
+ function finishSseJson(acc) {
58
+ const message = { role: "assistant", content: acc.content };
59
+ if (acc.reasoning) message.reasoning_content = acc.reasoning;
60
+ if (acc.toolCalls.length) {
61
+ message.tool_calls = acc.toolCalls
62
+ .filter(Boolean)
63
+ .map((c, i) => ({ ...c, index: undefined, id: c.id || `call_${i}`, function: { name: c.function.name || "", arguments: c.function.arguments || "{}" } }));
64
+ if (!message.content) message.content = "";
65
+ }
66
+ const json = {
67
+ id: acc.id || "chatcmpl-aggregated",
68
+ object: "chat.completion",
69
+ created: Math.floor(Date.now() / 1000),
70
+ model: acc.model,
71
+ choices: [{ index: 0, message, finish_reason: acc.finishReason || (acc.toolCalls.length ? "tool_calls" : "stop"), logprobs: null }],
72
+ };
73
+ if (acc.usage) json.usage = acc.usage;
74
+ return json;
75
+ }
76
+
77
+ /**
78
+ * 把上游 SSE 流聚合回非流式 chat completion JSON。
79
+ * 仅消费 body,不改状态;解析失败时抛错(调用方自行回退)。
80
+ */
81
+ export async function aggregateChatSse(res) {
82
+ const reader = res.body?.getReader?.();
83
+ if (!reader) return res;
84
+ const acc = { id: null, model: null, content: "", reasoning: "", toolCalls: [], finishReason: null, usage: null, error: null };
85
+ const decoder = new TextDecoder();
86
+ let buf = "";
87
+ const eat = (frame) => {
88
+ for (const line of frame.split("\n")) {
89
+ const s = line.trim();
90
+ if (!s.startsWith("data:")) continue;
91
+ const payload = s.slice(5).trim();
92
+ if (!payload || payload === "[DONE]") continue;
93
+ let j;
94
+ try { j = JSON.parse(payload); } catch { continue; }
95
+ if (j?.error) { acc.error = acc.error || j.error; continue; }
96
+ if (j?.id) acc.id = j.id;
97
+ if (j?.model) acc.model = j.model;
98
+ if (j?.usage) acc.usage = j.usage;
99
+ const ch = j?.choices?.[0];
100
+ if (!ch) continue;
101
+ const d = ch.delta || {};
102
+ if (typeof d.content === "string") acc.content += d.content;
103
+ if (typeof d.reasoning_content === "string") acc.reasoning += d.reasoning_content;
104
+ else if (typeof d.reasoning === "string") acc.reasoning += d.reasoning;
105
+ if (Array.isArray(d.tool_calls)) for (const tc of d.tool_calls) mergeToolCall(acc.toolCalls, tc.index ?? 0, tc);
106
+ if (ch.finish_reason) acc.finishReason = ch.finish_reason;
107
+ if (ch.message && typeof ch.message.content === "string" && !acc.content) {
108
+ acc.content = ch.message.content;
109
+ if (Array.isArray(ch.message.tool_calls)) for (const tc of ch.message.tool_calls) mergeToolCall(acc.toolCalls, tc.index ?? 0, tc);
110
+ }
111
+ }
112
+ };
113
+ try {
114
+ for (;;) {
115
+ const { done, value } = await reader.read();
116
+ if (done) break;
117
+ buf += decoder.decode(value, { stream: true });
118
+ const frames = buf.split("\n\n");
119
+ buf = frames.pop() || "";
120
+ for (const f of frames) eat(f);
121
+ }
122
+ if (buf.trim()) eat(buf);
123
+ } finally {
124
+ try { reader.releaseLock?.(); } catch {}
125
+ }
126
+ if (acc.error && !acc.content && !acc.toolCalls.length) {
127
+ const body = JSON.stringify({ error: acc.error });
128
+ return new Response(body, { status: 502, headers: { "content-type": "application/json" } });
129
+ }
130
+ return new Response(JSON.stringify(finishSseJson(acc)), { status: res.status, headers: { "content-type": "application/json" } });
131
+ }
@@ -3,9 +3,10 @@ import crypto from "node:crypto";
3
3
  // opencode 客户端身份规格单一来源(源码 packages/schema/src/identifier.ts +
4
4
  // packages/opencode/src/session/llm/request.ts):id = <prefix>_ + 12 位 hex
5
5
  // (timestamp*4096+同毫秒计数,截 48bit) + 14 位 base62,共 26 字符;UA 必须 "opencode/<semver>"。
6
- // zen 免费层 2026-09-17 起按该规格放行(缺版本 UA 或 id 形状不符 → 403 FreeTierError)。
6
+ // zen 免费层 2026-09-17 起按该规格放行(缺版本 UA 或 id 形状不符 → 403 FreeTierError);
7
+ // 2026-09-18 起还要求 UA 版本 ≥ 1.18.0(低版本 426 UpgradeRequired),默认跟 npm latest。
7
8
  const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
8
- const DEFAULT_OPENCODE_UA = "opencode/1.17.20";
9
+ const DEFAULT_OPENCODE_UA = "opencode/1.18.31";
9
10
  let idLastTs = 0;
10
11
  let idCounter = 0;
11
12
 
@@ -5,6 +5,7 @@
5
5
  import { createUpstreamClient, createOpencodeHeaderBuilder } from "../upstream.js";
6
6
  import { isResponsesModel } from "../upstream-responses.js";
7
7
  import { isFreeModel } from "../models.js";
8
+ import { ensureFreeLaneShape } from "../free-lane.js";
8
9
  import { createSdkChat } from "./sdk/chat.js";
9
10
  import { createSdkResponses } from "./sdk/responses.js";
10
11
  import { dispatcherFetch } from "./sdk/attempt.js";
@@ -32,9 +33,11 @@ export function createUpstreamEngine(opts = {}) {
32
33
  let logged = false;
33
34
 
34
35
  async function chat(body) {
35
- if (sdkDown) return legacy.chat(body);
36
- // doGenerate 聚合未实现:非流式(含 responses 非流式)委派 legacy,避免把 JSON 客户端 SSE 化
37
- if (body?.stream === false) return legacy.chat(body);
36
+ // zen 免费层 agent 形状门禁(2026-09-18):SDK 流式通道不经 legacy.chat,必须在这里补形状。
37
+ // 非流式先委派 legacy(它在自己内部注入并把 SSE 聚合回 JSON,避免这里先改 stream 导致误判)。
38
+ const wantsStream = body?.stream !== false;
39
+ if (sdkDown || !wantsStream) return legacy.chat(body);
40
+ if (isFreeModel(body?.model)) ensureFreeLaneShape(body);
38
41
  const useResponses = isResponsesModel(body?.model);
39
42
  try {
40
43
  const res = useResponses ? await responses.chat(body) : await sdk.chat(body);
package/src/upstream.js CHANGED
@@ -7,6 +7,7 @@ import { isFreeModel } from "./models.js";
7
7
  import { fmtShanghaiYMDHMS } from "./time.js";
8
8
  import { createTransport } from "./transport/index.js";
9
9
  import { isResponsesModel, chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "./upstream-responses.js";
10
+ import { ensureFreeLaneShape, aggregateChatSse } from "./free-lane.js";
10
11
  import { digestIdTail, genId, opencodeClientIdentity, opencodeUa } from "./opencode-identity.js";
11
12
 
12
13
  export { opencodeClientIdentity, opencodeUa };
@@ -136,6 +137,27 @@ export function createUpstreamClient({
136
137
  const sessionId = opts?.sessionId || sessionFromMessages(body?.messages) || null;
137
138
  const url = isResp ? `${baseUrl}/zen/v1/responses` : `${baseUrl}/zen/v1/chat/completions`;
138
139
  const reqBody = isResp ? chatToResponsesBody(body) : body;
140
+ const clientWantsStream = body?.stream !== false;
141
+ // zen 免费层 agent 形状门禁(2026-09-18):必须流式 + 含核心五工具名,否则 403 FreeTierError
142
+ const freeLane = authToken === "public" && isFreeModel(body?.model);
143
+ if (freeLane) ensureFreeLaneShape(reqBody, { responses: isResp });
144
+ const laneDebug = freeLane && envInt("MSLXDFF_FREE_LANE_DEBUG", 0) === 1;
145
+ if (laneDebug) {
146
+ try { console.error(`[free-lane] send model=${body?.model} url=${url} stream=${reqBody.stream} tools=${(reqBody.tools || []).length} ua=${opencodeUa()} want=${clientWantsStream}`); } catch {}
147
+ }
148
+ // 非流式调用者实际拿到的是被强制流式的上游 → 聚合回 chat completion JSON
149
+ async function agentJson(resp) {
150
+ if (!(freeLane && !clientWantsStream && resp.ok)) return resp;
151
+ const ct = resp.headers?.get?.("content-type") || "";
152
+ if (!ct.includes("text/event-stream")) return resp;
153
+ try {
154
+ const out = await aggregateChatSse(resp);
155
+ out._t = resp._t;
156
+ return out;
157
+ } catch {
158
+ return resp;
159
+ }
160
+ }
139
161
  const t0 = performance.now();
140
162
 
141
163
  // 首发请求(transport 已处理 network/429 等重试)
@@ -146,7 +168,7 @@ export function createUpstreamClient({
146
168
  method: "POST",
147
169
  headers: buildHeaders(reqBody, { sessionId }),
148
170
  body: reqBody,
149
- stream: body?.stream !== false,
171
+ stream: reqBody.stream !== false,
150
172
  timeoutMs: connectTimeoutMs,
151
173
  retry,
152
174
  });
@@ -154,6 +176,9 @@ export function createUpstreamClient({
154
176
  e._t = e._t || { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0) };
155
177
  throw e;
156
178
  }
179
+ if (laneDebug) {
180
+ try { console.error(`[free-lane] resp model=${body?.model} status=${res.status} ct=${res.headers?.get?.("content-type") || ""}`); } catch {}
181
+ }
157
182
 
158
183
  // 匿名兜底:仅非 anonFirst 时,public 429 + free 模型才走 hermes 空头重试
159
184
  if (!anonFirst && res.status === 429 && isFreeModel(body?.model) && shouldTryAnonFree()) {
@@ -168,7 +193,7 @@ export function createUpstreamClient({
168
193
  method: "POST",
169
194
  headers: buildHeaders(reqBody, { anonymous: true, sessionId }),
170
195
  body: reqBody,
171
- stream: body?.stream !== false,
196
+ stream: reqBody.stream !== false,
172
197
  timeoutMs: connectTimeoutMs,
173
198
  retry,
174
199
  });
@@ -180,7 +205,7 @@ export function createUpstreamClient({
180
205
  let outAnon = anonRes;
181
206
  if (isResp && anonRes.ok) {
182
207
  const ctAnon = anonRes.headers.get("content-type") || "";
183
- const isStreamAnon = body?.stream !== false && ctAnon.includes("text/event-stream");
208
+ const isStreamAnon = (clientWantsStream || freeLane) && ctAnon.includes("text/event-stream");
184
209
  if (isStreamAnon) {
185
210
  outAnon = reshapeResponsesSse(anonRes, body.model);
186
211
  } else {
@@ -201,7 +226,7 @@ export function createUpstreamClient({
201
226
  if (consecutiveHits >= 2) {
202
227
  try { appendFile(freeAnonLogFile(), ` -> 连续额外额度 ${consecutiveHits} 次\n`).catch(() => {}); } catch {}
203
228
  }
204
- return outAnon;
229
+ return await agentJson(outAnon);
205
230
  }
206
231
  }
207
232
  if (anonRes) {
@@ -215,11 +240,11 @@ export function createUpstreamClient({
215
240
  // responses 模型成功态转 chat(复用 upstream-responses)
216
241
  if (isResp && res.ok) {
217
242
  const ct = res.headers.get("content-type") || "";
218
- const isStream = body?.stream !== false && ct.includes("text/event-stream");
243
+ const isStream = (clientWantsStream || freeLane) && ct.includes("text/event-stream");
219
244
  if (isStream) {
220
245
  const transformed = reshapeResponsesSse(res, body.model);
221
246
  transformed._t = { ...(res._t || {}), totalMs: Math.round(performance.now() - t0) };
222
- return transformed;
247
+ return await agentJson(transformed);
223
248
  }
224
249
  try {
225
250
  const txt = await res.text();
@@ -238,7 +263,7 @@ export function createUpstreamClient({
238
263
  }
239
264
  }
240
265
  res._t = { ...(res._t || {}), totalMs: res._t?.totalMs ?? Math.round(performance.now() - t0) };
241
- return res;
266
+ return await agentJson(res);
242
267
  }
243
268
 
244
269
  async function preheat() {