@combycode/llm-sdk 1.2.0 → 1.3.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,26 @@ All notable changes to `@combycode/llm-sdk` are documented here. The format foll
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.0] - 2026-07-06
10
+
11
+ ### Added
12
+ - **Streaming file parity.** `stream()` now surfaces hosted code-execution output files with the
13
+ same coverage as `complete()`: a new `{ type: 'file', file }` `StreamEvent` is emitted as each
14
+ file finalizes mid-stream, and the files are collected onto the streamed final response's
15
+ `files` (via `onCompletion` / the agent final response). New `ProviderAdapter.createStreamParser()`
16
+ returns a per-stream parser so adapters can hold per-stream state — used by Google to route an
17
+ inline code-execution artifact (whose "code ran" marker and bytes arrive in separate SSE events)
18
+ to `files` rather than conversational `media`. Verified live on Anthropic, OpenAI, and Google.
19
+ - **File-content retrieval** for hosted-tool output files. `CompleteResult`, `LLMClient`, and
20
+ the agent result now expose `retrieveFile(file)` → `{ blob, name, mimeType, size }` and
21
+ `streamFile(file)` → `{ stream, name, mimeType, size }`, resolving a `FileOutput`'s inline
22
+ `data`, `url`, or provider file `id` through the same model + key the call used — no
23
+ re-passing credentials. `streamFile` pipes large files to a sink without buffering. Name /
24
+ mime / size come from the download response headers (Content-Disposition / Content-Type /
25
+ Content-Length, with a filename-extension mime fallback). New network `responseType: 'stream'`
26
+ returns the raw body and releases the queue slot immediately. Exports `RetrievedFile`,
27
+ `FileStream`. Auth is sent only to the provider's own host.
28
+
9
29
  ## [1.2.0] - 2026-07-05
10
30
 
11
31
  ### Fixed
package/README.md CHANGED
@@ -64,6 +64,28 @@ for await (const ev of llm.stream('Count to 5.')) {
64
64
  }
65
65
  ```
66
66
 
67
+ ### Hosted code execution + output files
68
+
69
+ Run the provider's code interpreter and pull the files it produces (charts, CSVs) — one
70
+ interface, any provider. `retrieveFile` / `streamFile` fetch the bytes plus `name` / `mimeType`
71
+ / `size`, bound to the same model + key:
72
+
73
+ ```ts
74
+ import { complete } from '@combycode/llm-sdk';
75
+
76
+ const { response, retrieveFile } = await complete({
77
+ model: 'anthropic/claude-haiku-4.5',
78
+ apiKey: process.env.ANTHROPIC_API_KEY,
79
+ prompt: 'Plot y = x**2 for x in 1..5, save a PNG, and return the file.',
80
+ tools: [{ type: 'code_interpreter' }],
81
+ maxTokens: 6000,
82
+ });
83
+
84
+ for (const file of response.files ?? []) {
85
+ const { blob, name, mimeType, size } = await retrieveFile(file); // or streamFile for large files
86
+ }
87
+ ```
88
+
67
89
  ## Documentation
68
90
 
69
91
  Full guide pages covering all export groups:
@@ -77,6 +99,8 @@ Full guide pages covering all export groups:
77
99
  - [Cost Tracking + estimate()](./docs/guide/cost.md)
78
100
  - [Observability / Telemetry](./docs/guide/telemetry.md)
79
101
  - [Media / Files / Batch](./docs/guide/media-files-batch.md)
102
+ - [Hosted Code Execution](./docs/guide/code-execution.md)
103
+ - [Retrieving Output Files](./docs/guide/retrieving-files.md)
80
104
  - [MCP (Model Context Protocol)](./docs/guide/mcp.md)
81
105
  - [Context Guard + Permissions + Persistence + Cache](./docs/guide/context-guard.md)
82
106
  - [OpenAI-Compatible Server](./docs/guide/server.md)
@@ -13,7 +13,8 @@
13
13
  import { HookBus } from '../bus/hook-bus';
14
14
  import { type ContentPart, type Message } from '../llm/types/messages';
15
15
  import type { ExecuteOptions } from '../llm/types/options';
16
- import { type CompletionResponse } from '../llm/types/response';
16
+ import { type CompletionResponse, type FileOutput } from '../llm/types/response';
17
+ import type { FileStream, RetrievedFile } from '../llm/files/retrieve';
17
18
  import type { LLMClient } from '../llm/client';
18
19
  import { ConversationHistory } from './history';
19
20
  import type { AgentLoopSnapshot, AgentRunReport, AgentStreamEvent, AgentTool } from './types';
@@ -52,6 +53,14 @@ export declare class AgentLoop {
52
53
  destroy(): void;
53
54
  /** Model is owned by client. */
54
55
  get model(): string;
56
+ /** Fetch a hosted-tool output file from the run's `response.files` — its bytes
57
+ * as a `Blob` plus `name` / `mimeType` / `size`. Delegates to the underlying
58
+ * client, so it uses this agent's provider + key (same as `retrieveFile` on a
59
+ * `complete()` result). */
60
+ retrieveFile(file: FileOutput): Promise<RetrievedFile>;
61
+ /** Stream a hosted-tool output file (large files) — a `ReadableStream` plus
62
+ * best-effort `name` / `mimeType` / `size`. Delegates to the underlying client. */
63
+ streamFile(file: FileOutput): Promise<FileStream>;
55
64
  get system(): string;
56
65
  set system(v: string);
57
66
  get context(): string;
@@ -18,7 +18,8 @@ import type { AgentTool } from '../agent/types';
18
18
  import type { LLMClientConfig } from '../llm/client-config';
19
19
  import type { AudioOptions } from '../llm/types/audio';
20
20
  import type { ContentPart, Message } from '../llm/types/messages';
21
- import type { CompletionResponse } from '../llm/types/response';
21
+ import type { CompletionResponse, FileOutput } from '../llm/types/response';
22
+ import type { FileStream, RetrievedFile } from '../llm/files/retrieve';
22
23
  import type { ProviderName } from '../llm/types/provider';
23
24
  import type { ServiceTier } from '../llm/types/tiers';
24
25
  import type { BuiltinTool } from '../llm/types/tools';
@@ -82,5 +83,11 @@ export interface CompleteResult<T = unknown> {
82
83
  * otherwise `undefined`. The generic on `complete<T>(...)` types this. */
83
84
  parsed?: T;
84
85
  response: CompletionResponse;
86
+ /** Fetch a hosted-tool output file (from `response.files`): bytes (`Blob`) +
87
+ * `name` / `mimeType` / `size` — bound to the SAME model + key this call used. */
88
+ retrieveFile(file: FileOutput): Promise<RetrievedFile>;
89
+ /** Stream a hosted-tool output file (`ReadableStream` + `name` / `mimeType` /
90
+ * `size`) — for large files piped straight to a sink. Same model + key. */
91
+ streamFile(file: FileOutput): Promise<FileStream>;
85
92
  }
86
93
  export declare function complete<T = unknown>(opts: CompleteOptions): Promise<CompleteResult<T>>;
@@ -2524,7 +2524,7 @@ var QueueState = class {
2524
2524
  });
2525
2525
  this.processed++;
2526
2526
  if (response.ok) {
2527
- const body = await parseResponseBody(response, entry.request.responseType ?? "json");
2527
+ const body = entry.request.responseType === "stream" ? response.body : await parseResponseBody(response, entry.request.responseType ?? "json");
2528
2528
  this.semaphore.release();
2529
2529
  entry.resolve({ status: response.status, headers: resHeaders, body });
2530
2530
  return;
@@ -22427,6 +22427,160 @@ async function* wrapParallel(raw, interval, moderate2) {
22427
22427
  }
22428
22428
  }
22429
22429
 
22430
+ // src/llm/files/retrieve.ts
22431
+ var DEFAULT_BASE = {
22432
+ anthropic: "https://api.anthropic.com",
22433
+ openai: "https://api.openai.com",
22434
+ xai: "https://api.x.ai",
22435
+ google: "https://generativelanguage.googleapis.com",
22436
+ openrouter: "https://openrouter.ai/api"
22437
+ };
22438
+ var OCTET_STREAM = "application/octet-stream";
22439
+ function contentRequest(ctx, file) {
22440
+ const base = ctx.baseURL ?? DEFAULT_BASE[ctx.provider];
22441
+ const id = file.id;
22442
+ if (ctx.provider === "anthropic") {
22443
+ return {
22444
+ url: `${base}/v1/files/${id}/content?beta=true`,
22445
+ headers: {
22446
+ "x-api-key": ctx.apiKey,
22447
+ "anthropic-version": "2023-06-01",
22448
+ "anthropic-beta": "files-api-2025-04-14",
22449
+ accept: "application/binary"
22450
+ }
22451
+ };
22452
+ }
22453
+ if (ctx.provider === "openai" || ctx.provider === "xai" || ctx.provider === "openrouter") {
22454
+ const containerId = file.ref?.containerId;
22455
+ const path = containerId ? `/v1/containers/${containerId}/files/${id}/content` : `/v1/files/${id}/content`;
22456
+ return { url: `${base}${path}`, headers: { authorization: `Bearer ${ctx.apiKey}` } };
22457
+ }
22458
+ if (ctx.provider === "google") {
22459
+ return {
22460
+ url: `${base}/v1beta/files/${id}:download?alt=media`,
22461
+ headers: { "x-goog-api-key": ctx.apiKey }
22462
+ };
22463
+ }
22464
+ throw new Error(`retrieveFile: no file-content endpoint for provider "${ctx.provider}"`);
22465
+ }
22466
+ function providerAuth(ctx, url) {
22467
+ const base = ctx.baseURL ?? DEFAULT_BASE[ctx.provider];
22468
+ if (!url.startsWith(base)) return {};
22469
+ if (ctx.provider === "anthropic") {
22470
+ return { "x-api-key": ctx.apiKey, "anthropic-version": "2023-06-01" };
22471
+ }
22472
+ if (ctx.provider === "google") return { "x-goog-api-key": ctx.apiKey };
22473
+ return { authorization: `Bearer ${ctx.apiKey}` };
22474
+ }
22475
+ async function fetchFileResponse(ctx, file, responseType) {
22476
+ if (file.url) {
22477
+ return ctx.fetch({
22478
+ url: file.url,
22479
+ method: "GET",
22480
+ headers: providerAuth(ctx, file.url),
22481
+ body: void 0,
22482
+ provider: ctx.provider,
22483
+ model: "files",
22484
+ responseType
22485
+ });
22486
+ }
22487
+ if (file.id) {
22488
+ const { url, headers } = contentRequest(ctx, file);
22489
+ return ctx.fetch({
22490
+ url,
22491
+ method: "GET",
22492
+ headers,
22493
+ body: void 0,
22494
+ provider: ctx.provider,
22495
+ model: "files",
22496
+ responseType
22497
+ });
22498
+ }
22499
+ throw new Error("retrieveFile: FileOutput has neither `data`, `url`, nor `id`");
22500
+ }
22501
+ function bytesToBlob(bytes, type) {
22502
+ const ab = new ArrayBuffer(bytes.byteLength);
22503
+ new Uint8Array(ab).set(bytes);
22504
+ return new Blob([ab], { type });
22505
+ }
22506
+ function singleChunkStream(bytes) {
22507
+ return new ReadableStream({
22508
+ start(controller) {
22509
+ controller.enqueue(bytes);
22510
+ controller.close();
22511
+ }
22512
+ });
22513
+ }
22514
+ var EXT_MIME = {
22515
+ png: "image/png",
22516
+ jpg: "image/jpeg",
22517
+ jpeg: "image/jpeg",
22518
+ gif: "image/gif",
22519
+ webp: "image/webp",
22520
+ svg: "image/svg+xml",
22521
+ bmp: "image/bmp",
22522
+ csv: "text/csv",
22523
+ tsv: "text/tab-separated-values",
22524
+ txt: "text/plain",
22525
+ json: "application/json",
22526
+ xml: "application/xml",
22527
+ html: "text/html",
22528
+ md: "text/markdown",
22529
+ pdf: "application/pdf",
22530
+ zip: "application/zip",
22531
+ parquet: "application/vnd.apache.parquet",
22532
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
22533
+ };
22534
+ function mimeFromName(name) {
22535
+ const ext = name?.includes(".") ? name.split(".").pop()?.toLowerCase() : void 0;
22536
+ return ext ? EXT_MIME[ext] : void 0;
22537
+ }
22538
+ function header(headers, name) {
22539
+ const lower = name.toLowerCase();
22540
+ for (const k in headers) if (k.toLowerCase() === lower) return headers[k];
22541
+ return void 0;
22542
+ }
22543
+ function filenameFromDisposition(cd) {
22544
+ if (!cd) return void 0;
22545
+ const ext = cd.match(/filename\*=[^']*''([^;]+)/i);
22546
+ if (ext) {
22547
+ try {
22548
+ return decodeURIComponent(ext[1].trim());
22549
+ } catch {
22550
+ }
22551
+ }
22552
+ const plain = cd.match(/filename="?([^";]+)"?/i);
22553
+ return plain ? plain[1].trim() : void 0;
22554
+ }
22555
+ async function retrieveFile(file, ctx) {
22556
+ if (file.data) {
22557
+ const blob2 = bytesToBlob(base64ToBytes(file.data), file.mimeType ?? OCTET_STREAM);
22558
+ return { blob: blob2, name: file.name, mimeType: blob2.type, size: blob2.size };
22559
+ }
22560
+ const res = await fetchFileResponse(ctx, file, "arraybuffer");
22561
+ const headers = res.headers ?? {};
22562
+ const name = filenameFromDisposition(header(headers, "content-disposition")) ?? file.name;
22563
+ const mimeType = header(headers, "content-type")?.split(";")[0].trim() || file.mimeType || mimeFromName(name) || OCTET_STREAM;
22564
+ const blob = new Blob([res.body], { type: mimeType });
22565
+ return { blob, name, mimeType, size: blob.size };
22566
+ }
22567
+ async function streamFile(file, ctx) {
22568
+ if (file.data) {
22569
+ const bytes = base64ToBytes(file.data);
22570
+ return { stream: singleChunkStream(bytes), name: file.name, mimeType: file.mimeType, size: bytes.byteLength };
22571
+ }
22572
+ const res = await fetchFileResponse(ctx, file, "stream");
22573
+ const headers = res.headers ?? {};
22574
+ const len = header(headers, "content-length");
22575
+ const name = filenameFromDisposition(header(headers, "content-disposition")) ?? file.name;
22576
+ return {
22577
+ stream: res.body,
22578
+ name,
22579
+ mimeType: header(headers, "content-type")?.split(";")[0].trim() || file.mimeType || mimeFromName(name),
22580
+ size: len ? Number(len) : void 0
22581
+ };
22582
+ }
22583
+
22430
22584
  // src/util/duration.ts
22431
22585
  var UNIT_MS = {
22432
22586
  s: 1e3,
@@ -22669,6 +22823,28 @@ var LLMClient = class {
22669
22823
  else next.output = result;
22670
22824
  return next;
22671
22825
  }
22826
+ // ─── File retrieval (hosted-tool output files) ─────────────────────────
22827
+ retrieveContext() {
22828
+ return {
22829
+ provider: this.provider,
22830
+ apiKey: this.apiKey,
22831
+ fetch: this.fetchFn,
22832
+ baseURL: this.adapter.baseURL()
22833
+ };
22834
+ }
22835
+ /** Fetch a hosted-tool output file (e.g. a code-execution file from
22836
+ * `response.files`): its bytes as a `Blob` plus `name` / `mimeType` / `size`.
22837
+ * Resolves inline `data`, a `url`, or a provider file `id` — all through this
22838
+ * client's provider + auth + engine. */
22839
+ retrieveFile(file) {
22840
+ return retrieveFile(file, this.retrieveContext());
22841
+ }
22842
+ /** Stream a hosted-tool output file: a `ReadableStream<Uint8Array>` plus
22843
+ * best-effort `name` / `mimeType` / `size` (from the response headers). For large
22844
+ * files piped straight to a file / GridFS / HTTP response without buffering. */
22845
+ streamFile(file) {
22846
+ return streamFile(file, this.retrieveContext());
22847
+ }
22672
22848
  /** Submit a request. Returns the parsed CompletionResponse. */
22673
22849
  async complete(input, options = {}) {
22674
22850
  const rawMessages = normalizeInput(input);
@@ -22880,17 +23056,18 @@ var LLMClient = class {
22880
23056
  let usage = emptyUsage();
22881
23057
  let finishReason = "stop";
22882
23058
  let moderationReport;
23059
+ const files = [];
22883
23060
  const fetchStream = this.fetchStreamFn;
22884
- const adapter = this.adapter;
22885
23061
  const queueName = this.queueName;
22886
23062
  const priority = this.priority;
23063
+ const parseStream = this.adapter.createStreamParser();
22887
23064
  async function* rawEvents() {
22888
23065
  for await (const sseEvent of fetchStream(httpReq, {
22889
23066
  queueName,
22890
23067
  priority,
22891
23068
  ctx
22892
23069
  })) {
22893
- for (const ev of adapter.parseStreamEvent(sseEvent)) yield ev;
23070
+ for (const ev of parseStream(sseEvent)) yield ev;
22894
23071
  }
22895
23072
  }
22896
23073
  const mod = options.moderation;
@@ -22928,6 +23105,9 @@ var LLMClient = class {
22928
23105
  case "done":
22929
23106
  finishReason = event.finishReason;
22930
23107
  break;
23108
+ case "file":
23109
+ files.push(event.file);
23110
+ break;
22931
23111
  case "moderation":
22932
23112
  moderationReport = this.mergeModeration(
22933
23113
  moderationReport,
@@ -22950,6 +23130,7 @@ var LLMClient = class {
22950
23130
  toolCalls: [],
22951
23131
  thinking: thinking || null,
22952
23132
  media: [],
23133
+ ...files.length ? { files } : {},
22953
23134
  ...moderationReport ? { moderation: moderationReport } : {},
22954
23135
  latencyMs: performance.now() - start,
22955
23136
  raw: null
@@ -23210,6 +23391,22 @@ function anthropicRequestTier(t) {
23210
23391
  function anthropicBilledTier(raw) {
23211
23392
  return typeof raw === "string" && raw ? { serviceTier: raw, pricingTier: raw } : {};
23212
23393
  }
23394
+ function filesFromCodeExecBlock(block) {
23395
+ if (block.type !== "bash_code_execution_tool_result" && block.type !== "code_execution_tool_result") {
23396
+ return [];
23397
+ }
23398
+ const result = block.content;
23399
+ if (!result || result.type !== "bash_code_execution_result" && result.type !== "code_execution_result" || !Array.isArray(result.content)) {
23400
+ return [];
23401
+ }
23402
+ const files = [];
23403
+ for (const out of result.content) {
23404
+ if ((out.type === "bash_code_execution_output" || out.type === "code_execution_output") && typeof out.file_id === "string") {
23405
+ files.push({ id: out.file_id, source: "code_execution" });
23406
+ }
23407
+ }
23408
+ return files;
23409
+ }
23213
23410
  var AnthropicAdapter = class {
23214
23411
  name = "anthropic";
23215
23412
  apiKey;
@@ -23390,15 +23587,8 @@ var AnthropicAdapter = class {
23390
23587
  };
23391
23588
  content.push(tc);
23392
23589
  toolCalls.push(tc);
23393
- } else if (block.type === "bash_code_execution_tool_result" || block.type === "code_execution_tool_result") {
23394
- const result = block.content;
23395
- if (result && (result.type === "bash_code_execution_result" || result.type === "code_execution_result") && Array.isArray(result.content)) {
23396
- for (const out of result.content) {
23397
- if ((out.type === "bash_code_execution_output" || out.type === "code_execution_output") && typeof out.file_id === "string") {
23398
- files.push({ id: out.file_id, source: "code_execution" });
23399
- }
23400
- }
23401
- }
23590
+ } else {
23591
+ files.push(...filesFromCodeExecBlock(block));
23402
23592
  }
23403
23593
  }
23404
23594
  const finishReason = extractFinishReason(toolCalls.length > 0, r.stop_reason, {
@@ -23437,6 +23627,8 @@ var AnthropicAdapter = class {
23437
23627
  if (block.type === "tool_use") {
23438
23628
  return [{ type: "tool_call_start", id: block.id, name: block.name }];
23439
23629
  }
23630
+ const files = filesFromCodeExecBlock(block);
23631
+ if (files.length) return files.map((file) => ({ type: "file", file }));
23440
23632
  }
23441
23633
  if (type === "content_block_stop") {
23442
23634
  }
@@ -23460,6 +23652,11 @@ var AnthropicAdapter = class {
23460
23652
  }
23461
23653
  return [];
23462
23654
  }
23655
+ /** Stateless — every event is self-contained (code-execution result blocks
23656
+ * arrive complete in a single content_block_start). */
23657
+ createStreamParser() {
23658
+ return (event) => this.parseStreamEvent(event);
23659
+ }
23463
23660
  parseUsage(u) {
23464
23661
  if (!u) return emptyUsage();
23465
23662
  const inputTokens = u.input_tokens ?? 0;
@@ -23984,6 +24181,18 @@ var GoogleAdapter = class {
23984
24181
  };
23985
24182
  }
23986
24183
  parseStreamEvent(event) {
24184
+ return this.streamEvents(event, { codeExec: false });
24185
+ }
24186
+ /** Stateful — Google splits the code-execution marker (`executableCode` /
24187
+ * `codeExecutionResult`) and the produced file (`inlineData`) across parts and
24188
+ * often across SSE events. The closure remembers "code execution began in this
24189
+ * stream" so a later `inlineData` blob is routed to `files` (a code-exec
24190
+ * artifact) rather than `media` (conversational output). */
24191
+ createStreamParser() {
24192
+ const state = { codeExec: false };
24193
+ return (event) => this.streamEvents(event, state);
24194
+ }
24195
+ streamEvents(event, state) {
23987
24196
  const data = JSON.parse(event.data);
23988
24197
  const candidates = data.candidates ?? [];
23989
24198
  const candidate = candidates[0];
@@ -23997,6 +24206,9 @@ var GoogleAdapter = class {
23997
24206
  const rawContent = candidate.content ?? {};
23998
24207
  const parts = rawContent.parts ?? [];
23999
24208
  const events = [];
24209
+ for (const part of parts) {
24210
+ if (part.executableCode || part.codeExecutionResult) state.codeExec = true;
24211
+ }
24000
24212
  for (const part of parts) {
24001
24213
  if (part.text !== void 0 && !part.thought)
24002
24214
  events.push({ type: "text", text: part.text });
@@ -24004,10 +24216,17 @@ var GoogleAdapter = class {
24004
24216
  if (part.inlineData) {
24005
24217
  const inline = part.inlineData;
24006
24218
  const mime = inline.mimeType;
24007
- const mediaType = mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : "video";
24008
- events.push({ type: "media_start", mediaType, mimeType: mime });
24009
- events.push({ type: "media_chunk", data: inline.data });
24010
- events.push({ type: "media_end" });
24219
+ if (state.codeExec) {
24220
+ events.push({
24221
+ type: "file",
24222
+ file: { data: inline.data, mimeType: mime, source: "code_execution" }
24223
+ });
24224
+ } else {
24225
+ const mediaType = mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : "video";
24226
+ events.push({ type: "media_start", mediaType, mimeType: mime });
24227
+ events.push({ type: "media_chunk", data: inline.data });
24228
+ events.push({ type: "media_end" });
24229
+ }
24011
24230
  }
24012
24231
  if (part.functionCall) {
24013
24232
  const fc = part.functionCall;
@@ -24302,6 +24521,10 @@ var GoogleInteractionsAdapter = class {
24302
24521
  }
24303
24522
  return events;
24304
24523
  }
24524
+ /** Stateless — the Interactions adapter surfaces no code-execution file outputs. */
24525
+ createStreamParser() {
24526
+ return (event) => this.parseStreamEvent(event);
24527
+ }
24305
24528
  parseUsage(u) {
24306
24529
  if (!u) return emptyUsage();
24307
24530
  const input = u.total_input_tokens ?? u.prompt_tokens ?? 0;
@@ -25267,6 +25490,10 @@ var OpenAIAdapter = class {
25267
25490
  }
25268
25491
  return events;
25269
25492
  }
25493
+ /** Stateless — Chat Completions has no hosted code-execution file outputs. */
25494
+ createStreamParser() {
25495
+ return (event) => this.parseStreamEvent(event);
25496
+ }
25270
25497
  parseUsage(u) {
25271
25498
  if (!u) return emptyUsage();
25272
25499
  const input = u.prompt_tokens ?? u.input_tokens ?? 0;
@@ -25782,6 +26009,33 @@ function filenameForMime(mimeType) {
25782
26009
  if (mimeType.startsWith("image/")) return `file.${mimeType.slice("image/".length)}`;
25783
26010
  return "file.bin";
25784
26011
  }
26012
+ function filesFromResponsesOutputItem(item) {
26013
+ const files = [];
26014
+ const type = item.type;
26015
+ if (type === "message") {
26016
+ for (const c of item.content ?? []) {
26017
+ if (c.type !== "output_text") continue;
26018
+ for (const a of c.annotations ?? []) {
26019
+ if (a.type === "container_file_citation" && typeof a.file_id === "string") {
26020
+ files.push({
26021
+ id: a.file_id,
26022
+ ...typeof a.filename === "string" ? { name: a.filename } : {},
26023
+ ...typeof a.container_id === "string" ? { ref: { containerId: a.container_id } } : {},
26024
+ source: "code_execution"
26025
+ });
26026
+ }
26027
+ }
26028
+ }
26029
+ }
26030
+ if (type === "code_interpreter_call") {
26031
+ for (const out of item.outputs ?? []) {
26032
+ if (out.type === "image" && typeof out.url === "string") {
26033
+ files.push({ url: out.url, source: "code_execution" });
26034
+ }
26035
+ }
26036
+ }
26037
+ return files;
26038
+ }
25785
26039
  var OpenAIResponsesAdapter = class {
25786
26040
  name = "openai";
25787
26041
  apiKey;
@@ -25962,6 +26216,7 @@ var OpenAIResponsesAdapter = class {
25962
26216
  let text = "";
25963
26217
  for (const item of output) {
25964
26218
  const type = item.type;
26219
+ files.push(...filesFromResponsesOutputItem(item));
25965
26220
  if (type === "message") {
25966
26221
  const itemContent = item.content ?? [];
25967
26222
  for (const c of itemContent) {
@@ -25969,22 +26224,6 @@ var OpenAIResponsesAdapter = class {
25969
26224
  const t = c.text;
25970
26225
  text += t;
25971
26226
  content.push({ type: "text", text: t });
25972
- for (const a of c.annotations ?? []) {
25973
- if (a.type === "container_file_citation" && typeof a.file_id === "string") {
25974
- files.push({
25975
- id: a.file_id,
25976
- ...typeof a.filename === "string" ? { name: a.filename } : {},
25977
- source: "code_execution"
25978
- });
25979
- }
25980
- }
25981
- }
25982
- }
25983
- }
25984
- if (type === "code_interpreter_call") {
25985
- for (const out of item.outputs ?? []) {
25986
- if (out.type === "image" && typeof out.url === "string") {
25987
- files.push({ url: out.url, source: "code_execution" });
25988
26227
  }
25989
26228
  }
25990
26229
  }
@@ -26079,6 +26318,7 @@ var OpenAIResponsesAdapter = class {
26079
26318
  }
26080
26319
  if (type === "response.output_item.done") {
26081
26320
  const item = data.item;
26321
+ for (const file of filesFromResponsesOutputItem(item)) events.push({ type: "file", file });
26082
26322
  if (item?.type === "function_call") {
26083
26323
  events.push({ type: "tool_call_end", id: item.call_id ?? "" });
26084
26324
  }
@@ -26109,6 +26349,11 @@ var OpenAIResponsesAdapter = class {
26109
26349
  }
26110
26350
  return events;
26111
26351
  }
26352
+ /** Stateless — each output item finalizes with all its file annotations in a
26353
+ * single response.output_item.done event. */
26354
+ createStreamParser() {
26355
+ return (event) => this.parseStreamEvent(event);
26356
+ }
26112
26357
  parseUsage(u) {
26113
26358
  if (!u) return emptyUsage();
26114
26359
  const input = u.input_tokens ?? 0;
@@ -27830,6 +28075,19 @@ var AgentLoop = class _AgentLoop {
27830
28075
  get model() {
27831
28076
  return this.client.model;
27832
28077
  }
28078
+ // ─── File retrieval (hosted-tool output files) ──────────────────────────
28079
+ /** Fetch a hosted-tool output file from the run's `response.files` — its bytes
28080
+ * as a `Blob` plus `name` / `mimeType` / `size`. Delegates to the underlying
28081
+ * client, so it uses this agent's provider + key (same as `retrieveFile` on a
28082
+ * `complete()` result). */
28083
+ retrieveFile(file) {
28084
+ return this.client.retrieveFile(file);
28085
+ }
28086
+ /** Stream a hosted-tool output file (large files) — a `ReadableStream` plus
28087
+ * best-effort `name` / `mimeType` / `size`. Delegates to the underlying client. */
28088
+ streamFile(file) {
28089
+ return this.client.streamFile(file);
28090
+ }
27833
28091
  get system() {
27834
28092
  return this._system;
27835
28093
  }
@@ -34464,7 +34722,13 @@ async function complete(opts) {
34464
34722
  serviceTier
34465
34723
  });
34466
34724
  }
34467
- const result = { text: res.text, response: res };
34725
+ const result = {
34726
+ text: res.text,
34727
+ response: res,
34728
+ // Bound to this call's client (same provider/model/key/engine).
34729
+ retrieveFile: (file) => llm.retrieveFile(file),
34730
+ streamFile: (file) => llm.streamFile(file)
34731
+ };
34468
34732
  if (opts.structured?.schema) {
34469
34733
  result.parsed = parseStructured(res.text);
34470
34734
  }
package/dist/index.d.ts CHANGED
@@ -49,6 +49,7 @@ 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
51
  export type { CompletionResponse, FileOutput, FinishReason, Usage } from './llm/types/response';
52
+ export type { FileStream, RetrievedFile } from './llm/files/retrieve';
52
53
  export type { NormalizedRequest, ReasoningContext, ThinkingConfig, CacheConfig } from './llm/types/request';
53
54
  export type { MediaStreamType, StreamEvent } from './llm/types/stream';
54
55
  export type { ExecuteOptions } from './llm/types/options';
package/dist/index.js CHANGED
@@ -2451,7 +2451,7 @@ var QueueState = class {
2451
2451
  });
2452
2452
  this.processed++;
2453
2453
  if (response.ok) {
2454
- const body = await parseResponseBody(response, entry.request.responseType ?? "json");
2454
+ const body = entry.request.responseType === "stream" ? response.body : await parseResponseBody(response, entry.request.responseType ?? "json");
2455
2455
  this.semaphore.release();
2456
2456
  entry.resolve({ status: response.status, headers: resHeaders, body });
2457
2457
  return;
@@ -22354,6 +22354,160 @@ async function* wrapParallel(raw, interval, moderate2) {
22354
22354
  }
22355
22355
  }
22356
22356
 
22357
+ // src/llm/files/retrieve.ts
22358
+ var DEFAULT_BASE = {
22359
+ anthropic: "https://api.anthropic.com",
22360
+ openai: "https://api.openai.com",
22361
+ xai: "https://api.x.ai",
22362
+ google: "https://generativelanguage.googleapis.com",
22363
+ openrouter: "https://openrouter.ai/api"
22364
+ };
22365
+ var OCTET_STREAM = "application/octet-stream";
22366
+ function contentRequest(ctx, file) {
22367
+ const base = ctx.baseURL ?? DEFAULT_BASE[ctx.provider];
22368
+ const id = file.id;
22369
+ if (ctx.provider === "anthropic") {
22370
+ return {
22371
+ url: `${base}/v1/files/${id}/content?beta=true`,
22372
+ headers: {
22373
+ "x-api-key": ctx.apiKey,
22374
+ "anthropic-version": "2023-06-01",
22375
+ "anthropic-beta": "files-api-2025-04-14",
22376
+ accept: "application/binary"
22377
+ }
22378
+ };
22379
+ }
22380
+ if (ctx.provider === "openai" || ctx.provider === "xai" || ctx.provider === "openrouter") {
22381
+ const containerId = file.ref?.containerId;
22382
+ const path = containerId ? `/v1/containers/${containerId}/files/${id}/content` : `/v1/files/${id}/content`;
22383
+ return { url: `${base}${path}`, headers: { authorization: `Bearer ${ctx.apiKey}` } };
22384
+ }
22385
+ if (ctx.provider === "google") {
22386
+ return {
22387
+ url: `${base}/v1beta/files/${id}:download?alt=media`,
22388
+ headers: { "x-goog-api-key": ctx.apiKey }
22389
+ };
22390
+ }
22391
+ throw new Error(`retrieveFile: no file-content endpoint for provider "${ctx.provider}"`);
22392
+ }
22393
+ function providerAuth(ctx, url) {
22394
+ const base = ctx.baseURL ?? DEFAULT_BASE[ctx.provider];
22395
+ if (!url.startsWith(base)) return {};
22396
+ if (ctx.provider === "anthropic") {
22397
+ return { "x-api-key": ctx.apiKey, "anthropic-version": "2023-06-01" };
22398
+ }
22399
+ if (ctx.provider === "google") return { "x-goog-api-key": ctx.apiKey };
22400
+ return { authorization: `Bearer ${ctx.apiKey}` };
22401
+ }
22402
+ async function fetchFileResponse(ctx, file, responseType) {
22403
+ if (file.url) {
22404
+ return ctx.fetch({
22405
+ url: file.url,
22406
+ method: "GET",
22407
+ headers: providerAuth(ctx, file.url),
22408
+ body: void 0,
22409
+ provider: ctx.provider,
22410
+ model: "files",
22411
+ responseType
22412
+ });
22413
+ }
22414
+ if (file.id) {
22415
+ const { url, headers } = contentRequest(ctx, file);
22416
+ return ctx.fetch({
22417
+ url,
22418
+ method: "GET",
22419
+ headers,
22420
+ body: void 0,
22421
+ provider: ctx.provider,
22422
+ model: "files",
22423
+ responseType
22424
+ });
22425
+ }
22426
+ throw new Error("retrieveFile: FileOutput has neither `data`, `url`, nor `id`");
22427
+ }
22428
+ function bytesToBlob(bytes, type) {
22429
+ const ab = new ArrayBuffer(bytes.byteLength);
22430
+ new Uint8Array(ab).set(bytes);
22431
+ return new Blob([ab], { type });
22432
+ }
22433
+ function singleChunkStream(bytes) {
22434
+ return new ReadableStream({
22435
+ start(controller) {
22436
+ controller.enqueue(bytes);
22437
+ controller.close();
22438
+ }
22439
+ });
22440
+ }
22441
+ var EXT_MIME = {
22442
+ png: "image/png",
22443
+ jpg: "image/jpeg",
22444
+ jpeg: "image/jpeg",
22445
+ gif: "image/gif",
22446
+ webp: "image/webp",
22447
+ svg: "image/svg+xml",
22448
+ bmp: "image/bmp",
22449
+ csv: "text/csv",
22450
+ tsv: "text/tab-separated-values",
22451
+ txt: "text/plain",
22452
+ json: "application/json",
22453
+ xml: "application/xml",
22454
+ html: "text/html",
22455
+ md: "text/markdown",
22456
+ pdf: "application/pdf",
22457
+ zip: "application/zip",
22458
+ parquet: "application/vnd.apache.parquet",
22459
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
22460
+ };
22461
+ function mimeFromName(name) {
22462
+ const ext = name?.includes(".") ? name.split(".").pop()?.toLowerCase() : void 0;
22463
+ return ext ? EXT_MIME[ext] : void 0;
22464
+ }
22465
+ function header(headers, name) {
22466
+ const lower = name.toLowerCase();
22467
+ for (const k in headers) if (k.toLowerCase() === lower) return headers[k];
22468
+ return void 0;
22469
+ }
22470
+ function filenameFromDisposition(cd) {
22471
+ if (!cd) return void 0;
22472
+ const ext = cd.match(/filename\*=[^']*''([^;]+)/i);
22473
+ if (ext) {
22474
+ try {
22475
+ return decodeURIComponent(ext[1].trim());
22476
+ } catch {
22477
+ }
22478
+ }
22479
+ const plain = cd.match(/filename="?([^";]+)"?/i);
22480
+ return plain ? plain[1].trim() : void 0;
22481
+ }
22482
+ async function retrieveFile(file, ctx) {
22483
+ if (file.data) {
22484
+ const blob2 = bytesToBlob(base64ToBytes(file.data), file.mimeType ?? OCTET_STREAM);
22485
+ return { blob: blob2, name: file.name, mimeType: blob2.type, size: blob2.size };
22486
+ }
22487
+ const res = await fetchFileResponse(ctx, file, "arraybuffer");
22488
+ const headers = res.headers ?? {};
22489
+ const name = filenameFromDisposition(header(headers, "content-disposition")) ?? file.name;
22490
+ const mimeType = header(headers, "content-type")?.split(";")[0].trim() || file.mimeType || mimeFromName(name) || OCTET_STREAM;
22491
+ const blob = new Blob([res.body], { type: mimeType });
22492
+ return { blob, name, mimeType, size: blob.size };
22493
+ }
22494
+ async function streamFile(file, ctx) {
22495
+ if (file.data) {
22496
+ const bytes = base64ToBytes(file.data);
22497
+ return { stream: singleChunkStream(bytes), name: file.name, mimeType: file.mimeType, size: bytes.byteLength };
22498
+ }
22499
+ const res = await fetchFileResponse(ctx, file, "stream");
22500
+ const headers = res.headers ?? {};
22501
+ const len = header(headers, "content-length");
22502
+ const name = filenameFromDisposition(header(headers, "content-disposition")) ?? file.name;
22503
+ return {
22504
+ stream: res.body,
22505
+ name,
22506
+ mimeType: header(headers, "content-type")?.split(";")[0].trim() || file.mimeType || mimeFromName(name),
22507
+ size: len ? Number(len) : void 0
22508
+ };
22509
+ }
22510
+
22357
22511
  // src/util/duration.ts
22358
22512
  var UNIT_MS = {
22359
22513
  s: 1e3,
@@ -22596,6 +22750,28 @@ var LLMClient = class {
22596
22750
  else next.output = result;
22597
22751
  return next;
22598
22752
  }
22753
+ // ─── File retrieval (hosted-tool output files) ─────────────────────────
22754
+ retrieveContext() {
22755
+ return {
22756
+ provider: this.provider,
22757
+ apiKey: this.apiKey,
22758
+ fetch: this.fetchFn,
22759
+ baseURL: this.adapter.baseURL()
22760
+ };
22761
+ }
22762
+ /** Fetch a hosted-tool output file (e.g. a code-execution file from
22763
+ * `response.files`): its bytes as a `Blob` plus `name` / `mimeType` / `size`.
22764
+ * Resolves inline `data`, a `url`, or a provider file `id` — all through this
22765
+ * client's provider + auth + engine. */
22766
+ retrieveFile(file) {
22767
+ return retrieveFile(file, this.retrieveContext());
22768
+ }
22769
+ /** Stream a hosted-tool output file: a `ReadableStream<Uint8Array>` plus
22770
+ * best-effort `name` / `mimeType` / `size` (from the response headers). For large
22771
+ * files piped straight to a file / GridFS / HTTP response without buffering. */
22772
+ streamFile(file) {
22773
+ return streamFile(file, this.retrieveContext());
22774
+ }
22599
22775
  /** Submit a request. Returns the parsed CompletionResponse. */
22600
22776
  async complete(input, options = {}) {
22601
22777
  const rawMessages = normalizeInput(input);
@@ -22807,17 +22983,18 @@ var LLMClient = class {
22807
22983
  let usage = emptyUsage();
22808
22984
  let finishReason = "stop";
22809
22985
  let moderationReport;
22986
+ const files = [];
22810
22987
  const fetchStream = this.fetchStreamFn;
22811
- const adapter = this.adapter;
22812
22988
  const queueName = this.queueName;
22813
22989
  const priority = this.priority;
22990
+ const parseStream = this.adapter.createStreamParser();
22814
22991
  async function* rawEvents() {
22815
22992
  for await (const sseEvent of fetchStream(httpReq, {
22816
22993
  queueName,
22817
22994
  priority,
22818
22995
  ctx
22819
22996
  })) {
22820
- for (const ev of adapter.parseStreamEvent(sseEvent)) yield ev;
22997
+ for (const ev of parseStream(sseEvent)) yield ev;
22821
22998
  }
22822
22999
  }
22823
23000
  const mod = options.moderation;
@@ -22855,6 +23032,9 @@ var LLMClient = class {
22855
23032
  case "done":
22856
23033
  finishReason = event.finishReason;
22857
23034
  break;
23035
+ case "file":
23036
+ files.push(event.file);
23037
+ break;
22858
23038
  case "moderation":
22859
23039
  moderationReport = this.mergeModeration(
22860
23040
  moderationReport,
@@ -22877,6 +23057,7 @@ var LLMClient = class {
22877
23057
  toolCalls: [],
22878
23058
  thinking: thinking || null,
22879
23059
  media: [],
23060
+ ...files.length ? { files } : {},
22880
23061
  ...moderationReport ? { moderation: moderationReport } : {},
22881
23062
  latencyMs: performance.now() - start,
22882
23063
  raw: null
@@ -23137,6 +23318,22 @@ function anthropicRequestTier(t) {
23137
23318
  function anthropicBilledTier(raw) {
23138
23319
  return typeof raw === "string" && raw ? { serviceTier: raw, pricingTier: raw } : {};
23139
23320
  }
23321
+ function filesFromCodeExecBlock(block) {
23322
+ if (block.type !== "bash_code_execution_tool_result" && block.type !== "code_execution_tool_result") {
23323
+ return [];
23324
+ }
23325
+ const result = block.content;
23326
+ if (!result || result.type !== "bash_code_execution_result" && result.type !== "code_execution_result" || !Array.isArray(result.content)) {
23327
+ return [];
23328
+ }
23329
+ const files = [];
23330
+ for (const out of result.content) {
23331
+ if ((out.type === "bash_code_execution_output" || out.type === "code_execution_output") && typeof out.file_id === "string") {
23332
+ files.push({ id: out.file_id, source: "code_execution" });
23333
+ }
23334
+ }
23335
+ return files;
23336
+ }
23140
23337
  var AnthropicAdapter = class {
23141
23338
  name = "anthropic";
23142
23339
  apiKey;
@@ -23317,15 +23514,8 @@ var AnthropicAdapter = class {
23317
23514
  };
23318
23515
  content.push(tc);
23319
23516
  toolCalls.push(tc);
23320
- } else if (block.type === "bash_code_execution_tool_result" || block.type === "code_execution_tool_result") {
23321
- const result = block.content;
23322
- if (result && (result.type === "bash_code_execution_result" || result.type === "code_execution_result") && Array.isArray(result.content)) {
23323
- for (const out of result.content) {
23324
- if ((out.type === "bash_code_execution_output" || out.type === "code_execution_output") && typeof out.file_id === "string") {
23325
- files.push({ id: out.file_id, source: "code_execution" });
23326
- }
23327
- }
23328
- }
23517
+ } else {
23518
+ files.push(...filesFromCodeExecBlock(block));
23329
23519
  }
23330
23520
  }
23331
23521
  const finishReason = extractFinishReason(toolCalls.length > 0, r.stop_reason, {
@@ -23364,6 +23554,8 @@ var AnthropicAdapter = class {
23364
23554
  if (block.type === "tool_use") {
23365
23555
  return [{ type: "tool_call_start", id: block.id, name: block.name }];
23366
23556
  }
23557
+ const files = filesFromCodeExecBlock(block);
23558
+ if (files.length) return files.map((file) => ({ type: "file", file }));
23367
23559
  }
23368
23560
  if (type === "content_block_stop") {
23369
23561
  }
@@ -23387,6 +23579,11 @@ var AnthropicAdapter = class {
23387
23579
  }
23388
23580
  return [];
23389
23581
  }
23582
+ /** Stateless — every event is self-contained (code-execution result blocks
23583
+ * arrive complete in a single content_block_start). */
23584
+ createStreamParser() {
23585
+ return (event) => this.parseStreamEvent(event);
23586
+ }
23390
23587
  parseUsage(u) {
23391
23588
  if (!u) return emptyUsage();
23392
23589
  const inputTokens = u.input_tokens ?? 0;
@@ -23911,6 +24108,18 @@ var GoogleAdapter = class {
23911
24108
  };
23912
24109
  }
23913
24110
  parseStreamEvent(event) {
24111
+ return this.streamEvents(event, { codeExec: false });
24112
+ }
24113
+ /** Stateful — Google splits the code-execution marker (`executableCode` /
24114
+ * `codeExecutionResult`) and the produced file (`inlineData`) across parts and
24115
+ * often across SSE events. The closure remembers "code execution began in this
24116
+ * stream" so a later `inlineData` blob is routed to `files` (a code-exec
24117
+ * artifact) rather than `media` (conversational output). */
24118
+ createStreamParser() {
24119
+ const state = { codeExec: false };
24120
+ return (event) => this.streamEvents(event, state);
24121
+ }
24122
+ streamEvents(event, state) {
23914
24123
  const data = JSON.parse(event.data);
23915
24124
  const candidates = data.candidates ?? [];
23916
24125
  const candidate = candidates[0];
@@ -23924,6 +24133,9 @@ var GoogleAdapter = class {
23924
24133
  const rawContent = candidate.content ?? {};
23925
24134
  const parts = rawContent.parts ?? [];
23926
24135
  const events = [];
24136
+ for (const part of parts) {
24137
+ if (part.executableCode || part.codeExecutionResult) state.codeExec = true;
24138
+ }
23927
24139
  for (const part of parts) {
23928
24140
  if (part.text !== void 0 && !part.thought)
23929
24141
  events.push({ type: "text", text: part.text });
@@ -23931,10 +24143,17 @@ var GoogleAdapter = class {
23931
24143
  if (part.inlineData) {
23932
24144
  const inline = part.inlineData;
23933
24145
  const mime = inline.mimeType;
23934
- const mediaType = mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : "video";
23935
- events.push({ type: "media_start", mediaType, mimeType: mime });
23936
- events.push({ type: "media_chunk", data: inline.data });
23937
- events.push({ type: "media_end" });
24146
+ if (state.codeExec) {
24147
+ events.push({
24148
+ type: "file",
24149
+ file: { data: inline.data, mimeType: mime, source: "code_execution" }
24150
+ });
24151
+ } else {
24152
+ const mediaType = mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : "video";
24153
+ events.push({ type: "media_start", mediaType, mimeType: mime });
24154
+ events.push({ type: "media_chunk", data: inline.data });
24155
+ events.push({ type: "media_end" });
24156
+ }
23938
24157
  }
23939
24158
  if (part.functionCall) {
23940
24159
  const fc = part.functionCall;
@@ -24229,6 +24448,10 @@ var GoogleInteractionsAdapter = class {
24229
24448
  }
24230
24449
  return events;
24231
24450
  }
24451
+ /** Stateless — the Interactions adapter surfaces no code-execution file outputs. */
24452
+ createStreamParser() {
24453
+ return (event) => this.parseStreamEvent(event);
24454
+ }
24232
24455
  parseUsage(u) {
24233
24456
  if (!u) return emptyUsage();
24234
24457
  const input = u.total_input_tokens ?? u.prompt_tokens ?? 0;
@@ -25194,6 +25417,10 @@ var OpenAIAdapter = class {
25194
25417
  }
25195
25418
  return events;
25196
25419
  }
25420
+ /** Stateless — Chat Completions has no hosted code-execution file outputs. */
25421
+ createStreamParser() {
25422
+ return (event) => this.parseStreamEvent(event);
25423
+ }
25197
25424
  parseUsage(u) {
25198
25425
  if (!u) return emptyUsage();
25199
25426
  const input = u.prompt_tokens ?? u.input_tokens ?? 0;
@@ -25709,6 +25936,33 @@ function filenameForMime(mimeType) {
25709
25936
  if (mimeType.startsWith("image/")) return `file.${mimeType.slice("image/".length)}`;
25710
25937
  return "file.bin";
25711
25938
  }
25939
+ function filesFromResponsesOutputItem(item) {
25940
+ const files = [];
25941
+ const type = item.type;
25942
+ if (type === "message") {
25943
+ for (const c of item.content ?? []) {
25944
+ if (c.type !== "output_text") continue;
25945
+ for (const a of c.annotations ?? []) {
25946
+ if (a.type === "container_file_citation" && typeof a.file_id === "string") {
25947
+ files.push({
25948
+ id: a.file_id,
25949
+ ...typeof a.filename === "string" ? { name: a.filename } : {},
25950
+ ...typeof a.container_id === "string" ? { ref: { containerId: a.container_id } } : {},
25951
+ source: "code_execution"
25952
+ });
25953
+ }
25954
+ }
25955
+ }
25956
+ }
25957
+ if (type === "code_interpreter_call") {
25958
+ for (const out of item.outputs ?? []) {
25959
+ if (out.type === "image" && typeof out.url === "string") {
25960
+ files.push({ url: out.url, source: "code_execution" });
25961
+ }
25962
+ }
25963
+ }
25964
+ return files;
25965
+ }
25712
25966
  var OpenAIResponsesAdapter = class {
25713
25967
  name = "openai";
25714
25968
  apiKey;
@@ -25889,6 +26143,7 @@ var OpenAIResponsesAdapter = class {
25889
26143
  let text = "";
25890
26144
  for (const item of output) {
25891
26145
  const type = item.type;
26146
+ files.push(...filesFromResponsesOutputItem(item));
25892
26147
  if (type === "message") {
25893
26148
  const itemContent = item.content ?? [];
25894
26149
  for (const c of itemContent) {
@@ -25896,22 +26151,6 @@ var OpenAIResponsesAdapter = class {
25896
26151
  const t = c.text;
25897
26152
  text += t;
25898
26153
  content.push({ type: "text", text: t });
25899
- for (const a of c.annotations ?? []) {
25900
- if (a.type === "container_file_citation" && typeof a.file_id === "string") {
25901
- files.push({
25902
- id: a.file_id,
25903
- ...typeof a.filename === "string" ? { name: a.filename } : {},
25904
- source: "code_execution"
25905
- });
25906
- }
25907
- }
25908
- }
25909
- }
25910
- }
25911
- if (type === "code_interpreter_call") {
25912
- for (const out of item.outputs ?? []) {
25913
- if (out.type === "image" && typeof out.url === "string") {
25914
- files.push({ url: out.url, source: "code_execution" });
25915
26154
  }
25916
26155
  }
25917
26156
  }
@@ -26006,6 +26245,7 @@ var OpenAIResponsesAdapter = class {
26006
26245
  }
26007
26246
  if (type === "response.output_item.done") {
26008
26247
  const item = data.item;
26248
+ for (const file of filesFromResponsesOutputItem(item)) events.push({ type: "file", file });
26009
26249
  if (item?.type === "function_call") {
26010
26250
  events.push({ type: "tool_call_end", id: item.call_id ?? "" });
26011
26251
  }
@@ -26036,6 +26276,11 @@ var OpenAIResponsesAdapter = class {
26036
26276
  }
26037
26277
  return events;
26038
26278
  }
26279
+ /** Stateless — each output item finalizes with all its file annotations in a
26280
+ * single response.output_item.done event. */
26281
+ createStreamParser() {
26282
+ return (event) => this.parseStreamEvent(event);
26283
+ }
26039
26284
  parseUsage(u) {
26040
26285
  if (!u) return emptyUsage();
26041
26286
  const input = u.input_tokens ?? 0;
@@ -27757,6 +28002,19 @@ var AgentLoop = class _AgentLoop {
27757
28002
  get model() {
27758
28003
  return this.client.model;
27759
28004
  }
28005
+ // ─── File retrieval (hosted-tool output files) ──────────────────────────
28006
+ /** Fetch a hosted-tool output file from the run's `response.files` — its bytes
28007
+ * as a `Blob` plus `name` / `mimeType` / `size`. Delegates to the underlying
28008
+ * client, so it uses this agent's provider + key (same as `retrieveFile` on a
28009
+ * `complete()` result). */
28010
+ retrieveFile(file) {
28011
+ return this.client.retrieveFile(file);
28012
+ }
28013
+ /** Stream a hosted-tool output file (large files) — a `ReadableStream` plus
28014
+ * best-effort `name` / `mimeType` / `size`. Delegates to the underlying client. */
28015
+ streamFile(file) {
28016
+ return this.client.streamFile(file);
28017
+ }
27760
28018
  get system() {
27761
28019
  return this._system;
27762
28020
  }
@@ -34391,7 +34649,13 @@ async function complete(opts) {
34391
34649
  serviceTier
34392
34650
  });
34393
34651
  }
34394
- const result = { text: res.text, response: res };
34652
+ const result = {
34653
+ text: res.text,
34654
+ response: res,
34655
+ // Bound to this call's client (same provider/model/key/engine).
34656
+ retrieveFile: (file) => llm.retrieveFile(file),
34657
+ streamFile: (file) => llm.streamFile(file)
34658
+ };
34395
34659
  if (opts.structured?.schema) {
34396
34660
  result.parsed = parseStructured(res.text);
34397
34661
  }
@@ -18,10 +18,11 @@
18
18
  * Hooks emitted: onClientCreate (in ctor), onMessageResolve, onBeforeSubmit,
19
19
  * onCompletion, onClientDestroy. */
20
20
  import type { HookBus } from '../bus/hook-bus';
21
+ import type { FileStream, RetrievedFile } from './files/retrieve';
21
22
  import type { ContentPart, Message } from './types/messages';
22
23
  import type { ExecuteOptions } from './types/options';
23
24
  import type { ApiType, ProviderName } from './types/provider';
24
- import type { CompletionResponse } from './types/response';
25
+ import type { CompletionResponse, FileOutput } from './types/response';
25
26
  import type { StreamEvent } from './types/stream';
26
27
  import type { LLMClientConfig } from './client-config';
27
28
  export declare class LLMClient {
@@ -67,6 +68,16 @@ export declare class LLMClient {
67
68
  private moderationConfig;
68
69
  /** Fold a moderation stream event into the accumulating report. */
69
70
  private mergeModeration;
71
+ private retrieveContext;
72
+ /** Fetch a hosted-tool output file (e.g. a code-execution file from
73
+ * `response.files`): its bytes as a `Blob` plus `name` / `mimeType` / `size`.
74
+ * Resolves inline `data`, a `url`, or a provider file `id` — all through this
75
+ * client's provider + auth + engine. */
76
+ retrieveFile(file: FileOutput): Promise<RetrievedFile>;
77
+ /** Stream a hosted-tool output file: a `ReadableStream<Uint8Array>` plus
78
+ * best-effort `name` / `mimeType` / `size` (from the response headers). For large
79
+ * files piped straight to a file / GridFS / HTTP response without buffering. */
80
+ streamFile(file: FileOutput): Promise<FileStream>;
70
81
  /** Submit a request. Returns the parsed CompletionResponse. */
71
82
  complete(input: string | ContentPart[] | Message[], options?: ExecuteOptions): Promise<CompletionResponse>;
72
83
  /** Run `complete` with a JSON Schema enforced via `structured`. Strips any
@@ -0,0 +1,44 @@
1
+ /** Retrieve the bytes of a hosted-tool output file (e.g. code-execution files).
2
+ *
3
+ * A `FileOutput` may carry inline base64 `data` (Google), a `url` (OpenAI
4
+ * code-interpreter images), or an `id` fetched from the provider's files API
5
+ * (Anthropic, OpenAI container files). These helpers resolve all three into
6
+ * bytes — buffered (`retrieveFile` → Blob) or streamed (`streamFile` →
7
+ * ReadableStream, for large files piped straight to a sink).
8
+ *
9
+ * All HTTP flows through the injected `EngineFetch` (auth, queue, cost, traces). */
10
+ import type { EngineFetch } from '../../network/types';
11
+ import type { ProviderName } from '../types/provider';
12
+ import type { FileOutput } from '../types/response';
13
+ export interface RetrieveContext {
14
+ provider: ProviderName;
15
+ apiKey: string;
16
+ fetch: EngineFetch;
17
+ /** Provider API base URL; defaults per provider when omitted. */
18
+ baseURL?: string;
19
+ }
20
+ /** A retrieved file's bytes plus the metadata an end user needs to display and
21
+ * open it as an attachment (correct name + type). */
22
+ export interface RetrievedFile {
23
+ blob: Blob;
24
+ /** Filename (from Content-Disposition, else the FileOutput name). */
25
+ name?: string;
26
+ /** MIME type (from Content-Type, else the FileOutput mime). */
27
+ mimeType: string;
28
+ /** Size in bytes. */
29
+ size: number;
30
+ }
31
+ /** A streamed file: its byte stream plus best-effort metadata (from the response
32
+ * headers). The stream itself carries only bytes — name/type/size come from here. */
33
+ export interface FileStream {
34
+ stream: ReadableStream<Uint8Array>;
35
+ name?: string;
36
+ mimeType?: string;
37
+ /** From Content-Length, when the provider sends it. */
38
+ size?: number;
39
+ }
40
+ /** Fetch the whole file as bytes (buffered) plus its name/type/size. */
41
+ export declare function retrieveFile(file: FileOutput, ctx: RetrieveContext): Promise<RetrievedFile>;
42
+ /** Stream the file's bytes (un-buffered) plus best-effort name/type/size. Pipe the
43
+ * stream straight to a file, GridFS, or an HTTP response — nothing is buffered. */
44
+ export declare function streamFile(file: FileOutput, ctx: RetrieveContext): Promise<FileStream>;
@@ -26,5 +26,8 @@ export declare class AnthropicAdapter implements ProviderAdapter {
26
26
  private buildContentPart;
27
27
  parseResponse(raw: unknown, latencyMs: number): CompletionResponse;
28
28
  parseStreamEvent(event: SSEEvent): StreamEvent[];
29
+ /** Stateless — every event is self-contained (code-execution result blocks
30
+ * arrive complete in a single content_block_start). */
31
+ createStreamParser(): (event: SSEEvent) => StreamEvent[];
29
32
  private parseUsage;
30
33
  }
@@ -23,5 +23,12 @@ export declare class GoogleAdapter implements ProviderAdapter {
23
23
  private buildContent;
24
24
  parseResponse(raw: unknown, latencyMs: number): CompletionResponse;
25
25
  parseStreamEvent(event: SSEEvent): StreamEvent[];
26
+ /** Stateful — Google splits the code-execution marker (`executableCode` /
27
+ * `codeExecutionResult`) and the produced file (`inlineData`) across parts and
28
+ * often across SSE events. The closure remembers "code execution began in this
29
+ * stream" so a later `inlineData` blob is routed to `files` (a code-exec
30
+ * artifact) rather than `media` (conversational output). */
31
+ createStreamParser(): (event: SSEEvent) => StreamEvent[];
32
+ private streamEvents;
26
33
  private parseUsage;
27
34
  }
@@ -26,5 +26,7 @@ export declare class GoogleInteractionsAdapter implements ProviderAdapter {
26
26
  enableStreaming(providerReq: ProviderHttpRequest): void;
27
27
  parseResponse(raw: unknown, latencyMs: number): CompletionResponse;
28
28
  parseStreamEvent(event: SSEEvent): StreamEvent[];
29
+ /** Stateless — the Interactions adapter surfaces no code-execution file outputs. */
30
+ createStreamParser(): (event: SSEEvent) => StreamEvent[];
29
31
  private parseUsage;
30
32
  }
@@ -21,5 +21,7 @@ export declare class OpenAIAdapter implements ProviderAdapter {
21
21
  enableStreaming(providerReq: ProviderHttpRequest, _req: NormalizedRequest): void;
22
22
  parseResponse(raw: unknown, latencyMs: number): CompletionResponse;
23
23
  parseStreamEvent(event: SSEEvent): StreamEvent[];
24
+ /** Stateless — Chat Completions has no hosted code-execution file outputs. */
25
+ createStreamParser(): (event: SSEEvent) => StreamEvent[];
24
26
  private parseUsage;
25
27
  }
@@ -25,5 +25,8 @@ export declare class OpenAIResponsesAdapter implements ProviderAdapter {
25
25
  enableStreaming(providerReq: ProviderHttpRequest): void;
26
26
  parseResponse(raw: unknown, latencyMs: number): CompletionResponse;
27
27
  parseStreamEvent(event: SSEEvent): StreamEvent[];
28
+ /** Stateless — each output item finalizes with all its file annotations in a
29
+ * single response.output_item.done event. */
30
+ createStreamParser(): (event: SSEEvent) => StreamEvent[];
28
31
  protected parseUsage(u: Record<string, unknown> | undefined): Usage;
29
32
  }
@@ -25,8 +25,16 @@ export interface ProviderAdapter {
25
25
  buildRequest(req: NormalizedRequest): ProviderHttpRequest;
26
26
  /** Parse provider's raw HTTP response body to a normalized CompletionResponse. */
27
27
  parseResponse(raw: unknown, latencyMs: number): CompletionResponse;
28
- /** Convert one SSE event from the provider stream into zero or more StreamEvents. */
28
+ /** Convert one SSE event from the provider stream into zero or more StreamEvents.
29
+ * Stateless per-event primitive — see `createStreamParser` for the per-stream
30
+ * entry point the client actually calls. */
29
31
  parseStreamEvent(event: SSEEvent): StreamEvent[];
32
+ /** Create a per-stream parser. Returns a function that converts each SSE event
33
+ * into zero or more StreamEvents, holding any per-stream state (e.g. whether the
34
+ * turn has begun hosted code execution) in its closure. The client calls this
35
+ * once per `stream()` so each stream gets isolated state. Stateless adapters
36
+ * return `parseStreamEvent` bound to themselves. */
37
+ createStreamParser(): (event: SSEEvent) => StreamEvent[];
30
38
  /** Auth headers (Bearer / x-api-key / etc.). */
31
39
  authHeaders(): Record<string, string>;
32
40
  /** Base URL — provider's domain root. */
@@ -39,6 +39,10 @@ export interface FileOutput {
39
39
  url?: string;
40
40
  /** What produced it (e.g. 'code_execution'). */
41
41
  source?: string;
42
+ /** Provider-specific retrieval hints, used by `retrieveFile`/`streamFile` when the
43
+ * file is fetched by `id` (e.g. OpenAI container files set `{ containerId }`).
44
+ * Absent for providers that don't need extra context. */
45
+ ref?: Record<string, unknown>;
42
46
  }
43
47
  export type FinishReason = 'stop' | 'tool_use' | 'length' | 'content_filter' | 'error';
44
48
  export interface Usage {
@@ -1,6 +1,6 @@
1
1
  /** Universal streaming event types. */
2
2
  import type { ModerationEntry } from '../moderation/types';
3
- import type { Usage } from './response';
3
+ import type { FileOutput, Usage } from './response';
4
4
  export type MediaStreamType = 'image' | 'audio' | 'video';
5
5
  export type StreamEvent = {
6
6
  type: 'text';
@@ -41,6 +41,14 @@ export type StreamEvent = {
41
41
  type: 'media_end';
42
42
  mediaId?: string;
43
43
  }
44
+ /** A hosted-tool output file (e.g. a code-execution chart/CSV) became available.
45
+ * Carries the `FileOutput` descriptor (id / url / inline data + name / mimeType),
46
+ * not the bytes — fetch those via `retrieveFile` / `streamFile`. The same file
47
+ * is also collected onto the streamed final response's `files`. */
48
+ | {
49
+ type: 'file';
50
+ file: FileOutput;
51
+ }
44
52
  /** A moderation result for the input or output. `source` distinguishes a
45
53
  * provider-native result from a client-emulated one. Emitted by the moderation
46
54
  * option (report-only). */
@@ -15,8 +15,10 @@ export interface HttpRequest {
15
15
  model: string;
16
16
  /** How to parse the response body. Default 'json' (LLM responses, image-gen
17
17
  * with b64_json, video-status JSON). Use 'arraybuffer' for binary downloads
18
- * (TTS audio bytes, video file bytes). 'text' for plain-text responses. */
19
- responseType?: 'json' | 'arraybuffer' | 'text';
18
+ * (TTS audio bytes, video file bytes). 'text' for plain-text responses.
19
+ * 'stream' returns the raw `ReadableStream<Uint8Array>` body un-buffered (large
20
+ * file downloads that pipe straight to a sink). */
21
+ responseType?: 'json' | 'arraybuffer' | 'text' | 'stream';
20
22
  /** When the request body is already a Uint8Array / ArrayBuffer (binary
21
23
  * upload like multipart) the queue should NOT JSON.stringify it. Default
22
24
  * false → body is JSON.stringify'd. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@combycode/llm-sdk",
3
- "version": "1.2.0",
3
+ "version": "1.3.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",