codemini-cli 0.3.4 → 0.3.6
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 +20 -18
- package/package.json +6 -6
- package/souls/anime.md +12 -9
- package/souls/caveman.md +6 -6
- package/souls/ceo.md +10 -9
- package/souls/default.md +1 -1
- package/souls/pirate.md +6 -6
- package/souls/playful.md +7 -7
- package/souls/professional.md +1 -1
- package/src/cli.js +3 -1
- package/src/commands/run.js +229 -16
- package/src/core/agent-loop.js +167 -49
- package/src/core/ast.js +40 -0
- package/src/core/chat-runtime.js +720 -126
- package/src/core/command-policy.js +56 -0
- package/src/core/config-store.js +0 -3
- package/src/core/crypto-utils.js +6 -2
- package/src/core/memory-store.js +3 -3
- package/src/core/project-index.js +4 -18
- package/src/core/provider/anthropic.js +15 -2
- package/src/core/provider/anthropic.sdk-backup.js +439 -0
- package/src/core/provider/openai-compatible.js +93 -11
- package/src/core/provider/openai-compatible.sdk-backup.js +412 -0
- package/src/core/session-store.js +90 -25
- package/src/core/shell-profile.js +26 -6
- package/src/core/string-utils.js +37 -0
- package/src/core/tools.js +216 -405
- package/src/tui/chat-app.js +490 -146
- package/src/tui/tool-activity/presenters/files.js +2 -2
- package/src/tui/tool-narration.js +0 -3
- package/src/tui/tool-narration/presenters/patch.js +0 -3
|
@@ -12,6 +12,20 @@ function extractTextContent(content) {
|
|
|
12
12
|
return '';
|
|
13
13
|
}
|
|
14
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
|
+
|
|
15
29
|
function emptyToolCall(index) {
|
|
16
30
|
return {
|
|
17
31
|
index,
|
|
@@ -43,6 +57,15 @@ async function parseJsonResponse(response) {
|
|
|
43
57
|
async function* iterateSseEvents(stream) {
|
|
44
58
|
const decoder = new TextDecoder();
|
|
45
59
|
let buffer = '';
|
|
60
|
+
const flushEvent = (rawEvent) => {
|
|
61
|
+
const dataLines = String(rawEvent || '')
|
|
62
|
+
.split(/\r?\n/)
|
|
63
|
+
.filter((line) => line.startsWith('data:'))
|
|
64
|
+
.map((line) => line.slice(5).trimStart());
|
|
65
|
+
const dataText = dataLines.join('\n');
|
|
66
|
+
if (!dataText || dataText === '[DONE]') return null;
|
|
67
|
+
return JSON.parse(dataText);
|
|
68
|
+
};
|
|
46
69
|
|
|
47
70
|
for await (const chunk of stream) {
|
|
48
71
|
buffer += decoder.decode(chunk, { stream: true });
|
|
@@ -55,15 +78,16 @@ async function* iterateSseEvents(stream) {
|
|
|
55
78
|
const separatorLength = useCrlf ? 4 : 2;
|
|
56
79
|
const rawEvent = buffer.slice(0, boundary);
|
|
57
80
|
buffer = buffer.slice(boundary + separatorLength);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
.filter((line) => line.startsWith('data:'))
|
|
61
|
-
.map((line) => line.slice(5).trimStart());
|
|
62
|
-
const dataText = dataLines.join('\n');
|
|
63
|
-
if (!dataText || dataText === '[DONE]') continue;
|
|
64
|
-
yield JSON.parse(dataText);
|
|
81
|
+
const parsed = flushEvent(rawEvent);
|
|
82
|
+
if (parsed) yield parsed;
|
|
65
83
|
}
|
|
66
84
|
}
|
|
85
|
+
|
|
86
|
+
buffer += decoder.decode();
|
|
87
|
+
const trailingEvent = flushEvent(buffer.trim());
|
|
88
|
+
if (trailingEvent) {
|
|
89
|
+
yield trailingEvent;
|
|
90
|
+
}
|
|
67
91
|
}
|
|
68
92
|
|
|
69
93
|
function isMiniMaxModel(model) {
|
|
@@ -112,6 +136,27 @@ function sanitizeGatewayMessages(messages) {
|
|
|
112
136
|
});
|
|
113
137
|
}
|
|
114
138
|
|
|
139
|
+
function buildAssistantMessage({ text = '', toolCalls = [], content, reasoningContent = '' }) {
|
|
140
|
+
const assistantMessage = {
|
|
141
|
+
role: 'assistant',
|
|
142
|
+
content: content ?? text
|
|
143
|
+
};
|
|
144
|
+
if (reasoningContent) {
|
|
145
|
+
assistantMessage.reasoning_content = reasoningContent;
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
148
|
+
assistantMessage.tool_calls = toolCalls.map((tc) => ({
|
|
149
|
+
id: tc.id,
|
|
150
|
+
type: 'function',
|
|
151
|
+
function: {
|
|
152
|
+
name: tc.name,
|
|
153
|
+
arguments: tc.arguments || '{}'
|
|
154
|
+
}
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
return assistantMessage;
|
|
158
|
+
}
|
|
159
|
+
|
|
115
160
|
function sanitizeMiniMaxMessages(messages) {
|
|
116
161
|
const source = Array.isArray(messages) ? messages : [];
|
|
117
162
|
const out = [];
|
|
@@ -282,6 +327,7 @@ export async function createChatCompletion({
|
|
|
282
327
|
const data = await parseJsonResponse(response);
|
|
283
328
|
const message = data?.choices?.[0]?.message || {};
|
|
284
329
|
const text = sanitizeMiniMaxText(model, extractTextContent(message.content));
|
|
330
|
+
const reasoningContent = extractReasoningContent(message.reasoning_content);
|
|
285
331
|
const toolCalls = (message.tool_calls || []).map((tc) => ({
|
|
286
332
|
id: tc.id,
|
|
287
333
|
name: tc.function?.name,
|
|
@@ -304,7 +350,13 @@ export async function createChatCompletion({
|
|
|
304
350
|
return {
|
|
305
351
|
text,
|
|
306
352
|
toolCalls,
|
|
307
|
-
usage: data?.usage || null
|
|
353
|
+
usage: data?.usage || null,
|
|
354
|
+
assistantMessage: buildAssistantMessage({
|
|
355
|
+
text,
|
|
356
|
+
toolCalls,
|
|
357
|
+
content: message.content ?? text,
|
|
358
|
+
reasoningContent
|
|
359
|
+
})
|
|
308
360
|
};
|
|
309
361
|
}
|
|
310
362
|
|
|
@@ -318,20 +370,34 @@ export async function createChatCompletionStream({
|
|
|
318
370
|
onTextDelta,
|
|
319
371
|
onToolCallDelta,
|
|
320
372
|
timeoutMs = 90000,
|
|
321
|
-
maxRetries = 2
|
|
373
|
+
maxRetries = 2,
|
|
374
|
+
signal: externalSignal
|
|
322
375
|
}) {
|
|
376
|
+
// 合并超时信号与外部中止信号,任一触发都会中止请求
|
|
377
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
378
|
+
const controller = new AbortController();
|
|
379
|
+
const onAbort = () => controller.abort();
|
|
380
|
+
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
|
381
|
+
if (externalSignal) {
|
|
382
|
+
if (externalSignal.aborted) {
|
|
383
|
+
controller.abort();
|
|
384
|
+
} else {
|
|
385
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
323
388
|
const payload = buildPayload({ model, temperature, messages, tools, stream: true });
|
|
324
389
|
const response = await fetch(buildChatCompletionsUrl(baseUrl), {
|
|
325
390
|
method: 'POST',
|
|
326
391
|
headers: createHeaders(apiKey),
|
|
327
392
|
body: JSON.stringify(payload),
|
|
328
|
-
signal:
|
|
393
|
+
signal: controller.signal
|
|
329
394
|
});
|
|
330
395
|
if (!response.ok || !response.body) {
|
|
331
396
|
const text = await response.text().catch(() => '');
|
|
332
397
|
throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
|
|
333
398
|
}
|
|
334
399
|
let text = '';
|
|
400
|
+
let reasoningContent = '';
|
|
335
401
|
const toolCallsByIndex = new Map();
|
|
336
402
|
let usage = null;
|
|
337
403
|
let miniMaxStreamState = { rawContent: '', visibleText: '' };
|
|
@@ -341,6 +407,10 @@ export async function createChatCompletionStream({
|
|
|
341
407
|
const choice0 = chunk?.choices?.[0] || {};
|
|
342
408
|
const delta = choice0?.delta || {};
|
|
343
409
|
const content = delta.content;
|
|
410
|
+
const reasoningDelta = extractReasoningContent(delta.reasoning_content);
|
|
411
|
+
if (reasoningDelta) {
|
|
412
|
+
reasoningContent += reasoningDelta;
|
|
413
|
+
}
|
|
344
414
|
if (isMiniMaxModel(model)) {
|
|
345
415
|
const next = nextMiniMaxVisibleChunk(miniMaxStreamState, content);
|
|
346
416
|
miniMaxStreamState = next.nextState;
|
|
@@ -378,7 +448,19 @@ export async function createChatCompletionStream({
|
|
|
378
448
|
});
|
|
379
449
|
}
|
|
380
450
|
}
|
|
451
|
+
|
|
452
|
+
if (choice0?.finish_reason) {
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
381
455
|
}
|
|
382
456
|
|
|
383
|
-
|
|
457
|
+
const result = buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
|
|
458
|
+
return {
|
|
459
|
+
...result,
|
|
460
|
+
assistantMessage: buildAssistantMessage({
|
|
461
|
+
text: result.text,
|
|
462
|
+
toolCalls: result.toolCalls,
|
|
463
|
+
reasoningContent
|
|
464
|
+
})
|
|
465
|
+
};
|
|
384
466
|
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
|
|
3
|
+
function extractTextContent(content) {
|
|
4
|
+
if (typeof content === 'string') return content;
|
|
5
|
+
if (Array.isArray(content)) {
|
|
6
|
+
return content
|
|
7
|
+
.map((part) => {
|
|
8
|
+
if (typeof part === 'string') return part;
|
|
9
|
+
if (part?.type === 'text') return part.text || '';
|
|
10
|
+
return '';
|
|
11
|
+
})
|
|
12
|
+
.join('');
|
|
13
|
+
}
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function extractReasoningText(details) {
|
|
18
|
+
const source = Array.isArray(details) ? details : [];
|
|
19
|
+
return source
|
|
20
|
+
.map((detail) => {
|
|
21
|
+
if (typeof detail === 'string') return detail;
|
|
22
|
+
return typeof detail?.text === 'string' ? detail.text : '';
|
|
23
|
+
})
|
|
24
|
+
.join('');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeReasoningDetails(details) {
|
|
28
|
+
if (!Array.isArray(details) || details.length === 0) return undefined;
|
|
29
|
+
const normalized = details
|
|
30
|
+
.map((detail) => {
|
|
31
|
+
if (!detail || typeof detail !== 'object') return null;
|
|
32
|
+
return { ...detail };
|
|
33
|
+
})
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function emptyToolCall(index) {
|
|
39
|
+
return {
|
|
40
|
+
index,
|
|
41
|
+
id: '',
|
|
42
|
+
name: '',
|
|
43
|
+
arguments: ''
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isMiniMaxModel(model) {
|
|
48
|
+
return String(model || '').toLowerCase().includes('minimax');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeToolCallArguments(argumentsText) {
|
|
52
|
+
const raw = typeof argumentsText === 'string' ? argumentsText : JSON.stringify(argumentsText ?? {});
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
56
|
+
return JSON.stringify(parsed);
|
|
57
|
+
}
|
|
58
|
+
} catch {}
|
|
59
|
+
return '{}';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeIncomingToolCallArguments(argumentsValue) {
|
|
63
|
+
if (typeof argumentsValue === 'string') return argumentsValue;
|
|
64
|
+
if (argumentsValue == null) return '{}';
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(argumentsValue);
|
|
67
|
+
} catch {
|
|
68
|
+
return '{}';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sanitizeGatewayMessages(messages) {
|
|
73
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
74
|
+
return source
|
|
75
|
+
.filter((message) => message && typeof message === 'object')
|
|
76
|
+
.map((message) => {
|
|
77
|
+
if (!Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
|
|
78
|
+
return message;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
...message,
|
|
82
|
+
tool_calls: message.tool_calls.map((toolCall) => ({
|
|
83
|
+
...toolCall,
|
|
84
|
+
function: {
|
|
85
|
+
...toolCall?.function,
|
|
86
|
+
arguments: normalizeToolCallArguments(toolCall?.function?.arguments)
|
|
87
|
+
}
|
|
88
|
+
}))
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sanitizeMiniMaxMessages(messages) {
|
|
94
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
95
|
+
const out = [];
|
|
96
|
+
let seenNonSystem = false;
|
|
97
|
+
let keptLeadingSystem = false;
|
|
98
|
+
|
|
99
|
+
for (const message of source) {
|
|
100
|
+
if (!message || typeof message !== 'object') continue;
|
|
101
|
+
if (message.role === 'system') {
|
|
102
|
+
if (!seenNonSystem && !keptLeadingSystem) {
|
|
103
|
+
out.push(message);
|
|
104
|
+
keptLeadingSystem = true;
|
|
105
|
+
} else {
|
|
106
|
+
out.push({
|
|
107
|
+
role: 'user',
|
|
108
|
+
content: `[system-note]\n${extractTextContent(message.content)}`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
seenNonSystem = true;
|
|
114
|
+
out.push(message);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildPayload({ model, temperature, messages, tools, stream = false }) {
|
|
121
|
+
const sanitizedMessages = sanitizeGatewayMessages(messages);
|
|
122
|
+
const payload = {
|
|
123
|
+
model,
|
|
124
|
+
temperature,
|
|
125
|
+
messages: isMiniMaxModel(model) ? sanitizeMiniMaxMessages(sanitizedMessages) : sanitizedMessages
|
|
126
|
+
};
|
|
127
|
+
if (stream) payload.stream = true;
|
|
128
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
129
|
+
payload.tools = tools;
|
|
130
|
+
payload.tool_choice = 'auto';
|
|
131
|
+
}
|
|
132
|
+
if (isMiniMaxModel(model)) {
|
|
133
|
+
payload.extra_body = { reasoning_split: true };
|
|
134
|
+
}
|
|
135
|
+
return payload;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function hasTrailingToolContext(messages) {
|
|
139
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
140
|
+
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
141
|
+
const message = source[index];
|
|
142
|
+
if (!message || typeof message !== 'object') continue;
|
|
143
|
+
if (message.role === 'tool') return true;
|
|
144
|
+
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function stripMiniMaxThinkContent(text) {
|
|
150
|
+
const input = String(text || '');
|
|
151
|
+
if (!input) return '';
|
|
152
|
+
|
|
153
|
+
let cursor = 0;
|
|
154
|
+
let out = '';
|
|
155
|
+
let removedThink = false;
|
|
156
|
+
|
|
157
|
+
while (cursor < input.length) {
|
|
158
|
+
const openIdx = input.indexOf('<think>', cursor);
|
|
159
|
+
const closeIdx = input.indexOf('</think>', cursor);
|
|
160
|
+
|
|
161
|
+
if (openIdx === -1 && closeIdx === -1) {
|
|
162
|
+
out += input.slice(cursor);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (closeIdx !== -1 && (openIdx === -1 || closeIdx < openIdx)) {
|
|
167
|
+
removedThink = true;
|
|
168
|
+
cursor = closeIdx + '</think>'.length;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
out += input.slice(cursor, openIdx);
|
|
173
|
+
const closingTagIdx = input.indexOf('</think>', openIdx + '<think>'.length);
|
|
174
|
+
removedThink = true;
|
|
175
|
+
if (closingTagIdx === -1) {
|
|
176
|
+
cursor = input.length;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
cursor = closingTagIdx + '</think>'.length;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return removedThink ? out.trimStart() : out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sanitizeMiniMaxText(model, text) {
|
|
186
|
+
return isMiniMaxModel(model) ? stripMiniMaxThinkContent(text) : text;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function nextMiniMaxVisibleChunk(state, content) {
|
|
190
|
+
const rawChunk = extractTextContent(content);
|
|
191
|
+
if (!rawChunk) {
|
|
192
|
+
return { textDelta: '', nextState: state };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const nextRawContent = rawChunk.startsWith(state.rawContent) ? rawChunk : `${state.rawContent}${rawChunk}`;
|
|
196
|
+
const nextVisibleText = stripMiniMaxThinkContent(nextRawContent);
|
|
197
|
+
const textDelta = nextVisibleText.startsWith(state.visibleText)
|
|
198
|
+
? nextVisibleText.slice(state.visibleText.length)
|
|
199
|
+
: nextVisibleText;
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
textDelta,
|
|
203
|
+
nextState: {
|
|
204
|
+
rawContent: nextRawContent,
|
|
205
|
+
visibleText: nextVisibleText
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildAssistantMessage({ text = '', toolCalls = [], content, reasoningDetails }) {
|
|
211
|
+
const assistantMessage = {
|
|
212
|
+
role: 'assistant',
|
|
213
|
+
content: content ?? text
|
|
214
|
+
};
|
|
215
|
+
const normalizedReasoningDetails = normalizeReasoningDetails(reasoningDetails);
|
|
216
|
+
if (normalizedReasoningDetails) {
|
|
217
|
+
assistantMessage.reasoning_details = normalizedReasoningDetails;
|
|
218
|
+
assistantMessage.reasoning_content = extractReasoningText(normalizedReasoningDetails);
|
|
219
|
+
}
|
|
220
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
221
|
+
assistantMessage.tool_calls = toolCalls.map((tc) => ({
|
|
222
|
+
id: tc.id,
|
|
223
|
+
type: 'function',
|
|
224
|
+
function: {
|
|
225
|
+
name: tc.name,
|
|
226
|
+
arguments: tc.arguments || '{}'
|
|
227
|
+
}
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
return assistantMessage;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function createClient({ baseUrl, apiKey, timeoutMs = 90000, maxRetries = 2 }) {
|
|
234
|
+
return new OpenAI({
|
|
235
|
+
apiKey,
|
|
236
|
+
baseURL: String(baseUrl || '').replace(/\/$/, ''),
|
|
237
|
+
timeout: timeoutMs,
|
|
238
|
+
maxRetries
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildFinalStreamResult({ text, toolCallsByIndex, usage, messages, reasoningDetails }) {
|
|
243
|
+
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
244
|
+
.sort((a, b) => a[0] - b[0])
|
|
245
|
+
.map(([, tc], i) => ({
|
|
246
|
+
id: tc.id || `tc-${i + 1}`,
|
|
247
|
+
name: tc.name,
|
|
248
|
+
arguments: tc.arguments || '{}'
|
|
249
|
+
}))
|
|
250
|
+
.filter((tc) => tc.name);
|
|
251
|
+
const normalizedText = String(text || '').trim();
|
|
252
|
+
|
|
253
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
254
|
+
if (hasTrailingToolContext(messages)) {
|
|
255
|
+
return {
|
|
256
|
+
text: '',
|
|
257
|
+
toolCalls: [],
|
|
258
|
+
usage,
|
|
259
|
+
incomplete: true
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
throw new Error('Gateway stream returned empty assistant response');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
text,
|
|
267
|
+
toolCalls,
|
|
268
|
+
usage,
|
|
269
|
+
incomplete: false,
|
|
270
|
+
assistantMessage: buildAssistantMessage({
|
|
271
|
+
text,
|
|
272
|
+
toolCalls,
|
|
273
|
+
reasoningDetails
|
|
274
|
+
})
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function createChatCompletion({
|
|
279
|
+
baseUrl,
|
|
280
|
+
apiKey,
|
|
281
|
+
model,
|
|
282
|
+
messages,
|
|
283
|
+
temperature = 0.2,
|
|
284
|
+
tools,
|
|
285
|
+
timeoutMs = 90000,
|
|
286
|
+
maxRetries = 2
|
|
287
|
+
}) {
|
|
288
|
+
const client = createClient({ baseUrl, apiKey, timeoutMs, maxRetries });
|
|
289
|
+
const response = await client.chat.completions.create(buildPayload({ model, temperature, messages, tools }));
|
|
290
|
+
const message = response?.choices?.[0]?.message || {};
|
|
291
|
+
const text = sanitizeMiniMaxText(model, extractTextContent(message.content));
|
|
292
|
+
const toolCalls = (message.tool_calls || []).map((tc) => ({
|
|
293
|
+
id: tc.id,
|
|
294
|
+
name: tc.function?.name,
|
|
295
|
+
arguments: normalizeIncomingToolCallArguments(tc.function?.arguments)
|
|
296
|
+
}));
|
|
297
|
+
const normalizedText = String(text || '').trim();
|
|
298
|
+
|
|
299
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
300
|
+
if (hasTrailingToolContext(messages)) {
|
|
301
|
+
return {
|
|
302
|
+
text: '',
|
|
303
|
+
toolCalls: [],
|
|
304
|
+
usage: response?.usage || null,
|
|
305
|
+
incomplete: true
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
throw new Error('Gateway returned empty assistant response');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
text,
|
|
313
|
+
toolCalls,
|
|
314
|
+
usage: response?.usage || null,
|
|
315
|
+
assistantMessage: buildAssistantMessage({
|
|
316
|
+
text,
|
|
317
|
+
toolCalls,
|
|
318
|
+
content: message.content ?? text,
|
|
319
|
+
reasoningDetails: message.reasoning_details
|
|
320
|
+
})
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function createChatCompletionStream({
|
|
325
|
+
baseUrl,
|
|
326
|
+
apiKey,
|
|
327
|
+
model,
|
|
328
|
+
messages,
|
|
329
|
+
temperature = 0.2,
|
|
330
|
+
tools,
|
|
331
|
+
onTextDelta,
|
|
332
|
+
onToolCallDelta,
|
|
333
|
+
timeoutMs = 90000,
|
|
334
|
+
maxRetries = 2
|
|
335
|
+
}) {
|
|
336
|
+
const client = createClient({ baseUrl, apiKey, timeoutMs, maxRetries });
|
|
337
|
+
const stream = await client.chat.completions.create(buildPayload({ model, temperature, messages, tools, stream: true }));
|
|
338
|
+
|
|
339
|
+
let text = '';
|
|
340
|
+
let usage = null;
|
|
341
|
+
const toolCallsByIndex = new Map();
|
|
342
|
+
let miniMaxStreamState = { rawContent: '', visibleText: '' };
|
|
343
|
+
let reasoningDetails = [];
|
|
344
|
+
let previousReasoningText = '';
|
|
345
|
+
|
|
346
|
+
for await (const chunk of stream) {
|
|
347
|
+
usage = chunk?.usage || usage;
|
|
348
|
+
const choice0 = chunk?.choices?.[0] || {};
|
|
349
|
+
const delta = choice0?.delta || {};
|
|
350
|
+
const content = delta.content;
|
|
351
|
+
const nextReasoningDetails = normalizeReasoningDetails(delta.reasoning_details);
|
|
352
|
+
if (nextReasoningDetails) {
|
|
353
|
+
const nextReasoningText = extractReasoningText(nextReasoningDetails);
|
|
354
|
+
if (nextReasoningText.length >= previousReasoningText.length) {
|
|
355
|
+
previousReasoningText = nextReasoningText;
|
|
356
|
+
} else {
|
|
357
|
+
previousReasoningText += nextReasoningText;
|
|
358
|
+
}
|
|
359
|
+
reasoningDetails = previousReasoningText ? [{ type: 'reasoning', text: previousReasoningText }] : reasoningDetails;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (isMiniMaxModel(model)) {
|
|
363
|
+
const next = nextMiniMaxVisibleChunk(miniMaxStreamState, content);
|
|
364
|
+
miniMaxStreamState = next.nextState;
|
|
365
|
+
if (next.textDelta) {
|
|
366
|
+
text += next.textDelta;
|
|
367
|
+
if (onTextDelta) onTextDelta(next.textDelta);
|
|
368
|
+
}
|
|
369
|
+
} else if (typeof content === 'string' && content.length > 0) {
|
|
370
|
+
text += content;
|
|
371
|
+
if (onTextDelta) onTextDelta(content);
|
|
372
|
+
} else if (Array.isArray(content) && content.length > 0) {
|
|
373
|
+
const chunkText = extractTextContent(content);
|
|
374
|
+
if (chunkText) {
|
|
375
|
+
text += chunkText;
|
|
376
|
+
if (onTextDelta) onTextDelta(chunkText);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const toolDeltas = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
381
|
+
for (const td of toolDeltas) {
|
|
382
|
+
const idx = typeof td.index === 'number' ? td.index : 0;
|
|
383
|
+
const current = toolCallsByIndex.get(idx) || emptyToolCall(idx);
|
|
384
|
+
if (td.id) current.id = td.id;
|
|
385
|
+
if (td.function?.name) current.name = `${current.name}${td.function.name}`;
|
|
386
|
+
if (td.function?.arguments !== undefined) {
|
|
387
|
+
current.arguments = `${current.arguments}${normalizeIncomingToolCallArguments(td.function.arguments)}`;
|
|
388
|
+
}
|
|
389
|
+
toolCallsByIndex.set(idx, current);
|
|
390
|
+
if (onToolCallDelta) {
|
|
391
|
+
onToolCallDelta({
|
|
392
|
+
index: idx,
|
|
393
|
+
id: current.id || `tc-${idx + 1}`,
|
|
394
|
+
name: current.name,
|
|
395
|
+
arguments: current.arguments || '{}'
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (choice0?.finish_reason) {
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return buildFinalStreamResult({
|
|
406
|
+
text,
|
|
407
|
+
toolCallsByIndex,
|
|
408
|
+
usage,
|
|
409
|
+
messages,
|
|
410
|
+
reasoningDetails
|
|
411
|
+
});
|
|
412
|
+
}
|