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.
@@ -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 dataLines = rawEvent
59
- .split(/\r?\n/)
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: AbortSignal.timeout(timeoutMs)
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
- return buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
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
+ }