@combycode/llm-sdk 1.3.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,41 @@ 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
+
23
+ ## [1.4.0] - 2026-07-06
24
+
25
+ ### Fixed
26
+ - **xAI hosted code-execution files** now surface on `response.files` (parity with the other
27
+ providers). xAI returns code-interpreter output files inline inside the `code_interpreter_call`
28
+ `logs` JSON (`output_files:[{file_name, mime_type, data:[…bytes]}]`) and only when the request
29
+ asks for them — so `XAIResponsesAdapter` now sends `include:['code_interpreter_call.outputs']`
30
+ when `code_interpreter` is used and extracts the inline bytes into `FileOutput`. Verified live on
31
+ `complete()` and `stream()`. `OpenAIResponsesAdapter.filesFromOutputItem` is now a `protected`
32
+ overridable method so Responses-compatible providers can extend file extraction.
33
+
34
+ ### Added
35
+ - **Adapter-sourced builtin-tool capabilities in the catalog.** Every tool-capable (chat-family)
36
+ model now carries `capabilities.builtinTools` — `['web_search', 'code_interpreter']` for
37
+ anthropic/openai/google/xai, `['web_search']` for openrouter (it doesn't route hosted code
38
+ execution) — injected at catalog load from a single provider map (`PROVIDER_BUILTIN_TOOLS`) that
39
+ mirrors what the adapters actually support. New `catalog.builtinToolsFor()` /
40
+ `supportsBuiltinTool()` accessors and `select('web_search')` / `select('code_interpreter')`
41
+ queries (`search` stays an alias for `web_search`). Non-tool models (embeddings/tts/image) get
42
+ none. A reliable per-model source can refine this later.
43
+
9
44
  ## [1.3.0] - 2026-07-06
10
45
 
11
46
  ### Added
@@ -22105,6 +22105,15 @@ var catalog_default5 = {
22105
22105
  }
22106
22106
  };
22107
22107
 
22108
+ // src/llm/providers/builtin-tools.ts
22109
+ var PROVIDER_BUILTIN_TOOLS = {
22110
+ anthropic: ["web_search", "code_interpreter"],
22111
+ openai: ["web_search", "code_interpreter"],
22112
+ google: ["web_search", "code_interpreter"],
22113
+ xai: ["web_search", "code_interpreter"],
22114
+ openrouter: ["web_search"]
22115
+ };
22116
+
22108
22117
  // src/plugins/model-catalog/catalog.ts
22109
22118
  var PROVIDER_DEFAULT_CATALOGS = [
22110
22119
  catalog_default,
@@ -22142,6 +22151,13 @@ var DEFAULT_REASONING = {
22142
22151
  encryptedContent: false,
22143
22152
  summaryAvailable: false
22144
22153
  };
22154
+ function withBuiltinTools(provider, caps) {
22155
+ if (caps.builtinTools !== void 0) return caps;
22156
+ if (!caps.toolUse) return caps;
22157
+ const tools = PROVIDER_BUILTIN_TOOLS[provider];
22158
+ if (!tools || tools.length === 0) return caps;
22159
+ return { ...caps, builtinTools: [...tools] };
22160
+ }
22145
22161
  var ModelCatalog = class {
22146
22162
  models = /* @__PURE__ */ new Map();
22147
22163
  /** `provider/alias` → `provider/canonical-slug`. Lets get()/resolveModelId
@@ -22160,7 +22176,7 @@ var ModelCatalog = class {
22160
22176
  supportedApis: info.supportedApis ?? [info.preferredApi ?? "completions"],
22161
22177
  contextWindow: info.contextWindow,
22162
22178
  maxOutput: info.maxOutput,
22163
- capabilities: { ...DEFAULT_CAPABILITIES, ...info.capabilities },
22179
+ capabilities: withBuiltinTools(provider, { ...DEFAULT_CAPABILITIES, ...info.capabilities }),
22164
22180
  reasoning: { ...DEFAULT_REASONING, ...info.reasoning },
22165
22181
  mediaOnly: info.mediaOnly,
22166
22182
  tokenizer: info.tokenizer,
@@ -22214,6 +22230,15 @@ var ModelCatalog = class {
22214
22230
  supportsTools(provider, model) {
22215
22231
  return this.get(provider, model)?.capabilities.toolUse ?? false;
22216
22232
  }
22233
+ /** Hosted server-side builtin tools this model supports (e.g. `['web_search',
22234
+ * 'code_interpreter']`). Empty when unknown or the model isn't tool-capable. */
22235
+ builtinToolsFor(provider, model) {
22236
+ return [...this.get(provider, model)?.capabilities.builtinTools ?? []];
22237
+ }
22238
+ /** Whether this model supports a specific hosted builtin tool. */
22239
+ supportsBuiltinTool(provider, model, tool) {
22240
+ return this.get(provider, model)?.capabilities.builtinTools?.includes(tool) ?? false;
22241
+ }
22217
22242
  supportsPreviousResponseId(provider, model) {
22218
22243
  const info = this.get(provider, model);
22219
22244
  if (info && !info.supportedApis.some((a) => a === "responses" || a === "interactions")) {
@@ -23057,6 +23082,7 @@ var LLMClient = class {
23057
23082
  let finishReason = "stop";
23058
23083
  let moderationReport;
23059
23084
  const files = [];
23085
+ const builtinToolCalls = [];
23060
23086
  const fetchStream = this.fetchStreamFn;
23061
23087
  const queueName = this.queueName;
23062
23088
  const priority = this.priority;
@@ -23108,6 +23134,9 @@ var LLMClient = class {
23108
23134
  case "file":
23109
23135
  files.push(event.file);
23110
23136
  break;
23137
+ case "builtin_tool_start":
23138
+ builtinToolCalls.push({ tool: event.tool, ...event.id ? { id: event.id } : {} });
23139
+ break;
23111
23140
  case "moderation":
23112
23141
  moderationReport = this.mergeModeration(
23113
23142
  moderationReport,
@@ -23131,6 +23160,7 @@ var LLMClient = class {
23131
23160
  thinking: thinking || null,
23132
23161
  media: [],
23133
23162
  ...files.length ? { files } : {},
23163
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23134
23164
  ...moderationReport ? { moderation: moderationReport } : {},
23135
23165
  latencyMs: performance.now() - start,
23136
23166
  raw: null
@@ -23364,6 +23394,24 @@ var AnthropicFileAdapter = class {
23364
23394
  }
23365
23395
  };
23366
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
+
23367
23415
  // src/llm/providers/_shared/constants.ts
23368
23416
  var AUDIO_PCM16_SAMPLE_RATE_HZ = 24e3;
23369
23417
  var DEFAULT_MAX_TOKENS = 4096;
@@ -23573,6 +23621,7 @@ var AnthropicAdapter = class {
23573
23621
  let thinking = null;
23574
23622
  const toolCalls = [];
23575
23623
  const files = [];
23624
+ const builtinToolCalls = [];
23576
23625
  for (const block of contentBlocks) {
23577
23626
  if (block.type === "text") {
23578
23627
  content.push({ type: "text", text: block.text });
@@ -23587,6 +23636,11 @@ var AnthropicAdapter = class {
23587
23636
  };
23588
23637
  content.push(tc);
23589
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
+ });
23590
23644
  } else {
23591
23645
  files.push(...filesFromCodeExecBlock(block));
23592
23646
  }
@@ -23604,6 +23658,7 @@ var AnthropicAdapter = class {
23604
23658
  toolCalls,
23605
23659
  media: [],
23606
23660
  ...files.length ? { files } : {},
23661
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23607
23662
  thinking,
23608
23663
  latencyMs,
23609
23664
  raw
@@ -23627,8 +23682,23 @@ var AnthropicAdapter = class {
23627
23682
  if (block.type === "tool_use") {
23628
23683
  return [{ type: "tool_call_start", id: block.id, name: block.name }];
23629
23684
  }
23630
- const files = filesFromCodeExecBlock(block);
23631
- 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;
23632
23702
  }
23633
23703
  if (type === "content_block_stop") {
23634
23704
  }
@@ -24164,6 +24234,9 @@ var GoogleAdapter = class {
24164
24234
  candidate.finishReason,
24165
24235
  { MAX_TOKENS: "length", SAFETY: "content_filter" }
24166
24236
  );
24237
+ const builtinToolCalls = [];
24238
+ if (hasCodeExec) builtinToolCalls.push({ tool: "code_interpreter" });
24239
+ if (candidate.groundingMetadata) builtinToolCalls.push({ tool: "web_search" });
24167
24240
  return {
24168
24241
  id: crypto.randomUUID(),
24169
24242
  // Google doesn't return a response ID in generateContent
@@ -24176,6 +24249,7 @@ var GoogleAdapter = class {
24176
24249
  thinking,
24177
24250
  media,
24178
24251
  ...files.length ? { files } : {},
24252
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
24179
24253
  latencyMs,
24180
24254
  raw
24181
24255
  };
@@ -24213,6 +24287,8 @@ var GoogleAdapter = class {
24213
24287
  if (part.text !== void 0 && !part.thought)
24214
24288
  events.push({ type: "text", text: part.text });
24215
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" });
24216
24292
  if (part.inlineData) {
24217
24293
  const inline = part.inlineData;
24218
24294
  const mime = inline.mimeType;
@@ -24243,6 +24319,11 @@ var GoogleAdapter = class {
24243
24319
  events.push({ type: "tool_call_end", id: "" });
24244
24320
  }
24245
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
+ }
24246
24327
  const fr = candidate.finishReason;
24247
24328
  if (fr)
24248
24329
  events.push({
@@ -26009,6 +26090,15 @@ function filenameForMime(mimeType) {
26009
26090
  if (mimeType.startsWith("image/")) return `file.${mimeType.slice("image/".length)}`;
26010
26091
  return "file.bin";
26011
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
+ }
26012
26102
  function filesFromResponsesOutputItem(item) {
26013
26103
  const files = [];
26014
26104
  const type = item.type;
@@ -26212,11 +26302,14 @@ var OpenAIResponsesAdapter = class {
26212
26302
  const toolCalls = [];
26213
26303
  const media = [];
26214
26304
  const files = [];
26305
+ const builtinToolCalls = [];
26215
26306
  let thinking = null;
26216
26307
  let text = "";
26217
26308
  for (const item of output) {
26218
26309
  const type = item.type;
26219
- files.push(...filesFromResponsesOutputItem(item));
26310
+ files.push(...this.filesFromOutputItem(item));
26311
+ const builtinCall = builtinCallFromResponsesItem(item);
26312
+ if (builtinCall) builtinToolCalls.push(builtinCall);
26220
26313
  if (type === "message") {
26221
26314
  const itemContent = item.content ?? [];
26222
26315
  for (const c of itemContent) {
@@ -26277,6 +26370,7 @@ var OpenAIResponsesAdapter = class {
26277
26370
  thinking,
26278
26371
  media,
26279
26372
  ...files.length ? { files } : {},
26373
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
26280
26374
  ...moderation ? { moderation } : {},
26281
26375
  latencyMs,
26282
26376
  raw
@@ -26308,6 +26402,10 @@ var OpenAIResponsesAdapter = class {
26308
26402
  if (item?.type === "image_generation_call") {
26309
26403
  events.push({ type: "media_start", mediaType: "image", mimeType: "image/png" });
26310
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
+ }
26311
26409
  }
26312
26410
  if (type === "response.image_generation_call.partial_image") {
26313
26411
  events.push({
@@ -26318,7 +26416,11 @@ var OpenAIResponsesAdapter = class {
26318
26416
  }
26319
26417
  if (type === "response.output_item.done") {
26320
26418
  const item = data.item;
26321
- for (const file of filesFromResponsesOutputItem(item)) events.push({ type: "file", file });
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
+ }
26322
26424
  if (item?.type === "function_call") {
26323
26425
  events.push({ type: "tool_call_end", id: item.call_id ?? "" });
26324
26426
  }
@@ -26354,6 +26456,12 @@ var OpenAIResponsesAdapter = class {
26354
26456
  createStreamParser() {
26355
26457
  return (event) => this.parseStreamEvent(event);
26356
26458
  }
26459
+ /** Hosted code-execution output files from one output item. Overridable so
26460
+ * Responses-compatible providers with a different file shape (e.g. xAI, which
26461
+ * embeds files in the code-interpreter `logs` payload) can extend it. */
26462
+ filesFromOutputItem(item) {
26463
+ return filesFromResponsesOutputItem(item);
26464
+ }
26357
26465
  parseUsage(u) {
26358
26466
  if (!u) return emptyUsage();
26359
26467
  const input = u.input_tokens ?? 0;
@@ -26411,6 +26519,9 @@ function filenameFor(mimeType) {
26411
26519
  }
26412
26520
 
26413
26521
  // src/llm/providers/openrouter/completions.ts
26522
+ function hasUrlCitation(annotations) {
26523
+ return Array.isArray(annotations) && annotations.some((a) => a?.type === "url_citation");
26524
+ }
26414
26525
  var OpenRouterAdapter = class extends OpenAIAdapter {
26415
26526
  name = "openrouter";
26416
26527
  constructor(config) {
@@ -26439,6 +26550,35 @@ var OpenRouterAdapter = class extends OpenAIAdapter {
26439
26550
  }
26440
26551
  return result;
26441
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
+ }
26442
26582
  };
26443
26583
 
26444
26584
  // src/llm/providers/openrouter/responses.ts
@@ -26918,6 +27058,29 @@ var XAIMediaAdapter = class {
26918
27058
  };
26919
27059
 
26920
27060
  // src/llm/providers/xai/responses.ts
27061
+ function xaiCodeExecFiles(item) {
27062
+ if (item.type !== "code_interpreter_call") return [];
27063
+ const files = [];
27064
+ for (const out of item.outputs ?? []) {
27065
+ if (out.type !== "logs" || typeof out.logs !== "string") continue;
27066
+ let parsed;
27067
+ try {
27068
+ parsed = JSON.parse(out.logs);
27069
+ } catch {
27070
+ continue;
27071
+ }
27072
+ for (const f of parsed.output_files ?? []) {
27073
+ if (!Array.isArray(f.data)) continue;
27074
+ files.push({
27075
+ data: bytesToBase64(Uint8Array.from(f.data)),
27076
+ ...typeof f.file_name === "string" ? { name: f.file_name } : {},
27077
+ ...typeof f.mime_type === "string" ? { mimeType: f.mime_type } : {},
27078
+ source: "code_execution"
27079
+ });
27080
+ }
27081
+ }
27082
+ return files;
27083
+ }
26921
27084
  var XAIResponsesAdapter = class extends OpenAIResponsesAdapter {
26922
27085
  name = "xai";
26923
27086
  constructor(config) {
@@ -26937,8 +27100,20 @@ var XAIResponsesAdapter = class extends OpenAIResponsesAdapter {
26937
27100
  if (!req.model.includes("multi-agent")) {
26938
27101
  delete body.reasoning;
26939
27102
  }
27103
+ const usesCodeInterpreter = req.tools?.some(
27104
+ (t) => !isFunctionTool(t) && t.type === "code_interpreter"
27105
+ );
27106
+ if (usesCodeInterpreter) {
27107
+ const include = /* @__PURE__ */ new Set([...body.include ?? [], "code_interpreter_call.outputs"]);
27108
+ body.include = [...include];
27109
+ }
26940
27110
  return result;
26941
27111
  }
27112
+ /** xAI embeds code-execution files inline in the `logs` payload — extend the base
27113
+ * extraction (which handles OpenAI-style annotations / image URLs) with the xAI shape. */
27114
+ filesFromOutputItem(item) {
27115
+ return [...super.filesFromOutputItem(item), ...xaiCodeExecFiles(item)];
27116
+ }
26942
27117
  };
26943
27118
 
26944
27119
  // src/util/json-schema.ts
@@ -28272,6 +28447,7 @@ var AgentLoop = class _AgentLoop {
28272
28447
  // Propagate hosted-tool file outputs + inline-moderation result from the final
28273
28448
  // LLM response (e.g. code-execution files produced during the run).
28274
28449
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28450
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28275
28451
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28276
28452
  latencyMs: performance.now() - startPerf,
28277
28453
  raw: lastResponse?.raw ?? null
@@ -28468,6 +28644,7 @@ var AgentLoop = class _AgentLoop {
28468
28644
  thinking: lastResponse?.thinking ?? null,
28469
28645
  media: [],
28470
28646
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28647
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28471
28648
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28472
28649
  latencyMs: performance.now() - startPerf,
28473
28650
  raw: null
@@ -36863,12 +37040,16 @@ var DEFAULT_TAGS = {
36863
37040
  huge: "context:large"
36864
37041
  };
36865
37042
  var CAP_KEYS = {
36866
- search: "webSearch",
36867
37043
  vision: "vision",
36868
37044
  tools: "toolUse",
36869
37045
  audio: "audio",
36870
37046
  structured: "structuredOutput"
36871
37047
  };
37048
+ var BUILTIN_TOOL_KEYS = {
37049
+ search: "web_search",
37050
+ web_search: "web_search",
37051
+ code_interpreter: "code_interpreter"
37052
+ };
36872
37053
  var KNOWN_KEYS = /* @__PURE__ */ new Set([
36873
37054
  "price",
36874
37055
  "context",
@@ -36878,7 +37059,8 @@ var KNOWN_KEYS = /* @__PURE__ */ new Set([
36878
37059
  "status",
36879
37060
  "provider",
36880
37061
  "active",
36881
- ...Object.keys(CAP_KEYS)
37062
+ ...Object.keys(CAP_KEYS),
37063
+ ...Object.keys(BUILTIN_TOOL_KEYS)
36882
37064
  ]);
36883
37065
  function parseNum(v) {
36884
37066
  const m = /^([\d.]+)\s*([kKmM]?)$/.exec(v.trim());
@@ -36947,6 +37129,13 @@ function matches(m, c, th, tier) {
36947
37129
  case "active":
36948
37130
  return isNo(c.value) ? m.active === false : m.active !== false;
36949
37131
  default: {
37132
+ const tool = BUILTIN_TOOL_KEYS[c.key];
37133
+ if (tool) {
37134
+ const inList = m.capabilities.builtinTools?.includes(tool) ?? false;
37135
+ const legacy = tool === "web_search" && !!m.capabilities.webSearch;
37136
+ const has2 = inList || legacy;
37137
+ return isNo(c.value) ? !has2 : has2;
37138
+ }
36950
37139
  const cap = CAP_KEYS[c.key];
36951
37140
  const has = !!m.capabilities[cap];
36952
37141
  return isNo(c.value) ? !has : has;
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
@@ -22032,6 +22032,15 @@ var catalog_default5 = {
22032
22032
  }
22033
22033
  };
22034
22034
 
22035
+ // src/llm/providers/builtin-tools.ts
22036
+ var PROVIDER_BUILTIN_TOOLS = {
22037
+ anthropic: ["web_search", "code_interpreter"],
22038
+ openai: ["web_search", "code_interpreter"],
22039
+ google: ["web_search", "code_interpreter"],
22040
+ xai: ["web_search", "code_interpreter"],
22041
+ openrouter: ["web_search"]
22042
+ };
22043
+
22035
22044
  // src/plugins/model-catalog/catalog.ts
22036
22045
  var PROVIDER_DEFAULT_CATALOGS = [
22037
22046
  catalog_default,
@@ -22069,6 +22078,13 @@ var DEFAULT_REASONING = {
22069
22078
  encryptedContent: false,
22070
22079
  summaryAvailable: false
22071
22080
  };
22081
+ function withBuiltinTools(provider, caps) {
22082
+ if (caps.builtinTools !== void 0) return caps;
22083
+ if (!caps.toolUse) return caps;
22084
+ const tools = PROVIDER_BUILTIN_TOOLS[provider];
22085
+ if (!tools || tools.length === 0) return caps;
22086
+ return { ...caps, builtinTools: [...tools] };
22087
+ }
22072
22088
  var ModelCatalog = class {
22073
22089
  models = /* @__PURE__ */ new Map();
22074
22090
  /** `provider/alias` → `provider/canonical-slug`. Lets get()/resolveModelId
@@ -22087,7 +22103,7 @@ var ModelCatalog = class {
22087
22103
  supportedApis: info.supportedApis ?? [info.preferredApi ?? "completions"],
22088
22104
  contextWindow: info.contextWindow,
22089
22105
  maxOutput: info.maxOutput,
22090
- capabilities: { ...DEFAULT_CAPABILITIES, ...info.capabilities },
22106
+ capabilities: withBuiltinTools(provider, { ...DEFAULT_CAPABILITIES, ...info.capabilities }),
22091
22107
  reasoning: { ...DEFAULT_REASONING, ...info.reasoning },
22092
22108
  mediaOnly: info.mediaOnly,
22093
22109
  tokenizer: info.tokenizer,
@@ -22141,6 +22157,15 @@ var ModelCatalog = class {
22141
22157
  supportsTools(provider, model) {
22142
22158
  return this.get(provider, model)?.capabilities.toolUse ?? false;
22143
22159
  }
22160
+ /** Hosted server-side builtin tools this model supports (e.g. `['web_search',
22161
+ * 'code_interpreter']`). Empty when unknown or the model isn't tool-capable. */
22162
+ builtinToolsFor(provider, model) {
22163
+ return [...this.get(provider, model)?.capabilities.builtinTools ?? []];
22164
+ }
22165
+ /** Whether this model supports a specific hosted builtin tool. */
22166
+ supportsBuiltinTool(provider, model, tool) {
22167
+ return this.get(provider, model)?.capabilities.builtinTools?.includes(tool) ?? false;
22168
+ }
22144
22169
  supportsPreviousResponseId(provider, model) {
22145
22170
  const info = this.get(provider, model);
22146
22171
  if (info && !info.supportedApis.some((a) => a === "responses" || a === "interactions")) {
@@ -22984,6 +23009,7 @@ var LLMClient = class {
22984
23009
  let finishReason = "stop";
22985
23010
  let moderationReport;
22986
23011
  const files = [];
23012
+ const builtinToolCalls = [];
22987
23013
  const fetchStream = this.fetchStreamFn;
22988
23014
  const queueName = this.queueName;
22989
23015
  const priority = this.priority;
@@ -23035,6 +23061,9 @@ var LLMClient = class {
23035
23061
  case "file":
23036
23062
  files.push(event.file);
23037
23063
  break;
23064
+ case "builtin_tool_start":
23065
+ builtinToolCalls.push({ tool: event.tool, ...event.id ? { id: event.id } : {} });
23066
+ break;
23038
23067
  case "moderation":
23039
23068
  moderationReport = this.mergeModeration(
23040
23069
  moderationReport,
@@ -23058,6 +23087,7 @@ var LLMClient = class {
23058
23087
  thinking: thinking || null,
23059
23088
  media: [],
23060
23089
  ...files.length ? { files } : {},
23090
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23061
23091
  ...moderationReport ? { moderation: moderationReport } : {},
23062
23092
  latencyMs: performance.now() - start,
23063
23093
  raw: null
@@ -23291,6 +23321,24 @@ var AnthropicFileAdapter = class {
23291
23321
  }
23292
23322
  };
23293
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
+
23294
23342
  // src/llm/providers/_shared/constants.ts
23295
23343
  var AUDIO_PCM16_SAMPLE_RATE_HZ = 24e3;
23296
23344
  var DEFAULT_MAX_TOKENS = 4096;
@@ -23500,6 +23548,7 @@ var AnthropicAdapter = class {
23500
23548
  let thinking = null;
23501
23549
  const toolCalls = [];
23502
23550
  const files = [];
23551
+ const builtinToolCalls = [];
23503
23552
  for (const block of contentBlocks) {
23504
23553
  if (block.type === "text") {
23505
23554
  content.push({ type: "text", text: block.text });
@@ -23514,6 +23563,11 @@ var AnthropicAdapter = class {
23514
23563
  };
23515
23564
  content.push(tc);
23516
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
+ });
23517
23571
  } else {
23518
23572
  files.push(...filesFromCodeExecBlock(block));
23519
23573
  }
@@ -23531,6 +23585,7 @@ var AnthropicAdapter = class {
23531
23585
  toolCalls,
23532
23586
  media: [],
23533
23587
  ...files.length ? { files } : {},
23588
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
23534
23589
  thinking,
23535
23590
  latencyMs,
23536
23591
  raw
@@ -23554,8 +23609,23 @@ var AnthropicAdapter = class {
23554
23609
  if (block.type === "tool_use") {
23555
23610
  return [{ type: "tool_call_start", id: block.id, name: block.name }];
23556
23611
  }
23557
- const files = filesFromCodeExecBlock(block);
23558
- 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;
23559
23629
  }
23560
23630
  if (type === "content_block_stop") {
23561
23631
  }
@@ -24091,6 +24161,9 @@ var GoogleAdapter = class {
24091
24161
  candidate.finishReason,
24092
24162
  { MAX_TOKENS: "length", SAFETY: "content_filter" }
24093
24163
  );
24164
+ const builtinToolCalls = [];
24165
+ if (hasCodeExec) builtinToolCalls.push({ tool: "code_interpreter" });
24166
+ if (candidate.groundingMetadata) builtinToolCalls.push({ tool: "web_search" });
24094
24167
  return {
24095
24168
  id: crypto.randomUUID(),
24096
24169
  // Google doesn't return a response ID in generateContent
@@ -24103,6 +24176,7 @@ var GoogleAdapter = class {
24103
24176
  thinking,
24104
24177
  media,
24105
24178
  ...files.length ? { files } : {},
24179
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
24106
24180
  latencyMs,
24107
24181
  raw
24108
24182
  };
@@ -24140,6 +24214,8 @@ var GoogleAdapter = class {
24140
24214
  if (part.text !== void 0 && !part.thought)
24141
24215
  events.push({ type: "text", text: part.text });
24142
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" });
24143
24219
  if (part.inlineData) {
24144
24220
  const inline = part.inlineData;
24145
24221
  const mime = inline.mimeType;
@@ -24170,6 +24246,11 @@ var GoogleAdapter = class {
24170
24246
  events.push({ type: "tool_call_end", id: "" });
24171
24247
  }
24172
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
+ }
24173
24254
  const fr = candidate.finishReason;
24174
24255
  if (fr)
24175
24256
  events.push({
@@ -25936,6 +26017,15 @@ function filenameForMime(mimeType) {
25936
26017
  if (mimeType.startsWith("image/")) return `file.${mimeType.slice("image/".length)}`;
25937
26018
  return "file.bin";
25938
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
+ }
25939
26029
  function filesFromResponsesOutputItem(item) {
25940
26030
  const files = [];
25941
26031
  const type = item.type;
@@ -26139,11 +26229,14 @@ var OpenAIResponsesAdapter = class {
26139
26229
  const toolCalls = [];
26140
26230
  const media = [];
26141
26231
  const files = [];
26232
+ const builtinToolCalls = [];
26142
26233
  let thinking = null;
26143
26234
  let text = "";
26144
26235
  for (const item of output) {
26145
26236
  const type = item.type;
26146
- files.push(...filesFromResponsesOutputItem(item));
26237
+ files.push(...this.filesFromOutputItem(item));
26238
+ const builtinCall = builtinCallFromResponsesItem(item);
26239
+ if (builtinCall) builtinToolCalls.push(builtinCall);
26147
26240
  if (type === "message") {
26148
26241
  const itemContent = item.content ?? [];
26149
26242
  for (const c of itemContent) {
@@ -26204,6 +26297,7 @@ var OpenAIResponsesAdapter = class {
26204
26297
  thinking,
26205
26298
  media,
26206
26299
  ...files.length ? { files } : {},
26300
+ ...builtinToolCalls.length ? { builtinToolCalls } : {},
26207
26301
  ...moderation ? { moderation } : {},
26208
26302
  latencyMs,
26209
26303
  raw
@@ -26235,6 +26329,10 @@ var OpenAIResponsesAdapter = class {
26235
26329
  if (item?.type === "image_generation_call") {
26236
26330
  events.push({ type: "media_start", mediaType: "image", mimeType: "image/png" });
26237
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
+ }
26238
26336
  }
26239
26337
  if (type === "response.image_generation_call.partial_image") {
26240
26338
  events.push({
@@ -26245,7 +26343,11 @@ var OpenAIResponsesAdapter = class {
26245
26343
  }
26246
26344
  if (type === "response.output_item.done") {
26247
26345
  const item = data.item;
26248
- for (const file of filesFromResponsesOutputItem(item)) events.push({ type: "file", file });
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
+ }
26249
26351
  if (item?.type === "function_call") {
26250
26352
  events.push({ type: "tool_call_end", id: item.call_id ?? "" });
26251
26353
  }
@@ -26281,6 +26383,12 @@ var OpenAIResponsesAdapter = class {
26281
26383
  createStreamParser() {
26282
26384
  return (event) => this.parseStreamEvent(event);
26283
26385
  }
26386
+ /** Hosted code-execution output files from one output item. Overridable so
26387
+ * Responses-compatible providers with a different file shape (e.g. xAI, which
26388
+ * embeds files in the code-interpreter `logs` payload) can extend it. */
26389
+ filesFromOutputItem(item) {
26390
+ return filesFromResponsesOutputItem(item);
26391
+ }
26284
26392
  parseUsage(u) {
26285
26393
  if (!u) return emptyUsage();
26286
26394
  const input = u.input_tokens ?? 0;
@@ -26338,6 +26446,9 @@ function filenameFor(mimeType) {
26338
26446
  }
26339
26447
 
26340
26448
  // src/llm/providers/openrouter/completions.ts
26449
+ function hasUrlCitation(annotations) {
26450
+ return Array.isArray(annotations) && annotations.some((a) => a?.type === "url_citation");
26451
+ }
26341
26452
  var OpenRouterAdapter = class extends OpenAIAdapter {
26342
26453
  name = "openrouter";
26343
26454
  constructor(config) {
@@ -26366,6 +26477,35 @@ var OpenRouterAdapter = class extends OpenAIAdapter {
26366
26477
  }
26367
26478
  return result;
26368
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
+ }
26369
26509
  };
26370
26510
 
26371
26511
  // src/llm/providers/openrouter/responses.ts
@@ -26845,6 +26985,29 @@ var XAIMediaAdapter = class {
26845
26985
  };
26846
26986
 
26847
26987
  // src/llm/providers/xai/responses.ts
26988
+ function xaiCodeExecFiles(item) {
26989
+ if (item.type !== "code_interpreter_call") return [];
26990
+ const files = [];
26991
+ for (const out of item.outputs ?? []) {
26992
+ if (out.type !== "logs" || typeof out.logs !== "string") continue;
26993
+ let parsed;
26994
+ try {
26995
+ parsed = JSON.parse(out.logs);
26996
+ } catch {
26997
+ continue;
26998
+ }
26999
+ for (const f of parsed.output_files ?? []) {
27000
+ if (!Array.isArray(f.data)) continue;
27001
+ files.push({
27002
+ data: bytesToBase64(Uint8Array.from(f.data)),
27003
+ ...typeof f.file_name === "string" ? { name: f.file_name } : {},
27004
+ ...typeof f.mime_type === "string" ? { mimeType: f.mime_type } : {},
27005
+ source: "code_execution"
27006
+ });
27007
+ }
27008
+ }
27009
+ return files;
27010
+ }
26848
27011
  var XAIResponsesAdapter = class extends OpenAIResponsesAdapter {
26849
27012
  name = "xai";
26850
27013
  constructor(config) {
@@ -26864,8 +27027,20 @@ var XAIResponsesAdapter = class extends OpenAIResponsesAdapter {
26864
27027
  if (!req.model.includes("multi-agent")) {
26865
27028
  delete body.reasoning;
26866
27029
  }
27030
+ const usesCodeInterpreter = req.tools?.some(
27031
+ (t) => !isFunctionTool(t) && t.type === "code_interpreter"
27032
+ );
27033
+ if (usesCodeInterpreter) {
27034
+ const include = /* @__PURE__ */ new Set([...body.include ?? [], "code_interpreter_call.outputs"]);
27035
+ body.include = [...include];
27036
+ }
26867
27037
  return result;
26868
27038
  }
27039
+ /** xAI embeds code-execution files inline in the `logs` payload — extend the base
27040
+ * extraction (which handles OpenAI-style annotations / image URLs) with the xAI shape. */
27041
+ filesFromOutputItem(item) {
27042
+ return [...super.filesFromOutputItem(item), ...xaiCodeExecFiles(item)];
27043
+ }
26869
27044
  };
26870
27045
 
26871
27046
  // src/util/json-schema.ts
@@ -28199,6 +28374,7 @@ var AgentLoop = class _AgentLoop {
28199
28374
  // Propagate hosted-tool file outputs + inline-moderation result from the final
28200
28375
  // LLM response (e.g. code-execution files produced during the run).
28201
28376
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28377
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28202
28378
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28203
28379
  latencyMs: performance.now() - startPerf,
28204
28380
  raw: lastResponse?.raw ?? null
@@ -28395,6 +28571,7 @@ var AgentLoop = class _AgentLoop {
28395
28571
  thinking: lastResponse?.thinking ?? null,
28396
28572
  media: [],
28397
28573
  ...lastResponse?.files ? { files: lastResponse.files } : {},
28574
+ ...lastResponse?.builtinToolCalls ? { builtinToolCalls: lastResponse.builtinToolCalls } : {},
28398
28575
  ...lastResponse?.moderation ? { moderation: lastResponse.moderation } : {},
28399
28576
  latencyMs: performance.now() - startPerf,
28400
28577
  raw: null
@@ -36790,12 +36967,16 @@ var DEFAULT_TAGS = {
36790
36967
  huge: "context:large"
36791
36968
  };
36792
36969
  var CAP_KEYS = {
36793
- search: "webSearch",
36794
36970
  vision: "vision",
36795
36971
  tools: "toolUse",
36796
36972
  audio: "audio",
36797
36973
  structured: "structuredOutput"
36798
36974
  };
36975
+ var BUILTIN_TOOL_KEYS = {
36976
+ search: "web_search",
36977
+ web_search: "web_search",
36978
+ code_interpreter: "code_interpreter"
36979
+ };
36799
36980
  var KNOWN_KEYS = /* @__PURE__ */ new Set([
36800
36981
  "price",
36801
36982
  "context",
@@ -36805,7 +36986,8 @@ var KNOWN_KEYS = /* @__PURE__ */ new Set([
36805
36986
  "status",
36806
36987
  "provider",
36807
36988
  "active",
36808
- ...Object.keys(CAP_KEYS)
36989
+ ...Object.keys(CAP_KEYS),
36990
+ ...Object.keys(BUILTIN_TOOL_KEYS)
36809
36991
  ]);
36810
36992
  function parseNum(v) {
36811
36993
  const m = /^([\d.]+)\s*([kKmM]?)$/.exec(v.trim());
@@ -36874,6 +37056,13 @@ function matches(m, c, th, tier) {
36874
37056
  case "active":
36875
37057
  return isNo(c.value) ? m.active === false : m.active !== false;
36876
37058
  default: {
37059
+ const tool = BUILTIN_TOOL_KEYS[c.key];
37060
+ if (tool) {
37061
+ const inList = m.capabilities.builtinTools?.includes(tool) ?? false;
37062
+ const legacy = tool === "web_search" && !!m.capabilities.webSearch;
37063
+ const has2 = inList || legacy;
37064
+ return isNo(c.value) ? !has2 : has2;
37065
+ }
36877
37066
  const cap = CAP_KEYS[c.key];
36878
37067
  const has = !!m.capabilities[cap];
36879
37068
  return isNo(c.value) ? !has : has;
@@ -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;
@@ -0,0 +1,15 @@
1
+ /** Hosted server-side tools each provider's chat models support.
2
+ *
3
+ * This is ADAPTER-SOURCED: it reflects what the provider adapters actually map
4
+ * the unified `{ type: 'web_search' | 'code_interpreter' }` builtins onto (each
5
+ * to the provider's native shape), verified by the live tools-parity test. It is
6
+ * applied PROVIDER-LEVEL to tool-capable (chat-family) models at catalog load —
7
+ * a deliberate first pass; a reliable per-model source can refine it later.
8
+ *
9
+ * Coverage (verified live 2026-07):
10
+ * - web_search → anthropic, openai, google, xai, openrouter
11
+ * - code_interpreter → anthropic, openai, google, xai (NOT openrouter: it
12
+ * proxies function tools + its own plugins, but does not route hosted code
13
+ * execution — confirmed against the API). */
14
+ import type { ProviderName } from '../types/provider';
15
+ export declare const PROVIDER_BUILTIN_TOOLS: Record<ProviderName, readonly string[]>;
@@ -5,7 +5,7 @@
5
5
  import type { SSEEvent } from '../../../network/types';
6
6
  import type { ProviderAdapter, ProviderHttpRequest } from '../../types/provider';
7
7
  import type { NormalizedRequest } from '../../types/request';
8
- import { type CompletionResponse, type Usage } from '../../types/response';
8
+ import { type CompletionResponse, type FileOutput, type Usage } from '../../types/response';
9
9
  import type { StreamEvent } from '../../types/stream';
10
10
  export interface OpenAIResponsesAdapterConfig {
11
11
  apiKey: string;
@@ -28,5 +28,9 @@ export declare class OpenAIResponsesAdapter implements ProviderAdapter {
28
28
  /** Stateless — each output item finalizes with all its file annotations in a
29
29
  * single response.output_item.done event. */
30
30
  createStreamParser(): (event: SSEEvent) => StreamEvent[];
31
+ /** Hosted code-execution output files from one output item. Overridable so
32
+ * Responses-compatible providers with a different file shape (e.g. xAI, which
33
+ * embeds files in the code-interpreter `logs` payload) can extend it. */
34
+ protected filesFromOutputItem(item: Record<string, unknown>): FileOutput[];
31
35
  protected parseUsage(u: Record<string, unknown> | undefined): Usage;
32
36
  }
@@ -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
  }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { ProviderAdapter, ProviderHttpRequest } from '../../types/provider';
9
9
  import type { NormalizedRequest } from '../../types/request';
10
+ import type { FileOutput } from '../../types/response';
10
11
  import { OpenAIResponsesAdapter } from '../openai/responses';
11
12
  export interface XAIResponsesAdapterConfig {
12
13
  apiKey: string;
@@ -17,4 +18,7 @@ export declare class XAIResponsesAdapter extends OpenAIResponsesAdapter {
17
18
  constructor(config: XAIResponsesAdapterConfig);
18
19
  baseURL(): string;
19
20
  buildRequest(req: NormalizedRequest): ProviderHttpRequest;
21
+ /** xAI embeds code-execution files inline in the `logs` payload — extend the base
22
+ * extraction (which handles OpenAI-style annotations / image URLs) with the xAI shape. */
23
+ protected filesFromOutputItem(item: Record<string, unknown>): FileOutput[];
20
24
  }
@@ -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). */
@@ -140,6 +140,11 @@ export declare class ModelCatalog {
140
140
  getPreferredApi(provider: string, model: string): ApiType | null;
141
141
  supportsApi(provider: string, model: string, api: ApiType): boolean;
142
142
  supportsTools(provider: string, model: string): boolean;
143
+ /** Hosted server-side builtin tools this model supports (e.g. `['web_search',
144
+ * 'code_interpreter']`). Empty when unknown or the model isn't tool-capable. */
145
+ builtinToolsFor(provider: string, model: string): string[];
146
+ /** Whether this model supports a specific hosted builtin tool. */
147
+ supportsBuiltinTool(provider: string, model: string, tool: string): boolean;
143
148
  supportsPreviousResponseId(provider: string, model: string): boolean;
144
149
  /** Server-state retention as a duration string ("30d", "72h"), or null if unsupported. */
145
150
  getStateRetention(provider: string, model: string): string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@combycode/llm-sdk",
3
- "version": "1.3.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",