clawmatrix 0.1.23 → 0.2.1
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/README.md +4 -1
- package/package.json +4 -2
- package/src/acp-proxy.ts +2183 -0
- package/src/audit.ts +42 -0
- package/src/auth.ts +2 -3
- package/src/cli.ts +76 -2
- package/src/cluster-service.ts +243 -3
- package/src/compat.ts +84 -3
- package/src/config.ts +117 -4
- package/src/connection.ts +288 -85
- package/src/crypto.ts +179 -0
- package/src/debug.ts +15 -2
- package/src/e2e/helpers.ts +318 -0
- package/src/handoff.ts +171 -92
- package/src/identity.ts +95 -0
- package/src/index.ts +433 -58
- package/src/knowledge-sync.ts +776 -207
- package/src/model-proxy.ts +144 -39
- package/src/peer-approval.ts +628 -0
- package/src/peer-manager.ts +261 -32
- package/src/rate-limiter.ts +88 -0
- package/src/router.ts +32 -10
- package/src/sentinel-manager.ts +142 -0
- package/src/sentinel.ts +618 -0
- package/src/task-activity.ts +74 -0
- package/src/terminal.ts +566 -0
- package/src/tool-proxy.ts +127 -3
- package/src/tools/cluster-acp.ts +237 -0
- package/src/tools/cluster-batch.ts +76 -0
- package/src/tools/cluster-diagnostic.ts +174 -0
- package/src/tools/cluster-edit.ts +70 -0
- package/src/tools/cluster-peers.ts +59 -14
- package/src/tools/cluster-terminal.ts +232 -0
- package/src/tools/cluster-tool.ts +26 -11
- package/src/types.ts +477 -3
- package/src/web.ts +2 -2
package/src/model-proxy.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
import { debug } from "./debug.ts";
|
|
12
12
|
import { readBody } from "./http-utils.ts";
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const DEFAULT_MODEL_TIMEOUT = 120_000; // 2 minutes
|
|
15
15
|
|
|
16
16
|
/** Normalize usage from OpenAI-compatible APIs (supports both field naming conventions). */
|
|
17
17
|
function parseUsage(usage: Record<string, number> | undefined): { inputTokens: number; outputTokens: number } | undefined {
|
|
@@ -62,17 +62,54 @@ export class ModelProxy {
|
|
|
62
62
|
private httpServer: Server | null = null;
|
|
63
63
|
private gatewayInfo: GatewayInfo;
|
|
64
64
|
private openclawConfig: OpenClawConfig;
|
|
65
|
+
private readonly modelTimeout: number;
|
|
66
|
+
|
|
67
|
+
/** Dynamically discovered proxy models from peer capabilities (auto-discovery). */
|
|
68
|
+
private discoveredModels: import("./config.ts").ProxyModel[] = [];
|
|
65
69
|
|
|
66
70
|
/** Cache of models that need a different API format than configured (detected at runtime).
|
|
67
71
|
* Entries expire after 10 minutes so upstream upgrades are eventually detected. */
|
|
68
72
|
private modelApiCache = new Map<string, { api: string; ts: number }>();
|
|
69
73
|
private static readonly MODEL_API_CACHE_TTL = 600_000; // 10 minutes
|
|
74
|
+
private cacheCleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
70
75
|
|
|
71
76
|
constructor(config: ClawMatrixConfig, peerManager: PeerManager, gatewayInfo: GatewayInfo, openclawConfig: OpenClawConfig) {
|
|
72
77
|
this.config = config;
|
|
73
78
|
this.peerManager = peerManager;
|
|
74
79
|
this.gatewayInfo = gatewayInfo;
|
|
75
80
|
this.openclawConfig = openclawConfig;
|
|
81
|
+
this.modelTimeout = config.modelTimeout ?? DEFAULT_MODEL_TIMEOUT;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** All proxy models: static config + dynamically discovered from peers. */
|
|
85
|
+
get allProxyModels(): import("./config.ts").ProxyModel[] {
|
|
86
|
+
if (this.discoveredModels.length === 0) return this.config.proxyModels;
|
|
87
|
+
return [...this.config.proxyModels, ...this.discoveredModels];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Rebuild discovered models from current peer capabilities.
|
|
91
|
+
* Skips models already present in static config.proxyModels. */
|
|
92
|
+
updateDiscoveredModels(peers: import("./router.ts").RouteEntry[]) {
|
|
93
|
+
const staticKeys = new Set(this.config.proxyModels.map((m) => `${m.nodeId}/${m.id}`));
|
|
94
|
+
const next: import("./config.ts").ProxyModel[] = [];
|
|
95
|
+
for (const peer of peers) {
|
|
96
|
+
for (const m of peer.models) {
|
|
97
|
+
const key = `${peer.nodeId}/${m.id}`;
|
|
98
|
+
if (staticKeys.has(key)) continue;
|
|
99
|
+
next.push({
|
|
100
|
+
id: m.id,
|
|
101
|
+
nodeId: peer.nodeId,
|
|
102
|
+
provider: m.provider,
|
|
103
|
+
description: m.description,
|
|
104
|
+
input: m.input,
|
|
105
|
+
compat: m.compat as import("./config.ts").ProxyModel["compat"],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.discoveredModels = next;
|
|
110
|
+
if (next.length > 0) {
|
|
111
|
+
debug("proxy", `Auto-discovered ${next.length} model(s) from peers: ${next.map((m) => `${m.nodeId}/${m.id}`).join(", ")}`);
|
|
112
|
+
}
|
|
76
113
|
}
|
|
77
114
|
|
|
78
115
|
/**
|
|
@@ -226,8 +263,18 @@ export class ModelProxy {
|
|
|
226
263
|
return items;
|
|
227
264
|
}
|
|
228
265
|
|
|
229
|
-
/**
|
|
230
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Resolve API endpoint for a model: explicit config > OpenClaw provider > null.
|
|
268
|
+
*
|
|
269
|
+
* ⚠️ 重要:绝对不能 fallback 到 OpenClaw gateway 的 /v1/chat/completions!
|
|
270
|
+
* OpenClaw gateway 的 /v1/chat/completions 会走 Agent 系统,每次请求都会创建
|
|
271
|
+
* 一个新的 Agent session(带记忆、system prompt 等)。这会导致:
|
|
272
|
+
* - 远程节点(如 iPhone)的每次 model_req 都在本地产生一个多余的 OpenClaw 会话
|
|
273
|
+
* - 模型响应被 OpenClaw Agent 的 system prompt 和记忆污染,结果不正确
|
|
274
|
+
*
|
|
275
|
+
* 如果找不到直连 API 端点,必须返回 null 让调用方报错,而不是静默 fallback。
|
|
276
|
+
*/
|
|
277
|
+
private resolveModelEndpoint(model: { id: string; provider: string; baseUrl?: string; apiKey?: string; api?: string }): { baseUrl: string; apiKey?: string; direct: boolean; api: string } | null {
|
|
231
278
|
const defaultApi = "openai-completions";
|
|
232
279
|
|
|
233
280
|
// 1. Explicit baseUrl in ClawMatrix model config
|
|
@@ -255,18 +302,20 @@ export class ModelProxy {
|
|
|
255
302
|
};
|
|
256
303
|
}
|
|
257
304
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
baseUrl: `http://127.0.0.1:${port}/v1`,
|
|
262
|
-
apiKey: undefined,
|
|
263
|
-
direct: false,
|
|
264
|
-
api: model.api ?? defaultApi,
|
|
265
|
-
};
|
|
305
|
+
// 找不到直连端点 → 返回 null(见上方注释,不能 fallback 到 gateway)
|
|
306
|
+
return null;
|
|
266
307
|
}
|
|
267
308
|
|
|
268
309
|
/** Start the local HTTP proxy server for OpenAI-compatible requests. */
|
|
269
310
|
start() {
|
|
311
|
+
// Periodically prune expired model API cache entries
|
|
312
|
+
this.cacheCleanupTimer = setInterval(() => {
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
for (const [id, entry] of this.modelApiCache) {
|
|
315
|
+
if (now - entry.ts > ModelProxy.MODEL_API_CACHE_TTL) this.modelApiCache.delete(id);
|
|
316
|
+
}
|
|
317
|
+
}, ModelProxy.MODEL_API_CACHE_TTL);
|
|
318
|
+
|
|
270
319
|
this.httpServer = createServer(async (req, res) => {
|
|
271
320
|
try {
|
|
272
321
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
@@ -308,6 +357,10 @@ export class ModelProxy {
|
|
|
308
357
|
}
|
|
309
358
|
|
|
310
359
|
stop() {
|
|
360
|
+
if (this.cacheCleanupTimer) {
|
|
361
|
+
clearInterval(this.cacheCleanupTimer);
|
|
362
|
+
this.cacheCleanupTimer = null;
|
|
363
|
+
}
|
|
311
364
|
if (this.httpServer) {
|
|
312
365
|
this.httpServer.close();
|
|
313
366
|
this.httpServer = null;
|
|
@@ -338,11 +391,22 @@ export class ModelProxy {
|
|
|
338
391
|
} else {
|
|
339
392
|
// Stream response
|
|
340
393
|
const reader = response.body.getReader();
|
|
394
|
+
let finished = false;
|
|
395
|
+
|
|
396
|
+
// Clean up stream when client disconnects mid-stream
|
|
397
|
+
res.on("close", () => {
|
|
398
|
+
if (!finished) {
|
|
399
|
+
finished = true;
|
|
400
|
+
reader.cancel().catch(() => {});
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
341
404
|
const pump = (): void => {
|
|
342
405
|
reader.read().then(({ done, value }) => {
|
|
343
|
-
if (done) {
|
|
406
|
+
if (done || finished) {
|
|
407
|
+
finished = true;
|
|
344
408
|
reader.releaseLock();
|
|
345
|
-
res.end();
|
|
409
|
+
if (!res.writableEnded) res.end();
|
|
346
410
|
return;
|
|
347
411
|
}
|
|
348
412
|
const ok = res.write(value);
|
|
@@ -352,8 +416,9 @@ export class ModelProxy {
|
|
|
352
416
|
res.once("drain", pump);
|
|
353
417
|
}
|
|
354
418
|
}).catch(() => {
|
|
419
|
+
finished = true;
|
|
355
420
|
reader.releaseLock();
|
|
356
|
-
res.end();
|
|
421
|
+
if (!res.writableEnded) res.end();
|
|
357
422
|
});
|
|
358
423
|
};
|
|
359
424
|
pump();
|
|
@@ -369,16 +434,17 @@ export class ModelProxy {
|
|
|
369
434
|
} | { error: { status: number; message: string } } {
|
|
370
435
|
const slashIdx = rawModelId.indexOf("/");
|
|
371
436
|
let modelId: string;
|
|
372
|
-
|
|
437
|
+
const proxyModels = this.allProxyModels;
|
|
438
|
+
let matchingModels: (typeof proxyModels)[number][];
|
|
373
439
|
|
|
374
440
|
if (slashIdx > 0) {
|
|
375
441
|
const nodeId = rawModelId.slice(0, slashIdx);
|
|
376
442
|
modelId = rawModelId.slice(slashIdx + 1);
|
|
377
443
|
// Explicit node/model — only target that specific node, no failover to others
|
|
378
|
-
matchingModels =
|
|
444
|
+
matchingModels = proxyModels.filter((m) => m.id === modelId && m.nodeId === nodeId);
|
|
379
445
|
} else {
|
|
380
446
|
modelId = rawModelId;
|
|
381
|
-
matchingModels =
|
|
447
|
+
matchingModels = proxyModels.filter((m) => m.id === modelId);
|
|
382
448
|
}
|
|
383
449
|
|
|
384
450
|
if (matchingModels.length === 0) {
|
|
@@ -397,6 +463,16 @@ export class ModelProxy {
|
|
|
397
463
|
}
|
|
398
464
|
}
|
|
399
465
|
|
|
466
|
+
// Sort candidates by latency (lowest first) for optimal first-try and failover order
|
|
467
|
+
candidates.sort((a, b) => {
|
|
468
|
+
const routeA = this.peerManager.router.getRoute(a.routeNodeId);
|
|
469
|
+
const routeB = this.peerManager.router.getRoute(b.routeNodeId);
|
|
470
|
+
const aDirect = routeA?.connection ? 0 : 1;
|
|
471
|
+
const bDirect = routeB?.connection ? 0 : 1;
|
|
472
|
+
if (aDirect !== bDirect) return aDirect - bDirect;
|
|
473
|
+
return (routeA?.latencyMs ?? 0) - (routeB?.latencyMs ?? 0);
|
|
474
|
+
});
|
|
475
|
+
|
|
400
476
|
debug("proxy", `model raw="${rawModelId}" modelId="${modelId}" candidates=${candidates.map((c) => c.routeNodeId).join(",") || "none"}`);
|
|
401
477
|
if (candidates.length === 0) {
|
|
402
478
|
return { error: { status: 502, message: `No reachable node for model "${rawModelId}"` } };
|
|
@@ -519,10 +595,21 @@ export class ModelProxy {
|
|
|
519
595
|
const encoder = new TextEncoder();
|
|
520
596
|
const model = frame.payload.model;
|
|
521
597
|
|
|
598
|
+
let streamController: ReadableStreamDefaultController;
|
|
522
599
|
const readable = new ReadableStream({
|
|
523
600
|
start: (controller) => {
|
|
601
|
+
streamController = controller;
|
|
524
602
|
this.startStreamAttempt(requestId, targetNodeId, frame, responseFormat, controller, encoder, model, failoverCandidates, buildFrame);
|
|
525
603
|
},
|
|
604
|
+
cancel: () => {
|
|
605
|
+
// Client disconnected — find and clean up the pending request using this controller
|
|
606
|
+
for (const [id, p] of this.pending) {
|
|
607
|
+
if (p.controller === streamController) {
|
|
608
|
+
this.cleanupRequest(id, p.stableStreamId);
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
},
|
|
526
613
|
});
|
|
527
614
|
|
|
528
615
|
// Emit setup events for responses API (once, before any attempts)
|
|
@@ -556,7 +643,7 @@ export class ModelProxy {
|
|
|
556
643
|
this.cleanupRequest(requestId);
|
|
557
644
|
this.peerManager.router.markFailed(requestId);
|
|
558
645
|
this.tryStreamFailover(stableId, responseFormat, controller, encoder, model, failoverCandidates, buildFrame, `model request to "${targetNodeId}" timed out`);
|
|
559
|
-
},
|
|
646
|
+
}, this.modelTimeout);
|
|
560
647
|
|
|
561
648
|
this.pending.set(requestId, {
|
|
562
649
|
resolve: () => {}, reject: () => {},
|
|
@@ -730,7 +817,7 @@ export class ModelProxy {
|
|
|
730
817
|
this.pending.delete(requestId);
|
|
731
818
|
this.peerManager.router.markFailed(requestId);
|
|
732
819
|
reject(new Error(`Model request to "${targetNodeId}" timed out`));
|
|
733
|
-
},
|
|
820
|
+
}, this.modelTimeout);
|
|
734
821
|
|
|
735
822
|
this.pending.set(requestId, {
|
|
736
823
|
resolve: resolve as (v: unknown) => void,
|
|
@@ -814,7 +901,7 @@ export class ModelProxy {
|
|
|
814
901
|
.map((p) => p.nodeId),
|
|
815
902
|
);
|
|
816
903
|
|
|
817
|
-
const models = this.
|
|
904
|
+
const models = this.allProxyModels.map((m) => {
|
|
818
905
|
const entry: Record<string, unknown> = {
|
|
819
906
|
id: m.id,
|
|
820
907
|
object: "model",
|
|
@@ -901,6 +988,7 @@ export class ModelProxy {
|
|
|
901
988
|
}
|
|
902
989
|
} catch {
|
|
903
990
|
this.cleanupRequest(frame.id, pending.stableStreamId);
|
|
991
|
+
try { pending.controller?.close(); } catch { /* already closed */ }
|
|
904
992
|
}
|
|
905
993
|
}
|
|
906
994
|
|
|
@@ -920,7 +1008,10 @@ export class ModelProxy {
|
|
|
920
1008
|
this.cleanupRequest(frame.id, stableId);
|
|
921
1009
|
} else {
|
|
922
1010
|
// Use full deltaObj when available (carries tool_calls etc.), otherwise simple text delta
|
|
923
|
-
const delta = frame.payload.deltaObj ?? {
|
|
1011
|
+
const delta = frame.payload.deltaObj ?? {
|
|
1012
|
+
content: frame.payload.delta,
|
|
1013
|
+
...(frame.payload.reasoningDelta && { reasoning_content: frame.payload.reasoningDelta }),
|
|
1014
|
+
};
|
|
924
1015
|
const chunkStableId = pending.stableStreamId ?? frame.id;
|
|
925
1016
|
const chunk = { id: `chatcmpl-${chunkStableId}`, object: "chat.completion.chunk", choices: [{ index: 0, delta, finish_reason: null }] };
|
|
926
1017
|
pending.controller!.enqueue(pending.encoder!.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
|
@@ -962,14 +1053,19 @@ export class ModelProxy {
|
|
|
962
1053
|
}
|
|
963
1054
|
}
|
|
964
1055
|
|
|
965
|
-
private sendStreamDelta(to: string, id: string, delta: string, deltaObj?: unknown) {
|
|
1056
|
+
private sendStreamDelta(to: string, id: string, delta: string, deltaObj?: unknown, reasoningDelta?: string) {
|
|
966
1057
|
this.peerManager.sendTo(to, {
|
|
967
1058
|
type: "model_stream",
|
|
968
1059
|
id,
|
|
969
1060
|
from: this.config.nodeId,
|
|
970
1061
|
to,
|
|
971
1062
|
timestamp: Date.now(),
|
|
972
|
-
payload: {
|
|
1063
|
+
payload: {
|
|
1064
|
+
delta,
|
|
1065
|
+
...(reasoningDelta && { reasoningDelta }),
|
|
1066
|
+
...(deltaObj !== undefined && { deltaObj }),
|
|
1067
|
+
done: false,
|
|
1068
|
+
},
|
|
973
1069
|
} satisfies ModelStreamChunk);
|
|
974
1070
|
}
|
|
975
1071
|
|
|
@@ -1007,6 +1103,13 @@ export class ModelProxy {
|
|
|
1007
1103
|
|
|
1008
1104
|
try {
|
|
1009
1105
|
const endpoint = this.resolveModelEndpoint(model);
|
|
1106
|
+
if (!endpoint) {
|
|
1107
|
+
this.peerManager.sendTo(from, {
|
|
1108
|
+
type: "model_res", id, from: this.config.nodeId, to: from, timestamp: Date.now(),
|
|
1109
|
+
payload: { success: false, error: `No direct API endpoint configured for model "${payload.model}" (provider: ${model.provider}). Configure baseUrl/apiKey in ClawMatrix model config or OpenClaw provider config.` },
|
|
1110
|
+
} satisfies ModelResponse);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1010
1113
|
// Use payload.api override from requesting side, or cached API from previous auto-detection
|
|
1011
1114
|
const cached = this.modelApiCache.get(model.id);
|
|
1012
1115
|
const cachedApi = (cached && Date.now() - cached.ts < ModelProxy.MODEL_API_CACHE_TTL) ? cached.api : undefined;
|
|
@@ -1017,16 +1120,10 @@ export class ModelProxy {
|
|
|
1017
1120
|
const url = `${endpoint.baseUrl}${path}`;
|
|
1018
1121
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
1019
1122
|
|
|
1020
|
-
if (endpoint.
|
|
1021
|
-
|
|
1022
|
-
debug("model_req", `direct API call to ${url} (api=${endpoint.api})`);
|
|
1023
|
-
} else {
|
|
1024
|
-
const { authHeader } = this.gatewayInfo;
|
|
1025
|
-
if (authHeader) headers["Authorization"] = authHeader;
|
|
1026
|
-
debug("model_req", `gateway fallback to ${url}`);
|
|
1027
|
-
}
|
|
1123
|
+
if (endpoint.apiKey) headers["Authorization"] = `Bearer ${endpoint.apiKey}`;
|
|
1124
|
+
debug("model_req", `direct API call to ${url} (api=${endpoint.api})`);
|
|
1028
1125
|
|
|
1029
|
-
const modelField =
|
|
1126
|
+
const modelField = model.id;
|
|
1030
1127
|
const srcFormat = payload.inputFormat ?? "chat";
|
|
1031
1128
|
|
|
1032
1129
|
// Convert messages between formats if source and target API differ
|
|
@@ -1142,11 +1239,12 @@ export class ModelProxy {
|
|
|
1142
1239
|
// Chat completions format
|
|
1143
1240
|
lastUsage = parseUsage(parsed.usage) ?? lastUsage;
|
|
1144
1241
|
const d = parsed.choices?.[0]?.delta;
|
|
1145
|
-
const delta = d?.content ||
|
|
1242
|
+
const delta = d?.content || "";
|
|
1243
|
+
const reasoningDelta = d?.reasoning_content || "";
|
|
1146
1244
|
// Pass full delta object when it contains tool_calls or other structured data
|
|
1147
1245
|
const hasStructured = d?.tool_calls || d?.refusal != null;
|
|
1148
|
-
if (delta || hasStructured) {
|
|
1149
|
-
this.sendStreamDelta(from, id, delta, hasStructured ? d : undefined);
|
|
1246
|
+
if (delta || reasoningDelta || hasStructured) {
|
|
1247
|
+
this.sendStreamDelta(from, id, delta, hasStructured ? d : undefined, reasoningDelta || undefined);
|
|
1150
1248
|
contentSent = true;
|
|
1151
1249
|
}
|
|
1152
1250
|
}
|
|
@@ -1206,11 +1304,13 @@ export class ModelProxy {
|
|
|
1206
1304
|
result = {}; // unused — chatFallbackResult takes precedence
|
|
1207
1305
|
}
|
|
1208
1306
|
let content: string;
|
|
1307
|
+
let reasoning = "";
|
|
1209
1308
|
let message: unknown | undefined;
|
|
1210
1309
|
let usage: { inputTokens: number; outputTokens: number } | undefined;
|
|
1211
1310
|
|
|
1212
1311
|
if (chatFallbackResult) {
|
|
1213
1312
|
({ content, message, usage } = chatFallbackResult);
|
|
1313
|
+
reasoning = chatFallbackResult.reasoning ?? "";
|
|
1214
1314
|
} else if (isResponsesApi) {
|
|
1215
1315
|
// Responses API: extract text from output[].content[].text
|
|
1216
1316
|
content = "";
|
|
@@ -1249,12 +1349,15 @@ export class ModelProxy {
|
|
|
1249
1349
|
} else {
|
|
1250
1350
|
// Chat completions format
|
|
1251
1351
|
const msg = result.choices?.[0]?.message;
|
|
1252
|
-
content = msg?.content ||
|
|
1352
|
+
content = msg?.content || "";
|
|
1353
|
+
reasoning = msg?.reasoning_content || "";
|
|
1253
1354
|
// Carry full message object when it has tool_calls or other structured data
|
|
1254
1355
|
if (msg?.tool_calls || msg?.refusal != null || msg?.function_call) {
|
|
1255
1356
|
message = msg;
|
|
1256
1357
|
}
|
|
1257
1358
|
usage = parseUsage(result.usage);
|
|
1359
|
+
// If no content but has reasoning, use reasoning as content fallback
|
|
1360
|
+
if (!content && reasoning) content = reasoning;
|
|
1258
1361
|
}
|
|
1259
1362
|
|
|
1260
1363
|
this.peerManager.sendTo(from, {
|
|
@@ -1266,6 +1369,7 @@ export class ModelProxy {
|
|
|
1266
1369
|
payload: {
|
|
1267
1370
|
success: true,
|
|
1268
1371
|
content,
|
|
1372
|
+
...(reasoning && { reasoning }),
|
|
1269
1373
|
...(message !== undefined && { message }),
|
|
1270
1374
|
usage,
|
|
1271
1375
|
},
|
|
@@ -1292,7 +1396,7 @@ export class ModelProxy {
|
|
|
1292
1396
|
modelField: string,
|
|
1293
1397
|
payload: ModelRequest["payload"],
|
|
1294
1398
|
headers: Record<string, string>,
|
|
1295
|
-
): Promise<{ content: string; message?: unknown; usage?: { inputTokens: number; outputTokens: number } } | null> {
|
|
1399
|
+
): Promise<{ content: string; reasoning?: string; message?: unknown; usage?: { inputTokens: number; outputTokens: number } } | null> {
|
|
1296
1400
|
try {
|
|
1297
1401
|
const srcFormat = payload.inputFormat ?? "chat";
|
|
1298
1402
|
const messages = srcFormat === "chat"
|
|
@@ -1316,10 +1420,11 @@ export class ModelProxy {
|
|
|
1316
1420
|
if (!chatResp.ok) return null;
|
|
1317
1421
|
const chatResult = await chatResp.json();
|
|
1318
1422
|
const msg = chatResult.choices?.[0]?.message;
|
|
1319
|
-
const content = msg?.content ||
|
|
1423
|
+
const content = msg?.content || "";
|
|
1424
|
+
const reasoningContent = msg?.reasoning_content || "";
|
|
1320
1425
|
const message = (msg?.tool_calls || msg?.refusal != null || msg?.function_call) ? msg : undefined;
|
|
1321
1426
|
const usage = parseUsage(chatResult.usage);
|
|
1322
|
-
return { content, message, usage };
|
|
1427
|
+
return { content: content || reasoningContent, reasoning: reasoningContent || undefined, message, usage };
|
|
1323
1428
|
} catch (err) {
|
|
1324
1429
|
debug("model_req", `retryWithChatCompletions failed for "${modelField}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1325
1430
|
return null;
|