@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 +20 -0
- package/README.md +24 -0
- package/dist/agent/loop.d.ts +10 -1
- package/dist/helpers/one-shot.d.ts +8 -1
- package/dist/index.browser.js +297 -33
- package/dist/index.d.ts +1 -0
- package/dist/index.js +297 -33
- package/dist/llm/client.d.ts +12 -1
- package/dist/llm/files/retrieve.d.ts +44 -0
- package/dist/llm/providers/anthropic/messages.d.ts +3 -0
- package/dist/llm/providers/google/generate.d.ts +7 -0
- package/dist/llm/providers/google/interactions.d.ts +2 -0
- package/dist/llm/providers/openai/completions.d.ts +2 -0
- package/dist/llm/providers/openai/responses.d.ts +3 -0
- package/dist/llm/types/provider.d.ts +9 -1
- package/dist/llm/types/response.d.ts +4 -0
- package/dist/llm/types/stream.d.ts +9 -1
- package/dist/network/types.d.ts +4 -2
- package/package.json +1 -1
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)
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -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>>;
|
package/dist/index.browser.js
CHANGED
|
@@ -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
|
|
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
|
|
23394
|
-
|
|
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
|
-
|
|
24008
|
-
|
|
24009
|
-
|
|
24010
|
-
|
|
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 = {
|
|
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
|
|
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
|
|
23321
|
-
|
|
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
|
-
|
|
23935
|
-
|
|
23936
|
-
|
|
23937
|
-
|
|
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 = {
|
|
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
|
}
|
package/dist/llm/client.d.ts
CHANGED
|
@@ -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). */
|
package/dist/network/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|