@combycode/llm-sdk 1.4.0 → 1.5.1

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/CHANGELOG.md CHANGED
@@ -6,6 +6,28 @@ All notable changes to `@combycode/llm-sdk` are documented here. The format foll
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.5.1] - 2026-07-06
10
+
11
+ ### Fixed
12
+ - **Anthropic file retrieval from the browser** was blocked by CORS. `retrieveFile` /
13
+ `streamFile` now send Anthropic's `anthropic-dangerous-direct-browser-access: true` header
14
+ when running in a browser (matching the completion adapter), so hosted code-execution files
15
+ download directly. No effect on Node/Bun.
16
+
17
+ ## [1.5.0] - 2026-07-06
18
+
19
+ ### Added
20
+ - **Builtin-tool activity in streams + a durable trail.** Provider-run hosted tools (web search,
21
+ code execution) now surface `{ type: 'builtin_tool_start' }` / `{ type: 'builtin_tool_end' }`
22
+ stream events as they run, and a `response.builtinToolCalls` trail (`BuiltinToolCall[]` —
23
+ `{ tool, id? }` with unified tool names) on both `complete()` and streamed responses (propagated
24
+ through `AgentLoop`). Informational only — unlike `tool_call_*` (a function call the client must
25
+ execute), the provider runs these itself. Normalized across providers (Anthropic `server_tool_use`
26
+ / `*_tool_result`, OpenAI/xAI `web_search_call` / `code_interpreter_call` output items, Google
27
+ `executableCode` / `codeExecutionResult` parts + `googleSearch` grounding, OpenRouter
28
+ `:online` `url_citation` annotations → `web_search`). Verified live on Anthropic, OpenAI,
29
+ Google, xAI, and OpenRouter. Exports `BuiltinToolCall`.
30
+
9
31
  ## [1.4.0] - 2026-07-06
10
32
 
11
33
  ### Fixed
@@ -22471,7 +22471,11 @@ function contentRequest(ctx, file) {
22471
22471
  "x-api-key": ctx.apiKey,
22472
22472
  "anthropic-version": "2023-06-01",
22473
22473
  "anthropic-beta": "files-api-2025-04-14",
22474
- accept: "application/binary"
22474
+ accept: "application/binary",
22475
+ // Enable CORS for direct browser requests (same header the completion
22476
+ // adapter sets); harmless on Node/Bun. Without it the browser blocks the
22477
+ // file download by CORS.
22478
+ ...isBrowser() ? { "anthropic-dangerous-direct-browser-access": "true" } : {}
22475
22479
  }
22476
22480
  };
22477
22481
  }
@@ -22492,7 +22496,11 @@ function providerAuth(ctx, url) {
22492
22496
  const base = ctx.baseURL ?? DEFAULT_BASE[ctx.provider];
22493
22497
  if (!url.startsWith(base)) return {};
22494
22498
  if (ctx.provider === "anthropic") {
22495
- return { "x-api-key": ctx.apiKey, "anthropic-version": "2023-06-01" };
22499
+ return {
22500
+ "x-api-key": ctx.apiKey,
22501
+ "anthropic-version": "2023-06-01",
22502
+ ...isBrowser() ? { "anthropic-dangerous-direct-browser-access": "true" } : {}
22503
+ };
22496
22504
  }
22497
22505
  if (ctx.provider === "google") return { "x-goog-api-key": ctx.apiKey };
22498
22506
  return { authorization: `Bearer ${ctx.apiKey}` };
@@ -23082,6 +23090,7 @@ var LLMClient = class {
23082
23090
  let finishReason = "stop";
23083
23091
  let moderationReport;
23084
23092
  const files = [];
23093
+ const builtinToolCalls = [];
23085
23094
  const fetchStream = this.fetchStreamFn;
23086
23095
  const queueName = this.queueName;
23087
23096
  const priority = this.priority;
@@ -23133,6 +23142,9 @@ var LLMClient = class {
23133
23142
  case "file":
23134
23143
  files.push(event.file);
23135
23144
  break;
23145
+ case "builtin_tool_start":
23146
+ builtinToolCalls.push({ tool: event.tool, ...event.id ? { id: event.id } : {} });
23147
+ break;
23136
23148
  case "moderation":
23137
23149
  moderationReport = this.mergeModeration(
23138
23150
  moderationReport,
@@ -23156,6 +23168,7 @@ var LLMClient = class {
23156
23168
  thinking: thinking || null,
23157
23169
  media: [],
23158
23170
  ...files.length ? { files } : {},
23171
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23159
23172
  ...moderationReport ? { moderation: moderationReport } : {},
23160
23173
  latencyMs: performance.now() - start,
23161
23174
  raw: null
@@ -23389,6 +23402,24 @@ var AnthropicFileAdapter = class {
23389
23402
  }
23390
23403
  };
23391
23404
 
23405
+ // src/llm/providers/_shared/builtin-tools.ts
23406
+ var NATIVE_TO_UNIFIED = {
23407
+ // OpenAI / xAI Responses output-item types
23408
+ web_search_call: "web_search",
23409
+ code_interpreter_call: "code_interpreter",
23410
+ // Anthropic server_tool_use names
23411
+ web_search: "web_search",
23412
+ code_execution: "code_interpreter",
23413
+ bash_code_execution: "code_interpreter",
23414
+ // Anthropic *_tool_result block types
23415
+ web_search_tool_result: "web_search",
23416
+ code_execution_tool_result: "code_interpreter",
23417
+ bash_code_execution_tool_result: "code_interpreter"
23418
+ };
23419
+ function unifiedBuiltinTool(native) {
23420
+ return NATIVE_TO_UNIFIED[native] ?? native.replace(/_call$/, "").replace(/_tool_result$/, "");
23421
+ }
23422
+
23392
23423
  // src/llm/providers/_shared/constants.ts
23393
23424
  var AUDIO_PCM16_SAMPLE_RATE_HZ = 24e3;
23394
23425
  var DEFAULT_MAX_TOKENS = 4096;
@@ -23598,6 +23629,7 @@ var AnthropicAdapter = class {
23598
23629
  let thinking = null;
23599
23630
  const toolCalls = [];
23600
23631
  const files = [];
23632
+ const builtinToolCalls = [];
23601
23633
  for (const block of contentBlocks) {
23602
23634
  if (block.type === "text") {
23603
23635
  content.push({ type: "text", text: block.text });
@@ -23612,6 +23644,11 @@ var AnthropicAdapter = class {
23612
23644
  };
23613
23645
  content.push(tc);
23614
23646
  toolCalls.push(tc);
23647
+ } else if (block.type === "server_tool_use") {
23648
+ builtinToolCalls.push({
23649
+ tool: unifiedBuiltinTool(block.name),
23650
+ ...typeof block.id === "string" ? { id: block.id } : {}
23651
+ });
23615
23652
  } else {
23616
23653
  files.push(...filesFromCodeExecBlock(block));
23617
23654
  }
@@ -23629,6 +23666,7 @@ var AnthropicAdapter = class {
23629
23666
  toolCalls,
23630
23667
  media: [],
23631
23668
  ...files.length ? { files } : {},
23669
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23632
23670
  thinking,
23633
23671
  latencyMs,
23634
23672
  raw
@@ -23652,8 +23690,23 @@ var AnthropicAdapter = class {
23652
23690
  if (block.type === "tool_use") {
23653
23691
  return [{ type: "tool_call_start", id: block.id, name: block.name }];
23654
23692
  }
23655
- const files = filesFromCodeExecBlock(block);
23656
- if (files.length) return files.map((file) => ({ type: "file", file }));
23693
+ const events = [];
23694
+ const blockType = block.type;
23695
+ if (blockType === "server_tool_use") {
23696
+ events.push({
23697
+ type: "builtin_tool_start",
23698
+ tool: unifiedBuiltinTool(block.name),
23699
+ ...typeof block.id === "string" ? { id: block.id } : {}
23700
+ });
23701
+ } else if (blockType?.endsWith("_tool_result")) {
23702
+ events.push({
23703
+ type: "builtin_tool_end",
23704
+ tool: unifiedBuiltinTool(blockType),
23705
+ ...typeof block.tool_use_id === "string" ? { id: block.tool_use_id } : {}
23706
+ });
23707
+ }
23708
+ for (const file of filesFromCodeExecBlock(block)) events.push({ type: "file", file });
23709
+ return events;
23657
23710
  }
23658
23711
  if (type === "content_block_stop") {
23659
23712
  }
@@ -24189,6 +24242,9 @@ var GoogleAdapter = class {
24189
24242
  candidate.finishReason,
24190
24243
  { MAX_TOKENS: "length", SAFETY: "content_filter" }
24191
24244
  );
24245
+ const builtinToolCalls = [];
24246
+ if (hasCodeExec) builtinToolCalls.push({ tool: "code_interpreter" });
24247
+ if (candidate.groundingMetadata) builtinToolCalls.push({ tool: "web_search" });
24192
24248
  return {
24193
24249
  id: crypto.randomUUID(),
24194
24250
  // Google doesn't return a response ID in generateContent
@@ -24201,6 +24257,7 @@ var GoogleAdapter = class {
24201
24257
  thinking,
24202
24258
  media,
24203
24259
  ...files.length ? { files } : {},
24260
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
24204
24261
  latencyMs,
24205
24262
  raw
24206
24263
  };
@@ -24238,6 +24295,8 @@ var GoogleAdapter = class {
24238
24295
  if (part.text !== void 0 && !part.thought)
24239
24296
  events.push({ type: "text", text: part.text });
24240
24297
  if (part.thought && part.text) events.push({ type: "thinking", text: part.text });
24298
+ if (part.executableCode) events.push({ type: "builtin_tool_start", tool: "code_interpreter" });
24299
+ if (part.codeExecutionResult) events.push({ type: "builtin_tool_end", tool: "code_interpreter" });
24241
24300
  if (part.inlineData) {
24242
24301
  const inline = part.inlineData;
24243
24302
  const mime = inline.mimeType;
@@ -24268,6 +24327,11 @@ var GoogleAdapter = class {
24268
24327
  events.push({ type: "tool_call_end", id: "" });
24269
24328
  }
24270
24329
  }
24330
+ if (candidate.groundingMetadata && !state.webSearchEmitted) {
24331
+ state.webSearchEmitted = true;
24332
+ events.push({ type: "builtin_tool_start", tool: "web_search" });
24333
+ events.push({ type: "builtin_tool_end", tool: "web_search" });
24334
+ }
24271
24335
  const fr = candidate.finishReason;
24272
24336
  if (fr)
24273
24337
  events.push({
@@ -26034,6 +26098,15 @@ function filenameForMime(mimeType) {
26034
26098
  if (mimeType.startsWith("image/")) return `file.${mimeType.slice("image/".length)}`;
26035
26099
  return "file.bin";
26036
26100
  }
26101
+ var RESPONSES_BUILTIN_ITEMS = /* @__PURE__ */ new Set(["web_search_call", "code_interpreter_call"]);
26102
+ function builtinCallFromResponsesItem(item) {
26103
+ const type = item.type;
26104
+ if (!RESPONSES_BUILTIN_ITEMS.has(type)) return null;
26105
+ return {
26106
+ tool: unifiedBuiltinTool(type),
26107
+ ...typeof item.id === "string" ? { id: item.id } : {}
26108
+ };
26109
+ }
26037
26110
  function filesFromResponsesOutputItem(item) {
26038
26111
  const files = [];
26039
26112
  const type = item.type;
@@ -26237,11 +26310,14 @@ var OpenAIResponsesAdapter = class {
26237
26310
  const toolCalls = [];
26238
26311
  const media = [];
26239
26312
  const files = [];
26313
+ const builtinToolCalls = [];
26240
26314
  let thinking = null;
26241
26315
  let text = "";
26242
26316
  for (const item of output) {
26243
26317
  const type = item.type;
26244
26318
  files.push(...this.filesFromOutputItem(item));
26319
+ const builtinCall = builtinCallFromResponsesItem(item);
26320
+ if (builtinCall) builtinToolCalls.push(builtinCall);
26245
26321
  if (type === "message") {
26246
26322
  const itemContent = item.content ?? [];
26247
26323
  for (const c of itemContent) {
@@ -26302,6 +26378,7 @@ var OpenAIResponsesAdapter = class {
26302
26378
  thinking,
26303
26379
  media,
26304
26380
  ...files.length ? { files } : {},
26381
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
26305
26382
  ...moderation ? { moderation } : {},
26306
26383
  latencyMs,
26307
26384
  raw
@@ -26333,6 +26410,10 @@ var OpenAIResponsesAdapter = class {
26333
26410
  if (item?.type === "image_generation_call") {
26334
26411
  events.push({ type: "media_start", mediaType: "image", mimeType: "image/png" });
26335
26412
  }
26413
+ const builtin = builtinCallFromResponsesItem(item ?? {});
26414
+ if (builtin) {
26415
+ events.push({ type: "builtin_tool_start", tool: builtin.tool, ...builtin.id ? { id: builtin.id } : {} });
26416
+ }
26336
26417
  }
26337
26418
  if (type === "response.image_generation_call.partial_image") {
26338
26419
  events.push({
@@ -26344,6 +26425,10 @@ var OpenAIResponsesAdapter = class {
26344
26425
  if (type === "response.output_item.done") {
26345
26426
  const item = data.item;
26346
26427
  for (const file of this.filesFromOutputItem(item)) events.push({ type: "file", file });
26428
+ const builtin = builtinCallFromResponsesItem(item ?? {});
26429
+ if (builtin) {
26430
+ events.push({ type: "builtin_tool_end", tool: builtin.tool, ...builtin.id ? { id: builtin.id } : {} });
26431
+ }
26347
26432
  if (item?.type === "function_call") {
26348
26433
  events.push({ type: "tool_call_end", id: item.call_id ?? "" });
26349
26434
  }
@@ -26442,6 +26527,9 @@ function filenameFor(mimeType) {
26442
26527
  }
26443
26528
 
26444
26529
  // src/llm/providers/openrouter/completions.ts
26530
+ function hasUrlCitation(annotations) {
26531
+ return Array.isArray(annotations) && annotations.some((a) => a?.type === "url_citation");
26532
+ }
26445
26533
  var OpenRouterAdapter = class extends OpenAIAdapter {
26446
26534
  name = "openrouter";
26447
26535
  constructor(config) {
@@ -26470,6 +26558,35 @@ var OpenRouterAdapter = class extends OpenAIAdapter {
26470
26558
  }
26471
26559
  return result;
26472
26560
  }
26561
+ parseResponse(raw, latencyMs) {
26562
+ const result = super.parseResponse(raw, latencyMs);
26563
+ const choices = raw.choices;
26564
+ const annotations = choices?.[0]?.message?.annotations;
26565
+ if (hasUrlCitation(annotations)) {
26566
+ result.builtinToolCalls = [...result.builtinToolCalls ?? [], { tool: "web_search" }];
26567
+ }
26568
+ return result;
26569
+ }
26570
+ /** Stateful — emit a single `web_search` builtin-tool pair the first time
26571
+ * `url_citation` annotations appear in the stream (the `:online` search signal). */
26572
+ createStreamParser() {
26573
+ let webSearchEmitted = false;
26574
+ return (event) => {
26575
+ const events = this.parseStreamEvent(event);
26576
+ if (!webSearchEmitted) {
26577
+ const choice = JSON.parse(event.data).choices?.[0];
26578
+ const annotations = choice?.delta?.annotations ?? choice?.message?.annotations;
26579
+ if (hasUrlCitation(annotations)) {
26580
+ webSearchEmitted = true;
26581
+ events.push(
26582
+ { type: "builtin_tool_start", tool: "web_search" },
26583
+ { type: "builtin_tool_end", tool: "web_search" }
26584
+ );
26585
+ }
26586
+ }
26587
+ return events;
26588
+ };
26589
+ }
26473
26590
  };
26474
26591
 
26475
26592
  // src/llm/providers/openrouter/responses.ts
@@ -28338,6 +28455,7 @@ var AgentLoop = class _AgentLoop {
28338
28455
  // Propagate hosted-tool file outputs + inline-moderation result from the final
28339
28456
  // LLM response (e.g. code-execution files produced during the run).
28340
28457
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28458
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28341
28459
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28342
28460
  latencyMs: performance.now() - startPerf,
28343
28461
  raw: lastResponse?.raw ?? null
@@ -28534,6 +28652,7 @@ var AgentLoop = class _AgentLoop {
28534
28652
  thinking: lastResponse?.thinking ?? null,
28535
28653
  media: [],
28536
28654
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28655
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28537
28656
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28538
28657
  latencyMs: performance.now() - startPerf,
28539
28658
  raw: null
package/dist/index.d.ts CHANGED
@@ -48,7 +48,7 @@ export type { Role, MessageOrigin, ContentPart, TextPart, ImagePart, DocumentPar
48
48
  export { isFunctionTool, isBuiltinTool } from './llm/types/tools';
49
49
  export type { FunctionTool, BuiltinTool, McpToolParams, Tool, ToolChoice, JsonSchema } from './llm/types/tools';
50
50
  export { emptyUsage } from './llm/types/response';
51
- export type { CompletionResponse, FileOutput, FinishReason, Usage } from './llm/types/response';
51
+ export type { BuiltinToolCall, CompletionResponse, FileOutput, FinishReason, Usage, } from './llm/types/response';
52
52
  export type { FileStream, RetrievedFile } from './llm/files/retrieve';
53
53
  export type { NormalizedRequest, ReasoningContext, ThinkingConfig, CacheConfig } from './llm/types/request';
54
54
  export type { MediaStreamType, StreamEvent } from './llm/types/stream';
package/dist/index.js CHANGED
@@ -22398,7 +22398,11 @@ function contentRequest(ctx, file) {
22398
22398
  "x-api-key": ctx.apiKey,
22399
22399
  "anthropic-version": "2023-06-01",
22400
22400
  "anthropic-beta": "files-api-2025-04-14",
22401
- accept: "application/binary"
22401
+ accept: "application/binary",
22402
+ // Enable CORS for direct browser requests (same header the completion
22403
+ // adapter sets); harmless on Node/Bun. Without it the browser blocks the
22404
+ // file download by CORS.
22405
+ ...isBrowser() ? { "anthropic-dangerous-direct-browser-access": "true" } : {}
22402
22406
  }
22403
22407
  };
22404
22408
  }
@@ -22419,7 +22423,11 @@ function providerAuth(ctx, url) {
22419
22423
  const base = ctx.baseURL ?? DEFAULT_BASE[ctx.provider];
22420
22424
  if (!url.startsWith(base)) return {};
22421
22425
  if (ctx.provider === "anthropic") {
22422
- return { "x-api-key": ctx.apiKey, "anthropic-version": "2023-06-01" };
22426
+ return {
22427
+ "x-api-key": ctx.apiKey,
22428
+ "anthropic-version": "2023-06-01",
22429
+ ...isBrowser() ? { "anthropic-dangerous-direct-browser-access": "true" } : {}
22430
+ };
22423
22431
  }
22424
22432
  if (ctx.provider === "google") return { "x-goog-api-key": ctx.apiKey };
22425
22433
  return { authorization: `Bearer ${ctx.apiKey}` };
@@ -23009,6 +23017,7 @@ var LLMClient = class {
23009
23017
  let finishReason = "stop";
23010
23018
  let moderationReport;
23011
23019
  const files = [];
23020
+ const builtinToolCalls = [];
23012
23021
  const fetchStream = this.fetchStreamFn;
23013
23022
  const queueName = this.queueName;
23014
23023
  const priority = this.priority;
@@ -23060,6 +23069,9 @@ var LLMClient = class {
23060
23069
  case "file":
23061
23070
  files.push(event.file);
23062
23071
  break;
23072
+ case "builtin_tool_start":
23073
+ builtinToolCalls.push({ tool: event.tool, ...event.id ? { id: event.id } : {} });
23074
+ break;
23063
23075
  case "moderation":
23064
23076
  moderationReport = this.mergeModeration(
23065
23077
  moderationReport,
@@ -23083,6 +23095,7 @@ var LLMClient = class {
23083
23095
  thinking: thinking || null,
23084
23096
  media: [],
23085
23097
  ...files.length ? { files } : {},
23098
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23086
23099
  ...moderationReport ? { moderation: moderationReport } : {},
23087
23100
  latencyMs: performance.now() - start,
23088
23101
  raw: null
@@ -23316,6 +23329,24 @@ var AnthropicFileAdapter = class {
23316
23329
  }
23317
23330
  };
23318
23331
 
23332
+ // src/llm/providers/_shared/builtin-tools.ts
23333
+ var NATIVE_TO_UNIFIED = {
23334
+ // OpenAI / xAI Responses output-item types
23335
+ web_search_call: "web_search",
23336
+ code_interpreter_call: "code_interpreter",
23337
+ // Anthropic server_tool_use names
23338
+ web_search: "web_search",
23339
+ code_execution: "code_interpreter",
23340
+ bash_code_execution: "code_interpreter",
23341
+ // Anthropic *_tool_result block types
23342
+ web_search_tool_result: "web_search",
23343
+ code_execution_tool_result: "code_interpreter",
23344
+ bash_code_execution_tool_result: "code_interpreter"
23345
+ };
23346
+ function unifiedBuiltinTool(native) {
23347
+ return NATIVE_TO_UNIFIED[native] ?? native.replace(/_call$/, "").replace(/_tool_result$/, "");
23348
+ }
23349
+
23319
23350
  // src/llm/providers/_shared/constants.ts
23320
23351
  var AUDIO_PCM16_SAMPLE_RATE_HZ = 24e3;
23321
23352
  var DEFAULT_MAX_TOKENS = 4096;
@@ -23525,6 +23556,7 @@ var AnthropicAdapter = class {
23525
23556
  let thinking = null;
23526
23557
  const toolCalls = [];
23527
23558
  const files = [];
23559
+ const builtinToolCalls = [];
23528
23560
  for (const block of contentBlocks) {
23529
23561
  if (block.type === "text") {
23530
23562
  content.push({ type: "text", text: block.text });
@@ -23539,6 +23571,11 @@ var AnthropicAdapter = class {
23539
23571
  };
23540
23572
  content.push(tc);
23541
23573
  toolCalls.push(tc);
23574
+ } else if (block.type === "server_tool_use") {
23575
+ builtinToolCalls.push({
23576
+ tool: unifiedBuiltinTool(block.name),
23577
+ ...typeof block.id === "string" ? { id: block.id } : {}
23578
+ });
23542
23579
  } else {
23543
23580
  files.push(...filesFromCodeExecBlock(block));
23544
23581
  }
@@ -23556,6 +23593,7 @@ var AnthropicAdapter = class {
23556
23593
  toolCalls,
23557
23594
  media: [],
23558
23595
  ...files.length ? { files } : {},
23596
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23559
23597
  thinking,
23560
23598
  latencyMs,
23561
23599
  raw
@@ -23579,8 +23617,23 @@ var AnthropicAdapter = class {
23579
23617
  if (block.type === "tool_use") {
23580
23618
  return [{ type: "tool_call_start", id: block.id, name: block.name }];
23581
23619
  }
23582
- const files = filesFromCodeExecBlock(block);
23583
- if (files.length) return files.map((file) => ({ type: "file", file }));
23620
+ const events = [];
23621
+ const blockType = block.type;
23622
+ if (blockType === "server_tool_use") {
23623
+ events.push({
23624
+ type: "builtin_tool_start",
23625
+ tool: unifiedBuiltinTool(block.name),
23626
+ ...typeof block.id === "string" ? { id: block.id } : {}
23627
+ });
23628
+ } else if (blockType?.endsWith("_tool_result")) {
23629
+ events.push({
23630
+ type: "builtin_tool_end",
23631
+ tool: unifiedBuiltinTool(blockType),
23632
+ ...typeof block.tool_use_id === "string" ? { id: block.tool_use_id } : {}
23633
+ });
23634
+ }
23635
+ for (const file of filesFromCodeExecBlock(block)) events.push({ type: "file", file });
23636
+ return events;
23584
23637
  }
23585
23638
  if (type === "content_block_stop") {
23586
23639
  }
@@ -24116,6 +24169,9 @@ var GoogleAdapter = class {
24116
24169
  candidate.finishReason,
24117
24170
  { MAX_TOKENS: "length", SAFETY: "content_filter" }
24118
24171
  );
24172
+ const builtinToolCalls = [];
24173
+ if (hasCodeExec) builtinToolCalls.push({ tool: "code_interpreter" });
24174
+ if (candidate.groundingMetadata) builtinToolCalls.push({ tool: "web_search" });
24119
24175
  return {
24120
24176
  id: crypto.randomUUID(),
24121
24177
  // Google doesn't return a response ID in generateContent
@@ -24128,6 +24184,7 @@ var GoogleAdapter = class {
24128
24184
  thinking,
24129
24185
  media,
24130
24186
  ...files.length ? { files } : {},
24187
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
24131
24188
  latencyMs,
24132
24189
  raw
24133
24190
  };
@@ -24165,6 +24222,8 @@ var GoogleAdapter = class {
24165
24222
  if (part.text !== void 0 && !part.thought)
24166
24223
  events.push({ type: "text", text: part.text });
24167
24224
  if (part.thought && part.text) events.push({ type: "thinking", text: part.text });
24225
+ if (part.executableCode) events.push({ type: "builtin_tool_start", tool: "code_interpreter" });
24226
+ if (part.codeExecutionResult) events.push({ type: "builtin_tool_end", tool: "code_interpreter" });
24168
24227
  if (part.inlineData) {
24169
24228
  const inline = part.inlineData;
24170
24229
  const mime = inline.mimeType;
@@ -24195,6 +24254,11 @@ var GoogleAdapter = class {
24195
24254
  events.push({ type: "tool_call_end", id: "" });
24196
24255
  }
24197
24256
  }
24257
+ if (candidate.groundingMetadata && !state.webSearchEmitted) {
24258
+ state.webSearchEmitted = true;
24259
+ events.push({ type: "builtin_tool_start", tool: "web_search" });
24260
+ events.push({ type: "builtin_tool_end", tool: "web_search" });
24261
+ }
24198
24262
  const fr = candidate.finishReason;
24199
24263
  if (fr)
24200
24264
  events.push({
@@ -25961,6 +26025,15 @@ function filenameForMime(mimeType) {
25961
26025
  if (mimeType.startsWith("image/")) return `file.${mimeType.slice("image/".length)}`;
25962
26026
  return "file.bin";
25963
26027
  }
26028
+ var RESPONSES_BUILTIN_ITEMS = /* @__PURE__ */ new Set(["web_search_call", "code_interpreter_call"]);
26029
+ function builtinCallFromResponsesItem(item) {
26030
+ const type = item.type;
26031
+ if (!RESPONSES_BUILTIN_ITEMS.has(type)) return null;
26032
+ return {
26033
+ tool: unifiedBuiltinTool(type),
26034
+ ...typeof item.id === "string" ? { id: item.id } : {}
26035
+ };
26036
+ }
25964
26037
  function filesFromResponsesOutputItem(item) {
25965
26038
  const files = [];
25966
26039
  const type = item.type;
@@ -26164,11 +26237,14 @@ var OpenAIResponsesAdapter = class {
26164
26237
  const toolCalls = [];
26165
26238
  const media = [];
26166
26239
  const files = [];
26240
+ const builtinToolCalls = [];
26167
26241
  let thinking = null;
26168
26242
  let text = "";
26169
26243
  for (const item of output) {
26170
26244
  const type = item.type;
26171
26245
  files.push(...this.filesFromOutputItem(item));
26246
+ const builtinCall = builtinCallFromResponsesItem(item);
26247
+ if (builtinCall) builtinToolCalls.push(builtinCall);
26172
26248
  if (type === "message") {
26173
26249
  const itemContent = item.content ?? [];
26174
26250
  for (const c of itemContent) {
@@ -26229,6 +26305,7 @@ var OpenAIResponsesAdapter = class {
26229
26305
  thinking,
26230
26306
  media,
26231
26307
  ...files.length ? { files } : {},
26308
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
26232
26309
  ...moderation ? { moderation } : {},
26233
26310
  latencyMs,
26234
26311
  raw
@@ -26260,6 +26337,10 @@ var OpenAIResponsesAdapter = class {
26260
26337
  if (item?.type === "image_generation_call") {
26261
26338
  events.push({ type: "media_start", mediaType: "image", mimeType: "image/png" });
26262
26339
  }
26340
+ const builtin = builtinCallFromResponsesItem(item ?? {});
26341
+ if (builtin) {
26342
+ events.push({ type: "builtin_tool_start", tool: builtin.tool, ...builtin.id ? { id: builtin.id } : {} });
26343
+ }
26263
26344
  }
26264
26345
  if (type === "response.image_generation_call.partial_image") {
26265
26346
  events.push({
@@ -26271,6 +26352,10 @@ var OpenAIResponsesAdapter = class {
26271
26352
  if (type === "response.output_item.done") {
26272
26353
  const item = data.item;
26273
26354
  for (const file of this.filesFromOutputItem(item)) events.push({ type: "file", file });
26355
+ const builtin = builtinCallFromResponsesItem(item ?? {});
26356
+ if (builtin) {
26357
+ events.push({ type: "builtin_tool_end", tool: builtin.tool, ...builtin.id ? { id: builtin.id } : {} });
26358
+ }
26274
26359
  if (item?.type === "function_call") {
26275
26360
  events.push({ type: "tool_call_end", id: item.call_id ?? "" });
26276
26361
  }
@@ -26369,6 +26454,9 @@ function filenameFor(mimeType) {
26369
26454
  }
26370
26455
 
26371
26456
  // src/llm/providers/openrouter/completions.ts
26457
+ function hasUrlCitation(annotations) {
26458
+ return Array.isArray(annotations) && annotations.some((a) => a?.type === "url_citation");
26459
+ }
26372
26460
  var OpenRouterAdapter = class extends OpenAIAdapter {
26373
26461
  name = "openrouter";
26374
26462
  constructor(config) {
@@ -26397,6 +26485,35 @@ var OpenRouterAdapter = class extends OpenAIAdapter {
26397
26485
  }
26398
26486
  return result;
26399
26487
  }
26488
+ parseResponse(raw, latencyMs) {
26489
+ const result = super.parseResponse(raw, latencyMs);
26490
+ const choices = raw.choices;
26491
+ const annotations = choices?.[0]?.message?.annotations;
26492
+ if (hasUrlCitation(annotations)) {
26493
+ result.builtinToolCalls = [...result.builtinToolCalls ?? [], { tool: "web_search" }];
26494
+ }
26495
+ return result;
26496
+ }
26497
+ /** Stateful — emit a single `web_search` builtin-tool pair the first time
26498
+ * `url_citation` annotations appear in the stream (the `:online` search signal). */
26499
+ createStreamParser() {
26500
+ let webSearchEmitted = false;
26501
+ return (event) => {
26502
+ const events = this.parseStreamEvent(event);
26503
+ if (!webSearchEmitted) {
26504
+ const choice = JSON.parse(event.data).choices?.[0];
26505
+ const annotations = choice?.delta?.annotations ?? choice?.message?.annotations;
26506
+ if (hasUrlCitation(annotations)) {
26507
+ webSearchEmitted = true;
26508
+ events.push(
26509
+ { type: "builtin_tool_start", tool: "web_search" },
26510
+ { type: "builtin_tool_end", tool: "web_search" }
26511
+ );
26512
+ }
26513
+ }
26514
+ return events;
26515
+ };
26516
+ }
26400
26517
  };
26401
26518
 
26402
26519
  // src/llm/providers/openrouter/responses.ts
@@ -28265,6 +28382,7 @@ var AgentLoop = class _AgentLoop {
28265
28382
  // Propagate hosted-tool file outputs + inline-moderation result from the final
28266
28383
  // LLM response (e.g. code-execution files produced during the run).
28267
28384
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28385
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28268
28386
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28269
28387
  latencyMs: performance.now() - startPerf,
28270
28388
  raw: lastResponse?.raw ?? null
@@ -28461,6 +28579,7 @@ var AgentLoop = class _AgentLoop {
28461
28579
  thinking: lastResponse?.thinking ?? null,
28462
28580
  media: [],
28463
28581
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28582
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28464
28583
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28465
28584
  latencyMs: performance.now() - startPerf,
28466
28585
  raw: null
@@ -0,0 +1,5 @@
1
+ /** Normalize a provider-native builtin-tool marker (output-item type, server-tool
2
+ * name, or result-block type) to the unified tool name used across the SDK
3
+ * (`web_search` | `code_interpreter` | …). Unknown values fall back to a stripped
4
+ * form so a new provider tool still yields a sensible name. */
5
+ export declare function unifiedBuiltinTool(native: string): string;
@@ -1,6 +1,9 @@
1
1
  /** OpenRouter provider adapter — OpenAI-compatible with extensions. */
2
+ import type { SSEEvent } from '../../../network/types';
2
3
  import type { ProviderAdapter, ProviderHttpRequest } from '../../types/provider';
3
4
  import type { NormalizedRequest } from '../../types/request';
5
+ import type { CompletionResponse } from '../../types/response';
6
+ import type { StreamEvent } from '../../types/stream';
4
7
  import { OpenAIAdapter } from '../openai/completions';
5
8
  export interface OpenRouterAdapterConfig {
6
9
  apiKey: string;
@@ -12,4 +15,8 @@ export declare class OpenRouterAdapter extends OpenAIAdapter {
12
15
  baseURL(): string;
13
16
  completionPath(): string;
14
17
  buildRequest(req: NormalizedRequest): ProviderHttpRequest;
18
+ parseResponse(raw: unknown, latencyMs: number): CompletionResponse;
19
+ /** Stateful — emit a single `web_search` builtin-tool pair the first time
20
+ * `url_citation` annotations appear in the stream (the `:online` search signal). */
21
+ createStreamParser(): (event: SSEEvent) => StreamEvent[];
15
22
  }
@@ -16,6 +16,10 @@ export interface CompletionResponse {
16
16
  * etc. Unified across providers. When only `id` is set, fetch the bytes via the
17
17
  * provider's files API; some providers return `data` inline. Absent when none. */
18
18
  files?: FileOutput[];
19
+ /** Hosted (provider-run) builtin tools the model invoked this turn — e.g. a web
20
+ * search or code-execution run. A durable trail of what was called (the provider
21
+ * ran them server-side; nothing for the client to execute). Absent when none. */
22
+ builtinToolCalls?: BuiltinToolCall[];
19
23
  /** Inline-moderation outcome, when the `moderation` request option was used.
20
24
  * Report-only: present for observability; it never blocks the call. Absent when
21
25
  * moderation was not requested. */
@@ -44,6 +48,14 @@ export interface FileOutput {
44
48
  * Absent for providers that don't need extra context. */
45
49
  ref?: Record<string, unknown>;
46
50
  }
51
+ /** A hosted builtin tool the model invoked (provider-run). */
52
+ export interface BuiltinToolCall {
53
+ /** Unified tool name: `'web_search'` | `'code_interpreter'` | … (normalized from
54
+ * each provider's native name — e.g. Anthropic `code_execution`). */
55
+ tool: string;
56
+ /** Provider call id, when available (correlates the stream start/end events). */
57
+ id?: string;
58
+ }
47
59
  export type FinishReason = 'stop' | 'tool_use' | 'length' | 'content_filter' | 'error';
48
60
  export interface Usage {
49
61
  inputTokens: number;
@@ -49,6 +49,23 @@ export type StreamEvent = {
49
49
  type: 'file';
50
50
  file: FileOutput;
51
51
  }
52
+ /** A hosted (provider-run) builtin tool began executing server-side — e.g. the
53
+ * model started a web search or code-execution run. `tool` is the unified name
54
+ * (`'web_search'` | `'code_interpreter'` | …). Informational progress: unlike
55
+ * `tool_call_*` (a function call the CLIENT must run), the provider runs these
56
+ * itself, so there is nothing to execute or return. Also collected onto the
57
+ * streamed final response's `builtinToolCalls`. */
58
+ | {
59
+ type: 'builtin_tool_start';
60
+ tool: string;
61
+ id?: string;
62
+ }
63
+ /** A hosted builtin tool finished executing server-side. */
64
+ | {
65
+ type: 'builtin_tool_end';
66
+ tool: string;
67
+ id?: string;
68
+ }
52
69
  /** A moderation result for the input or output. `source` distinguishes a
53
70
  * provider-native result from a client-emulated one. Emitted by the moderation
54
71
  * option (report-only). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@combycode/llm-sdk",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Unified, pluggable AI SDK for accessing the LLMs of every major provider (Anthropic, OpenAI, Google, xAI, OpenRouter) through one API. Cross-environment: Node, Bun, and the browser.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",