codemini-cli 0.5.7 → 0.5.9

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.
@@ -11,6 +11,8 @@ import { readSkillRegistry } from './skill-registry.js';
11
11
 
12
12
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
13
13
  const BUNDLED_SKILLS_DIR = path.resolve(MODULE_DIR, '..', '..', 'skills');
14
+ const SKILL_CATALOG_FILE = 'codemini.skills.json';
15
+ const FRONTMATTER_READ_BYTES = 16 * 1024;
14
16
 
15
17
  function parseArrayText(value) {
16
18
  const inner = value.slice(1, -1).trim();
@@ -46,6 +48,79 @@ function parseFrontmatter(raw) {
46
48
  return { metadata, content };
47
49
  }
48
50
 
51
+ function readFrontmatterMetadata(filePath) {
52
+ let fd;
53
+ try {
54
+ fd = fs.openSync(filePath, 'r');
55
+ const buffer = Buffer.alloc(FRONTMATTER_READ_BYTES);
56
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
57
+ const raw = buffer.subarray(0, bytesRead).toString('utf8');
58
+ if (!raw.startsWith('---\n')) return {};
59
+ const end = raw.indexOf('\n---\n', 4);
60
+ if (end === -1) return {};
61
+ return parseFrontmatter(raw.slice(0, end + 5)).metadata;
62
+ } catch {
63
+ return {};
64
+ } finally {
65
+ if (fd !== undefined) {
66
+ try { fs.closeSync(fd); } catch {}
67
+ }
68
+ }
69
+ }
70
+
71
+ function readSkillCatalog(baseDir) {
72
+ const catalogPath = path.join(baseDir, SKILL_CATALOG_FILE);
73
+ try {
74
+ const parsed = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
75
+ return parsed && typeof parsed === 'object' && parsed.skills && typeof parsed.skills === 'object'
76
+ ? parsed.skills
77
+ : {};
78
+ } catch {
79
+ return {};
80
+ }
81
+ }
82
+
83
+ function normalizeStringArray(value) {
84
+ if (Array.isArray(value)) {
85
+ return value.map((item) => String(item || '').trim()).filter(Boolean);
86
+ }
87
+ const single = String(value || '').trim();
88
+ return single ? [single] : [];
89
+ }
90
+
91
+ function catalogMetadata(catalog, name) {
92
+ const entry = catalog?.[name];
93
+ if (!entry || typeof entry !== 'object') return {};
94
+ return {
95
+ ...(entry.description ? { description: String(entry.description) } : {}),
96
+ ...(entry.mode ? { mode: String(entry.mode) } : {}),
97
+ ...(entry.enabled !== undefined ? { enabled: entry.enabled !== false } : {}),
98
+ ...(entry.priority !== undefined ? { priority: Number(entry.priority) } : {}),
99
+ triggers: normalizeStringArray(entry.triggers)
100
+ };
101
+ }
102
+
103
+ function commandWithContent(command, parsedContent) {
104
+ if (parsedContent !== undefined) {
105
+ return { ...command, content: parsedContent };
106
+ }
107
+
108
+ let cached;
109
+ let loaded = false;
110
+ return Object.defineProperty({ ...command }, 'content', {
111
+ enumerable: true,
112
+ configurable: true,
113
+ get() {
114
+ if (!loaded) {
115
+ const raw = fs.readFileSync(command.path, 'utf8');
116
+ cached = parseFrontmatter(raw).content;
117
+ loaded = true;
118
+ }
119
+ return cached;
120
+ }
121
+ });
122
+ }
123
+
49
124
  function safeEntries(dir) {
50
125
  try {
51
126
  return fs.readdirSync(dir);
@@ -104,77 +179,95 @@ function loadMarkdownCommandsFromDir(baseDir, source, out) {
104
179
 
105
180
  function loadLegacySkillsFromDir(baseDir, source, out) {
106
181
  if (!fs.existsSync(baseDir)) return;
182
+ const catalog = readSkillCatalog(baseDir);
107
183
  for (const entry of safeEntries(baseDir)) {
108
184
  if (!isSafeEntry(entry)) continue;
109
185
  const full = path.join(baseDir, entry);
110
186
  const stat = fs.statSync(full);
111
187
  if (!stat.isDirectory()) continue;
188
+ const catalogMeta = catalogMetadata(catalog, entry);
112
189
  const skillFile = path.join(full, 'SKILL.md');
113
190
  if (!fs.existsSync(skillFile)) continue;
114
- const raw = fs.readFileSync(skillFile, 'utf8');
115
- const parsed = parseFrontmatter(raw);
116
- setCommand(out, entry, {
191
+ const frontmatter = readFrontmatterMetadata(skillFile);
192
+ setCommand(out, entry, commandWithContent({
117
193
  name: entry,
118
194
  source: `${source}-skill`,
119
195
  path: skillFile,
120
196
  metadata: {
121
- description: parsed.metadata.description || 'Legacy skill',
197
+ ...frontmatter,
198
+ ...catalogMeta,
199
+ description: catalogMeta.description || frontmatter.description || 'Legacy skill',
122
200
  type: 'skill'
123
- },
124
- content: parsed.content
125
- });
201
+ }
202
+ }));
126
203
  }
127
204
  }
128
205
 
129
206
  function loadBundledSkillsFromDir(baseDir, out) {
130
207
  if (!fs.existsSync(baseDir)) return;
208
+ const catalog = readSkillCatalog(baseDir);
131
209
  for (const entry of safeEntries(baseDir)) {
132
210
  if (!isSafeEntry(entry)) continue;
133
211
  const full = path.join(baseDir, entry);
134
212
  const stat = fs.statSync(full);
135
213
  if (!stat.isDirectory()) continue;
214
+ const catalogMeta = catalogMetadata(catalog, entry);
136
215
  const skillFile = path.join(full, 'SKILL.md');
137
216
  if (!fs.existsSync(skillFile)) continue;
138
- const raw = fs.readFileSync(skillFile, 'utf8');
139
- const parsed = parseFrontmatter(raw);
140
- setCommand(out, entry, {
217
+ const frontmatter = readFrontmatterMetadata(skillFile);
218
+ setCommand(out, entry, commandWithContent({
141
219
  name: entry,
142
220
  source: 'bundled-skill',
143
221
  path: skillFile,
144
222
  metadata: {
145
- ...parsed.metadata,
223
+ ...frontmatter,
224
+ ...catalogMeta,
146
225
  type: 'skill',
147
- version: parsed.metadata.version || '0.1.0',
148
- description: parsed.metadata.description || 'Bundled skill'
149
- },
150
- content: parsed.content
151
- });
226
+ version: frontmatter.version || '0.1.0',
227
+ description: catalogMeta.description || frontmatter.description || 'Bundled skill'
228
+ }
229
+ }));
230
+ }
231
+ }
232
+
233
+ function applySkillCatalogPatches(baseDir, out) {
234
+ const catalog = readSkillCatalog(baseDir);
235
+ for (const name of Object.keys(catalog)) {
236
+ const existing = out.get(name);
237
+ if (!existing || existing.metadata?.type !== 'skill') continue;
238
+ const meta = catalogMetadata(catalog, name);
239
+ existing.metadata = {
240
+ ...existing.metadata,
241
+ ...meta,
242
+ description: meta.description || existing.metadata.description || ''
243
+ };
152
244
  }
153
245
  }
154
246
 
155
247
  function loadInstalledSkillsFromRegistry(baseDir, registry, out) {
156
248
  if (!registry || !Array.isArray(registry.skills)) return;
249
+ const catalog = readSkillCatalog(baseDir);
157
250
  for (const skill of registry.skills) {
158
251
  if (skill.enabled === false) continue;
159
252
  const name = skill.name;
160
253
  if (out.has(name)) continue;
254
+ const catalogMeta = catalogMetadata(catalog, name);
161
255
  const entry = skill.entryFile || 'SKILL.md';
162
256
  const full = path.join(baseDir, name, entry);
163
257
  if (!fs.existsSync(full)) continue;
164
- const raw = fs.readFileSync(full, 'utf8');
165
- const parsed = parseFrontmatter(raw);
166
- setCommand(out, name, {
258
+ const frontmatter = readFrontmatterMetadata(full);
259
+ setCommand(out, name, commandWithContent({
167
260
  name,
168
261
  source: 'registry-skill',
169
262
  path: full,
170
263
  metadata: {
171
- ...parsed.metadata,
264
+ ...frontmatter,
265
+ ...catalogMeta,
172
266
  type: 'skill',
173
- version: skill.version || parsed.metadata.version || '0.0.0',
174
- description: skill.description || parsed.metadata.description || 'Installed skill'
175
- },
176
- content: parsed.content
177
- });
267
+ version: skill.version || frontmatter.version || '0.0.0',
268
+ description: catalogMeta.description || skill.description || frontmatter.description || 'Installed skill'
269
+ }
270
+ }));
178
271
  }
179
272
  }
180
273
 
@@ -201,10 +294,12 @@ export async function loadCommandsAndSkills(cwd = process.cwd()) {
201
294
  const commands = new Map();
202
295
 
203
296
  loadBundledSkillsFromDir(BUNDLED_SKILLS_DIR, commands);
297
+ applySkillCatalogPatches(getProjectSkillsDir(cwd), commands);
204
298
  loadMarkdownCommandsFromDir(getCommandsDir(), 'global', commands);
205
299
  loadMarkdownCommandsFromDir(getProjectCommandsDir(cwd), 'project', commands);
206
300
  loadLegacySkillsFromDir(getSkillsDir(), 'global', commands);
207
301
  loadLegacySkillsFromDir(getProjectSkillsDir(cwd), 'project', commands);
302
+ applySkillCatalogPatches(getProjectSkillsDir(cwd), commands);
208
303
  const registry = await readSkillRegistry();
209
304
  loadInstalledSkillsFromRegistry(getSkillsDir(), registry, commands);
210
305
 
@@ -196,7 +196,25 @@ function suggestionForToken(token, config) {
196
196
  return 'Prefer structured tools like read, edit, write, grep, glob, and list first. If you need shell fallback, use allowed shell commands for search and local context such as rg, find, grep, sed, cat, or ls.';
197
197
  }
198
198
 
199
- function validateCdSegment(command, workspaceRoot) {
199
+ function allowedPathRoots(workspaceRoot, config = {}) {
200
+ return [
201
+ workspaceRoot,
202
+ ...(Array.isArray(config?.policy?.allowed_paths) ? config.policy.allowed_paths : [])
203
+ ]
204
+ .map((item) => String(item || '').trim())
205
+ .filter(Boolean)
206
+ .map((item) => path.resolve(item));
207
+ }
208
+
209
+ function isWithinAnyRoot(candidatePath, roots = []) {
210
+ const resolvedCandidate = path.resolve(candidatePath);
211
+ return roots.some((root) => {
212
+ const relative = path.relative(root, resolvedCandidate);
213
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
214
+ });
215
+ }
216
+
217
+ function validateCdSegment(command, workspaceRoot, config = {}) {
200
218
  const tokens = tokenizeTopLevel(command);
201
219
  if (tokens.length === 1) {
202
220
  return { allowed: false, reason: 'cd requires a target path in safe mode' };
@@ -210,11 +228,9 @@ function validateCdSegment(command, workspaceRoot) {
210
228
  return { allowed: false, reason: 'cd target is not allowed in safe mode' };
211
229
  }
212
230
 
213
- const resolvedRoot = path.resolve(workspaceRoot);
214
- const resolvedTarget = path.resolve(resolvedRoot, rawTarget);
215
- const relative = path.relative(resolvedRoot, resolvedTarget);
216
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
217
- return { allowed: false, reason: `cd escapes workspace: ${rawTarget}` };
231
+ const resolvedTarget = path.resolve(path.resolve(workspaceRoot), rawTarget);
232
+ if (!isWithinAnyRoot(resolvedTarget, allowedPathRoots(workspaceRoot, config))) {
233
+ return { allowed: false, reason: `cd escapes workspace or allowed paths: ${rawTarget}` };
218
234
  }
219
235
 
220
236
  return { allowed: true };
@@ -246,7 +262,7 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
246
262
  for (const item of inspectedTokens) {
247
263
  if (SHELL_KEYWORDS.has(item.token)) continue;
248
264
  if (item.token === 'cd') {
249
- const cdCheck = validateCdSegment(item.raw, workspaceRoot);
265
+ const cdCheck = validateCdSegment(item.raw, workspaceRoot, config);
250
266
  if (!cdCheck.allowed) {
251
267
  return { allowed: false, reason: cdCheck.reason, suggestion: suggestionForToken(item.token, config) };
252
268
  }
@@ -263,11 +279,19 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
263
279
  }
264
280
  }
265
281
 
266
- const workspaceLower = String(workspaceRoot).toLowerCase().replace(/\//g, '\\');
282
+ const allowedLower = allowedPathRoots(workspaceRoot, config).map((item) => item.toLowerCase().replace(/\//g, '\\'));
267
283
  const windowsAbsPath = lower.match(/[a-z]:\\[^\s'"]+/g) || [];
268
284
  for (const p of windowsAbsPath) {
269
- if (!p.startsWith(workspaceLower)) {
270
- return { allowed: false, reason: `absolute path outside workspace: ${p}`, suggestion: suggestionForToken(token, config) };
285
+ if (!allowedLower.some((root) => p === root || p.startsWith(`${root}\\`))) {
286
+ return { allowed: false, reason: `absolute path outside workspace or allowed paths: ${p}`, suggestion: suggestionForToken(token, config) };
287
+ }
288
+ }
289
+
290
+ const posixAbsPath = cmd.match(/(?<![:/\w])\/(?!\/)[^\s'"]+/g) || [];
291
+ const allowedResolved = allowedPathRoots(workspaceRoot, config);
292
+ for (const p of posixAbsPath) {
293
+ if (!isWithinAnyRoot(p, allowedResolved)) {
294
+ return { allowed: false, reason: `absolute path outside workspace or allowed paths: ${p}`, suggestion: suggestionForToken(token, config) };
271
295
  }
272
296
  }
273
297
 
@@ -89,6 +89,7 @@ const DEFAULT_CONFIG = {
89
89
  policy: {
90
90
  safe_mode: true,
91
91
  allow_dangerous_commands: false,
92
+ allowed_paths: [],
92
93
  command_allowlist: [],
93
94
  blocked_commands: [],
94
95
  blocked_path_patterns: [],
@@ -195,6 +196,9 @@ function normalizePolicyLists(config) {
195
196
  next.policy.command_allowlist = uniqueStrings(
196
197
  Array.isArray(next.policy.command_allowlist) ? next.policy.command_allowlist : []
197
198
  );
199
+ next.policy.allowed_paths = uniqueStrings(
200
+ Array.isArray(next.policy.allowed_paths) ? next.policy.allowed_paths : []
201
+ );
198
202
  next.policy.blocked_commands = uniqueStrings(
199
203
  Array.isArray(next.policy.blocked_commands) ? next.policy.blocked_commands : []
200
204
  );
@@ -212,6 +216,13 @@ function parseValue(input) {
212
216
  if (input === 'true') return true;
213
217
  if (input === 'false') return false;
214
218
  if (input === 'null') return null;
219
+ if ((input.startsWith('[') && input.endsWith(']')) || (input.startsWith('{') && input.endsWith('}'))) {
220
+ try {
221
+ return JSON.parse(input);
222
+ } catch {
223
+ return input;
224
+ }
225
+ }
215
226
  if (!Number.isNaN(Number(input)) && input.trim() !== '') return Number(input);
216
227
  return input;
217
228
  }
@@ -12,6 +12,72 @@ function extractTextContent(content) {
12
12
  return '';
13
13
  }
14
14
 
15
+ function cloneAnthropicContentBlock(block) {
16
+ if (!block || typeof block !== 'object') return null;
17
+ if (block.type === 'thinking') {
18
+ const thinking = String(block.thinking || block.text || '');
19
+ if (!thinking) return null;
20
+ return {
21
+ type: 'thinking',
22
+ thinking,
23
+ ...(block.signature ? { signature: String(block.signature) } : {})
24
+ };
25
+ }
26
+ if (block.type === 'redacted_thinking') {
27
+ const data = block.data != null ? String(block.data) : '';
28
+ if (!data) return null;
29
+ return { type: 'redacted_thinking', data };
30
+ }
31
+ if (block.type === 'text') {
32
+ const text = String(block.text || '');
33
+ return text ? { type: 'text', text } : null;
34
+ }
35
+ if (block.type === 'tool_use') {
36
+ const name = String(block.name || '').trim();
37
+ if (!name) return null;
38
+ return {
39
+ type: 'tool_use',
40
+ id: String(block.id || ''),
41
+ name,
42
+ input: block.input && typeof block.input === 'object' && !Array.isArray(block.input) ? block.input : {}
43
+ };
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function extractThinkingBlocks(message) {
49
+ const source = [
50
+ ...(Array.isArray(message?.reasoning_details) ? message.reasoning_details : []),
51
+ ...(Array.isArray(message?.content) ? message.content : [])
52
+ ];
53
+ return source
54
+ .filter((block) => block?.type === 'thinking' || block?.type === 'redacted_thinking')
55
+ .map(cloneAnthropicContentBlock)
56
+ .filter(Boolean);
57
+ }
58
+
59
+ function buildAssistantMessage({ text = '', toolCalls = [], thinkingBlocks = [] }) {
60
+ const assistantMessage = {
61
+ role: 'assistant',
62
+ content: text
63
+ };
64
+ const reasoningDetails = Array.isArray(thinkingBlocks)
65
+ ? thinkingBlocks.map(cloneAnthropicContentBlock).filter(Boolean)
66
+ : [];
67
+ if (reasoningDetails.length > 0) assistantMessage.reasoning_details = reasoningDetails;
68
+ if (Array.isArray(toolCalls) && toolCalls.length > 0) {
69
+ assistantMessage.tool_calls = toolCalls.map((tc) => ({
70
+ id: tc.id,
71
+ type: 'function',
72
+ function: {
73
+ name: tc.name,
74
+ arguments: tc.arguments || '{}'
75
+ }
76
+ }));
77
+ }
78
+ return assistantMessage;
79
+ }
80
+
15
81
  function normalizeIncomingToolCallArguments(argumentsValue) {
16
82
  if (typeof argumentsValue === 'string') return argumentsValue;
17
83
  if (argumentsValue == null) return '{}';
@@ -41,7 +107,8 @@ function normalizeMessages(messages) {
41
107
  const systemParts = [];
42
108
  const out = [];
43
109
 
44
- for (const message of source) {
110
+ for (let i = 0; i < source.length; i += 1) {
111
+ const message = source[i];
45
112
  if (!message || typeof message !== 'object') continue;
46
113
  if (message.role === 'system') {
47
114
  const text = extractTextContent(message.content);
@@ -50,35 +117,45 @@ function normalizeMessages(messages) {
50
117
  }
51
118
 
52
119
  if (message.role === 'tool') {
120
+ const toolResults = [];
121
+ while (i < source.length) {
122
+ const toolMessage = source[i];
123
+ if (!toolMessage || typeof toolMessage !== 'object' || toolMessage.role !== 'tool') break;
124
+ toolResults.push({
125
+ type: 'tool_result',
126
+ tool_use_id: String(toolMessage.tool_call_id || ''),
127
+ content: extractTextContent(toolMessage.content)
128
+ });
129
+ i += 1;
130
+ }
131
+ i -= 1;
53
132
  out.push({
54
133
  role: 'user',
55
- content: [
56
- {
57
- type: 'tool_result',
58
- tool_use_id: String(message.tool_call_id || ''),
59
- content: extractTextContent(message.content)
60
- }
61
- ]
134
+ content: toolResults
62
135
  });
63
136
  continue;
64
137
  }
65
138
 
66
- const contentBlocks = [];
139
+ const contentBlocks = message.role === 'assistant' ? extractThinkingBlocks(message) : [];
67
140
  const text = extractTextContent(message.content);
68
141
  if (text) {
69
142
  contentBlocks.push({ type: 'text', text });
70
143
  }
71
144
 
145
+ const hasContentToolUse = Array.isArray(message.content)
146
+ && message.content.some((block) => block?.type === 'tool_use');
72
147
  if (message.role === 'assistant' && Array.isArray(message.tool_calls)) {
73
- for (const toolCall of message.tool_calls) {
74
- const name = String(toolCall?.function?.name || toolCall?.name || '').trim();
75
- if (!name) continue;
76
- contentBlocks.push({
77
- type: 'tool_use',
78
- id: String(toolCall?.id || ''),
79
- name,
80
- input: tryParseJsonObject(toolCall?.function?.arguments ?? toolCall?.arguments)
81
- });
148
+ if (!hasContentToolUse) {
149
+ for (const toolCall of message.tool_calls) {
150
+ const name = String(toolCall?.function?.name || toolCall?.name || '').trim();
151
+ if (!name) continue;
152
+ contentBlocks.push({
153
+ type: 'tool_use',
154
+ id: String(toolCall?.id || ''),
155
+ name,
156
+ input: tryParseJsonObject(toolCall?.function?.arguments ?? toolCall?.arguments)
157
+ });
158
+ }
82
159
  }
83
160
  }
84
161
 
@@ -142,6 +219,10 @@ function hasTrailingToolContext(messages) {
142
219
 
143
220
  function extractAssistantResult(data, messages) {
144
221
  const content = Array.isArray(data?.content) ? data.content : [];
222
+ const thinkingBlocks = content
223
+ .filter((block) => block?.type === 'thinking' || block?.type === 'redacted_thinking')
224
+ .map(cloneAnthropicContentBlock)
225
+ .filter(Boolean);
145
226
  const text = content
146
227
  .filter((block) => block?.type === 'text')
147
228
  .map((block) => block.text || '')
@@ -163,7 +244,8 @@ function extractAssistantResult(data, messages) {
163
244
  toolCalls: [],
164
245
  usage: data?.usage || null,
165
246
  incomplete: true,
166
- content
247
+ content,
248
+ assistantMessage: buildAssistantMessage({ text: '', toolCalls: [], thinkingBlocks })
167
249
  };
168
250
  }
169
251
  throw new Error('Anthropic gateway returned empty assistant response');
@@ -173,7 +255,8 @@ function extractAssistantResult(data, messages) {
173
255
  text,
174
256
  toolCalls,
175
257
  usage: data?.usage || null,
176
- content
258
+ content,
259
+ assistantMessage: buildAssistantMessage({ text, toolCalls, thinkingBlocks })
177
260
  };
178
261
  }
179
262
 
@@ -214,7 +297,7 @@ function emptyToolCall(index) {
214
297
  };
215
298
  }
216
299
 
217
- function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
300
+ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages, thinkingBlocks = []) {
218
301
  const toolCalls = Array.from(toolCallsByIndex.entries())
219
302
  .sort((a, b) => a[0] - b[0])
220
303
  .map(([, tc], i) => ({
@@ -225,6 +308,10 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
225
308
  .filter((tc) => tc.name);
226
309
  const normalizedText = String(text || '').trim();
227
310
  const content = [];
311
+ for (const block of thinkingBlocks) {
312
+ const cloned = cloneAnthropicContentBlock(block);
313
+ if (cloned) content.push(cloned);
314
+ }
228
315
  if (text) content.push({ type: 'text', text });
229
316
  for (const toolCall of toolCalls) {
230
317
  content.push({
@@ -242,7 +329,8 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
242
329
  toolCalls: [],
243
330
  usage,
244
331
  incomplete: true,
245
- content: []
332
+ content: [],
333
+ assistantMessage: buildAssistantMessage({ text: '', toolCalls: [], thinkingBlocks })
246
334
  };
247
335
  }
248
336
  throw new Error('Anthropic gateway stream returned empty assistant response');
@@ -253,7 +341,8 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
253
341
  toolCalls,
254
342
  usage,
255
343
  incomplete: false,
256
- content
344
+ content,
345
+ assistantMessage: buildAssistantMessage({ text, toolCalls, thinkingBlocks })
257
346
  };
258
347
  }
259
348
 
@@ -349,6 +438,7 @@ export async function createChatCompletionStream({
349
438
  let text = '';
350
439
  let usage = null;
351
440
  const toolCallsByIndex = new Map();
441
+ const thinkingBlocksByIndex = new Map();
352
442
 
353
443
  for await (const chunk of iterateSseEvents(response.body)) {
354
444
  usage = mergeUsage(usage, chunk?.data?.usage);
@@ -366,6 +456,10 @@ export async function createChatCompletionStream({
366
456
  : '';
367
457
  current.arguments = current.arguments || initialInput;
368
458
  toolCallsByIndex.set(index, current);
459
+ } else if (contentBlock.type === 'thinking' || contentBlock.type === 'redacted_thinking') {
460
+ const current = cloneAnthropicContentBlock(contentBlock) || { type: contentBlock.type };
461
+ if (current.type === 'thinking' && current.thinking == null) current.thinking = '';
462
+ thinkingBlocksByIndex.set(index, current);
369
463
  }
370
464
  continue;
371
465
  }
@@ -382,6 +476,20 @@ export async function createChatCompletionStream({
382
476
  continue;
383
477
  }
384
478
 
479
+ if (delta.type === 'thinking_delta') {
480
+ const current = thinkingBlocksByIndex.get(index) || { type: 'thinking', thinking: '' };
481
+ current.thinking = `${current.thinking || ''}${String(delta.thinking || '')}`;
482
+ thinkingBlocksByIndex.set(index, current);
483
+ continue;
484
+ }
485
+
486
+ if (delta.type === 'signature_delta') {
487
+ const current = thinkingBlocksByIndex.get(index) || { type: 'thinking', thinking: '' };
488
+ current.signature = String(delta.signature || '');
489
+ thinkingBlocksByIndex.set(index, current);
490
+ continue;
491
+ }
492
+
385
493
  if (delta.type === 'input_json_delta') {
386
494
  const current = toolCallsByIndex.get(index) || emptyToolCall(index);
387
495
  current.arguments = `${current.arguments || ''}${String(delta.partial_json || '')}`;
@@ -397,5 +505,10 @@ export async function createChatCompletionStream({
397
505
  }
398
506
  }
399
507
 
400
- return buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
508
+ const thinkingBlocks = Array.from(thinkingBlocksByIndex.entries())
509
+ .sort((a, b) => a[0] - b[0])
510
+ .map(([, block]) => cloneAnthropicContentBlock(block))
511
+ .filter(Boolean);
512
+
513
+ return buildFinalStreamResult(text, toolCallsByIndex, usage, messages, thinkingBlocks);
401
514
  }