@combycode/llm-sdk 1.4.0 → 1.5.0

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,20 @@ All notable changes to `@combycode/llm-sdk` are documented here. The format foll
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.5.0] - 2026-07-06
10
+
11
+ ### Added
12
+ - **Builtin-tool activity in streams + a durable trail.** Provider-run hosted tools (web search,
13
+ code execution) now surface `{ type: 'builtin_tool_start' }` / `{ type: 'builtin_tool_end' }`
14
+ stream events as they run, and a `response.builtinToolCalls` trail (`BuiltinToolCall[]` —
15
+ `{ tool, id? }` with unified tool names) on both `complete()` and streamed responses (propagated
16
+ through `AgentLoop`). Informational only — unlike `tool_call_*` (a function call the client must
17
+ execute), the provider runs these itself. Normalized across providers (Anthropic `server_tool_use`
18
+ / `*_tool_result`, OpenAI/xAI `web_search_call` / `code_interpreter_call` output items, Google
19
+ `executableCode` / `codeExecutionResult` parts + `googleSearch` grounding, OpenRouter
20
+ `:online` `url_citation` annotations → `web_search`). Verified live on Anthropic, OpenAI,
21
+ Google, xAI, and OpenRouter. Exports `BuiltinToolCall`.
22
+
9
23
  ## [1.4.0] - 2026-07-06
10
24
 
11
25
  ### Fixed
@@ -23082,6 +23082,7 @@ var LLMClient = class {
23082
23082
  let finishReason = "stop";
23083
23083
  let moderationReport;
23084
23084
  const files = [];
23085
+ const builtinToolCalls = [];
23085
23086
  const fetchStream = this.fetchStreamFn;
23086
23087
  const queueName = this.queueName;
23087
23088
  const priority = this.priority;
@@ -23133,6 +23134,9 @@ var LLMClient = class {
23133
23134
  case "file":
23134
23135
  files.push(event.file);
23135
23136
  break;
23137
+ case "builtin_tool_start":
23138
+ builtinToolCalls.push({ tool: event.tool, ...event.id ? { id: event.id } : {} });
23139
+ break;
23136
23140
  case "moderation":
23137
23141
  moderationReport = this.mergeModeration(
23138
23142
  moderationReport,
@@ -23156,6 +23160,7 @@ var LLMClient = class {
23156
23160
  thinking: thinking || null,
23157
23161
  media: [],
23158
23162
  ...files.length ? { files } : {},
23163
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23159
23164
  ...moderationReport ? { moderation: moderationReport } : {},
23160
23165
  latencyMs: performance.now() - start,
23161
23166
  raw: null
@@ -23389,6 +23394,24 @@ var AnthropicFileAdapter = class {
23389
23394
  }
23390
23395
  };
23391
23396
 
23397
+ // src/llm/providers/_shared/builtin-tools.ts
23398
+ var NATIVE_TO_UNIFIED = {
23399
+ // OpenAI / xAI Responses output-item types
23400
+ web_search_call: "web_search",
23401
+ code_interpreter_call: "code_interpreter",
23402
+ // Anthropic server_tool_use names
23403
+ web_search: "web_search",
23404
+ code_execution: "code_interpreter",
23405
+ bash_code_execution: "code_interpreter",
23406
+ // Anthropic *_tool_result block types
23407
+ web_search_tool_result: "web_search",
23408
+ code_execution_tool_result: "code_interpreter",
23409
+ bash_code_execution_tool_result: "code_interpreter"
23410
+ };
23411
+ function unifiedBuiltinTool(native) {
23412
+ return NATIVE_TO_UNIFIED[native] ?? native.replace(/_call$/, "").replace(/_tool_result$/, "");
23413
+ }
23414
+
23392
23415
  // src/llm/providers/_shared/constants.ts
23393
23416
  var AUDIO_PCM16_SAMPLE_RATE_HZ = 24e3;
23394
23417
  var DEFAULT_MAX_TOKENS = 4096;
@@ -23598,6 +23621,7 @@ var AnthropicAdapter = class {
23598
23621
  let thinking = null;
23599
23622
  const toolCalls = [];
23600
23623
  const files = [];
23624
+ const builtinToolCalls = [];
23601
23625
  for (const block of contentBlocks) {
23602
23626
  if (block.type === "text") {
23603
23627
  content.push({ type: "text", text: block.text });
@@ -23612,6 +23636,11 @@ var AnthropicAdapter = class {
23612
23636
  };
23613
23637
  content.push(tc);
23614
23638
  toolCalls.push(tc);
23639
+ } else if (block.type === "server_tool_use") {
23640
+ builtinToolCalls.push({
23641
+ tool: unifiedBuiltinTool(block.name),
23642
+ ...typeof block.id === "string" ? { id: block.id } : {}
23643
+ });
23615
23644
  } else {
23616
23645
  files.push(...filesFromCodeExecBlock(block));
23617
23646
  }
@@ -23629,6 +23658,7 @@ var AnthropicAdapter = class {
23629
23658
  toolCalls,
23630
23659
  media: [],
23631
23660
  ...files.length ? { files } : {},
23661
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23632
23662
  thinking,
23633
23663
  latencyMs,
23634
23664
  raw
@@ -23652,8 +23682,23 @@ var AnthropicAdapter = class {
23652
23682
  if (block.type === "tool_use") {
23653
23683
  return [{ type: "tool_call_start", id: block.id, name: block.name }];
23654
23684
  }
23655
- const files = filesFromCodeExecBlock(block);
23656
- if (files.length) return files.map((file) => ({ type: "file", file }));
23685
+ const events = [];
23686
+ const blockType = block.type;
23687
+ if (blockType === "server_tool_use") {
23688
+ events.push({
23689
+ type: "builtin_tool_start",
23690
+ tool: unifiedBuiltinTool(block.name),
23691
+ ...typeof block.id === "string" ? { id: block.id } : {}
23692
+ });
23693
+ } else if (blockType?.endsWith("_tool_result")) {
23694
+ events.push({
23695
+ type: "builtin_tool_end",
23696
+ tool: unifiedBuiltinTool(blockType),
23697
+ ...typeof block.tool_use_id === "string" ? { id: block.tool_use_id } : {}
23698
+ });
23699
+ }
23700
+ for (const file of filesFromCodeExecBlock(block)) events.push({ type: "file", file });
23701
+ return events;
23657
23702
  }
23658
23703
  if (type === "content_block_stop") {
23659
23704
  }
@@ -24189,6 +24234,9 @@ var GoogleAdapter = class {
24189
24234
  candidate.finishReason,
24190
24235
  { MAX_TOKENS: "length", SAFETY: "content_filter" }
24191
24236
  );
24237
+ const builtinToolCalls = [];
24238
+ if (hasCodeExec) builtinToolCalls.push({ tool: "code_interpreter" });
24239
+ if (candidate.groundingMetadata) builtinToolCalls.push({ tool: "web_search" });
24192
24240
  return {
24193
24241
  id: crypto.randomUUID(),
24194
24242
  // Google doesn't return a response ID in generateContent
@@ -24201,6 +24249,7 @@ var GoogleAdapter = class {
24201
24249
  thinking,
24202
24250
  media,
24203
24251
  ...files.length ? { files } : {},
24252
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
24204
24253
  latencyMs,
24205
24254
  raw
24206
24255
  };
@@ -24238,6 +24287,8 @@ var GoogleAdapter = class {
24238
24287
  if (part.text !== void 0 && !part.thought)
24239
24288
  events.push({ type: "text", text: part.text });
24240
24289
  if (part.thought && part.text) events.push({ type: "thinking", text: part.text });
24290
+ if (part.executableCode) events.push({ type: "builtin_tool_start", tool: "code_interpreter" });
24291
+ if (part.codeExecutionResult) events.push({ type: "builtin_tool_end", tool: "code_interpreter" });
24241
24292
  if (part.inlineData) {
24242
24293
  const inline = part.inlineData;
24243
24294
  const mime = inline.mimeType;
@@ -24268,6 +24319,11 @@ var GoogleAdapter = class {
24268
24319
  events.push({ type: "tool_call_end", id: "" });
24269
24320
  }
24270
24321
  }
24322
+ if (candidate.groundingMetadata && !state.webSearchEmitted) {
24323
+ state.webSearchEmitted = true;
24324
+ events.push({ type: "builtin_tool_start", tool: "web_search" });
24325
+ events.push({ type: "builtin_tool_end", tool: "web_search" });
24326
+ }
24271
24327
  const fr = candidate.finishReason;
24272
24328
  if (fr)
24273
24329
  events.push({
@@ -26034,6 +26090,15 @@ function filenameForMime(mimeType) {
26034
26090
  if (mimeType.startsWith("image/")) return `file.${mimeType.slice("image/".length)}`;
26035
26091
  return "file.bin";
26036
26092
  }
26093
+ var RESPONSES_BUILTIN_ITEMS = /* @__PURE__ */ new Set(["web_search_call", "code_interpreter_call"]);
26094
+ function builtinCallFromResponsesItem(item) {
26095
+ const type = item.type;
26096
+ if (!RESPONSES_BUILTIN_ITEMS.has(type)) return null;
26097
+ return {
26098
+ tool: unifiedBuiltinTool(type),
26099
+ ...typeof item.id === "string" ? { id: item.id } : {}
26100
+ };
26101
+ }
26037
26102
  function filesFromResponsesOutputItem(item) {
26038
26103
  const files = [];
26039
26104
  const type = item.type;
@@ -26237,11 +26302,14 @@ var OpenAIResponsesAdapter = class {
26237
26302
  const toolCalls = [];
26238
26303
  const media = [];
26239
26304
  const files = [];
26305
+ const builtinToolCalls = [];
26240
26306
  let thinking = null;
26241
26307
  let text = "";
26242
26308
  for (const item of output) {
26243
26309
  const type = item.type;
26244
26310
  files.push(...this.filesFromOutputItem(item));
26311
+ const builtinCall = builtinCallFromResponsesItem(item);
26312
+ if (builtinCall) builtinToolCalls.push(builtinCall);
26245
26313
  if (type === "message") {
26246
26314
  const itemContent = item.content ?? [];
26247
26315
  for (const c of itemContent) {
@@ -26302,6 +26370,7 @@ var OpenAIResponsesAdapter = class {
26302
26370
  thinking,
26303
26371
  media,
26304
26372
  ...files.length ? { files } : {},
26373
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
26305
26374
  ...moderation ? { moderation } : {},
26306
26375
  latencyMs,
26307
26376
  raw
@@ -26333,6 +26402,10 @@ var OpenAIResponsesAdapter = class {
26333
26402
  if (item?.type === "image_generation_call") {
26334
26403
  events.push({ type: "media_start", mediaType: "image", mimeType: "image/png" });
26335
26404
  }
26405
+ const builtin = builtinCallFromResponsesItem(item ?? {});
26406
+ if (builtin) {
26407
+ events.push({ type: "builtin_tool_start", tool: builtin.tool, ...builtin.id ? { id: builtin.id } : {} });
26408
+ }
26336
26409
  }
26337
26410
  if (type === "response.image_generation_call.partial_image") {
26338
26411
  events.push({
@@ -26344,6 +26417,10 @@ var OpenAIResponsesAdapter = class {
26344
26417
  if (type === "response.output_item.done") {
26345
26418
  const item = data.item;
26346
26419
  for (const file of this.filesFromOutputItem(item)) events.push({ type: "file", file });
26420
+ const builtin = builtinCallFromResponsesItem(item ?? {});
26421
+ if (builtin) {
26422
+ events.push({ type: "builtin_tool_end", tool: builtin.tool, ...builtin.id ? { id: builtin.id } : {} });
26423
+ }
26347
26424
  if (item?.type === "function_call") {
26348
26425
  events.push({ type: "tool_call_end", id: item.call_id ?? "" });
26349
26426
  }
@@ -26442,6 +26519,9 @@ function filenameFor(mimeType) {
26442
26519
  }
26443
26520
 
26444
26521
  // src/llm/providers/openrouter/completions.ts
26522
+ function hasUrlCitation(annotations) {
26523
+ return Array.isArray(annotations) && annotations.some((a) => a?.type === "url_citation");
26524
+ }
26445
26525
  var OpenRouterAdapter = class extends OpenAIAdapter {
26446
26526
  name = "openrouter";
26447
26527
  constructor(config) {
@@ -26470,6 +26550,35 @@ var OpenRouterAdapter = class extends OpenAIAdapter {
26470
26550
  }
26471
26551
  return result;
26472
26552
  }
26553
+ parseResponse(raw, latencyMs) {
26554
+ const result = super.parseResponse(raw, latencyMs);
26555
+ const choices = raw.choices;
26556
+ const annotations = choices?.[0]?.message?.annotations;
26557
+ if (hasUrlCitation(annotations)) {
26558
+ result.builtinToolCalls = [...result.builtinToolCalls ?? [], { tool: "web_search" }];
26559
+ }
26560
+ return result;
26561
+ }
26562
+ /** Stateful — emit a single `web_search` builtin-tool pair the first time
26563
+ * `url_citation` annotations appear in the stream (the `:online` search signal). */
26564
+ createStreamParser() {
26565
+ let webSearchEmitted = false;
26566
+ return (event) => {
26567
+ const events = this.parseStreamEvent(event);
26568
+ if (!webSearchEmitted) {
26569
+ const choice = JSON.parse(event.data).choices?.[0];
26570
+ const annotations = choice?.delta?.annotations ?? choice?.message?.annotations;
26571
+ if (hasUrlCitation(annotations)) {
26572
+ webSearchEmitted = true;
26573
+ events.push(
26574
+ { type: "builtin_tool_start", tool: "web_search" },
26575
+ { type: "builtin_tool_end", tool: "web_search" }
26576
+ );
26577
+ }
26578
+ }
26579
+ return events;
26580
+ };
26581
+ }
26473
26582
  };
26474
26583
 
26475
26584
  // src/llm/providers/openrouter/responses.ts
@@ -28338,6 +28447,7 @@ var AgentLoop = class _AgentLoop {
28338
28447
  // Propagate hosted-tool file outputs + inline-moderation result from the final
28339
28448
  // LLM response (e.g. code-execution files produced during the run).
28340
28449
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28450
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28341
28451
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28342
28452
  latencyMs: performance.now() - startPerf,
28343
28453
  raw: lastResponse?.raw ?? null
@@ -28534,6 +28644,7 @@ var AgentLoop = class _AgentLoop {
28534
28644
  thinking: lastResponse?.thinking ?? null,
28535
28645
  media: [],
28536
28646
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28647
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28537
28648
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28538
28649
  latencyMs: performance.now() - startPerf,
28539
28650
  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
@@ -23009,6 +23009,7 @@ var LLMClient = class {
23009
23009
  let finishReason = "stop";
23010
23010
  let moderationReport;
23011
23011
  const files = [];
23012
+ const builtinToolCalls = [];
23012
23013
  const fetchStream = this.fetchStreamFn;
23013
23014
  const queueName = this.queueName;
23014
23015
  const priority = this.priority;
@@ -23060,6 +23061,9 @@ var LLMClient = class {
23060
23061
  case "file":
23061
23062
  files.push(event.file);
23062
23063
  break;
23064
+ case "builtin_tool_start":
23065
+ builtinToolCalls.push({ tool: event.tool, ...event.id ? { id: event.id } : {} });
23066
+ break;
23063
23067
  case "moderation":
23064
23068
  moderationReport = this.mergeModeration(
23065
23069
  moderationReport,
@@ -23083,6 +23087,7 @@ var LLMClient = class {
23083
23087
  thinking: thinking || null,
23084
23088
  media: [],
23085
23089
  ...files.length ? { files } : {},
23090
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23086
23091
  ...moderationReport ? { moderation: moderationReport } : {},
23087
23092
  latencyMs: performance.now() - start,
23088
23093
  raw: null
@@ -23316,6 +23321,24 @@ var AnthropicFileAdapter = class {
23316
23321
  }
23317
23322
  };
23318
23323
 
23324
+ // src/llm/providers/_shared/builtin-tools.ts
23325
+ var NATIVE_TO_UNIFIED = {
23326
+ // OpenAI / xAI Responses output-item types
23327
+ web_search_call: "web_search",
23328
+ code_interpreter_call: "code_interpreter",
23329
+ // Anthropic server_tool_use names
23330
+ web_search: "web_search",
23331
+ code_execution: "code_interpreter",
23332
+ bash_code_execution: "code_interpreter",
23333
+ // Anthropic *_tool_result block types
23334
+ web_search_tool_result: "web_search",
23335
+ code_execution_tool_result: "code_interpreter",
23336
+ bash_code_execution_tool_result: "code_interpreter"
23337
+ };
23338
+ function unifiedBuiltinTool(native) {
23339
+ return NATIVE_TO_UNIFIED[native] ?? native.replace(/_call$/, "").replace(/_tool_result$/, "");
23340
+ }
23341
+
23319
23342
  // src/llm/providers/_shared/constants.ts
23320
23343
  var AUDIO_PCM16_SAMPLE_RATE_HZ = 24e3;
23321
23344
  var DEFAULT_MAX_TOKENS = 4096;
@@ -23525,6 +23548,7 @@ var AnthropicAdapter = class {
23525
23548
  let thinking = null;
23526
23549
  const toolCalls = [];
23527
23550
  const files = [];
23551
+ const builtinToolCalls = [];
23528
23552
  for (const block of contentBlocks) {
23529
23553
  if (block.type === "text") {
23530
23554
  content.push({ type: "text", text: block.text });
@@ -23539,6 +23563,11 @@ var AnthropicAdapter = class {
23539
23563
  };
23540
23564
  content.push(tc);
23541
23565
  toolCalls.push(tc);
23566
+ } else if (block.type === "server_tool_use") {
23567
+ builtinToolCalls.push({
23568
+ tool: unifiedBuiltinTool(block.name),
23569
+ ...typeof block.id === "string" ? { id: block.id } : {}
23570
+ });
23542
23571
  } else {
23543
23572
  files.push(...filesFromCodeExecBlock(block));
23544
23573
  }
@@ -23556,6 +23585,7 @@ var AnthropicAdapter = class {
23556
23585
  toolCalls,
23557
23586
  media: [],
23558
23587
  ...files.length ? { files } : {},
23588
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23559
23589
  thinking,
23560
23590
  latencyMs,
23561
23591
  raw
@@ -23579,8 +23609,23 @@ var AnthropicAdapter = class {
23579
23609
  if (block.type === "tool_use") {
23580
23610
  return [{ type: "tool_call_start", id: block.id, name: block.name }];
23581
23611
  }
23582
- const files = filesFromCodeExecBlock(block);
23583
- if (files.length) return files.map((file) => ({ type: "file", file }));
23612
+ const events = [];
23613
+ const blockType = block.type;
23614
+ if (blockType === "server_tool_use") {
23615
+ events.push({
23616
+ type: "builtin_tool_start",
23617
+ tool: unifiedBuiltinTool(block.name),
23618
+ ...typeof block.id === "string" ? { id: block.id } : {}
23619
+ });
23620
+ } else if (blockType?.endsWith("_tool_result")) {
23621
+ events.push({
23622
+ type: "builtin_tool_end",
23623
+ tool: unifiedBuiltinTool(blockType),
23624
+ ...typeof block.tool_use_id === "string" ? { id: block.tool_use_id } : {}
23625
+ });
23626
+ }
23627
+ for (const file of filesFromCodeExecBlock(block)) events.push({ type: "file", file });
23628
+ return events;
23584
23629
  }
23585
23630
  if (type === "content_block_stop") {
23586
23631
  }
@@ -24116,6 +24161,9 @@ var GoogleAdapter = class {
24116
24161
  candidate.finishReason,
24117
24162
  { MAX_TOKENS: "length", SAFETY: "content_filter" }
24118
24163
  );
24164
+ const builtinToolCalls = [];
24165
+ if (hasCodeExec) builtinToolCalls.push({ tool: "code_interpreter" });
24166
+ if (candidate.groundingMetadata) builtinToolCalls.push({ tool: "web_search" });
24119
24167
  return {
24120
24168
  id: crypto.randomUUID(),
24121
24169
  // Google doesn't return a response ID in generateContent
@@ -24128,6 +24176,7 @@ var GoogleAdapter = class {
24128
24176
  thinking,
24129
24177
  media,
24130
24178
  ...files.length ? { files } : {},
24179
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
24131
24180
  latencyMs,
24132
24181
  raw
24133
24182
  };
@@ -24165,6 +24214,8 @@ var GoogleAdapter = class {
24165
24214
  if (part.text !== void 0 && !part.thought)
24166
24215
  events.push({ type: "text", text: part.text });
24167
24216
  if (part.thought && part.text) events.push({ type: "thinking", text: part.text });
24217
+ if (part.executableCode) events.push({ type: "builtin_tool_start", tool: "code_interpreter" });
24218
+ if (part.codeExecutionResult) events.push({ type: "builtin_tool_end", tool: "code_interpreter" });
24168
24219
  if (part.inlineData) {
24169
24220
  const inline = part.inlineData;
24170
24221
  const mime = inline.mimeType;
@@ -24195,6 +24246,11 @@ var GoogleAdapter = class {
24195
24246
  events.push({ type: "tool_call_end", id: "" });
24196
24247
  }
24197
24248
  }
24249
+ if (candidate.groundingMetadata && !state.webSearchEmitted) {
24250
+ state.webSearchEmitted = true;
24251
+ events.push({ type: "builtin_tool_start", tool: "web_search" });
24252
+ events.push({ type: "builtin_tool_end", tool: "web_search" });
24253
+ }
24198
24254
  const fr = candidate.finishReason;
24199
24255
  if (fr)
24200
24256
  events.push({
@@ -25961,6 +26017,15 @@ function filenameForMime(mimeType) {
25961
26017
  if (mimeType.startsWith("image/")) return `file.${mimeType.slice("image/".length)}`;
25962
26018
  return "file.bin";
25963
26019
  }
26020
+ var RESPONSES_BUILTIN_ITEMS = /* @__PURE__ */ new Set(["web_search_call", "code_interpreter_call"]);
26021
+ function builtinCallFromResponsesItem(item) {
26022
+ const type = item.type;
26023
+ if (!RESPONSES_BUILTIN_ITEMS.has(type)) return null;
26024
+ return {
26025
+ tool: unifiedBuiltinTool(type),
26026
+ ...typeof item.id === "string" ? { id: item.id } : {}
26027
+ };
26028
+ }
25964
26029
  function filesFromResponsesOutputItem(item) {
25965
26030
  const files = [];
25966
26031
  const type = item.type;
@@ -26164,11 +26229,14 @@ var OpenAIResponsesAdapter = class {
26164
26229
  const toolCalls = [];
26165
26230
  const media = [];
26166
26231
  const files = [];
26232
+ const builtinToolCalls = [];
26167
26233
  let thinking = null;
26168
26234
  let text = "";
26169
26235
  for (const item of output) {
26170
26236
  const type = item.type;
26171
26237
  files.push(...this.filesFromOutputItem(item));
26238
+ const builtinCall = builtinCallFromResponsesItem(item);
26239
+ if (builtinCall) builtinToolCalls.push(builtinCall);
26172
26240
  if (type === "message") {
26173
26241
  const itemContent = item.content ?? [];
26174
26242
  for (const c of itemContent) {
@@ -26229,6 +26297,7 @@ var OpenAIResponsesAdapter = class {
26229
26297
  thinking,
26230
26298
  media,
26231
26299
  ...files.length ? { files } : {},
26300
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
26232
26301
  ...moderation ? { moderation } : {},
26233
26302
  latencyMs,
26234
26303
  raw
@@ -26260,6 +26329,10 @@ var OpenAIResponsesAdapter = class {
26260
26329
  if (item?.type === "image_generation_call") {
26261
26330
  events.push({ type: "media_start", mediaType: "image", mimeType: "image/png" });
26262
26331
  }
26332
+ const builtin = builtinCallFromResponsesItem(item ?? {});
26333
+ if (builtin) {
26334
+ events.push({ type: "builtin_tool_start", tool: builtin.tool, ...builtin.id ? { id: builtin.id } : {} });
26335
+ }
26263
26336
  }
26264
26337
  if (type === "response.image_generation_call.partial_image") {
26265
26338
  events.push({
@@ -26271,6 +26344,10 @@ var OpenAIResponsesAdapter = class {
26271
26344
  if (type === "response.output_item.done") {
26272
26345
  const item = data.item;
26273
26346
  for (const file of this.filesFromOutputItem(item)) events.push({ type: "file", file });
26347
+ const builtin = builtinCallFromResponsesItem(item ?? {});
26348
+ if (builtin) {
26349
+ events.push({ type: "builtin_tool_end", tool: builtin.tool, ...builtin.id ? { id: builtin.id } : {} });
26350
+ }
26274
26351
  if (item?.type === "function_call") {
26275
26352
  events.push({ type: "tool_call_end", id: item.call_id ?? "" });
26276
26353
  }
@@ -26369,6 +26446,9 @@ function filenameFor(mimeType) {
26369
26446
  }
26370
26447
 
26371
26448
  // src/llm/providers/openrouter/completions.ts
26449
+ function hasUrlCitation(annotations) {
26450
+ return Array.isArray(annotations) && annotations.some((a) => a?.type === "url_citation");
26451
+ }
26372
26452
  var OpenRouterAdapter = class extends OpenAIAdapter {
26373
26453
  name = "openrouter";
26374
26454
  constructor(config) {
@@ -26397,6 +26477,35 @@ var OpenRouterAdapter = class extends OpenAIAdapter {
26397
26477
  }
26398
26478
  return result;
26399
26479
  }
26480
+ parseResponse(raw, latencyMs) {
26481
+ const result = super.parseResponse(raw, latencyMs);
26482
+ const choices = raw.choices;
26483
+ const annotations = choices?.[0]?.message?.annotations;
26484
+ if (hasUrlCitation(annotations)) {
26485
+ result.builtinToolCalls = [...result.builtinToolCalls ?? [], { tool: "web_search" }];
26486
+ }
26487
+ return result;
26488
+ }
26489
+ /** Stateful — emit a single `web_search` builtin-tool pair the first time
26490
+ * `url_citation` annotations appear in the stream (the `:online` search signal). */
26491
+ createStreamParser() {
26492
+ let webSearchEmitted = false;
26493
+ return (event) => {
26494
+ const events = this.parseStreamEvent(event);
26495
+ if (!webSearchEmitted) {
26496
+ const choice = JSON.parse(event.data).choices?.[0];
26497
+ const annotations = choice?.delta?.annotations ?? choice?.message?.annotations;
26498
+ if (hasUrlCitation(annotations)) {
26499
+ webSearchEmitted = true;
26500
+ events.push(
26501
+ { type: "builtin_tool_start", tool: "web_search" },
26502
+ { type: "builtin_tool_end", tool: "web_search" }
26503
+ );
26504
+ }
26505
+ }
26506
+ return events;
26507
+ };
26508
+ }
26400
26509
  };
26401
26510
 
26402
26511
  // src/llm/providers/openrouter/responses.ts
@@ -28265,6 +28374,7 @@ var AgentLoop = class _AgentLoop {
28265
28374
  // Propagate hosted-tool file outputs + inline-moderation result from the final
28266
28375
  // LLM response (e.g. code-execution files produced during the run).
28267
28376
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28377
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28268
28378
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28269
28379
  latencyMs: performance.now() - startPerf,
28270
28380
  raw: lastResponse?.raw ?? null
@@ -28461,6 +28571,7 @@ var AgentLoop = class _AgentLoop {
28461
28571
  thinking: lastResponse?.thinking ?? null,
28462
28572
  media: [],
28463
28573
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28574
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28464
28575
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28465
28576
  latencyMs: performance.now() - startPerf,
28466
28577
  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.0",
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",