mslxdff 0.1.231 → 0.1.233
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/bin/mslxdff.js +3 -3
- package/package.json +1 -1
- package/src/auto.js +264 -254
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +155 -145
- package/src/chat-pipeline/policy.js +84 -84
- package/src/cli/commands/group.js +266 -266
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +139 -139
- package/src/cli/commands/provider/models.js +149 -149
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/provider/raccoon-quota.js +156 -156
- package/src/cli/commands/sync.js +310 -310
- package/src/cli/status.js +279 -279
- package/src/daemon.js +246 -246
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/model-trace.js +10 -2
- package/src/models.js +292 -225
- package/src/peers.js +345 -345
- package/src/providers/agentrouter.js +238 -238
- package/src/providers/base.js +164 -164
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/codearts/chat.js +207 -207
- package/src/providers/dispatcher.js +147 -147
- package/src/providers/globalqwenwork/cosy.js +123 -123
- package/src/providers/globalqwenwork/crypto.js +218 -218
- package/src/providers/globalqwenwork/http.js +20 -20
- package/src/providers/globalqwenwork/rsa.js +54 -54
- package/src/providers/globalqwenwork/sse.js +268 -268
- package/src/providers/globalqwenwork/stream.js +130 -130
- package/src/providers/globalqwenwork/upstream.js +122 -122
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/account-store.js +18 -0
- package/src/providers/qoder/chat.js +292 -203
- package/src/providers/qoder/checkin.js +25 -12
- package/src/providers/qoder/index.js +311 -249
- package/src/providers/qoder/models.js +64 -2
- package/src/providers/qoder/oauth.js +42 -1
- package/src/providers/qoder/payload.js +116 -80
- package/src/providers/qoder/request.js +7 -2
- package/src/providers/qoder/session.js +26 -10
- package/src/providers/qoder/sse.js +233 -145
- package/src/providers/qoder/sticky.js +5 -2
- package/src/providers/qoder/wire-messages.js +242 -0
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +337 -337
- package/src/providers/qwenwork/payload.js +144 -144
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/raccoon/chat.js +1 -1
- package/src/providers/raccoon/const.js +219 -202
- package/src/providers/raccoon/credits.js +285 -285
- package/src/providers/raccoon/index.js +15 -2
- package/src/providers/raccoon/models.js +68 -17
- package/src/providers/registry.js +89 -89
- package/src/providers/workbuddy/auth.js +16 -0
- package/src/providers/workbuddy/chat.js +298 -264
- package/src/providers/workbuddy/client-headers.js +148 -0
- package/src/providers/workbuddy/desktop-info.js +17 -1
- package/src/providers/workbuddy/models.js +8 -20
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/reasoning.js +32 -32
- package/src/routes/chat/broadband-handler.js +80 -80
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +282 -282
- package/src/routes/chat/via-route-handler.js +156 -156
- package/src/routes/hedge.js +260 -260
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +373 -373
- package/src/routes/stream.js +364 -347
- package/src/runtime/bootstrap.js +75 -75
- package/src/runtime/keep-warm.js +135 -135
- package/src/runtime/provider-gate.js +75 -75
- package/src/runtime/providers-setup.js +187 -187
- package/src/runtime/raccoon-checkin.js +175 -175
- package/src/server.js +85 -85
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +355 -355
- package/docs/cli_help_mini.md +0 -148
- package/src/providers/AGENTS.md +0 -49
- package/src/routes/AGENTS.md +0 -37
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// qoder 模型服务(对齐官方 qodercli + 外部参考 magpie qoder 插件/CLIProxyAPI):
|
|
2
|
+
// 原生 COSY 签名拉 /model/list,10min 缓存(按 region 分桶);
|
|
3
|
+
// 只暴露 enable=true 的可调用模型(enable=false 调对话必挂,不展示)。
|
|
4
|
+
// context_config(多窗口 200K/400K/1M)与 thinking_config(推理档位)随目录带出,
|
|
5
|
+
// 供 payload 选窗口/档位、供能力层展示——旧版只留 max_input_tokens,拿不到多窗口是"不好用"的一环。
|
|
3
6
|
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
4
7
|
import { getEndpoints, normalizeRegion } from "./constants.js";
|
|
5
8
|
import { buildCosyHeaders, pathSigFrom } from "./session.js";
|
|
@@ -9,10 +12,57 @@ const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
|
9
12
|
const CREATED = 1753600000;
|
|
10
13
|
const OWNER = "qoder";
|
|
11
14
|
|
|
15
|
+
const positive = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
|
|
16
|
+
|
|
17
|
+
// context_config → { windows:[...升序], defaultWindow }(Qoder 的 Context 设置:200K/400K/1M)
|
|
18
|
+
function parseContextConfig(value) {
|
|
19
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
20
|
+
const windows = [];
|
|
21
|
+
let defaultWindow;
|
|
22
|
+
for (const raw of Object.values(value)) {
|
|
23
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
24
|
+
const tc = positive(raw.token_count);
|
|
25
|
+
if (tc === undefined) continue;
|
|
26
|
+
windows.push(tc);
|
|
27
|
+
if (raw.is_default === true) defaultWindow = tc;
|
|
28
|
+
}
|
|
29
|
+
windows.sort((a, b) => a - b);
|
|
30
|
+
return {
|
|
31
|
+
...(windows.length ? { windows } : {}),
|
|
32
|
+
...(defaultWindow !== undefined ? { defaultWindow } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// thinking_config → { efforts:[...], defaultEffort, enabled }(推理档位与是否可关思考)
|
|
37
|
+
const EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"];
|
|
38
|
+
function parseThinkingConfig(value) {
|
|
39
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
40
|
+
const out = {};
|
|
41
|
+
const isDefault = (e) => typeof e === "object" && e !== null && e.is_default === true;
|
|
42
|
+
const enabled = value.enabled && typeof value.enabled === "object" ? value.enabled : null;
|
|
43
|
+
if (enabled) {
|
|
44
|
+
out.thinks = true;
|
|
45
|
+
const raw = typeof enabled.efforts === "object" && enabled.efforts !== null && !Array.isArray(enabled.efforts) ? enabled.efforts : null;
|
|
46
|
+
if (raw) {
|
|
47
|
+
const efforts = Object.keys(raw).filter((k) => k.trim());
|
|
48
|
+
efforts.sort((a, b) => (EFFORT_ORDER.indexOf(a) < 0 ? 99 : EFFORT_ORDER.indexOf(a)) - (EFFORT_ORDER.indexOf(b) < 0 ? 99 : EFFORT_ORDER.indexOf(b)));
|
|
49
|
+
if (efforts.length) out.efforts = efforts;
|
|
50
|
+
for (const [k, v] of Object.entries(raw)) if (isDefault(v)) out.defaultEffort = k;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (value.disabled && typeof value.disabled === "object") out.alwaysThinks = false;
|
|
54
|
+
// enabled.is_default 与 disabled.is_default 都为 true/都 false → 上游给的是"默认开还是默认关"
|
|
55
|
+
if (enabled && isDefault(value.disabled)) out.thinkDefault = false;
|
|
56
|
+
else if (enabled && isDefault(enabled)) out.thinkDefault = true;
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
12
60
|
export function extractModels(chatList) {
|
|
13
61
|
const out = [];
|
|
14
62
|
for (const m of Array.isArray(chatList) ? chatList : []) {
|
|
15
63
|
if (!m?.key) continue;
|
|
64
|
+
const ctx = parseContextConfig(m.context_config);
|
|
65
|
+
const think = parseThinkingConfig(m.thinking_config);
|
|
16
66
|
out.push({
|
|
17
67
|
id: m.key,
|
|
18
68
|
name: String(m.display_name || m.key),
|
|
@@ -20,12 +70,18 @@ export function extractModels(chatList) {
|
|
|
20
70
|
isDefault: Boolean(m.is_default),
|
|
21
71
|
isReasoning: Boolean(m.is_reasoning),
|
|
22
72
|
contextLength: Number(m.max_input_tokens) || 0,
|
|
73
|
+
maxOutputTokens: positive(m.max_output_tokens),
|
|
23
74
|
priceFactor: Number(m.price_factor) || 0,
|
|
75
|
+
isVl: m.is_vl === true,
|
|
76
|
+
source: m.source || "system",
|
|
77
|
+
...ctx,
|
|
78
|
+
...think,
|
|
24
79
|
});
|
|
25
80
|
}
|
|
26
81
|
return out;
|
|
27
82
|
}
|
|
28
83
|
|
|
84
|
+
|
|
29
85
|
export function createModelsService({ id = "qoder", fetchImpl, clock = Date.now } = {}) {
|
|
30
86
|
let cache = null, fetchedAt = 0;
|
|
31
87
|
|
|
@@ -59,6 +115,12 @@ export function createModelsService({ id = "qoder", fetchImpl, clock = Date.now
|
|
|
59
115
|
enable: true,
|
|
60
116
|
is_reasoning: m.isReasoning,
|
|
61
117
|
price_factor: m.priceFactor,
|
|
118
|
+
// 供 payload 选窗口/档位、能力层展示(随目录带出,不进 /v1/models 也无害,额外字段客户端忽略)
|
|
119
|
+
...(m.windows ? { context_windows: m.windows } : {}),
|
|
120
|
+
...(m.defaultWindow ? { default_window: m.defaultWindow } : {}),
|
|
121
|
+
...(m.efforts ? { reasoning_efforts: m.efforts } : {}),
|
|
122
|
+
...(m.defaultEffort ? { default_reasoning_effort: m.defaultEffort } : {}),
|
|
123
|
+
...(m.maxOutputTokens ? { max_output_tokens: m.maxOutputTokens } : {}),
|
|
62
124
|
}));
|
|
63
125
|
cacheByRegion.set(normRegion, { list, at: now });
|
|
64
126
|
// 兼容旧单测 _getCache:默认读 global 桶
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 3. fetchUserInfo: GET /api/v1/userinfo(Bearer dt-xxx)→ uid/name/organization_id
|
|
5
5
|
import { randomBytes, createHash } from "node:crypto";
|
|
6
6
|
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
7
|
-
import { OAUTH_CLIENT_ID, getEndpoints } from "./constants.js";
|
|
7
|
+
import { OAUTH_CLIENT_ID, getEndpoints, openapiBase } from "./constants.js";
|
|
8
8
|
|
|
9
9
|
const POLL_MS = 3000;
|
|
10
10
|
export const POLL_TIMEOUT_MS = 10 * 60 * 1000; // 对齐 qoder2api 10 分钟
|
|
@@ -82,4 +82,45 @@ export async function waitForAuth({ region, nonce, verifier, fetchImpl = compatF
|
|
|
82
82
|
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
83
83
|
try { process.stdout.write("."); } catch {}
|
|
84
84
|
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// device token 续期(对齐官方 CLI + magpie refreshDevice):deviceToken 时效短(约 1 天),
|
|
88
|
+
// 官方在收到 105 Login expired 时用 refresh_token 换全新的一对(旧 refresh_token 一次性,换到即弃)。
|
|
89
|
+
// POST {openapiBase}/api/v1/deviceToken/refresh body{refresh_token} → { token|device_token, refresh_token }。
|
|
90
|
+
// 只带 refresh_token 认证(过期 deviceToken 无需当 Bearer);401/403 = refresh 也被拒 → 真需重登。
|
|
91
|
+
// 抛错语义:{ retryable } 标记——网络/5xx 可稍后再试,4xx(refresh 作废)不可重试。
|
|
92
|
+
export async function refreshDeviceToken({ region = "global", refreshToken, fetchImpl = compatFetch } = {}) {
|
|
93
|
+
const rt = String(refreshToken || "").trim();
|
|
94
|
+
if (!rt) { const e = new Error("无 refresh_token,需重新登录"); e.retryable = false; throw e; }
|
|
95
|
+
const url = `${openapiBase(region)}/api/v1/deviceToken/refresh`;
|
|
96
|
+
let res;
|
|
97
|
+
try {
|
|
98
|
+
res = await fetchImpl(url, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
101
|
+
body: JSON.stringify({ refresh_token: rt }),
|
|
102
|
+
signal: timeoutSignal(20000),
|
|
103
|
+
});
|
|
104
|
+
} catch (e) {
|
|
105
|
+
const err = new Error(`deviceToken 刷新请求失败: ${String(e?.message || e).slice(0, 160)}`);
|
|
106
|
+
err.retryable = true; throw err;
|
|
107
|
+
}
|
|
108
|
+
const txt = await res.text().catch(() => "");
|
|
109
|
+
if (res.status === 401 || res.status === 403) {
|
|
110
|
+
const err = new Error(`deviceToken 刷新被拒 HTTP ${res.status}(refresh_token 已作废,需重新登录)`);
|
|
111
|
+
err.retryable = false; throw err;
|
|
112
|
+
}
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
const err = new Error(`deviceToken 刷新上游 HTTP ${res.status}: ${txt.slice(0, 160)}`);
|
|
115
|
+
err.retryable = true; throw err;
|
|
116
|
+
}
|
|
117
|
+
let j;
|
|
118
|
+
try { j = JSON.parse(txt); } catch { const err = new Error(`deviceToken 刷新返回非 JSON: ${txt.slice(0, 160)}`); err.retryable = false; throw err; }
|
|
119
|
+
const deviceToken = String(j.token || j.device_token || "").trim();
|
|
120
|
+
const newRefresh = String(j.refresh_token || j.refreshToken || "").trim();
|
|
121
|
+
if (!deviceToken || !newRefresh) {
|
|
122
|
+
const err = new Error("deviceToken 刷新返回不完整 token 对(缺 token 或 refresh_token)");
|
|
123
|
+
err.retryable = false; throw err;
|
|
124
|
+
}
|
|
125
|
+
return { deviceToken, refreshToken: newRefresh };
|
|
85
126
|
}
|
|
@@ -1,13 +1,28 @@
|
|
|
1
|
-
// qoder
|
|
2
|
-
//
|
|
1
|
+
// qoder 请求体装配(对齐官方 qodercli 线格式;外部参考:dsh-qoder-connect wire/serialize.ts、magpie qoder 插件)。
|
|
2
|
+
// 消息形状转换层(OpenAI→Qoder wire messages / 会话指纹 / 缓存标记 / tool 配对)已拆至 wire-messages.js
|
|
3
|
+
// (体积门 + 职责边界)。本文件只管:
|
|
4
|
+
// - 模板外壳加载(baseprompt.json 只作 parameters/model_config 骨架,messages/tools 一律重建);
|
|
5
|
+
// - business.id = request_set_id(上游把排队条目与 business id 绑定,各发一个 UUID 会让排队/回放关联失效);
|
|
6
|
+
// - business.product=cli(我们伪装的是 qodercli,不是 IDE);
|
|
7
|
+
// - 思考档位/上下文窗口按模型目录(meta)补写;模型名映射 mapModel。
|
|
3
8
|
import { readFileSync } from "node:fs";
|
|
4
9
|
import { join, dirname } from "node:path";
|
|
5
10
|
import { fileURLToPath } from "node:url";
|
|
6
11
|
import { newUUID, unixMs } from "./fingerprint.js";
|
|
12
|
+
import {
|
|
13
|
+
normalizeContent, buildUserMessage, buildStructuredMessage,
|
|
14
|
+
toQoderWireMessages, conversationSessionId, cacheMark,
|
|
15
|
+
} from "./wire-messages.js";
|
|
16
|
+
|
|
17
|
+
// 转换层符号 re-export(外部 import 路径保持 payload.js 不变;新代码建议直接 import wire-messages.js)
|
|
18
|
+
export {
|
|
19
|
+
blankResponseMeta, buildUserMessage, buildStructuredMessage, normalizeContent,
|
|
20
|
+
withQoderPersona, toQoderWireMessages, conversationSessionId, cacheMark,
|
|
21
|
+
} from "./wire-messages.js";
|
|
7
22
|
|
|
8
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
24
|
|
|
10
|
-
// 模板单例:进程内加载一次(替换占位符后 JSON.parse
|
|
25
|
+
// 模板单例:进程内加载一次(替换占位符后 JSON.parse)
|
|
11
26
|
let _template = null;
|
|
12
27
|
export function loadTemplate(templatePath) {
|
|
13
28
|
if (_template && !templatePath) return _template;
|
|
@@ -18,56 +33,113 @@ export function loadTemplate(templatePath) {
|
|
|
18
33
|
return _template;
|
|
19
34
|
}
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
contents: [{ type: "text", text: String(text || "") }],
|
|
37
|
-
response_meta: blankResponseMeta(),
|
|
38
|
-
reasoning_content_signature: "",
|
|
39
|
-
};
|
|
36
|
+
// 档位对齐(参考 magpie effortFor):把客户端要档位映射到模型声明档位集合里的最近一档(平手取高)。
|
|
37
|
+
const EFFORT_RANK = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
38
|
+
export function fitEffort(want, levels) {
|
|
39
|
+
const w = String(want || "").toLowerCase();
|
|
40
|
+
if (!Array.isArray(levels) || !levels.length || levels.includes(w)) return w;
|
|
41
|
+
const at = EFFORT_RANK.indexOf(w);
|
|
42
|
+
if (at < 0) return w;
|
|
43
|
+
let best = w, dist = EFFORT_RANK.length;
|
|
44
|
+
for (const l of levels) {
|
|
45
|
+
const i = EFFORT_RANK.indexOf(l);
|
|
46
|
+
if (i < 0 || l === "none") continue;
|
|
47
|
+
const d = Math.abs(i - at);
|
|
48
|
+
if (d < dist || (d === dist && i > at)) { best = l; dist = d; }
|
|
49
|
+
}
|
|
50
|
+
return best;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
// 上下文窗口选择(参考 magpie windowFor):请求装得下默认窗口就用默认;
|
|
54
|
+
// 装不下按能容纳的最小窗口升档(bytes/3 粗估 + 预留回复长度),全装不下给最大档。
|
|
55
|
+
export function pickContextWindow(need, meta) {
|
|
56
|
+
const base = Number(meta?.defaultWindow) || Number(meta?.context_length) || 0;
|
|
57
|
+
const windows = Array.isArray(meta?.windows) ? meta.windows : [];
|
|
58
|
+
if (base > 0 && need <= base) return base;
|
|
59
|
+
return windows.find((w) => w >= need) ?? (windows.length ? windows[windows.length - 1] : 0);
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
// 组装上游请求体。template 外壳字段(parameters/model_config 骨架等)保留,
|
|
63
|
+
// messages/tools/ids/chat_context/business 一律重建。
|
|
64
|
+
// meta:模型目录行(context_windows/default_window/reasoning_efforts/default_reasoning_effort/max_output_tokens);
|
|
65
|
+
// effort:客户端声明的思考档位原始串(可空)。
|
|
66
|
+
export function buildQoderBody({ template, userId, userType, model, messages, tools, maxTokens, isReasoning, meta, effort, preserveTemplateHistory = false }) {
|
|
67
|
+
const tmpl = template || loadTemplate();
|
|
68
|
+
const body = JSON.parse(JSON.stringify(tmpl));
|
|
69
|
+
const nid = newUUID();
|
|
70
|
+
const setId = newUUID();
|
|
71
|
+
body.request_id = nid;
|
|
72
|
+
body.chat_record_id = nid;
|
|
73
|
+
body.request_set_id = setId;
|
|
74
|
+
body.stream = true;
|
|
75
|
+
body.aliyun_user_type = userType || "personal_standard";
|
|
76
|
+
if (body.model_config) {
|
|
77
|
+
body.model_config.key = model;
|
|
78
|
+
if (isReasoning) body.model_config.is_reasoning = true;
|
|
79
|
+
}
|
|
80
|
+
// 推理标志两处镜像写(对齐官方 CLI 请求形状),只写 model_config 上游不吐 reasoning_content
|
|
81
|
+
if (isReasoning && body.chat_context?.extra?.modelConfig) {
|
|
82
|
+
body.chat_context.extra.modelConfig.is_reasoning = true;
|
|
83
|
+
body.chat_context.extra.modelConfig.key = model; // 与 model_config.key 同步,不留模板 "auto"
|
|
84
|
+
}
|
|
85
|
+
const wireMessages = cacheMark(preserveTemplateHistory
|
|
86
|
+
? buildQoderMessages(tmpl, messages, Array.isArray(tools) && tools.length > 0) // 回退通道:MSLXDFF_QODER_LEGACY_WIRE=1
|
|
87
|
+
: toQoderWireMessages(messages));
|
|
88
|
+
body.session_id = conversationSessionId(userId, model, wireMessages);
|
|
89
|
+
const outCap = maxTokens > 0 ? maxTokens : (Number(meta?.max_output_tokens) || 0);
|
|
90
|
+
if (outCap > 0 && body.parameters) body.parameters.max_tokens = outCap;
|
|
91
|
+
// 思考参数按真值口径补写(models.js 目录行为 snake_case):enable_thinking + 命中模型声明档位才发 reasoning_effort。
|
|
92
|
+
// 只在模型确实支持时补写,避免把未知参数/无效档位打给不认的模型(fitEffort 兜底后仍须 ∈ efforts 才发)。
|
|
93
|
+
if (body.parameters) {
|
|
94
|
+
const efforts = Array.isArray(meta?.reasoning_efforts) ? meta.reasoning_efforts : [];
|
|
95
|
+
const want = String(effort || "").trim().toLowerCase();
|
|
96
|
+
if (isReasoning) {
|
|
97
|
+
body.parameters.enable_thinking = true;
|
|
98
|
+
if (efforts.length) {
|
|
99
|
+
const cand = want && EFFORT_RANK.includes(want)
|
|
100
|
+
? fitEffort(want, efforts)
|
|
101
|
+
: String(meta?.default_reasoning_effort || efforts[0] || "");
|
|
102
|
+
if (efforts.includes(cand)) body.parameters.reasoning_effort = cand;
|
|
103
|
+
}
|
|
104
|
+
} else if (want === "none" && efforts.length) {
|
|
105
|
+
body.parameters.enable_thinking = false; // 模型可关思考才发关;不可关的(无 efforts)保持默认不干预
|
|
106
|
+
}
|
|
107
|
+
// 上下文窗口升档(仅模型声明了多窗口才发)
|
|
108
|
+
const windows = Array.isArray(meta?.context_windows) ? meta.context_windows : [];
|
|
109
|
+
if (windows.length) {
|
|
110
|
+
const need = Math.ceil(JSON.stringify({ m: wireMessages, t: tools || [] }).length / 3) + (outCap || 32000);
|
|
111
|
+
const w = pickContextWindow(need, { defaultWindow: meta.default_window, context_length: meta.context_length, windows });
|
|
112
|
+
if (w > 0) body.parameters.context_length = w;
|
|
54
113
|
}
|
|
55
|
-
return out;
|
|
56
114
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
115
|
+
// chat_context 的 text/originalContent 是纯字符串(旧模板形状是 {type,text} 对象——官方从不发对象形状)
|
|
116
|
+
const prompt = (() => {
|
|
117
|
+
for (let i = wireMessages.length - 1; i >= 0; i--) {
|
|
118
|
+
const m = wireMessages[i];
|
|
119
|
+
if (m?.role === "user") return normalizeContent(m.contents?.map((p) => p.text ?? "").join("\n\n") || m.content);
|
|
120
|
+
}
|
|
121
|
+
return "";
|
|
122
|
+
})();
|
|
123
|
+
if (body.business) {
|
|
124
|
+
body.business.id = setId; // 排队条目与 business.id 绑定,必须 = request_set_id
|
|
125
|
+
body.business.product = "cli";
|
|
126
|
+
body.business.version = "1.0.0";
|
|
127
|
+
body.business.begin_at = unixMs();
|
|
128
|
+
body.business.name = prompt.length > 30 ? prompt.slice(0, 30) : prompt;
|
|
129
|
+
}
|
|
130
|
+
if (body.chat_context) {
|
|
131
|
+
body.chat_context.text = prompt;
|
|
132
|
+
if (body.chat_context.extra) body.chat_context.extra.originalContent = prompt;
|
|
65
133
|
}
|
|
66
|
-
|
|
134
|
+
body.messages = wireMessages;
|
|
135
|
+
// tools 空数组契约:模板内嵌的 14 个 Qoder CLI 工具永不出门;无工具时上游官方亦发 []
|
|
136
|
+
body.tools = Array.isArray(tools) && tools.length ? tools : [];
|
|
137
|
+
return { body, prompt, mcSource: body.model_config?.source || "system" };
|
|
67
138
|
}
|
|
68
139
|
|
|
69
|
-
// OpenAI messages → Qoder messages
|
|
70
|
-
//
|
|
140
|
+
// OpenAI messages → Qoder messages(旧版形状:无 system 前置模板 system;user=contents[];
|
|
141
|
+
// assistant tool_calls 透传;含 tool_result 数组拆成 tool 行)。
|
|
142
|
+
// 仅保留给 MSLXDFF_QODER_LEGACY_WIRE=1 回退通道;正常路径用 wire-messages.js 的 toQoderWireMessages。
|
|
71
143
|
export function buildQoderMessages(template, incoming, toolsEnabled) {
|
|
72
144
|
const rebuilt = [];
|
|
73
145
|
const hasSystem = (incoming || []).some((m) => m?.role === "system");
|
|
@@ -105,42 +177,6 @@ export function buildQoderMessages(template, incoming, toolsEnabled) {
|
|
|
105
177
|
return rebuilt;
|
|
106
178
|
}
|
|
107
179
|
|
|
108
|
-
// 组装上游请求体(仿 CallQoderWithOpts)
|
|
109
|
-
export function buildQoderBody({ template, userType, model, messages, tools, maxTokens, isReasoning }) {
|
|
110
|
-
const body = JSON.parse(JSON.stringify(template || loadTemplate()));
|
|
111
|
-
const nid = newUUID();
|
|
112
|
-
body.request_id = nid;
|
|
113
|
-
body.chat_record_id = nid;
|
|
114
|
-
body.request_set_id = newUUID();
|
|
115
|
-
body.session_id = newUUID();
|
|
116
|
-
body.stream = true;
|
|
117
|
-
body.aliyun_user_type = userType || "personal_standard";
|
|
118
|
-
if (body.model_config) {
|
|
119
|
-
body.model_config.key = model;
|
|
120
|
-
if (isReasoning) body.model_config.is_reasoning = true;
|
|
121
|
-
}
|
|
122
|
-
// 推理标志两处镜像写(对齐官方 CLI 请求形状),只写 model_config 上游不吐 reasoning_content
|
|
123
|
-
if (isReasoning && body.chat_context?.extra?.modelConfig) {
|
|
124
|
-
body.chat_context.extra.modelConfig.is_reasoning = true;
|
|
125
|
-
body.chat_context.extra.modelConfig.key = model; // 与 model_config.key 同步,不留模板 "auto"(评审路1 P1#5)
|
|
126
|
-
}
|
|
127
|
-
if (maxTokens > 0 && body.parameters) body.parameters.max_tokens = maxTokens;
|
|
128
|
-
const prompt = extractLatestUserPrompt(messages);
|
|
129
|
-
if (body.business) {
|
|
130
|
-
body.business.id = newUUID();
|
|
131
|
-
body.business.begin_at = unixMs();
|
|
132
|
-
body.business.name = prompt.length > 30 ? prompt.slice(0, 30) : prompt;
|
|
133
|
-
}
|
|
134
|
-
if (body.chat_context) {
|
|
135
|
-
if (body.chat_context.text) body.chat_context.text.text = prompt;
|
|
136
|
-
if (body.chat_context.extra?.originalContent) body.chat_context.extra.originalContent.text = prompt;
|
|
137
|
-
}
|
|
138
|
-
body.messages = messages;
|
|
139
|
-
// tools 空数组契约:模板内嵌的 14 个 Qoder CLI 工具永不出门;无工具时上游官方亦发 []
|
|
140
|
-
body.tools = Array.isArray(tools) && tools.length ? tools : [];
|
|
141
|
-
return { body, prompt, mcSource: body.model_config?.source || "system" };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
180
|
// 客户端模型名 → 上游 key(仿 defaultModelMapping 双向 substring,家族关键字兜底)
|
|
145
181
|
const DEFAULT_MAPPING = {
|
|
146
182
|
opus: "qmodel_38max", sonnet: "gmodel", haiku: "qfmodel",
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
// qoder 上游请求装配(纯函数,无网络):OpenAI body → COSY 签名请求(url/headers/bodyStr)。
|
|
2
2
|
// 从 chat.js 拆出,使真流式/聚合两条管线共享同一装配口(单一来源,防两处签名漂移)。
|
|
3
|
+
// MSLXDFF_QODER_LEGACY_WIRE=1 → 回退旧形状(模板注入历史 + OpenAI messages 原样)。
|
|
3
4
|
import { getEndpoints, normalizeRegion } from "./constants.js";
|
|
4
5
|
import { buildCosyHeaders, pathSigFrom } from "./session.js";
|
|
5
6
|
import { cosyEncode } from "./encode.js";
|
|
6
7
|
import { buildQoderBody, mapModel } from "./payload.js";
|
|
7
8
|
|
|
8
|
-
export function buildUpstreamRequest({ sess, region, model, messages, tools, maxTokens, isReasoning }) {
|
|
9
|
+
export function buildUpstreamRequest({ sess, region, model, messages, tools, maxTokens, isReasoning, meta, effort }) {
|
|
9
10
|
const upstreamModel = mapModel(model);
|
|
10
11
|
const { body, mcSource } = buildQoderBody({
|
|
11
12
|
template: undefined,
|
|
13
|
+
userId: sess?.identity?.uid,
|
|
12
14
|
userType: sess?.identity?.userType,
|
|
13
15
|
model: upstreamModel,
|
|
14
16
|
messages,
|
|
15
17
|
tools,
|
|
16
18
|
maxTokens,
|
|
17
19
|
isReasoning,
|
|
20
|
+
meta,
|
|
21
|
+
effort,
|
|
22
|
+
preserveTemplateHistory: process.env.MSLXDFF_QODER_LEGACY_WIRE === "1",
|
|
18
23
|
});
|
|
19
24
|
const ep = getEndpoints(normalizeRegion(region));
|
|
20
25
|
const url = ep.chatStreamURL;
|
|
@@ -23,5 +28,5 @@ export function buildUpstreamRequest({ sess, region, model, messages, tools, max
|
|
|
23
28
|
headers["x-model-key"] = upstreamModel;
|
|
24
29
|
headers["x-model-source"] = mcSource;
|
|
25
30
|
headers["accept"] = "text/event-stream";
|
|
26
|
-
return { url, headers, bodyStr, upstreamModel, mcSource };
|
|
31
|
+
return { url, headers, bodyStr, upstreamModel, mcSource, requestSetId: body.request_set_id, sessionId: body.session_id, modelKey: upstreamModel };
|
|
27
32
|
}
|
|
@@ -6,7 +6,12 @@ import { cosyEncode } from "./encode.js";
|
|
|
6
6
|
import { newUUID, unixSec } from "./fingerprint.js";
|
|
7
7
|
|
|
8
8
|
export const APP_CODE = "cosy";
|
|
9
|
-
|
|
9
|
+
// 身份版本(对齐外部参考 dsh-qoder-connect wire/cosy.ts 的 CLI 口径 1.1.47;env 可覆盖以便上游升版)
|
|
10
|
+
export const VERSION = process.env.MSLXDFF_QODER_CLI_VERSION || "1.1.47";
|
|
11
|
+
export const CLI_UA = `qoder/${VERSION}`;
|
|
12
|
+
const MACHINE_OS = process.platform === "win32"
|
|
13
|
+
? (process.arch === "arm64" ? "aarch64_windows" : "x86_64_windows")
|
|
14
|
+
: (process.arch === "arm64" ? "aarch64_linux" : "x86_64_linux");
|
|
10
15
|
// base64("war, war never changes")
|
|
11
16
|
const SECRET_B64 = "d2FyLCB3YXIgbmV2ZXIgY2hhbmdlcw==";
|
|
12
17
|
|
|
@@ -106,30 +111,41 @@ export function pathSigFrom(url) {
|
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
//
|
|
110
|
-
|
|
114
|
+
// 构造请求头(身份对齐 qodercli 参考实现:UA=qoder/<ver>、补 bodyhash/sigpath/machineos/x-request-id,
|
|
115
|
+
// 摘掉 IDE 伪装头 cosy-scene/business-*——上游按请求头指纹分级,IDE+Go UA 是排队重灾区)
|
|
116
|
+
// correlation: { requestId, sessionId } —— 队列探活与推理必须携带同一对 id 才能关联。
|
|
117
|
+
export function buildCosyHeaders(sess, pathSig, bodyStr, accept, correlation) {
|
|
111
118
|
const payloadB64 = buildPayloadB64(sess.info);
|
|
112
119
|
const date = String(unixSec());
|
|
113
|
-
const
|
|
120
|
+
const body = bodyStr || "";
|
|
121
|
+
const sig = signRequest(payloadB64, sess.cosyKey, date, body, pathSig);
|
|
114
122
|
const bearer = composeBearer(payloadB64, sig);
|
|
123
|
+
const bodyHash = createHash("md5").update(body).digest("hex");
|
|
115
124
|
return {
|
|
116
|
-
"cosy-data-policy": "
|
|
125
|
+
"cosy-data-policy": "disagree",
|
|
117
126
|
"content-type": "application/json",
|
|
118
127
|
"cosy-machinetype": sess.machineType,
|
|
128
|
+
"cosy-machinetoken": sess.machineToken,
|
|
129
|
+
"cosy-machineos": MACHINE_OS,
|
|
119
130
|
"cosy-clienttype": "5",
|
|
131
|
+
"cosy-clientip": "127.0.0.1",
|
|
132
|
+
"cosy-bodyhash": bodyHash,
|
|
133
|
+
"cosy-bodylength": String(Buffer.byteLength(body)),
|
|
134
|
+
"cosy-sigpath": pathSig,
|
|
135
|
+
"cosy-organization-id": sess.identity?.organizationId || "",
|
|
136
|
+
"cosy-organization-tags": "",
|
|
120
137
|
"cosy-date": date,
|
|
121
138
|
"cosy-user": sess.identity.uid,
|
|
122
139
|
"cosy-key": sess.cosyKey,
|
|
123
140
|
"cache-control": "no-cache",
|
|
124
141
|
"accept": accept || "application/json",
|
|
142
|
+
"accept-encoding": "identity",
|
|
125
143
|
"authorization": bearer,
|
|
126
144
|
"cosy-version": VERSION,
|
|
127
145
|
"cosy-machineid": sess.machineId,
|
|
128
|
-
"cosy-machinetoken": sess.machineToken,
|
|
129
146
|
"login-version": "v2",
|
|
130
|
-
"user-agent":
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"cosy-business-type": "agent",
|
|
147
|
+
"user-agent": CLI_UA,
|
|
148
|
+
"x-request-id": correlation?.requestId || newUUID(),
|
|
149
|
+
...(correlation?.sessionId ? { "x-session-id": String(correlation.sessionId) } : {}),
|
|
134
150
|
};
|
|
135
151
|
}
|