mslxdff 0.1.110 → 0.1.112

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.
@@ -0,0 +1,93 @@
1
+ // AI SDK parts → OpenAI SSE 帧(供 reshape/转发链按原生 workbuddy 帧同等处理)。
2
+ // 纯函数序列化器:push(part) 返回该 part 产生的 SSE 文本(可能为空串/组合帧),end() 返回 [DONE]。
3
+ // 见 .scratch/workbuddy-sdk-channel/SPEC.md。
4
+
5
+ export function usageToOpenAI(u) {
6
+ if (!u || typeof u !== "object") return undefined;
7
+ if (u.raw && typeof u.raw === "object") return u.raw;
8
+ const prompt = u.inputTokens?.total ?? 0;
9
+ const completion = u.outputTokens?.total ?? 0;
10
+ return { prompt_tokens: prompt, completion_tokens: completion, total_tokens: prompt + completion };
11
+ }
12
+
13
+ export function createSseSerializer() {
14
+ const meta = { id: "chatcmpl-wb-sdk", model: "", created: Math.floor(Date.now() / 1000) };
15
+ let roleSent = false;
16
+ let nextIndex = 0;
17
+ const toolIndex = new Map();
18
+ const toolDeltaIds = new Set();
19
+
20
+ function frame(delta, { finishReason = null, usage } = {}) {
21
+ const obj = {
22
+ id: meta.id,
23
+ object: "chat.completion.chunk",
24
+ created: meta.created,
25
+ model: meta.model,
26
+ choices: [{ index: 0, delta, finish_reason: finishReason }],
27
+ };
28
+ if (usage !== undefined) obj.usage = usage;
29
+ return `data: ${JSON.stringify(obj)}\n\n`;
30
+ }
31
+
32
+ function ensureRole() {
33
+ if (roleSent) return "";
34
+ roleSent = true;
35
+ return frame({ role: "assistant" });
36
+ }
37
+
38
+ function push(part) {
39
+ if (!part || typeof part !== "object") return null;
40
+ switch (part.type) {
41
+ case "response-metadata": {
42
+ if (part.id) meta.id = String(part.id);
43
+ if (part.modelId) meta.model = String(part.modelId);
44
+ if (part.timestamp) {
45
+ const t = new Date(part.timestamp).getTime();
46
+ if (Number.isFinite(t)) meta.created = Math.floor(t / 1000);
47
+ }
48
+ return null;
49
+ }
50
+ case "reasoning-delta":
51
+ return ensureRole() + frame({ reasoning_content: String(part.delta ?? "") });
52
+ case "text-delta":
53
+ return ensureRole() + frame({ content: String(part.delta ?? "") });
54
+ case "tool-input-start": {
55
+ const idx = nextIndex++;
56
+ toolIndex.set(part.id, idx);
57
+ toolDeltaIds.add(part.id);
58
+ return ensureRole() + frame({
59
+ tool_calls: [{ index: idx, id: part.id, type: "function", function: { name: part.toolName || "", arguments: "" } }],
60
+ });
61
+ }
62
+ case "tool-input-delta": {
63
+ const idx = toolIndex.has(part.id) ? toolIndex.get(part.id) : 0;
64
+ return frame({ tool_calls: [{ index: idx, function: { arguments: String(part.delta ?? "") } }] });
65
+ }
66
+ case "tool-call": {
67
+ const id = part.toolCallId;
68
+ const idx = toolIndex.has(id) ? toolIndex.get(id) : nextIndex++;
69
+ toolIndex.set(id, idx);
70
+ if (toolDeltaIds.has(id)) return null;
71
+ const args = typeof part.input === "string" ? part.input : JSON.stringify(part.input ?? {});
72
+ return ensureRole() + frame({ tool_calls: [{ index: idx, id, type: "function", function: { name: part.toolName || "", arguments: args } }] });
73
+ }
74
+ case "finish": {
75
+ const reason = part.finishReason?.raw ?? part.finishReason?.unified ?? "stop";
76
+ return frame({}, { finishReason: reason, usage: usageToOpenAI(part.usage) });
77
+ }
78
+ case "error": {
79
+ const message = part.error?.message
80
+ || (typeof part.error === "string" ? part.error : JSON.stringify(part.error ?? "unknown error"));
81
+ return `data: ${JSON.stringify({ error: { message } })}\n\n`;
82
+ }
83
+ default:
84
+ return null;
85
+ }
86
+ }
87
+
88
+ function end() {
89
+ return "data: [DONE]\n\n";
90
+ }
91
+
92
+ return { push, end };
93
+ }
@@ -0,0 +1,52 @@
1
+ // ADR-0015 展示渲染:-group list 行缀 + -status 汇总(纯函数,输入 routes 数据)
2
+ // 四态文案:无数据→引导;有数据→摘要;坏数据→标 offline
3
+
4
+ function bestDisplay(entry) {
5
+ if (!entry) return null;
6
+ const best = String(entry.best || "direct");
7
+ const ms = best === "direct" ? (entry.direct?.ttfbMs ?? entry.direct?.totalMs) : (entry.via?.[best.slice(4)]?.ttfbMs ?? entry.via?.[best.slice(4)]?.totalMs);
8
+ const tag = best === "direct" ? "direct" : `via ${best.slice(4)}`;
9
+ if (entry.direct?.ok === false && !best.startsWith("via:")) return `${tag} (${entry.direct?.label || "offline"})`;
10
+ return ms != null ? `${tag} ${ms}ms` : tag;
11
+ }
12
+
13
+ // -group list 静态成员行尾缀:该成员作为 best 出现时的供应商与相对直连收益
14
+ export function groupRowSuffix(peerUrl, routes) {
15
+ if (!peerUrl || !routes || typeof routes !== "object") return null;
16
+ const label = (() => {
17
+ const raw = String(peerUrl);
18
+ if (raw.includes("://")) {
19
+ try { const u = new URL(raw); return `${u.hostname}${u.port ? `:${u.port}` : ""}`; } catch { return raw.slice(-16); }
20
+ }
21
+ return raw;
22
+ })();
23
+ const wins = [];
24
+ for (const [key, entry] of Object.entries(routes)) {
25
+ if (!key.startsWith("provider:")) continue;
26
+ const prov = key.slice(9);
27
+ if (entry?.best === `via:${label}`) {
28
+ const d = entry.deltaMs;
29
+ wins.push(d != null ? `${prov} ${d > 0 ? "+" : ""}${d}ms` : prov);
30
+ }
31
+ }
32
+ if (!wins.length) return null;
33
+ return ` via-routes: ${wins.join(", ")}`;
34
+ }
35
+
36
+ // -status 汇总(多行字符串):探针条目摘要 ≤5 条;空数据给引导
37
+ export function statusViaSummary({ routes, meta, probeMs }) {
38
+ const list = Object.entries(routes || {}).filter(([k]) => k.startsWith("provider:"));
39
+ if (!list.length) {
40
+ return `via-routes: 无探针数据(后台探针 ${probeMs === 0 ? "已关闭" : "自动运行中"},加入组并配置 key/token 类供应商后自动生成)`;
41
+ }
42
+ const at = meta?.at ? new Date(meta.at) : null;
43
+ const atStr = at && !Number.isNaN(at.getTime()) ? at.toISOString().slice(11, 19) + "Z" : "";
44
+ const lines = [`via-routes: ${list.length} 条(探针自动${atStr ? `,${atStr} 刷新` : ""})`];
45
+ for (const [key, entry] of list.slice(0, 5)) {
46
+ const prov = key.slice(9);
47
+ const b = bestDisplay(entry);
48
+ if (b) lines.push(` ${prov} → ${b}${entry.deltaMs != null ? ` (${entry.deltaMs > 0 ? "+" : ""}${entry.deltaMs}ms vs direct)` : ""}`);
49
+ }
50
+ if (list.length > 5) lines.push(` … 其余 ${list.length - 5} 条见 via-routes.json`);
51
+ return lines.join("\n");
52
+ }
@@ -0,0 +1,49 @@
1
+ // ADR-0015 探针薄层:direct GET 探针 + 组员 /v1/relay 代发探针
2
+ // 只测 GET <modelsPath>,不烧 token;组长侧计时。
3
+ import { errMsg } from "../cli/util.js";
4
+ import { compatFetch, timeoutSignal } from "../compat.js";
5
+
6
+ export const PROBE_TIMEOUT_MS = 5000;
7
+
8
+ export async function directProbe({ baseUrl, modelsPath = "/models", key = "", timeoutMs = PROBE_TIMEOUT_MS, fetchImpl = compatFetch, clock = Date.now, sampleMs } = {}) {
9
+ const base = String(baseUrl || "").replace(/\/+$/, "");
10
+ if (!base) return { ok: false, label: "配置错误", error: "missing baseUrl", ttfbMs: null };
11
+ const p = String(modelsPath || "/models");
12
+ const url = `${base}${p.startsWith("/") ? p : `/${p}`}`;
13
+ const t0 = sampleMs ? sampleMs() : clock();
14
+ try {
15
+ const headers = { Accept: "application/json" };
16
+ if (key) headers.Authorization = `Bearer ${key}`;
17
+ const res = await fetchImpl(url, { method: "GET", headers, signal: timeoutSignal(timeoutMs) });
18
+ const ms = sampleMs ? sampleMs() - t0 : clock() - t0;
19
+ const status = typeof res.status === "number" ? res.status : (res.ok ? 200 : 0);
20
+ if (status >= 400) return { ok: false, label: `HTTP ${status}`, ttfbMs: null };
21
+ return { ok: true, ttfbMs: Math.max(0, Math.round(ms)) };
22
+ } catch (e) {
23
+ return { ok: false, label: errMsg(e).slice(0, 60), ttfbMs: null };
24
+ }
25
+ }
26
+
27
+ // 组员代发:POST <peer>/v1/relay { targetUrl, method:"GET", headers } → 组员 fetch 上游后原样回
28
+ export async function relayViaProbe({ peerUrl, peerToken = "", targetUrl, authHeader = "", timeoutMs = PROBE_TIMEOUT_MS, fetchImpl = compatFetch, clock = Date.now, sampleMs } = {}) {
29
+ const peer = String(peerUrl || "").replace(/\/+$/, "");
30
+ if (!peer || !targetUrl) return { ok: false, label: "配置错误", ttfbMs: null };
31
+ const t0 = sampleMs ? sampleMs() : clock();
32
+ try {
33
+ const headers = { "Content-Type": "application/json", Accept: "application/json" };
34
+ if (peerToken) headers.Authorization = `Bearer ${peerToken}`;
35
+ const res = await fetchImpl(`${peer}/v1/relay`, {
36
+ method: "POST",
37
+ headers,
38
+ body: JSON.stringify({ targetUrl, method: "GET", headers: authHeader ? { Authorization: authHeader } : {} }),
39
+ signal: timeoutSignal(timeoutMs),
40
+ });
41
+ const ms = sampleMs ? sampleMs() - t0 : clock() - t0;
42
+ const relayStatus = res.headers?.get?.("x-mslxdff-relay-status");
43
+ const status = relayStatus != null ? Number(relayStatus) : (res.status ?? 0);
44
+ if (status >= 400) return { ok: false, label: `HTTP ${status}`, ttfbMs: null };
45
+ return { ok: true, ttfbMs: Math.max(0, Math.round(ms)) };
46
+ } catch (e) {
47
+ return { ok: false, label: errMsg(e).slice(0, 60), ttfbMs: null };
48
+ }
49
+ }
@@ -0,0 +1,110 @@
1
+ // ADR-0015 轮转调度:每 tick 探 1 家供应商(direct + 逐 peer relay),EMA 合并旧值后落 via-routes.json
2
+ // 复用 bench/via-routes.js 的表格式与读写;探针写 provider:<id> 键,getViaRoute 精确键优先、provider 级回退。
3
+ import { directProbe, relayViaProbe } from "./probe.js";
4
+ import { classifyProvider, emaMerge } from "../providers/classify.js";
5
+ import { saveViaRoutes, loadViaRoutes } from "../bench/via-routes.js";
6
+ import { errMsg } from "../cli/util.js";
7
+
8
+ export const PROBE_DELAY_MS = Number(process.env.MSLXDFF_BENCH_DELAY_MS || 120) || 0;
9
+
10
+ // 从 providerConfigs 收集探针目标:local-only(workbuddy)/quota-pool(opencode) 排除,无 baseUrl 跳过
11
+ export function probeTargetsFromState({ loadProviderConfigs, loadProviderKeys, loadProviderBaseUrl } = {}) {
12
+ const out = [];
13
+ const ids = new Set(Object.keys(loadProviderConfigs?.() || {}));
14
+ for (const id of ids) {
15
+ if (classifyProvider(id) !== "latency-compare") continue;
16
+ const cfg = (loadProviderConfigs?.() || {})[id] || {};
17
+ const baseUrl = String(loadProviderBaseUrl?.(id) || cfg.baseUrl || "").replace(/\/+$/, "");
18
+ if (!baseUrl) continue;
19
+ out.push({ id, baseUrl, modelsPath: cfg.modelsPath || "/models", key: (loadProviderKeys?.(id) || [])[0] || "" });
20
+ }
21
+ return out;
22
+ }
23
+
24
+ export function nextCursor(cursor, total) {
25
+ if (!Number.isInteger(total) || total <= 0) return 0;
26
+ const c = Number.isInteger(cursor) ? cursor : 0;
27
+ return c + 1 >= total ? 0 : c + 1;
28
+ }
29
+
30
+ function peerLabel(p) {
31
+ const raw = String(p?.url || "");
32
+ if (!raw) return "";
33
+ if (raw.includes("://")) {
34
+ try { const u = new URL(raw); return `${u.hostname}${u.port ? `:${u.port}` : ""}`; } catch { return raw.slice(-16); }
35
+ }
36
+ return raw;
37
+ }
38
+
39
+ // 单 tick:探 targets[cursor] 一家;返回 { probed, skipped }
40
+ export async function rotateTick({
41
+ targets,
42
+ cursor = 0,
43
+ peers,
44
+ timeoutMs = 5000,
45
+ delayMs = PROBE_DELAY_MS,
46
+ evt = () => {},
47
+ fetchImpl,
48
+ clock = Date.now,
49
+ sampleMs,
50
+ file,
51
+ } = {}) {
52
+ const list = Array.isArray(targets) ? targets : [];
53
+ if (!list.length) return { probed: null, skipped: true };
54
+ const idx = cursor % list.length;
55
+ const target = list[idx];
56
+ const peerList = (peers?.all?.() || []).filter((p) => p?.url && !String(p.url).startsWith("relay://"));
57
+ if (!peerList.length) {
58
+ evt("upstream-probe-skip", { provider: target.id, reason: "no peers" });
59
+ return { probed: null, skipped: true };
60
+ }
61
+ const authHeader = target.key ? `Bearer ${target.key}` : "";
62
+ const direct = await directProbe({ baseUrl: target.baseUrl, modelsPath: target.modelsPath, key: target.key, timeoutMs, fetchImpl, clock, sampleMs });
63
+ const via = {};
64
+ for (const p of peerList) {
65
+ const label = peerLabel(p) || String(p.url);
66
+ const r = await relayViaProbe({ peerUrl: p.url, peerToken: p.token || "", targetUrl: `${target.baseUrl}${target.modelsPath}`, authHeader, timeoutMs, fetchImpl, clock, sampleMs });
67
+ via[label] = { ok: r.ok, ttfbMs: r.ok ? r.ttfbMs : null, totalMs: r.ok ? r.ttfbMs : null, label: r.ok ? null : r.label };
68
+ if (delayMs) await new Promise((res) => setTimeout(res, delayMs));
69
+ }
70
+ const prev = loadViaRoutes(file).routes?.[`provider:${target.id}`] || null;
71
+ const fresh = prev?.at && Date.now() - Date.parse(prev.at) < 10 * 60_000 ? prev : null;
72
+ const merged = mergeEntry({ prev: fresh, direct, via });
73
+ const results = [{ model: `provider:${target.id}`, ...merged, provider: target.id }];
74
+ try {
75
+ saveViaRoutes(results, { file, meta: { probe: true } });
76
+ } catch (e) {
77
+ evt("upstream-probe-error", { provider: target.id, error: errMsg(e).slice(0, 120) });
78
+ return { probed: target.id, skipped: false, error: errMsg(e) };
79
+ }
80
+ evt("upstream-probe", { provider: target.id, direct: direct.ok ? direct.ttfbMs : null, best: merged.best, peers: Object.keys(via).length });
81
+ return { probed: target.id, skipped: false };
82
+ }
83
+
84
+ // best 计算 + EMA 合并(相对 direct:via 胜出记负 delta)
85
+ function mergeEntry({ prev, direct, via }) {
86
+ const ema = (p, r) => emaMerge(p, r?.ok ? r.ttfbMs : null);
87
+ const directOut = {
88
+ ok: Boolean(direct?.ok),
89
+ ttfbMs: direct?.ok ? ema(prev?.direct?.ttfbMs, direct) : null,
90
+ totalMs: direct?.ok ? ema(prev?.direct?.totalMs ?? prev?.direct?.ttfbMs, direct) : null,
91
+ label: direct?.ok ? null : (direct?.label || "offline"),
92
+ error: direct?.ok ? null : (direct?.label || null),
93
+ };
94
+ const viaOut = {};
95
+ for (const [k, v] of Object.entries(via || {})) {
96
+ viaOut[k] = v.ok
97
+ ? { ok: true, ttfbMs: ema(prev?.via?.[k]?.ttfbMs, v), totalMs: ema(prev?.via?.[k]?.totalMs ?? prev?.via?.[k]?.ttfbMs, v) }
98
+ : { ok: false, ttfbMs: v.ttfbMs ?? null, totalMs: v.totalMs ?? null, label: v.label || "offline" };
99
+ }
100
+ const directMs = directOut.ok ? (directOut.ttfbMs ?? directOut.totalMs) : Infinity;
101
+ let best = "direct";
102
+ let bestMs = directMs;
103
+ for (const [k, v] of Object.entries(viaOut)) {
104
+ if (!v.ok) continue;
105
+ const t = v.ttfbMs ?? v.totalMs;
106
+ if (Number.isFinite(t) && t < bestMs) { bestMs = t; best = `via:${k}`; }
107
+ }
108
+ const deltaMs = best.startsWith("via:") && Number.isFinite(directMs) ? Math.round(bestMs - directMs) : null;
109
+ return { best, direct: directOut, via: viaOut, deltaMs };
110
+ }
@@ -0,0 +1,45 @@
1
+ // ADR-0015 daemon 装配:60s 节拍轮转探针;0=完全关闭
2
+ import { probeTargetsFromState, rotateTick, nextCursor } from "./rotate.js";
3
+ import { loadProviderConfigs, loadProviderKeys, loadProviderBaseUrl } from "../state.js";
4
+ import { loadPeers } from "../state.js";
5
+
6
+ export function upstreamProbeIntervalMs() {
7
+ const n = Number(process.env.MSLXDFF_UPSTREAM_PROBE_MS);
8
+ return Number.isInteger(n) && n >= 0 ? n : 60_000;
9
+ }
10
+
11
+ export function startUpstreamProbe({ evt = () => {}, peers, file, delayMs } = {}) {
12
+ const interval = upstreamProbeIntervalMs();
13
+ if (interval === 0) return null;
14
+ let cursor = 0;
15
+ let busy = false;
16
+ const loadTargets = () => {
17
+ try {
18
+ return probeTargetsFromState({
19
+ loadProviderConfigs: () => loadProviderConfigs({}),
20
+ loadProviderKeys: (id) => loadProviderKeys(id, {}),
21
+ loadProviderBaseUrl: (id) => loadProviderBaseUrl(id, {}),
22
+ });
23
+ } catch {
24
+ return [];
25
+ }
26
+ };
27
+ const tick = async () => {
28
+ if (busy) return;
29
+ busy = true;
30
+ try {
31
+ const targets = loadTargets();
32
+ if (!targets.length) return;
33
+ const out = await rotateTick({ targets, cursor, peers, evt, file, delayMs });
34
+ if (out.probed != null) cursor = nextCursor(cursor, targets.length);
35
+ } catch {
36
+ // 探针永不影响主链路
37
+ } finally {
38
+ busy = false;
39
+ }
40
+ };
41
+ void tick();
42
+ const timer = setInterval(tick, interval);
43
+ timer.unref?.();
44
+ return { stop: () => clearInterval(timer), interval, tick };
45
+ }
package/src/upstream.js CHANGED
@@ -22,23 +22,11 @@ function isPreheatDisabled() {
22
22
  const s = String(raw).trim().toLowerCase();
23
23
  return s === "0" || s === "off" || s === "false" || s === "no" || s === "disable" || s === "disabled";
24
24
  }
25
- export function createUpstreamClient({
26
- baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
27
- authToken = process.env.UPSTREAM_AUTH_TOKEN || "public",
28
- connectTimeoutMs = Number(process.env.UPSTREAM_CONNECT_TIMEOUT_MS) || 30_000,
29
- retry = {
30
- network: { attempts: 2, delayMs: 300 },
31
- 429: { attempts: 1, delayMs: 100 },
32
- 502: { attempts: 1, delayMs: 100 },
33
- 503: { attempts: 1, delayMs: 100 },
34
- 504: { attempts: 1, delayMs: 100 },
35
- },
36
- fetchImpl,
37
- hooks,
38
- keepAlive = true,
39
- } = {}) {
25
+ // opencode 上游身份头单一来源(legacy 与 SDK 引擎共用,free 层门禁头必须一致)。
26
+ // anonFirst 语义:authToken==="public" 时默认匿名(hermes referer);env 可显式覆盖。
27
+ export function createOpencodeHeaderBuilder({ authToken = "public", env = process.env } = {}) {
40
28
  function isAnonFirst() {
41
- const raw = process.env.MSLXDFF_OPENCOD_ANON_FIRST ?? process.env.MSLXDFF_ANON_FIRST ?? process.env.MSLXDFF_OPENCOD_ANON;
29
+ const raw = env.MSLXDFF_OPENCOD_ANON_FIRST ?? env.MSLXDFF_ANON_FIRST ?? env.MSLXDFF_OPENCOD_ANON;
42
30
  if (raw === undefined || raw === null || raw === "") return authToken === "public";
43
31
  const s = String(raw).trim().toLowerCase();
44
32
  if (s === "0" || s === "false" || s === "off" || s === "no" || s === "disable" || s === "disabled") return false;
@@ -78,6 +66,25 @@ export function createUpstreamClient({
78
66
  }
79
67
  return base;
80
68
  }
69
+ return { buildHeaders, anonFirst };
70
+ }
71
+
72
+ export function createUpstreamClient({
73
+ baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
74
+ authToken = process.env.UPSTREAM_AUTH_TOKEN || "public",
75
+ connectTimeoutMs = Number(process.env.UPSTREAM_CONNECT_TIMEOUT_MS) || 30_000,
76
+ retry = {
77
+ network: { attempts: 2, delayMs: 300 },
78
+ 429: { attempts: 1, delayMs: 100 },
79
+ 502: { attempts: 1, delayMs: 100 },
80
+ 503: { attempts: 1, delayMs: 100 },
81
+ 504: { attempts: 1, delayMs: 100 },
82
+ },
83
+ fetchImpl,
84
+ hooks,
85
+ keepAlive = true,
86
+ } = {}) {
87
+ const { buildHeaders, anonFirst } = createOpencodeHeaderBuilder({ authToken });
81
88
  function shouldTryAnonFree() {
82
89
  const raw = process.env.MSLXDFF_FREE_ANON;
83
90
  if (raw === "0" || raw === "off" || raw === "false" || raw === "no") return false;