codemini-cli 0.3.3 → 0.3.5
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/package.json +1 -3
- package/souls/anime.md +9 -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/core/agent-loop.js +8 -2
- package/src/core/chat-runtime.js +8 -0
- package/src/core/provider/anthropic.sdk-backup.js +439 -0
- package/src/core/provider/openai-compatible.js +132 -18
- package/src/core/provider/openai-compatible.sdk-backup.js +412 -0
- package/src/core/session-store.js +8 -0
- package/src/core/shell-profile.js +9 -5
- package/src/core/tools.js +64 -12
- package/src/tui/chat-app.js +40 -21
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
2
|
-
|
|
3
1
|
function extractTextContent(content) {
|
|
4
2
|
if (typeof content === 'string') return content;
|
|
5
3
|
if (Array.isArray(content)) {
|
|
@@ -14,6 +12,20 @@ function extractTextContent(content) {
|
|
|
14
12
|
return '';
|
|
15
13
|
}
|
|
16
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
|
+
|
|
17
29
|
function emptyToolCall(index) {
|
|
18
30
|
return {
|
|
19
31
|
index,
|
|
@@ -23,13 +35,59 @@ function emptyToolCall(index) {
|
|
|
23
35
|
};
|
|
24
36
|
}
|
|
25
37
|
|
|
26
|
-
function
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
async function* iterateSseEvents(stream) {
|
|
58
|
+
const decoder = new TextDecoder();
|
|
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
|
+
};
|
|
69
|
+
|
|
70
|
+
for await (const chunk of stream) {
|
|
71
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
72
|
+
while (true) {
|
|
73
|
+
const lfBoundary = buffer.indexOf('\n\n');
|
|
74
|
+
const crlfBoundary = buffer.indexOf('\r\n\r\n');
|
|
75
|
+
if (lfBoundary === -1 && crlfBoundary === -1) break;
|
|
76
|
+
const useCrlf = crlfBoundary !== -1 && (lfBoundary === -1 || crlfBoundary < lfBoundary);
|
|
77
|
+
const boundary = useCrlf ? crlfBoundary : lfBoundary;
|
|
78
|
+
const separatorLength = useCrlf ? 4 : 2;
|
|
79
|
+
const rawEvent = buffer.slice(0, boundary);
|
|
80
|
+
buffer = buffer.slice(boundary + separatorLength);
|
|
81
|
+
const parsed = flushEvent(rawEvent);
|
|
82
|
+
if (parsed) yield parsed;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
buffer += decoder.decode();
|
|
87
|
+
const trailingEvent = flushEvent(buffer.trim());
|
|
88
|
+
if (trailingEvent) {
|
|
89
|
+
yield trailingEvent;
|
|
90
|
+
}
|
|
33
91
|
}
|
|
34
92
|
|
|
35
93
|
function isMiniMaxModel(model) {
|
|
@@ -78,6 +136,27 @@ function sanitizeGatewayMessages(messages) {
|
|
|
78
136
|
});
|
|
79
137
|
}
|
|
80
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
|
+
|
|
81
160
|
function sanitizeMiniMaxMessages(messages) {
|
|
82
161
|
const source = Array.isArray(messages) ? messages : [];
|
|
83
162
|
const out = [];
|
|
@@ -238,12 +317,17 @@ export async function createChatCompletion({
|
|
|
238
317
|
timeoutMs = 90000,
|
|
239
318
|
maxRetries = 2
|
|
240
319
|
}) {
|
|
241
|
-
const client = createClient({ baseUrl, apiKey, timeoutMs, maxRetries });
|
|
242
320
|
const payload = buildPayload({ model, temperature, messages, tools });
|
|
243
|
-
|
|
244
|
-
|
|
321
|
+
const response = await fetch(buildChatCompletionsUrl(baseUrl), {
|
|
322
|
+
method: 'POST',
|
|
323
|
+
headers: createHeaders(apiKey),
|
|
324
|
+
body: JSON.stringify(payload),
|
|
325
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
326
|
+
});
|
|
327
|
+
const data = await parseJsonResponse(response);
|
|
245
328
|
const message = data?.choices?.[0]?.message || {};
|
|
246
329
|
const text = sanitizeMiniMaxText(model, extractTextContent(message.content));
|
|
330
|
+
const reasoningContent = extractReasoningContent(message.reasoning_content);
|
|
247
331
|
const toolCalls = (message.tool_calls || []).map((tc) => ({
|
|
248
332
|
id: tc.id,
|
|
249
333
|
name: tc.function?.name,
|
|
@@ -266,7 +350,13 @@ export async function createChatCompletion({
|
|
|
266
350
|
return {
|
|
267
351
|
text,
|
|
268
352
|
toolCalls,
|
|
269
|
-
usage: data?.usage || null
|
|
353
|
+
usage: data?.usage || null,
|
|
354
|
+
assistantMessage: buildAssistantMessage({
|
|
355
|
+
text,
|
|
356
|
+
toolCalls,
|
|
357
|
+
content: message.content ?? text,
|
|
358
|
+
reasoningContent
|
|
359
|
+
})
|
|
270
360
|
};
|
|
271
361
|
}
|
|
272
362
|
|
|
@@ -282,20 +372,32 @@ export async function createChatCompletionStream({
|
|
|
282
372
|
timeoutMs = 90000,
|
|
283
373
|
maxRetries = 2
|
|
284
374
|
}) {
|
|
285
|
-
const client = createClient({ baseUrl, apiKey, timeoutMs, maxRetries });
|
|
286
375
|
const payload = buildPayload({ model, temperature, messages, tools, stream: true });
|
|
287
|
-
|
|
288
|
-
|
|
376
|
+
const response = await fetch(buildChatCompletionsUrl(baseUrl), {
|
|
377
|
+
method: 'POST',
|
|
378
|
+
headers: createHeaders(apiKey),
|
|
379
|
+
body: JSON.stringify(payload),
|
|
380
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
381
|
+
});
|
|
382
|
+
if (!response.ok || !response.body) {
|
|
383
|
+
const text = await response.text().catch(() => '');
|
|
384
|
+
throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
|
|
385
|
+
}
|
|
289
386
|
let text = '';
|
|
387
|
+
let reasoningContent = '';
|
|
290
388
|
const toolCallsByIndex = new Map();
|
|
291
389
|
let usage = null;
|
|
292
390
|
let miniMaxStreamState = { rawContent: '', visibleText: '' };
|
|
293
391
|
|
|
294
|
-
for await (const chunk of
|
|
392
|
+
for await (const chunk of iterateSseEvents(response.body)) {
|
|
295
393
|
usage = chunk?.usage || usage;
|
|
296
394
|
const choice0 = chunk?.choices?.[0] || {};
|
|
297
395
|
const delta = choice0?.delta || {};
|
|
298
396
|
const content = delta.content;
|
|
397
|
+
const reasoningDelta = extractReasoningContent(delta.reasoning_content);
|
|
398
|
+
if (reasoningDelta) {
|
|
399
|
+
reasoningContent += reasoningDelta;
|
|
400
|
+
}
|
|
299
401
|
if (isMiniMaxModel(model)) {
|
|
300
402
|
const next = nextMiniMaxVisibleChunk(miniMaxStreamState, content);
|
|
301
403
|
miniMaxStreamState = next.nextState;
|
|
@@ -333,7 +435,19 @@ export async function createChatCompletionStream({
|
|
|
333
435
|
});
|
|
334
436
|
}
|
|
335
437
|
}
|
|
438
|
+
|
|
439
|
+
if (choice0?.finish_reason) {
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
336
442
|
}
|
|
337
443
|
|
|
338
|
-
|
|
444
|
+
const result = buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
|
|
445
|
+
return {
|
|
446
|
+
...result,
|
|
447
|
+
assistantMessage: buildAssistantMessage({
|
|
448
|
+
text: result.text,
|
|
449
|
+
toolCalls: result.toolCalls,
|
|
450
|
+
reasoningContent
|
|
451
|
+
})
|
|
452
|
+
};
|
|
339
453
|
}
|
|
@@ -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
|
+
}
|
|
@@ -41,6 +41,14 @@ function sanitizeMessage(msg) {
|
|
|
41
41
|
if (msg?.tool_call_id) out.tool_call_id = String(msg.tool_call_id);
|
|
42
42
|
if (typeof msg?.name === 'string' && msg.name.trim()) out.name = msg.name.trim();
|
|
43
43
|
if (typeof msg?.at === 'string' && msg.at.trim()) out.at = msg.at;
|
|
44
|
+
if (typeof msg?.reasoning_content === 'string' && msg.reasoning_content) {
|
|
45
|
+
out.reasoning_content = msg.reasoning_content;
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(msg?.reasoning_details) && msg.reasoning_details.length > 0) {
|
|
48
|
+
out.reasoning_details = msg.reasoning_details
|
|
49
|
+
.filter((detail) => detail && typeof detail === 'object')
|
|
50
|
+
.map((detail) => ({ ...detail }));
|
|
51
|
+
}
|
|
44
52
|
|
|
45
53
|
if (Array.isArray(msg?.tool_calls)) {
|
|
46
54
|
const toolCalls = msg.tool_calls.map(sanitizeToolCall).filter(Boolean);
|
|
@@ -144,13 +144,15 @@ Use update_todos with these rules:
|
|
|
144
144
|
|
|
145
145
|
Some tools are loaded on demand through tool_search. Common examples:
|
|
146
146
|
- glob for pattern-based file lookup
|
|
147
|
-
- ast_query and read_ast_node for AST-scoped edits
|
|
147
|
+
- ast_query and read_ast_node for advanced AST-scoped reads and edits
|
|
148
148
|
- generate_diff and patch for explicit diff workflows
|
|
149
149
|
- list_background_tasks, get_background_task, and stop_background_task for managing long-running background commands
|
|
150
150
|
- remember_user, remember_global, remember_project, list_memory, search_memory, and forget_memory for persistent memory operations
|
|
151
151
|
|
|
152
|
-
For structural code edits (functions, classes, methods),
|
|
153
|
-
|
|
152
|
+
For structural code edits (functions, classes, methods), prefer AST-scoped reads before editing:
|
|
153
|
+
- Common one-shot workflow: read(path, query=..., capture_name=...) → edit with symbol or ast_target
|
|
154
|
+
- If you already have ast_target: read(ast_target=...) → edit with ast_target
|
|
155
|
+
- Advanced multi-step workflow: tool_search("ast_query") → ast_query → read_ast_node → edit with ast_target and kind=replace_block
|
|
154
156
|
Fall back to plain grep/read/edit only when AST is not appropriate.
|
|
155
157
|
|
|
156
158
|
For background commands: use run to launch. If you need management tools that are not currently visible, load list_background_tasks/get_background_task/stop_background_task with tool_search. Prefer reading the returned output_file with read instead of asking for a separate logs tool.
|
|
@@ -192,8 +194,10 @@ Common tool call patterns:
|
|
|
192
194
|
|
|
193
195
|
# Tone and style
|
|
194
196
|
|
|
195
|
-
-
|
|
196
|
-
-
|
|
197
|
+
- Keep answers compact and easy to scan
|
|
198
|
+
- Lead with the answer or next action, not scene-setting
|
|
199
|
+
- Do not restate the user's request unless a brief restatement prevents ambiguity
|
|
197
200
|
- When referencing code, use file_path:line_number format
|
|
201
|
+
- Keep technical wording, commands, paths, and error details exact
|
|
198
202
|
- Only use emojis if the user explicitly requests it`;
|
|
199
203
|
}
|