codemini-cli 0.5.10 → 0.5.11
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/OPERATIONS.md +242 -242
- package/README.md +588 -588
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
- package/codemini-web/dist/assets/{index-BK75hMb2.js → index-B71xykPM.js} +108 -108
- package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
- package/codemini-web/dist/index.html +23 -23
- package/codemini-web/lib/approval-manager.js +32 -32
- package/codemini-web/lib/runtime-bridge.js +17 -11
- package/codemini-web/server.js +534 -205
- package/deployment.md +212 -212
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +77 -77
- package/skills/codemini.skills.json +40 -40
- package/skills/grill-me/SKILL.md +30 -30
- package/skills/superpowers-lite/SKILL.md +82 -82
- package/src/cli.js +74 -74
- package/src/commands/chat.js +210 -210
- package/src/commands/run.js +313 -313
- package/src/commands/skill.js +438 -304
- package/src/commands/web.js +57 -57
- package/src/core/agent-loop.js +980 -980
- package/src/core/ast.js +309 -307
- package/src/core/chat-runtime.js +6261 -6253
- package/src/core/command-evaluator.js +72 -72
- package/src/core/command-loader.js +311 -311
- package/src/core/command-policy.js +301 -301
- package/src/core/command-risk.js +156 -156
- package/src/core/config-store.js +289 -289
- package/src/core/constants.js +18 -1
- package/src/core/context-compact.js +365 -365
- package/src/core/default-system-prompt.js +114 -107
- package/src/core/dream-audit.js +105 -105
- package/src/core/dream-consolidate.js +229 -229
- package/src/core/dream-evaluator.js +185 -185
- package/src/core/fff-adapter.js +383 -383
- package/src/core/memory-store.js +543 -543
- package/src/core/project-index.js +737 -548
- package/src/core/project-instructions.js +98 -98
- package/src/core/provider/anthropic.js +514 -514
- package/src/core/provider/openai-compatible.js +501 -501
- package/src/core/reflect-skill.js +178 -178
- package/src/core/reply-language.js +40 -40
- package/src/core/session-store.js +474 -474
- package/src/core/shell-profile.js +237 -237
- package/src/core/shell.js +323 -323
- package/src/core/soul.js +69 -69
- package/src/core/system-prompt-composer.js +52 -52
- package/src/core/tool-args.js +199 -154
- package/src/core/tool-output.js +184 -184
- package/src/core/tool-result-store.js +206 -206
- package/src/core/tools.js +3024 -2893
- package/src/core/version.js +11 -11
- package/src/tui/chat-app.js +5171 -5171
- package/src/tui/tool-activity/presenters/misc.js +30 -30
- package/src/tui/tool-activity/presenters/system.js +20 -20
- package/templates/project-requirements/report-shell.html +582 -582
- package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +0 -1
|
@@ -1,501 +1,501 @@
|
|
|
1
|
-
function extractTextContent(content) {
|
|
2
|
-
if (typeof content === 'string') return content;
|
|
3
|
-
if (Array.isArray(content)) {
|
|
4
|
-
return content
|
|
5
|
-
.map((part) => {
|
|
6
|
-
if (typeof part === 'string') return part;
|
|
7
|
-
if (part?.type === 'text') return part.text || '';
|
|
8
|
-
return '';
|
|
9
|
-
})
|
|
10
|
-
.join('');
|
|
11
|
-
}
|
|
12
|
-
return '';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function extractReasoningContent(payload) {
|
|
16
|
-
if (typeof payload === 'string') return payload;
|
|
17
|
-
if (Array.isArray(payload)) {
|
|
18
|
-
return payload
|
|
19
|
-
.map((part) => {
|
|
20
|
-
if (part?.type === 'reasoning') return part.text || '';
|
|
21
|
-
if (part?.type === 'reasoning_content') return part.text || part.reasoning_content || '';
|
|
22
|
-
return '';
|
|
23
|
-
})
|
|
24
|
-
.join('');
|
|
25
|
-
}
|
|
26
|
-
return '';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function emptyToolCall(index) {
|
|
30
|
-
return {
|
|
31
|
-
index,
|
|
32
|
-
id: '',
|
|
33
|
-
name: '',
|
|
34
|
-
arguments: ''
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function createHeaders(apiKey) {
|
|
39
|
-
return {
|
|
40
|
-
'content-type': 'application/json',
|
|
41
|
-
authorization: `Bearer ${apiKey}`
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function buildChatCompletionsUrl(baseUrl) {
|
|
46
|
-
return `${String(baseUrl || '').replace(/\/$/, '')}/chat/completions`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function parseJsonResponse(response) {
|
|
50
|
-
if (!response.ok) {
|
|
51
|
-
const text = await response.text().catch(() => '');
|
|
52
|
-
throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
|
|
53
|
-
}
|
|
54
|
-
return response.json();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isRetryableStatus(status) {
|
|
58
|
-
return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isRetryableError(error) {
|
|
62
|
-
const name = String(error?.name || '');
|
|
63
|
-
if (name === 'AbortError' || name === 'TimeoutError') return false;
|
|
64
|
-
const message = String(error?.message || error || '');
|
|
65
|
-
return /fetch failed|network|socket|ECONNRESET|ETIMEDOUT|EAI_AGAIN/i.test(message);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function fetchWithRetry(url, init, { maxRetries = 0 } = {}) {
|
|
69
|
-
const attempts = Math.max(0, Number(maxRetries) || 0) + 1;
|
|
70
|
-
let lastError;
|
|
71
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
72
|
-
try {
|
|
73
|
-
const response = await fetch(url, init);
|
|
74
|
-
if (response.ok || !isRetryableStatus(response.status) || attempt === attempts - 1) {
|
|
75
|
-
return response;
|
|
76
|
-
}
|
|
77
|
-
await response.arrayBuffer().catch(() => null);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
lastError = error;
|
|
80
|
-
if (!isRetryableError(error) || attempt === attempts - 1) throw error;
|
|
81
|
-
}
|
|
82
|
-
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
83
|
-
}
|
|
84
|
-
throw lastError || new Error('Gateway request failed');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function* iterateSseEvents(stream) {
|
|
88
|
-
const decoder = new TextDecoder();
|
|
89
|
-
let buffer = '';
|
|
90
|
-
const flushEvent = (rawEvent) => {
|
|
91
|
-
const dataLines = String(rawEvent || '')
|
|
92
|
-
.split(/\r?\n/)
|
|
93
|
-
.filter((line) => line.startsWith('data:'))
|
|
94
|
-
.map((line) => line.slice(5).trimStart());
|
|
95
|
-
const dataText = dataLines.join('\n');
|
|
96
|
-
if (!dataText || dataText === '[DONE]') return null;
|
|
97
|
-
return JSON.parse(dataText);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
for await (const chunk of stream) {
|
|
101
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
102
|
-
while (true) {
|
|
103
|
-
const lfBoundary = buffer.indexOf('\n\n');
|
|
104
|
-
const crlfBoundary = buffer.indexOf('\r\n\r\n');
|
|
105
|
-
if (lfBoundary === -1 && crlfBoundary === -1) break;
|
|
106
|
-
const useCrlf = crlfBoundary !== -1 && (lfBoundary === -1 || crlfBoundary < lfBoundary);
|
|
107
|
-
const boundary = useCrlf ? crlfBoundary : lfBoundary;
|
|
108
|
-
const separatorLength = useCrlf ? 4 : 2;
|
|
109
|
-
const rawEvent = buffer.slice(0, boundary);
|
|
110
|
-
buffer = buffer.slice(boundary + separatorLength);
|
|
111
|
-
const parsed = flushEvent(rawEvent);
|
|
112
|
-
if (parsed) yield parsed;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
buffer += decoder.decode();
|
|
117
|
-
const trailingEvent = flushEvent(buffer.trim());
|
|
118
|
-
if (trailingEvent) {
|
|
119
|
-
yield trailingEvent;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function isMiniMaxModel(model) {
|
|
124
|
-
return String(model || '').toLowerCase().includes('minimax');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function normalizeToolCallArguments(argumentsText) {
|
|
128
|
-
const raw = typeof argumentsText === 'string' ? argumentsText : JSON.stringify(argumentsText ?? {});
|
|
129
|
-
try {
|
|
130
|
-
const parsed = JSON.parse(raw);
|
|
131
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
132
|
-
return JSON.stringify(parsed);
|
|
133
|
-
}
|
|
134
|
-
} catch {}
|
|
135
|
-
return '{}';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function normalizeIncomingToolCallArguments(argumentsValue) {
|
|
139
|
-
if (typeof argumentsValue === 'string') return argumentsValue;
|
|
140
|
-
if (argumentsValue == null) return '{}';
|
|
141
|
-
try {
|
|
142
|
-
return JSON.stringify(argumentsValue);
|
|
143
|
-
} catch {
|
|
144
|
-
return '{}';
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function sanitizeGatewayMessages(messages) {
|
|
149
|
-
const source = Array.isArray(messages) ? messages : [];
|
|
150
|
-
return source
|
|
151
|
-
.filter((message) => message && typeof message === 'object')
|
|
152
|
-
.map((message) => {
|
|
153
|
-
if (!Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
|
|
154
|
-
return message;
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
...message,
|
|
158
|
-
tool_calls: message.tool_calls.map((toolCall) => ({
|
|
159
|
-
...toolCall,
|
|
160
|
-
function: {
|
|
161
|
-
...toolCall?.function,
|
|
162
|
-
arguments: normalizeToolCallArguments(toolCall?.function?.arguments)
|
|
163
|
-
}
|
|
164
|
-
}))
|
|
165
|
-
};
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function buildAssistantMessage({ text = '', toolCalls = [], content, reasoningContent = '' }) {
|
|
170
|
-
const assistantMessage = {
|
|
171
|
-
role: 'assistant',
|
|
172
|
-
content: content ?? text
|
|
173
|
-
};
|
|
174
|
-
if (reasoningContent) {
|
|
175
|
-
assistantMessage.reasoning_content = reasoningContent;
|
|
176
|
-
}
|
|
177
|
-
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
178
|
-
assistantMessage.tool_calls = toolCalls.map((tc) => ({
|
|
179
|
-
id: tc.id,
|
|
180
|
-
type: 'function',
|
|
181
|
-
function: {
|
|
182
|
-
name: tc.name,
|
|
183
|
-
arguments: tc.arguments || '{}'
|
|
184
|
-
}
|
|
185
|
-
}));
|
|
186
|
-
}
|
|
187
|
-
return assistantMessage;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function sanitizeMiniMaxMessages(messages) {
|
|
191
|
-
const source = Array.isArray(messages) ? messages : [];
|
|
192
|
-
const out = [];
|
|
193
|
-
let seenNonSystem = false;
|
|
194
|
-
let keptLeadingSystem = false;
|
|
195
|
-
|
|
196
|
-
for (const message of source) {
|
|
197
|
-
if (!message || typeof message !== 'object') continue;
|
|
198
|
-
if (message.role === 'system') {
|
|
199
|
-
if (!seenNonSystem && !keptLeadingSystem) {
|
|
200
|
-
out.push(message);
|
|
201
|
-
keptLeadingSystem = true;
|
|
202
|
-
} else {
|
|
203
|
-
out.push({
|
|
204
|
-
role: 'user',
|
|
205
|
-
content: `[system-note]\n${extractTextContent(message.content)}`
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
seenNonSystem = true;
|
|
211
|
-
out.push(message);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return out;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function buildPayload({ model, temperature, messages, tools, stream = false }) {
|
|
218
|
-
const sanitizedMessages = sanitizeGatewayMessages(messages);
|
|
219
|
-
const payload = {
|
|
220
|
-
model,
|
|
221
|
-
temperature,
|
|
222
|
-
messages: isMiniMaxModel(model) ? sanitizeMiniMaxMessages(sanitizedMessages) : sanitizedMessages
|
|
223
|
-
};
|
|
224
|
-
if (stream) {
|
|
225
|
-
payload.stream = true;
|
|
226
|
-
}
|
|
227
|
-
if (Array.isArray(tools) && tools.length > 0) {
|
|
228
|
-
payload.tools = tools;
|
|
229
|
-
payload.tool_choice = 'auto';
|
|
230
|
-
}
|
|
231
|
-
if (isMiniMaxModel(model)) {
|
|
232
|
-
payload.extra_body = { reasoning_split: true };
|
|
233
|
-
}
|
|
234
|
-
return payload;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function hasTrailingToolContext(messages) {
|
|
238
|
-
const source = Array.isArray(messages) ? messages : [];
|
|
239
|
-
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
240
|
-
const message = source[index];
|
|
241
|
-
if (!message || typeof message !== 'object') continue;
|
|
242
|
-
if (message.role === 'tool') return true;
|
|
243
|
-
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
244
|
-
}
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
|
|
249
|
-
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
250
|
-
.sort((a, b) => a[0] - b[0])
|
|
251
|
-
.map(([, tc], i) => ({
|
|
252
|
-
id: tc.id || `tc-${i + 1}`,
|
|
253
|
-
name: tc.name,
|
|
254
|
-
arguments: tc.arguments || '{}'
|
|
255
|
-
}))
|
|
256
|
-
.filter((tc) => tc.name);
|
|
257
|
-
const normalizedText = String(text || '').trim();
|
|
258
|
-
|
|
259
|
-
if (!normalizedText && toolCalls.length === 0) {
|
|
260
|
-
if (hasTrailingToolContext(messages)) {
|
|
261
|
-
return {
|
|
262
|
-
text: '',
|
|
263
|
-
toolCalls: [],
|
|
264
|
-
usage,
|
|
265
|
-
incomplete: true
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
throw new Error('Gateway stream returned empty assistant response');
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
text,
|
|
273
|
-
toolCalls,
|
|
274
|
-
usage,
|
|
275
|
-
incomplete: false
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function stripMiniMaxThinkContent(text) {
|
|
280
|
-
const input = String(text || '');
|
|
281
|
-
if (!input) return '';
|
|
282
|
-
|
|
283
|
-
let cursor = 0;
|
|
284
|
-
let out = '';
|
|
285
|
-
let removedThink = false;
|
|
286
|
-
|
|
287
|
-
while (cursor < input.length) {
|
|
288
|
-
const openIdx = input.indexOf('<think>', cursor);
|
|
289
|
-
const closeIdx = input.indexOf('</think>', cursor);
|
|
290
|
-
|
|
291
|
-
if (openIdx === -1 && closeIdx === -1) {
|
|
292
|
-
out += input.slice(cursor);
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (closeIdx !== -1 && (openIdx === -1 || closeIdx < openIdx)) {
|
|
297
|
-
removedThink = true;
|
|
298
|
-
cursor = closeIdx + '</think>'.length;
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
out += input.slice(cursor, openIdx);
|
|
303
|
-
const closingTagIdx = input.indexOf('</think>', openIdx + '<think>'.length);
|
|
304
|
-
removedThink = true;
|
|
305
|
-
if (closingTagIdx === -1) {
|
|
306
|
-
cursor = input.length;
|
|
307
|
-
break;
|
|
308
|
-
}
|
|
309
|
-
cursor = closingTagIdx + '</think>'.length;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return removedThink ? out.trimStart() : out;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function sanitizeMiniMaxText(model, text) {
|
|
316
|
-
return isMiniMaxModel(model) ? stripMiniMaxThinkContent(text) : text;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function nextMiniMaxVisibleChunk(state, content) {
|
|
320
|
-
const rawChunk = extractTextContent(content);
|
|
321
|
-
if (!rawChunk) {
|
|
322
|
-
return { textDelta: '', nextState: state };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const nextRawContent = rawChunk.startsWith(state.rawContent) ? rawChunk : `${state.rawContent}${rawChunk}`;
|
|
326
|
-
const nextVisibleText = stripMiniMaxThinkContent(nextRawContent);
|
|
327
|
-
const textDelta = nextVisibleText.startsWith(state.visibleText)
|
|
328
|
-
? nextVisibleText.slice(state.visibleText.length)
|
|
329
|
-
: nextVisibleText;
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
textDelta,
|
|
333
|
-
nextState: {
|
|
334
|
-
rawContent: nextRawContent,
|
|
335
|
-
visibleText: nextVisibleText
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export async function createChatCompletion({
|
|
341
|
-
baseUrl,
|
|
342
|
-
apiKey,
|
|
343
|
-
model,
|
|
344
|
-
messages,
|
|
345
|
-
temperature = 0.2,
|
|
346
|
-
tools,
|
|
347
|
-
timeoutMs = 1800000,
|
|
348
|
-
maxRetries = 2
|
|
349
|
-
}) {
|
|
350
|
-
const payload = buildPayload({ model, temperature, messages, tools });
|
|
351
|
-
const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
|
|
352
|
-
method: 'POST',
|
|
353
|
-
headers: createHeaders(apiKey),
|
|
354
|
-
body: JSON.stringify(payload),
|
|
355
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
356
|
-
}, { maxRetries });
|
|
357
|
-
const data = await parseJsonResponse(response);
|
|
358
|
-
const message = data?.choices?.[0]?.message || {};
|
|
359
|
-
const text = sanitizeMiniMaxText(model, extractTextContent(message.content));
|
|
360
|
-
const reasoningContent = extractReasoningContent(message.reasoning_content);
|
|
361
|
-
const toolCalls = (message.tool_calls || []).map((tc) => ({
|
|
362
|
-
id: tc.id,
|
|
363
|
-
name: tc.function?.name,
|
|
364
|
-
arguments: normalizeIncomingToolCallArguments(tc.function?.arguments)
|
|
365
|
-
}));
|
|
366
|
-
const normalizedText = String(text || '').trim();
|
|
367
|
-
|
|
368
|
-
if (!normalizedText && toolCalls.length === 0) {
|
|
369
|
-
if (hasTrailingToolContext(messages)) {
|
|
370
|
-
return {
|
|
371
|
-
text: '',
|
|
372
|
-
toolCalls: [],
|
|
373
|
-
usage: data?.usage || null,
|
|
374
|
-
incomplete: true
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
throw new Error('Gateway returned empty assistant response');
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
text,
|
|
382
|
-
toolCalls,
|
|
383
|
-
usage: data?.usage || null,
|
|
384
|
-
assistantMessage: buildAssistantMessage({
|
|
385
|
-
text,
|
|
386
|
-
toolCalls,
|
|
387
|
-
content: message.content ?? text,
|
|
388
|
-
reasoningContent
|
|
389
|
-
})
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export async function createChatCompletionStream({
|
|
394
|
-
baseUrl,
|
|
395
|
-
apiKey,
|
|
396
|
-
model,
|
|
397
|
-
messages,
|
|
398
|
-
temperature = 0.2,
|
|
399
|
-
tools,
|
|
400
|
-
onTextDelta,
|
|
401
|
-
onToolCallDelta,
|
|
402
|
-
timeoutMs = 1800000,
|
|
403
|
-
maxRetries = 2,
|
|
404
|
-
signal: externalSignal
|
|
405
|
-
}) {
|
|
406
|
-
// 合并超时信号与外部中止信号,任一触发都会中止请求
|
|
407
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
408
|
-
const controller = new AbortController();
|
|
409
|
-
const onAbort = () => controller.abort();
|
|
410
|
-
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
|
411
|
-
if (externalSignal) {
|
|
412
|
-
if (externalSignal.aborted) {
|
|
413
|
-
controller.abort();
|
|
414
|
-
} else {
|
|
415
|
-
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
const payload = buildPayload({ model, temperature, messages, tools, stream: true });
|
|
419
|
-
const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
|
|
420
|
-
method: 'POST',
|
|
421
|
-
headers: createHeaders(apiKey),
|
|
422
|
-
body: JSON.stringify(payload),
|
|
423
|
-
signal: controller.signal
|
|
424
|
-
}, { maxRetries });
|
|
425
|
-
if (!response.ok || !response.body) {
|
|
426
|
-
const text = await response.text().catch(() => '');
|
|
427
|
-
throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
|
|
428
|
-
}
|
|
429
|
-
let text = '';
|
|
430
|
-
let reasoningContent = '';
|
|
431
|
-
const toolCallsByIndex = new Map();
|
|
432
|
-
let usage = null;
|
|
433
|
-
let miniMaxStreamState = { rawContent: '', visibleText: '' };
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
for await (const chunk of iterateSseEvents(response.body)) {
|
|
437
|
-
usage = chunk?.usage || usage;
|
|
438
|
-
const choice0 = chunk?.choices?.[0] || {};
|
|
439
|
-
const delta = choice0?.delta || {};
|
|
440
|
-
const content = delta.content;
|
|
441
|
-
const reasoningDelta = extractReasoningContent(delta.reasoning_content);
|
|
442
|
-
if (reasoningDelta) {
|
|
443
|
-
reasoningContent += reasoningDelta;
|
|
444
|
-
}
|
|
445
|
-
if (isMiniMaxModel(model)) {
|
|
446
|
-
const next = nextMiniMaxVisibleChunk(miniMaxStreamState, content);
|
|
447
|
-
miniMaxStreamState = next.nextState;
|
|
448
|
-
if (next.textDelta) {
|
|
449
|
-
text += next.textDelta;
|
|
450
|
-
if (onTextDelta) onTextDelta(next.textDelta);
|
|
451
|
-
}
|
|
452
|
-
} else if (typeof content === 'string' && content.length > 0) {
|
|
453
|
-
text += content;
|
|
454
|
-
if (onTextDelta) onTextDelta(content);
|
|
455
|
-
} else if (Array.isArray(content) && content.length > 0) {
|
|
456
|
-
const chunkText = extractTextContent(content);
|
|
457
|
-
if (chunkText) {
|
|
458
|
-
text += chunkText;
|
|
459
|
-
if (onTextDelta) onTextDelta(chunkText);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const toolDeltas = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
464
|
-
for (const td of toolDeltas) {
|
|
465
|
-
const idx = typeof td.index === 'number' ? td.index : 0;
|
|
466
|
-
const current = toolCallsByIndex.get(idx) || emptyToolCall(idx);
|
|
467
|
-
if (td.id) current.id = td.id;
|
|
468
|
-
if (td.function?.name) current.name = `${current.name}${td.function.name}`;
|
|
469
|
-
if (td.function?.arguments !== undefined) {
|
|
470
|
-
current.arguments = `${current.arguments}${normalizeIncomingToolCallArguments(td.function.arguments)}`;
|
|
471
|
-
}
|
|
472
|
-
toolCallsByIndex.set(idx, current);
|
|
473
|
-
if (onToolCallDelta) {
|
|
474
|
-
onToolCallDelta({
|
|
475
|
-
index: idx,
|
|
476
|
-
id: current.id || `tc-${idx + 1}`,
|
|
477
|
-
name: current.name,
|
|
478
|
-
arguments: current.arguments || '{}'
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (choice0?.finish_reason) {
|
|
484
|
-
break;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
} finally {
|
|
488
|
-
timeoutSignal.removeEventListener('abort', onAbort);
|
|
489
|
-
if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const result = buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
|
|
493
|
-
return {
|
|
494
|
-
...result,
|
|
495
|
-
assistantMessage: buildAssistantMessage({
|
|
496
|
-
text: result.text,
|
|
497
|
-
toolCalls: result.toolCalls,
|
|
498
|
-
reasoningContent
|
|
499
|
-
})
|
|
500
|
-
};
|
|
501
|
-
}
|
|
1
|
+
function extractTextContent(content) {
|
|
2
|
+
if (typeof content === 'string') return content;
|
|
3
|
+
if (Array.isArray(content)) {
|
|
4
|
+
return content
|
|
5
|
+
.map((part) => {
|
|
6
|
+
if (typeof part === 'string') return part;
|
|
7
|
+
if (part?.type === 'text') return part.text || '';
|
|
8
|
+
return '';
|
|
9
|
+
})
|
|
10
|
+
.join('');
|
|
11
|
+
}
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function extractReasoningContent(payload) {
|
|
16
|
+
if (typeof payload === 'string') return payload;
|
|
17
|
+
if (Array.isArray(payload)) {
|
|
18
|
+
return payload
|
|
19
|
+
.map((part) => {
|
|
20
|
+
if (part?.type === 'reasoning') return part.text || '';
|
|
21
|
+
if (part?.type === 'reasoning_content') return part.text || part.reasoning_content || '';
|
|
22
|
+
return '';
|
|
23
|
+
})
|
|
24
|
+
.join('');
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function emptyToolCall(index) {
|
|
30
|
+
return {
|
|
31
|
+
index,
|
|
32
|
+
id: '',
|
|
33
|
+
name: '',
|
|
34
|
+
arguments: ''
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createHeaders(apiKey) {
|
|
39
|
+
return {
|
|
40
|
+
'content-type': 'application/json',
|
|
41
|
+
authorization: `Bearer ${apiKey}`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildChatCompletionsUrl(baseUrl) {
|
|
46
|
+
return `${String(baseUrl || '').replace(/\/$/, '')}/chat/completions`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function parseJsonResponse(response) {
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const text = await response.text().catch(() => '');
|
|
52
|
+
throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
|
|
53
|
+
}
|
|
54
|
+
return response.json();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isRetryableStatus(status) {
|
|
58
|
+
return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isRetryableError(error) {
|
|
62
|
+
const name = String(error?.name || '');
|
|
63
|
+
if (name === 'AbortError' || name === 'TimeoutError') return false;
|
|
64
|
+
const message = String(error?.message || error || '');
|
|
65
|
+
return /fetch failed|network|socket|ECONNRESET|ETIMEDOUT|EAI_AGAIN/i.test(message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fetchWithRetry(url, init, { maxRetries = 0 } = {}) {
|
|
69
|
+
const attempts = Math.max(0, Number(maxRetries) || 0) + 1;
|
|
70
|
+
let lastError;
|
|
71
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(url, init);
|
|
74
|
+
if (response.ok || !isRetryableStatus(response.status) || attempt === attempts - 1) {
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
await response.arrayBuffer().catch(() => null);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
lastError = error;
|
|
80
|
+
if (!isRetryableError(error) || attempt === attempts - 1) throw error;
|
|
81
|
+
}
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
83
|
+
}
|
|
84
|
+
throw lastError || new Error('Gateway request failed');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function* iterateSseEvents(stream) {
|
|
88
|
+
const decoder = new TextDecoder();
|
|
89
|
+
let buffer = '';
|
|
90
|
+
const flushEvent = (rawEvent) => {
|
|
91
|
+
const dataLines = String(rawEvent || '')
|
|
92
|
+
.split(/\r?\n/)
|
|
93
|
+
.filter((line) => line.startsWith('data:'))
|
|
94
|
+
.map((line) => line.slice(5).trimStart());
|
|
95
|
+
const dataText = dataLines.join('\n');
|
|
96
|
+
if (!dataText || dataText === '[DONE]') return null;
|
|
97
|
+
return JSON.parse(dataText);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for await (const chunk of stream) {
|
|
101
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
102
|
+
while (true) {
|
|
103
|
+
const lfBoundary = buffer.indexOf('\n\n');
|
|
104
|
+
const crlfBoundary = buffer.indexOf('\r\n\r\n');
|
|
105
|
+
if (lfBoundary === -1 && crlfBoundary === -1) break;
|
|
106
|
+
const useCrlf = crlfBoundary !== -1 && (lfBoundary === -1 || crlfBoundary < lfBoundary);
|
|
107
|
+
const boundary = useCrlf ? crlfBoundary : lfBoundary;
|
|
108
|
+
const separatorLength = useCrlf ? 4 : 2;
|
|
109
|
+
const rawEvent = buffer.slice(0, boundary);
|
|
110
|
+
buffer = buffer.slice(boundary + separatorLength);
|
|
111
|
+
const parsed = flushEvent(rawEvent);
|
|
112
|
+
if (parsed) yield parsed;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
buffer += decoder.decode();
|
|
117
|
+
const trailingEvent = flushEvent(buffer.trim());
|
|
118
|
+
if (trailingEvent) {
|
|
119
|
+
yield trailingEvent;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isMiniMaxModel(model) {
|
|
124
|
+
return String(model || '').toLowerCase().includes('minimax');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeToolCallArguments(argumentsText) {
|
|
128
|
+
const raw = typeof argumentsText === 'string' ? argumentsText : JSON.stringify(argumentsText ?? {});
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
132
|
+
return JSON.stringify(parsed);
|
|
133
|
+
}
|
|
134
|
+
} catch {}
|
|
135
|
+
return '{}';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeIncomingToolCallArguments(argumentsValue) {
|
|
139
|
+
if (typeof argumentsValue === 'string') return argumentsValue;
|
|
140
|
+
if (argumentsValue == null) return '{}';
|
|
141
|
+
try {
|
|
142
|
+
return JSON.stringify(argumentsValue);
|
|
143
|
+
} catch {
|
|
144
|
+
return '{}';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sanitizeGatewayMessages(messages) {
|
|
149
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
150
|
+
return source
|
|
151
|
+
.filter((message) => message && typeof message === 'object')
|
|
152
|
+
.map((message) => {
|
|
153
|
+
if (!Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
|
|
154
|
+
return message;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
...message,
|
|
158
|
+
tool_calls: message.tool_calls.map((toolCall) => ({
|
|
159
|
+
...toolCall,
|
|
160
|
+
function: {
|
|
161
|
+
...toolCall?.function,
|
|
162
|
+
arguments: normalizeToolCallArguments(toolCall?.function?.arguments)
|
|
163
|
+
}
|
|
164
|
+
}))
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildAssistantMessage({ text = '', toolCalls = [], content, reasoningContent = '' }) {
|
|
170
|
+
const assistantMessage = {
|
|
171
|
+
role: 'assistant',
|
|
172
|
+
content: content ?? text
|
|
173
|
+
};
|
|
174
|
+
if (reasoningContent) {
|
|
175
|
+
assistantMessage.reasoning_content = reasoningContent;
|
|
176
|
+
}
|
|
177
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
178
|
+
assistantMessage.tool_calls = toolCalls.map((tc) => ({
|
|
179
|
+
id: tc.id,
|
|
180
|
+
type: 'function',
|
|
181
|
+
function: {
|
|
182
|
+
name: tc.name,
|
|
183
|
+
arguments: tc.arguments || '{}'
|
|
184
|
+
}
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
return assistantMessage;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sanitizeMiniMaxMessages(messages) {
|
|
191
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
192
|
+
const out = [];
|
|
193
|
+
let seenNonSystem = false;
|
|
194
|
+
let keptLeadingSystem = false;
|
|
195
|
+
|
|
196
|
+
for (const message of source) {
|
|
197
|
+
if (!message || typeof message !== 'object') continue;
|
|
198
|
+
if (message.role === 'system') {
|
|
199
|
+
if (!seenNonSystem && !keptLeadingSystem) {
|
|
200
|
+
out.push(message);
|
|
201
|
+
keptLeadingSystem = true;
|
|
202
|
+
} else {
|
|
203
|
+
out.push({
|
|
204
|
+
role: 'user',
|
|
205
|
+
content: `[system-note]\n${extractTextContent(message.content)}`
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
seenNonSystem = true;
|
|
211
|
+
out.push(message);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildPayload({ model, temperature, messages, tools, stream = false }) {
|
|
218
|
+
const sanitizedMessages = sanitizeGatewayMessages(messages);
|
|
219
|
+
const payload = {
|
|
220
|
+
model,
|
|
221
|
+
temperature,
|
|
222
|
+
messages: isMiniMaxModel(model) ? sanitizeMiniMaxMessages(sanitizedMessages) : sanitizedMessages
|
|
223
|
+
};
|
|
224
|
+
if (stream) {
|
|
225
|
+
payload.stream = true;
|
|
226
|
+
}
|
|
227
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
228
|
+
payload.tools = tools;
|
|
229
|
+
payload.tool_choice = 'auto';
|
|
230
|
+
}
|
|
231
|
+
if (isMiniMaxModel(model)) {
|
|
232
|
+
payload.extra_body = { reasoning_split: true };
|
|
233
|
+
}
|
|
234
|
+
return payload;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function hasTrailingToolContext(messages) {
|
|
238
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
239
|
+
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
240
|
+
const message = source[index];
|
|
241
|
+
if (!message || typeof message !== 'object') continue;
|
|
242
|
+
if (message.role === 'tool') return true;
|
|
243
|
+
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
|
|
249
|
+
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
250
|
+
.sort((a, b) => a[0] - b[0])
|
|
251
|
+
.map(([, tc], i) => ({
|
|
252
|
+
id: tc.id || `tc-${i + 1}`,
|
|
253
|
+
name: tc.name,
|
|
254
|
+
arguments: tc.arguments || '{}'
|
|
255
|
+
}))
|
|
256
|
+
.filter((tc) => tc.name);
|
|
257
|
+
const normalizedText = String(text || '').trim();
|
|
258
|
+
|
|
259
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
260
|
+
if (hasTrailingToolContext(messages)) {
|
|
261
|
+
return {
|
|
262
|
+
text: '',
|
|
263
|
+
toolCalls: [],
|
|
264
|
+
usage,
|
|
265
|
+
incomplete: true
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
throw new Error('Gateway stream returned empty assistant response');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
text,
|
|
273
|
+
toolCalls,
|
|
274
|
+
usage,
|
|
275
|
+
incomplete: false
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function stripMiniMaxThinkContent(text) {
|
|
280
|
+
const input = String(text || '');
|
|
281
|
+
if (!input) return '';
|
|
282
|
+
|
|
283
|
+
let cursor = 0;
|
|
284
|
+
let out = '';
|
|
285
|
+
let removedThink = false;
|
|
286
|
+
|
|
287
|
+
while (cursor < input.length) {
|
|
288
|
+
const openIdx = input.indexOf('<think>', cursor);
|
|
289
|
+
const closeIdx = input.indexOf('</think>', cursor);
|
|
290
|
+
|
|
291
|
+
if (openIdx === -1 && closeIdx === -1) {
|
|
292
|
+
out += input.slice(cursor);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (closeIdx !== -1 && (openIdx === -1 || closeIdx < openIdx)) {
|
|
297
|
+
removedThink = true;
|
|
298
|
+
cursor = closeIdx + '</think>'.length;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
out += input.slice(cursor, openIdx);
|
|
303
|
+
const closingTagIdx = input.indexOf('</think>', openIdx + '<think>'.length);
|
|
304
|
+
removedThink = true;
|
|
305
|
+
if (closingTagIdx === -1) {
|
|
306
|
+
cursor = input.length;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
cursor = closingTagIdx + '</think>'.length;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return removedThink ? out.trimStart() : out;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function sanitizeMiniMaxText(model, text) {
|
|
316
|
+
return isMiniMaxModel(model) ? stripMiniMaxThinkContent(text) : text;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function nextMiniMaxVisibleChunk(state, content) {
|
|
320
|
+
const rawChunk = extractTextContent(content);
|
|
321
|
+
if (!rawChunk) {
|
|
322
|
+
return { textDelta: '', nextState: state };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const nextRawContent = rawChunk.startsWith(state.rawContent) ? rawChunk : `${state.rawContent}${rawChunk}`;
|
|
326
|
+
const nextVisibleText = stripMiniMaxThinkContent(nextRawContent);
|
|
327
|
+
const textDelta = nextVisibleText.startsWith(state.visibleText)
|
|
328
|
+
? nextVisibleText.slice(state.visibleText.length)
|
|
329
|
+
: nextVisibleText;
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
textDelta,
|
|
333
|
+
nextState: {
|
|
334
|
+
rawContent: nextRawContent,
|
|
335
|
+
visibleText: nextVisibleText
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function createChatCompletion({
|
|
341
|
+
baseUrl,
|
|
342
|
+
apiKey,
|
|
343
|
+
model,
|
|
344
|
+
messages,
|
|
345
|
+
temperature = 0.2,
|
|
346
|
+
tools,
|
|
347
|
+
timeoutMs = 1800000,
|
|
348
|
+
maxRetries = 2
|
|
349
|
+
}) {
|
|
350
|
+
const payload = buildPayload({ model, temperature, messages, tools });
|
|
351
|
+
const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
headers: createHeaders(apiKey),
|
|
354
|
+
body: JSON.stringify(payload),
|
|
355
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
356
|
+
}, { maxRetries });
|
|
357
|
+
const data = await parseJsonResponse(response);
|
|
358
|
+
const message = data?.choices?.[0]?.message || {};
|
|
359
|
+
const text = sanitizeMiniMaxText(model, extractTextContent(message.content));
|
|
360
|
+
const reasoningContent = extractReasoningContent(message.reasoning_content);
|
|
361
|
+
const toolCalls = (message.tool_calls || []).map((tc) => ({
|
|
362
|
+
id: tc.id,
|
|
363
|
+
name: tc.function?.name,
|
|
364
|
+
arguments: normalizeIncomingToolCallArguments(tc.function?.arguments)
|
|
365
|
+
}));
|
|
366
|
+
const normalizedText = String(text || '').trim();
|
|
367
|
+
|
|
368
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
369
|
+
if (hasTrailingToolContext(messages)) {
|
|
370
|
+
return {
|
|
371
|
+
text: '',
|
|
372
|
+
toolCalls: [],
|
|
373
|
+
usage: data?.usage || null,
|
|
374
|
+
incomplete: true
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
throw new Error('Gateway returned empty assistant response');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
text,
|
|
382
|
+
toolCalls,
|
|
383
|
+
usage: data?.usage || null,
|
|
384
|
+
assistantMessage: buildAssistantMessage({
|
|
385
|
+
text,
|
|
386
|
+
toolCalls,
|
|
387
|
+
content: message.content ?? text,
|
|
388
|
+
reasoningContent
|
|
389
|
+
})
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function createChatCompletionStream({
|
|
394
|
+
baseUrl,
|
|
395
|
+
apiKey,
|
|
396
|
+
model,
|
|
397
|
+
messages,
|
|
398
|
+
temperature = 0.2,
|
|
399
|
+
tools,
|
|
400
|
+
onTextDelta,
|
|
401
|
+
onToolCallDelta,
|
|
402
|
+
timeoutMs = 1800000,
|
|
403
|
+
maxRetries = 2,
|
|
404
|
+
signal: externalSignal
|
|
405
|
+
}) {
|
|
406
|
+
// 合并超时信号与外部中止信号,任一触发都会中止请求
|
|
407
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
408
|
+
const controller = new AbortController();
|
|
409
|
+
const onAbort = () => controller.abort();
|
|
410
|
+
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
|
411
|
+
if (externalSignal) {
|
|
412
|
+
if (externalSignal.aborted) {
|
|
413
|
+
controller.abort();
|
|
414
|
+
} else {
|
|
415
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const payload = buildPayload({ model, temperature, messages, tools, stream: true });
|
|
419
|
+
const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
|
|
420
|
+
method: 'POST',
|
|
421
|
+
headers: createHeaders(apiKey),
|
|
422
|
+
body: JSON.stringify(payload),
|
|
423
|
+
signal: controller.signal
|
|
424
|
+
}, { maxRetries });
|
|
425
|
+
if (!response.ok || !response.body) {
|
|
426
|
+
const text = await response.text().catch(() => '');
|
|
427
|
+
throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
|
|
428
|
+
}
|
|
429
|
+
let text = '';
|
|
430
|
+
let reasoningContent = '';
|
|
431
|
+
const toolCallsByIndex = new Map();
|
|
432
|
+
let usage = null;
|
|
433
|
+
let miniMaxStreamState = { rawContent: '', visibleText: '' };
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
for await (const chunk of iterateSseEvents(response.body)) {
|
|
437
|
+
usage = chunk?.usage || usage;
|
|
438
|
+
const choice0 = chunk?.choices?.[0] || {};
|
|
439
|
+
const delta = choice0?.delta || {};
|
|
440
|
+
const content = delta.content;
|
|
441
|
+
const reasoningDelta = extractReasoningContent(delta.reasoning_content);
|
|
442
|
+
if (reasoningDelta) {
|
|
443
|
+
reasoningContent += reasoningDelta;
|
|
444
|
+
}
|
|
445
|
+
if (isMiniMaxModel(model)) {
|
|
446
|
+
const next = nextMiniMaxVisibleChunk(miniMaxStreamState, content);
|
|
447
|
+
miniMaxStreamState = next.nextState;
|
|
448
|
+
if (next.textDelta) {
|
|
449
|
+
text += next.textDelta;
|
|
450
|
+
if (onTextDelta) onTextDelta(next.textDelta);
|
|
451
|
+
}
|
|
452
|
+
} else if (typeof content === 'string' && content.length > 0) {
|
|
453
|
+
text += content;
|
|
454
|
+
if (onTextDelta) onTextDelta(content);
|
|
455
|
+
} else if (Array.isArray(content) && content.length > 0) {
|
|
456
|
+
const chunkText = extractTextContent(content);
|
|
457
|
+
if (chunkText) {
|
|
458
|
+
text += chunkText;
|
|
459
|
+
if (onTextDelta) onTextDelta(chunkText);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const toolDeltas = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
464
|
+
for (const td of toolDeltas) {
|
|
465
|
+
const idx = typeof td.index === 'number' ? td.index : 0;
|
|
466
|
+
const current = toolCallsByIndex.get(idx) || emptyToolCall(idx);
|
|
467
|
+
if (td.id) current.id = td.id;
|
|
468
|
+
if (td.function?.name) current.name = `${current.name}${td.function.name}`;
|
|
469
|
+
if (td.function?.arguments !== undefined) {
|
|
470
|
+
current.arguments = `${current.arguments}${normalizeIncomingToolCallArguments(td.function.arguments)}`;
|
|
471
|
+
}
|
|
472
|
+
toolCallsByIndex.set(idx, current);
|
|
473
|
+
if (onToolCallDelta) {
|
|
474
|
+
onToolCallDelta({
|
|
475
|
+
index: idx,
|
|
476
|
+
id: current.id || `tc-${idx + 1}`,
|
|
477
|
+
name: current.name,
|
|
478
|
+
arguments: current.arguments || '{}'
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (choice0?.finish_reason) {
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} finally {
|
|
488
|
+
timeoutSignal.removeEventListener('abort', onAbort);
|
|
489
|
+
if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const result = buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
|
|
493
|
+
return {
|
|
494
|
+
...result,
|
|
495
|
+
assistantMessage: buildAssistantMessage({
|
|
496
|
+
text: result.text,
|
|
497
|
+
toolCalls: result.toolCalls,
|
|
498
|
+
reasoningContent
|
|
499
|
+
})
|
|
500
|
+
};
|
|
501
|
+
}
|