codemini-cli 0.3.4 → 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.
@@ -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
 
@@ -332,6 +384,7 @@ export async function createChatCompletionStream({
332
384
  throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
333
385
  }
334
386
  let text = '';
387
+ let reasoningContent = '';
335
388
  const toolCallsByIndex = new Map();
336
389
  let usage = null;
337
390
  let miniMaxStreamState = { rawContent: '', visibleText: '' };
@@ -341,6 +394,10 @@ export async function createChatCompletionStream({
341
394
  const choice0 = chunk?.choices?.[0] || {};
342
395
  const delta = choice0?.delta || {};
343
396
  const content = delta.content;
397
+ const reasoningDelta = extractReasoningContent(delta.reasoning_content);
398
+ if (reasoningDelta) {
399
+ reasoningContent += reasoningDelta;
400
+ }
344
401
  if (isMiniMaxModel(model)) {
345
402
  const next = nextMiniMaxVisibleChunk(miniMaxStreamState, content);
346
403
  miniMaxStreamState = next.nextState;
@@ -378,7 +435,19 @@ export async function createChatCompletionStream({
378
435
  });
379
436
  }
380
437
  }
438
+
439
+ if (choice0?.finish_reason) {
440
+ break;
441
+ }
381
442
  }
382
443
 
383
- return buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
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
+ };
384
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), load the AST tools and use the AST-first workflow:
153
- tool_search("ast_query") ast_query read_ast_node → edit with ast_target and kind=replace_block.
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
- - Be concise. Go straight to the point
196
- - Do not restate what the user said
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
  }