mslxdff 0.1.141 → 0.1.142

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.141",
3
+ "version": "0.1.142",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,9 @@ import { envInt, joinUrl, getUndici, createAgent, collectApiKeysGeneric, createC
4
4
  import { compatFetch } from "../compat.js";
5
5
  import crypto from "node:crypto";
6
6
  import { genId, opencodeUa, digestIdTail } from "../opencode-identity.js";
7
+ import { isResponsesModel } from "../upstream-responses.js";
8
+ import { createResponsesChannel, resolveResponsesPath, envSlug } from "./responses-channel.js";
9
+ import { resolveEngineMode } from "../upstream-engine/mode.js";
7
10
 
8
11
  const { UndiciFetch } = getUndici();
9
12
 
@@ -41,6 +44,7 @@ export function createGenericProvider({
41
44
  apiKey,
42
45
  modelsPath,
43
46
  chatPath,
47
+ responsesPath,
44
48
  connectTimeoutMs = Number(process.env.MSLXDFF_GENERIC_TIMEOUT_MS) || 30_000,
45
49
  cooldownMs = envInt("MSLXDFF_GENERIC_COOLDOWN_MS", 30_000),
46
50
  retry = {
@@ -106,12 +110,29 @@ export function createGenericProvider({
106
110
  });
107
111
  }
108
112
 
113
+ // responses 类模型(muse-spark* 等)只在 /responses 挂载 → 单独通道,出参恒 chat 形状。
114
+ const responses = createResponsesChannel({
115
+ id,
116
+ url: joinUrl(resolvedBase, resolveResponsesPath(id, responsesPath)),
117
+ connectTimeoutMs,
118
+ retry,
119
+ cooldownMs,
120
+ // 供应商级 <ID>_SDK 未设置即继承全局总闸(MSLXDFF_UPSTREAM_ENGINE)
121
+ sdkEnabled: resolveEngineMode(process.env, `MSLXDFF_${envSlug(id)}_SDK`) === "sdk",
122
+ buildHeaders,
123
+ fetchImpl,
124
+ dispatcher,
125
+ });
126
+
109
127
  async function chat(body, opts) {
128
+ const sourceKey = `MSLXDFF_${envSlug(id)}_KEY`;
129
+ if (isResponsesModel(body?.model)) return responses.run(body, ring, sourceKey, opts);
110
130
  const { runChat } = scopedRunner(ring, opts);
111
- return runChat(body, ring, `MSLXDFF_${id.toUpperCase()}_KEY`);
131
+ return runChat(body, ring, sourceKey);
112
132
  }
113
133
  async function chatWithKeys(body, keys, opts) {
114
134
  const tmp = createKeyRing(keys, { cooldownMs });
135
+ if (isResponsesModel(body?.model)) return responses.run(body, tmp, "shared provider keys", opts);
115
136
  const { runChat } = scopedRunner(tmp, opts);
116
137
  return runChat(body, tmp, "shared provider keys");
117
138
  }
@@ -131,5 +152,5 @@ export function createGenericProvider({
131
152
  if (agent && typeof agent.close === "function") { try { await agent.close(); } catch {} }
132
153
  }
133
154
 
134
- return { id, chat, chatWithKeys, listModels, preheat, close, agent, keyRing: ring, baseUrl: resolvedBase };
155
+ return { id, chat, chatWithKeys, listModels, preheat, close, agent, keyRing: ring, baseUrl: resolvedBase, responsesUrl: responses.url };
135
156
  }
@@ -0,0 +1,120 @@
1
+ // 通用(带 key)供应商的 responses 通道:muse-spark* 这类模型上游只挂 /responses,
2
+ // 打同 host 的 /chat/completions 会 503 "Endpoint is unavailable"(ocgo 实测 2026-09-22)。
3
+ // 判定单一来源 = isResponsesModel(models.dev 模型级 provider.npm + muse-spark 前缀兜底),
4
+ // 与 GET /v1/models 的 capabilities.upstreamApi 同源,新模型无需改码。
5
+ //
6
+ // 契约:run() 的出参恒为 **chat 形状**(SSE 或 JSON),调用方无需再判分支。
7
+ // · 流式优先 @ai-sdk/openai 的 responses 适配器(含加密思考往返,ADR-0017 复用);
8
+ // · SDK 不可用 / 非流式 → 原生 fetch + chatToResponsesBody 正转换 + 反向整形。
9
+ // key 轮换、退避重试、_t 计时与 createChatRunner(base.js)同语义,避免两条通道行为漂移。
10
+ import { chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "../upstream-responses.js";
11
+ import { attemptOnceResponsesSdk } from "../upstream-engine/sdk/responses.js";
12
+ import { ENGINE_MARKER } from "../upstream-engine/sdk/chat.js";
13
+ import { sleep } from "./base.js";
14
+
15
+ /** 供应商 id → env 片段(`my-api` → `MY_API`),与 providerKeyEnv 同规则 */
16
+ export function envSlug(id) {
17
+ return String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_");
18
+ }
19
+
20
+ /**
21
+ * responses 端点路径:显式入参 > env `MSLXDFF_<ID>_RESPONSES_PATH` > 缺省 `/responses`。
22
+ * 归一为以 `/` 开头(joinUrl 会再拼 baseUrl)。
23
+ */
24
+ export function resolveResponsesPath(id, responsesPath) {
25
+ const raw = responsesPath || process.env[`MSLXDFF_${envSlug(id)}_RESPONSES_PATH`] || "";
26
+ const s = String(raw).trim();
27
+ if (!s) return "/responses";
28
+ return s.startsWith("/") ? s : `/${s}`;
29
+ }
30
+
31
+ /**
32
+ * 建 responses 通道。
33
+ * @param {object} o
34
+ * @param {string} o.id 供应商 id(用于报错文案与 providerName)
35
+ * @param {string} o.url 已拼好的 responses 绝对 URL
36
+ * @param {number} o.connectTimeoutMs 单次尝试超时
37
+ * @param {object} o.retry 重试表(与 createChatRunner 同形状:network/429/50x → {attempts, delayMs})
38
+ * @param {number} o.cooldownMs 全 key 冷却时的人话报错文案用
39
+ * @param {boolean} o.sdkEnabled 是否允许走 AI SDK responses 适配器(流式路径)
40
+ * @param {Function} o.buildHeaders (body, key, opts) → headers
41
+ * @param {Function} o.fetchImpl 注入的 fetch(连接池/测试接缝)
42
+ * @param {object} [o.dispatcher] undici Agent
43
+ */
44
+ export function createResponsesChannel({
45
+ id,
46
+ url,
47
+ connectTimeoutMs = 30_000,
48
+ retry = {},
49
+ cooldownMs = 30_000,
50
+ sdkEnabled = true,
51
+ buildHeaders,
52
+ fetchImpl,
53
+ dispatcher = null,
54
+ } = {}) {
55
+ async function attemptOnce(body, key, opts) {
56
+ const headers = buildHeaders ? buildHeaders(body, key, opts) : {};
57
+ const wantsStream = body?.stream !== false;
58
+ if (sdkEnabled && wantsStream) {
59
+ try {
60
+ const r = await attemptOnceResponsesSdk({
61
+ url, body, headers, providerName: id, marker: ENGINE_MARKER, fetchImpl,
62
+ });
63
+ if (r) return r; // 适配器已产出 chat SSE
64
+ } catch (e) {
65
+ if (!e || (!e._sdkLoadFailed && !e._sdkUnsupported)) throw e;
66
+ // SDK 装载失败 / URL 异形 → 落原生兜底(保底不变差)
67
+ }
68
+ }
69
+ const controller = new AbortController();
70
+ const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
71
+ try {
72
+ const fetchOpts = { method: "POST", headers, body: JSON.stringify(chatToResponsesBody(body)), signal: controller.signal };
73
+ if (dispatcher) fetchOpts.dispatcher = dispatcher;
74
+ const res = await fetchImpl(url, fetchOpts);
75
+ if (!res.ok) return res;
76
+ if (wantsStream) return reshapeResponsesSse(res, body?.model);
77
+ const json = await res.json().catch(() => null);
78
+ return json ? toChatResponse(res, json) : res;
79
+ } finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+
84
+ /** key 轮换 + 退避重试 + _t 计时;语义对齐 base.js 的 createChatRunner.runChat */
85
+ async function run(body, activeRing, sourceKey, opts) {
86
+ const t0 = performance.now();
87
+ const attempts = [];
88
+ let waitMs = 0;
89
+ const key = activeRing.next();
90
+ if (!key && activeRing.size > 0) {
91
+ const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
92
+ err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
93
+ throw err;
94
+ }
95
+ for (let attempt = 0; ; attempt++) {
96
+ const t = performance.now();
97
+ let result;
98
+ try {
99
+ result = await attemptOnce(body, key, opts);
100
+ } catch (err) {
101
+ result = err;
102
+ }
103
+ attempts.push({ attempt, type: result instanceof Error ? "network" : `http${result?.status}`, ms: Math.round(performance.now() - t) });
104
+ if (result instanceof Error) {
105
+ const entry = retry?.network;
106
+ if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
107
+ activeRing.onError(key);
108
+ result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
109
+ throw result;
110
+ }
111
+ const entry = retry?.[result.status];
112
+ if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
113
+ if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
114
+ try { result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) }; } catch {}
115
+ return result;
116
+ }
117
+ }
118
+
119
+ return { url, attemptOnce, run };
120
+ }