@combycode/llm-sdk 3.1.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -1
- package/dist/agent/loop-step-state.d.ts +5 -1
- package/dist/index.browser.js +218 -24
- package/dist/index.d.ts +1 -1
- package/dist/index.js +218 -24
- package/dist/llm/providers/_shared/citations.d.ts +25 -0
- package/dist/llm/types/response.d.ts +23 -0
- package/dist/llm/types/stream.d.ts +14 -1
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,101 @@ All notable changes to `@combycode/llm-sdk` are documented here. The format foll
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/) and the project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [3.2.1] — 2026-08-31
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Three TypeScript errors in an MCP test file** that made v3.2.0 unpublishable: `searchParams.get()`
|
|
12
|
+
returns `string | null` while `.at(-1)` returns `string | undefined`, and `toBe` has no overload
|
|
13
|
+
spanning both. The assertions are unchanged; only their types are aligned.
|
|
14
|
+
|
|
15
|
+
- **Two lint warnings** left standing in `network/engine.ts` (an unused type import) and an MCP test
|
|
16
|
+
helper (`let x!` forward declarations that nothing forward-references).
|
|
17
|
+
|
|
18
|
+
### Internal
|
|
19
|
+
|
|
20
|
+
- **The release gate now runs lint, typecheck and the tests** (`G1 build-green` in CombyCode's shared
|
|
21
|
+
quality-gate). v3.2.0 was tagged with a green gate and a red typecheck, because the gate checked
|
|
22
|
+
documentation and consumers while lint and typecheck lived in a playbook sentence — the half a
|
|
23
|
+
human has to remember. The gate's own README names that failure mode: "a checklist you have to
|
|
24
|
+
remember to read is guarded by the same attention that failed in the first place." The check ships
|
|
25
|
+
with the good/bad/blind fixtures the selftest requires, and was verified against the exact error
|
|
26
|
+
that escaped.
|
|
27
|
+
|
|
28
|
+
## [3.2.0] — 2026-08-31
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`response.citations` — the sources an answer cited, unified across providers.** Reaching them
|
|
33
|
+
meant regexing `response.raw`; the SDK's own web-search example did exactly that, which is every
|
|
34
|
+
consumer reimplementing provider knowledge that belongs here. Four wire shapes are read: Anthropic's
|
|
35
|
+
text-block `citations[]` (the only provider that also reports the cited passage), Google's
|
|
36
|
+
`groundingMetadata.groundingChunks[]`, OpenAI Responses/Chat `url_citation` annotations, and xAI's
|
|
37
|
+
bare top-level `citations[]`.
|
|
38
|
+
|
|
39
|
+
Distinct from `builtinToolCalls`, which records what the model *invoked*: a turn can run three
|
|
40
|
+
searches and cite one page. Through an agent run the sources **accumulate across steps**, deduped
|
|
41
|
+
by URL — a run that searches in step 1 and answers in step 3 keeps the sources its answer rests on.
|
|
42
|
+
Optional per R3 (`response.citations ?? []`); absent on `stream()`, which holds no raw payload, and
|
|
43
|
+
on Google's Interactions surface, whose grounding shape has not been measured.
|
|
44
|
+
|
|
45
|
+
**Streaming reports them too**, as a `citation` StreamEvent per source, collected onto the streamed
|
|
46
|
+
final response so `stream()` and `complete()` agree. Four more measured shapes, since a stream never
|
|
47
|
+
assembles the body the buffered reader parses: Anthropic `citations_delta`, Responses
|
|
48
|
+
`response.output_text.annotation.added` (OpenAI and xAI), chat-completions `delta.annotations`, and
|
|
49
|
+
Google's *late* populated `groundingMetadata` chunk — the first one carrying that key is empty, so
|
|
50
|
+
latching on first sight would report a search and no sources.
|
|
51
|
+
|
|
52
|
+
Verified live on Anthropic, OpenAI, Google, xAI and OpenRouter, buffered and streamed: 5/5 providers
|
|
53
|
+
return real cited URLs for the web-search scenario, which previously reported `no-citation` on all
|
|
54
|
+
five. Note Google reports each source as a `grounding-api-redirect` URL, not the page itself.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- **xAI file `delete`, `getInfo` and `list` never worked.** `XAIFileAdapter` is exported, and three
|
|
59
|
+
of its four methods rejected with `unknown spec: xai/files.<op>` before any HTTP — only
|
|
60
|
+
`xai/files.upload` had ever been written, so roughly twenty lines of response mapping below them
|
|
61
|
+
had never run. The three specs follow `xai.upload`'s own note that the surface differs from
|
|
62
|
+
OpenAI's only in the upload `purpose`, so they carry the same paths and the same Bearer base.
|
|
63
|
+
|
|
64
|
+
- **`listModelsLive` was blocked by CORS in the browser, for Anthropic only.** Anthropic refuses a
|
|
65
|
+
browser request without an explicit opt-in header. The chat path sends it and the Anthropic files
|
|
66
|
+
specs carry it, but `models.list` lost it when that request moved to a spec: the header had lived
|
|
67
|
+
in the helper's own table, which nothing called any more. Measured from a browser with a raw
|
|
68
|
+
fetch — `/v1/models` returns 200 with the header and fails without it. The sandbox listed models
|
|
69
|
+
through `Promise.allSettled` and kept only the fulfilled results, so the rejection was swallowed
|
|
70
|
+
and Anthropic simply showed no models rather than an error.
|
|
71
|
+
|
|
72
|
+
### Removed
|
|
73
|
+
|
|
74
|
+
- **Five private functions left behind by the spec migration.** Each was live until its area moved to
|
|
75
|
+
the spec-driven builder, which renamed it with a leading underscore instead of deleting it:
|
|
76
|
+
`_buildForm`, `_batchName`, `_toOpenAIAudioFormat`, `_toResponseModalities` and
|
|
77
|
+
`LiveSpec.headers`. Each was checked field by field against the spec that replaced it before
|
|
78
|
+
removal — and the last of them was **not** equivalent, which is how the CORS bug above was found.
|
|
79
|
+
|
|
80
|
+
### Internal
|
|
81
|
+
|
|
82
|
+
- **Line coverage 91.62% → 99.53%**, function coverage 87.53% → 97.41%, across 2290 → 4051 tests.
|
|
83
|
+
Eleven modules had no function coverage at all — `batcher.ts` ran 3.5% of its lines,
|
|
84
|
+
`plugins/internal-tools/runner/runner.ts` 3.7%, `transport-ws.ts` 9.6%. Every new test is mutation-verified: the
|
|
85
|
+
source line it claims to cover was broken and the test watched to fail.
|
|
86
|
+
|
|
87
|
+
The exercise found ten defects no existing test caught, each pinned as a `DEFECT:` test rather
|
|
88
|
+
than fixed in the same pass: `ResponseStore.list(null)` returning every user's response ids,
|
|
89
|
+
`chunker.ts` silently dropping space-free text (3000 characters of CJK or base64 yield one
|
|
90
|
+
400-character chunk), `attachment.ts fromBlob` leaving the MIME type empty because a type-less
|
|
91
|
+
`Blob` reports `''` rather than `null`, and `oauth.ts tryRefresh` discarding the refresh token it
|
|
92
|
+
means to preserve.
|
|
93
|
+
|
|
94
|
+
Two pre-existing tests turned out to execute no code at all — one asserts against `readFileSync`
|
|
95
|
+
of `realtime.ts` with regexes over the source text, which is why that file sat at 0% functions
|
|
96
|
+
while appearing tested.
|
|
97
|
+
|
|
98
|
+
- **A coverage floor that can actually fail.** `bunfig.toml`'s `coverageThreshold` is accepted and
|
|
99
|
+
ignored by bun 1.3.14 — set to an impossible 1.0 the run still exits 0 — so the floor is its own
|
|
100
|
+
step (`bun run coverage:gate`), carrying a `--self-test` that proves it discriminates.
|
|
101
|
+
|
|
8
102
|
|
|
9
103
|
## [3.1.0] — 2026-08-25
|
|
10
104
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Mutable accumulator for a single agent loop step.
|
|
2
2
|
* Passed through stream-event helpers so they don't fight closure state. */
|
|
3
3
|
import type { ToolCallPart } from '../llm/types/messages';
|
|
4
|
-
import type { Usage } from '../llm/types/response';
|
|
4
|
+
import type { Citation, Usage } from '../llm/types/response';
|
|
5
5
|
/** Accumulation bucket for one in-progress tool call (before tool_call_end). */
|
|
6
6
|
export interface ToolCallAccumEntry {
|
|
7
7
|
id: string;
|
|
@@ -19,4 +19,8 @@ export interface StepState {
|
|
|
19
19
|
toolCallAccum: Map<string, ToolCallAccumEntry>;
|
|
20
20
|
stepUsage: Usage;
|
|
21
21
|
stepFinishReason: string;
|
|
22
|
+
/** Sources cited during this step, keyed by url. A Map rather than an array
|
|
23
|
+
* because Google repeats its grounding chunks across late chunks, and one page
|
|
24
|
+
* cited twice is one source. */
|
|
25
|
+
stepCitations: Map<string, Citation>;
|
|
22
26
|
}
|
package/dist/index.browser.js
CHANGED
|
@@ -30969,7 +30969,7 @@ var xai_create_default = {
|
|
|
30969
30969
|
var xai_addRequests_default = {
|
|
30970
30970
|
id: "xai/batch.addRequests",
|
|
30971
30971
|
extends: "xai/batch.base",
|
|
30972
|
-
_note: "Step 2 of submit
|
|
30972
|
+
_note: "Step 2 of submit: the batch opened in step 1 is filled here. batch_request is a TAGGED UNION whose variant name is the KEY -- {responses: <body>}, never {endpoint: 'responses', body: <body>}. Measured live 2026-08-28: the endpoint-field form is rejected 422 with 'unknown variant endpoint, expected one of chat_get_completion, responses, image_generation, image_edit, video_generation, video_extension'. That form had never been sent to xAI, here or in the TypeScript adapter this spec came from.",
|
|
30973
30973
|
envelope: {
|
|
30974
30974
|
method: "POST",
|
|
30975
30975
|
url: { $join: [{ $config: "baseURL" }, "/v1/batches/", { $: "batchId" }, "/requests"] },
|
|
@@ -30985,7 +30985,7 @@ var xai_addRequests_default = {
|
|
|
30985
30985
|
{
|
|
30986
30986
|
value: {
|
|
30987
30987
|
batch_request_id: { $: "@customId" },
|
|
30988
|
-
batch_request: {
|
|
30988
|
+
batch_request: { responses: { $: "@body" } }
|
|
30989
30989
|
}
|
|
30990
30990
|
}
|
|
30991
30991
|
]
|
|
@@ -31337,6 +31337,42 @@ var xai_upload_default = {
|
|
|
31337
31337
|
multipart: [{ name: "file", file: true }, { name: "purpose", value: "assistants" }]
|
|
31338
31338
|
};
|
|
31339
31339
|
|
|
31340
|
+
// src/wire/specs/files/xai.delete.json
|
|
31341
|
+
var xai_delete_default = {
|
|
31342
|
+
id: "xai/files.delete",
|
|
31343
|
+
extends: "openai/files.base",
|
|
31344
|
+
_note: "Same shape as OpenAI's, per xai.upload's note that the surface differs only in the upload purpose. baseURL is supplied by the adapter, so the path is all that is declared here.",
|
|
31345
|
+
envelope: {
|
|
31346
|
+
method: "DELETE",
|
|
31347
|
+
bodyKind: "none",
|
|
31348
|
+
url: { $join: [{ $config: "baseURL" }, "/v1/files/", { $: "remoteId" }] }
|
|
31349
|
+
}
|
|
31350
|
+
};
|
|
31351
|
+
|
|
31352
|
+
// src/wire/specs/files/xai.getInfo.json
|
|
31353
|
+
var xai_getInfo_default = {
|
|
31354
|
+
id: "xai/files.getInfo",
|
|
31355
|
+
extends: "openai/files.base",
|
|
31356
|
+
_note: "Same shape as OpenAI's, per xai.upload's note that the surface differs only in the upload purpose. baseURL is supplied by the adapter, so the path is all that is declared here.",
|
|
31357
|
+
envelope: {
|
|
31358
|
+
method: "GET",
|
|
31359
|
+
bodyKind: "none",
|
|
31360
|
+
url: { $join: [{ $config: "baseURL" }, "/v1/files/", { $: "remoteId" }] }
|
|
31361
|
+
}
|
|
31362
|
+
};
|
|
31363
|
+
|
|
31364
|
+
// src/wire/specs/files/xai.list.json
|
|
31365
|
+
var xai_list_default = {
|
|
31366
|
+
id: "xai/files.list",
|
|
31367
|
+
extends: "openai/files.base",
|
|
31368
|
+
_note: "Same shape as OpenAI's, per xai.upload's note that the surface differs only in the upload purpose. baseURL is supplied by the adapter, so the path is all that is declared here.",
|
|
31369
|
+
envelope: {
|
|
31370
|
+
method: "GET",
|
|
31371
|
+
bodyKind: "none",
|
|
31372
|
+
url: { $join: [{ $config: "baseURL" }, "/v1/files"] }
|
|
31373
|
+
}
|
|
31374
|
+
};
|
|
31375
|
+
|
|
31340
31376
|
// src/wire/specs/files/google.finishUpload.json
|
|
31341
31377
|
var google_finishUpload_default = {
|
|
31342
31378
|
id: "google/files.finishUpload",
|
|
@@ -31577,6 +31613,9 @@ var DELTAS = new Map(
|
|
|
31577
31613
|
google_getInfo_default,
|
|
31578
31614
|
google_list_default,
|
|
31579
31615
|
xai_upload_default,
|
|
31616
|
+
xai_delete_default,
|
|
31617
|
+
xai_getInfo_default,
|
|
31618
|
+
xai_list_default,
|
|
31580
31619
|
google_finishUpload_default,
|
|
31581
31620
|
videos_status_default,
|
|
31582
31621
|
videos_cancel_default,
|
|
@@ -32387,6 +32426,14 @@ var anthropic_default3 = {
|
|
|
32387
32426
|
value: {
|
|
32388
32427
|
$config: "apiVersion"
|
|
32389
32428
|
}
|
|
32429
|
+
},
|
|
32430
|
+
{
|
|
32431
|
+
name: "anthropic-dangerous-direct-browser-access",
|
|
32432
|
+
value: "true",
|
|
32433
|
+
_note: "Only in a browser, where the request is otherwise blocked by CORS. The chat path sends it (messages.ts) and so do the files specs; models.list lost it when this request moved to a spec, because the header lived in the helper's own table.",
|
|
32434
|
+
when: {
|
|
32435
|
+
truthy: "browser"
|
|
32436
|
+
}
|
|
32390
32437
|
}
|
|
32391
32438
|
]
|
|
32392
32439
|
}
|
|
@@ -36663,6 +36710,7 @@ var LLMClient = class {
|
|
|
36663
36710
|
let moderationReport;
|
|
36664
36711
|
const files = [];
|
|
36665
36712
|
const builtinToolCalls = [];
|
|
36713
|
+
const citationsByUrl = /* @__PURE__ */ new Map();
|
|
36666
36714
|
const fetchStream = this.fetchStreamFn;
|
|
36667
36715
|
const queueName = this.queueName;
|
|
36668
36716
|
const priority = this.priority;
|
|
@@ -36716,6 +36764,9 @@ var LLMClient = class {
|
|
|
36716
36764
|
case "file":
|
|
36717
36765
|
files.push(event.file);
|
|
36718
36766
|
break;
|
|
36767
|
+
case "citation":
|
|
36768
|
+
citationsByUrl.set(event.citation.url, event.citation);
|
|
36769
|
+
break;
|
|
36719
36770
|
case "builtin_tool_end":
|
|
36720
36771
|
builtinToolCalls.push({
|
|
36721
36772
|
tool: event.tool,
|
|
@@ -36750,6 +36801,7 @@ var LLMClient = class {
|
|
|
36750
36801
|
media: [],
|
|
36751
36802
|
...files.length ? { files } : {},
|
|
36752
36803
|
...builtinToolCalls.length ? { builtinToolCalls } : {},
|
|
36804
|
+
...citationsByUrl.size ? { citations: [...citationsByUrl.values()] } : {},
|
|
36753
36805
|
...moderationReport ? { moderation: moderationReport } : {},
|
|
36754
36806
|
latencyMs: performance.now() - start,
|
|
36755
36807
|
raw: null
|
|
@@ -38560,6 +38612,87 @@ function pinFor(model, pins) {
|
|
|
38560
38612
|
var ANTHROPIC_MESSAGE_PINS = anthropic_messages_default;
|
|
38561
38613
|
var GOOGLE_GENERATE_PINS = google_generate_default;
|
|
38562
38614
|
|
|
38615
|
+
// src/llm/providers/_shared/citations.ts
|
|
38616
|
+
function fromAnthropic(raw) {
|
|
38617
|
+
const out = [];
|
|
38618
|
+
for (const block of raw.content ?? []) {
|
|
38619
|
+
for (const cite of block.citations ?? []) {
|
|
38620
|
+
const url = cite.url;
|
|
38621
|
+
if (url) {
|
|
38622
|
+
out.push({
|
|
38623
|
+
url,
|
|
38624
|
+
...cite.title ? { title: cite.title } : {},
|
|
38625
|
+
// Anthropic is the only provider that reports the cited passage.
|
|
38626
|
+
...cite.cited_text ? { text: cite.cited_text } : {}
|
|
38627
|
+
});
|
|
38628
|
+
}
|
|
38629
|
+
}
|
|
38630
|
+
}
|
|
38631
|
+
return out;
|
|
38632
|
+
}
|
|
38633
|
+
function fromGoogle(raw) {
|
|
38634
|
+
const out = [];
|
|
38635
|
+
for (const candidate of raw.candidates ?? []) {
|
|
38636
|
+
const grounding = candidate.groundingMetadata ?? {};
|
|
38637
|
+
for (const chunk of grounding.groundingChunks ?? []) {
|
|
38638
|
+
const web = chunk.web ?? {};
|
|
38639
|
+
const uri = web.uri;
|
|
38640
|
+
if (uri) out.push({ url: uri, ...web.title ? { title: web.title } : {} });
|
|
38641
|
+
}
|
|
38642
|
+
}
|
|
38643
|
+
return out;
|
|
38644
|
+
}
|
|
38645
|
+
function fromResponses(raw) {
|
|
38646
|
+
const out = [];
|
|
38647
|
+
for (const item of raw.output ?? []) {
|
|
38648
|
+
for (const part of item.content ?? []) {
|
|
38649
|
+
for (const note of part.annotations ?? []) {
|
|
38650
|
+
if (note.type === "url_citation" && note.url) {
|
|
38651
|
+
out.push({
|
|
38652
|
+
url: note.url,
|
|
38653
|
+
...note.title ? { title: note.title } : {}
|
|
38654
|
+
});
|
|
38655
|
+
}
|
|
38656
|
+
}
|
|
38657
|
+
}
|
|
38658
|
+
}
|
|
38659
|
+
return out;
|
|
38660
|
+
}
|
|
38661
|
+
function fromCompletions(raw) {
|
|
38662
|
+
const out = [];
|
|
38663
|
+
const message = (raw.choices ?? [])[0]?.message ?? {};
|
|
38664
|
+
for (const note of message.annotations ?? []) {
|
|
38665
|
+
const detail = note.url_citation ?? note;
|
|
38666
|
+
if (note.type === "url_citation" && detail.url) {
|
|
38667
|
+
out.push({
|
|
38668
|
+
url: detail.url,
|
|
38669
|
+
...detail.title ? { title: detail.title } : {}
|
|
38670
|
+
});
|
|
38671
|
+
}
|
|
38672
|
+
}
|
|
38673
|
+
for (const url of raw.citations ?? []) {
|
|
38674
|
+
if (typeof url === "string") out.push({ url });
|
|
38675
|
+
}
|
|
38676
|
+
return out;
|
|
38677
|
+
}
|
|
38678
|
+
var READERS = {
|
|
38679
|
+
messages: fromAnthropic,
|
|
38680
|
+
generate: fromGoogle,
|
|
38681
|
+
responses: fromResponses,
|
|
38682
|
+
completions: fromCompletions
|
|
38683
|
+
// `interactions` is DELIBERATELY absent. Google's Interactions API returns a
|
|
38684
|
+
// `steps[]` machine, not `candidates[]`, so `fromGoogle` would read a key that
|
|
38685
|
+
// is never there -- and no recorded Interactions response carries grounding, so
|
|
38686
|
+
// there is nothing to write a reader against. Guessing the shape would produce
|
|
38687
|
+
// a reader that is confidently wrong and passes a test built from the same
|
|
38688
|
+
// guess. The adapter is wired anyway: when the shape is measured, only this
|
|
38689
|
+
// table changes.
|
|
38690
|
+
};
|
|
38691
|
+
function extractCitations(api, raw) {
|
|
38692
|
+
if (raw == null || typeof raw !== "object") return [];
|
|
38693
|
+
return READERS[api]?.(raw) ?? [];
|
|
38694
|
+
}
|
|
38695
|
+
|
|
38563
38696
|
// src/llm/providers/_shared/builtin-tools.ts
|
|
38564
38697
|
var NATIVE_TO_UNIFIED = {
|
|
38565
38698
|
// OpenAI / xAI Responses output-item types
|
|
@@ -38778,6 +38911,7 @@ var AnthropicAdapter = class {
|
|
|
38778
38911
|
model_context_window_exceeded: "length",
|
|
38779
38912
|
refusal: "content_filter"
|
|
38780
38913
|
});
|
|
38914
|
+
const citations = extractCitations("messages", raw);
|
|
38781
38915
|
return {
|
|
38782
38916
|
id: r.id,
|
|
38783
38917
|
model: r.model,
|
|
@@ -38787,6 +38921,7 @@ var AnthropicAdapter = class {
|
|
|
38787
38921
|
text: content.filter((p) => p.type === "text").map((p) => p.text).join(""),
|
|
38788
38922
|
toolCalls,
|
|
38789
38923
|
media: [],
|
|
38924
|
+
...citations.length ? { citations } : {},
|
|
38790
38925
|
...files.length ? { files } : {},
|
|
38791
38926
|
...builtinToolCalls.length ? { builtinToolCalls } : {},
|
|
38792
38927
|
thinking,
|
|
@@ -38814,6 +38949,20 @@ var AnthropicAdapter = class {
|
|
|
38814
38949
|
if (delta.type === "text_delta") return [{ type: "text", text: delta.text }];
|
|
38815
38950
|
if (delta.type === "thinking_delta")
|
|
38816
38951
|
return [{ type: "thinking", text: delta.thinking }];
|
|
38952
|
+
if (delta.type === "citations_delta") {
|
|
38953
|
+
const cite = delta.citation ?? {};
|
|
38954
|
+
const url = cite.url;
|
|
38955
|
+
return url ? [
|
|
38956
|
+
{
|
|
38957
|
+
type: "citation",
|
|
38958
|
+
citation: {
|
|
38959
|
+
url,
|
|
38960
|
+
...cite.title ? { title: cite.title } : {},
|
|
38961
|
+
...cite.cited_text ? { text: cite.cited_text } : {}
|
|
38962
|
+
}
|
|
38963
|
+
}
|
|
38964
|
+
] : [];
|
|
38965
|
+
}
|
|
38817
38966
|
if (delta.type === "input_json_delta") {
|
|
38818
38967
|
if (state.current) {
|
|
38819
38968
|
state.current.json += delta.partial_json ?? "";
|
|
@@ -39364,6 +39513,7 @@ var GoogleAdapter = class {
|
|
|
39364
39513
|
builtinToolCalls.push({ tool: "web_fetch", ...typeof url === "string" ? { url } : {} });
|
|
39365
39514
|
}
|
|
39366
39515
|
}
|
|
39516
|
+
const citations = extractCitations("generate", raw);
|
|
39367
39517
|
return {
|
|
39368
39518
|
// generateContent DOES return an id — `responseId`, at the top level. The
|
|
39369
39519
|
// fallback stays for older payloads, but minting one unconditionally made
|
|
@@ -39379,6 +39529,7 @@ var GoogleAdapter = class {
|
|
|
39379
39529
|
toolCalls,
|
|
39380
39530
|
thinking,
|
|
39381
39531
|
media,
|
|
39532
|
+
...citations.length ? { citations } : {},
|
|
39382
39533
|
...files.length ? { files } : {},
|
|
39383
39534
|
...builtinToolCalls.length ? { builtinToolCalls } : {},
|
|
39384
39535
|
latencyMs,
|
|
@@ -39463,6 +39614,18 @@ var GoogleAdapter = class {
|
|
|
39463
39614
|
events.push({ type: "tool_call_end", id: "" });
|
|
39464
39615
|
}
|
|
39465
39616
|
}
|
|
39617
|
+
for (const chunk of candidate.groundingMetadata?.groundingChunks ?? []) {
|
|
39618
|
+
const web = chunk.web ?? {};
|
|
39619
|
+
if (web.uri) {
|
|
39620
|
+
events.push({
|
|
39621
|
+
type: "citation",
|
|
39622
|
+
citation: {
|
|
39623
|
+
url: web.uri,
|
|
39624
|
+
...web.title ? { title: web.title } : {}
|
|
39625
|
+
}
|
|
39626
|
+
});
|
|
39627
|
+
}
|
|
39628
|
+
}
|
|
39466
39629
|
if (candidate.groundingMetadata && !state.webSearchEmitted) {
|
|
39467
39630
|
state.webSearchEmitted = true;
|
|
39468
39631
|
const q = candidate.groundingMetadata.webSearchQueries?.[0];
|
|
@@ -39685,6 +39848,7 @@ var GoogleInteractionsAdapter = class {
|
|
|
39685
39848
|
queued: "pending",
|
|
39686
39849
|
in_progress: "pending"
|
|
39687
39850
|
});
|
|
39851
|
+
const citations = extractCitations("interactions", raw);
|
|
39688
39852
|
return {
|
|
39689
39853
|
id: r.id ?? crypto.randomUUID(),
|
|
39690
39854
|
model: "",
|
|
@@ -39694,6 +39858,7 @@ var GoogleInteractionsAdapter = class {
|
|
|
39694
39858
|
text,
|
|
39695
39859
|
toolCalls,
|
|
39696
39860
|
media,
|
|
39861
|
+
...citations.length ? { citations } : {},
|
|
39697
39862
|
thinking,
|
|
39698
39863
|
latencyMs,
|
|
39699
39864
|
raw
|
|
@@ -41035,13 +41200,14 @@ var OpenAIAdapter = class {
|
|
|
41035
41200
|
content.push(parsed);
|
|
41036
41201
|
toolCalls.push(parsed);
|
|
41037
41202
|
}
|
|
41038
|
-
const finishReason = extractFinishReason(
|
|
41039
|
-
|
|
41040
|
-
|
|
41041
|
-
|
|
41042
|
-
);
|
|
41203
|
+
const finishReason = extractFinishReason(toolCalls.length > 0, choice.finish_reason, {
|
|
41204
|
+
tool_calls: "tool_use",
|
|
41205
|
+
length: "length",
|
|
41206
|
+
content_filter: "content_filter"
|
|
41207
|
+
});
|
|
41043
41208
|
const reasoningContent = message.reasoning_content ?? null;
|
|
41044
41209
|
const moderation = parseNativeModeration(r.moderation);
|
|
41210
|
+
const citations = extractCitations("completions", raw);
|
|
41045
41211
|
return {
|
|
41046
41212
|
id: r.id,
|
|
41047
41213
|
model: r.model,
|
|
@@ -41051,6 +41217,7 @@ var OpenAIAdapter = class {
|
|
|
41051
41217
|
text,
|
|
41052
41218
|
toolCalls,
|
|
41053
41219
|
media,
|
|
41220
|
+
...citations.length ? { citations } : {},
|
|
41054
41221
|
thinking: reasoningContent,
|
|
41055
41222
|
...moderation ? { moderation } : {},
|
|
41056
41223
|
latencyMs,
|
|
@@ -41083,6 +41250,18 @@ var OpenAIAdapter = class {
|
|
|
41083
41250
|
if (delta.content) {
|
|
41084
41251
|
events.push({ type: "text", text: delta.content });
|
|
41085
41252
|
}
|
|
41253
|
+
for (const note of delta.annotations ?? []) {
|
|
41254
|
+
const detail = note.url_citation ?? note;
|
|
41255
|
+
if (note.type === "url_citation" && detail.url) {
|
|
41256
|
+
events.push({
|
|
41257
|
+
type: "citation",
|
|
41258
|
+
citation: {
|
|
41259
|
+
url: detail.url,
|
|
41260
|
+
...detail.title ? { title: detail.title } : {}
|
|
41261
|
+
}
|
|
41262
|
+
});
|
|
41263
|
+
}
|
|
41264
|
+
}
|
|
41086
41265
|
const toolIdByIndex = state?.toolIdByIndex ?? /* @__PURE__ */ new Map();
|
|
41087
41266
|
const toolCalls = delta.tool_calls ?? [];
|
|
41088
41267
|
for (const tc of toolCalls) {
|
|
@@ -41993,6 +42172,7 @@ var OpenAIResponsesAdapter = class {
|
|
|
41993
42172
|
if (text && content.length === 0) content.push({ type: "text", text });
|
|
41994
42173
|
}
|
|
41995
42174
|
const moderation = parseNativeModeration(r.moderation);
|
|
42175
|
+
const citations = extractCitations("responses", raw);
|
|
41996
42176
|
return {
|
|
41997
42177
|
id: r.id,
|
|
41998
42178
|
model: r.model ?? "",
|
|
@@ -42003,6 +42183,7 @@ var OpenAIResponsesAdapter = class {
|
|
|
42003
42183
|
toolCalls,
|
|
42004
42184
|
thinking,
|
|
42005
42185
|
media,
|
|
42186
|
+
...citations.length ? { citations } : {},
|
|
42006
42187
|
...files.length ? { files } : {},
|
|
42007
42188
|
...builtinToolCalls.length ? { builtinToolCalls } : {},
|
|
42008
42189
|
...moderation ? { moderation } : {},
|
|
@@ -42015,6 +42196,19 @@ var OpenAIResponsesAdapter = class {
|
|
|
42015
42196
|
const data = sseJson(event);
|
|
42016
42197
|
const type = data.type;
|
|
42017
42198
|
const events = [];
|
|
42199
|
+
if (type === "response.output_text.annotation.added") {
|
|
42200
|
+
const note = data.annotation ?? {};
|
|
42201
|
+
if (note.type === "url_citation" && note.url) {
|
|
42202
|
+
events.push({
|
|
42203
|
+
type: "citation",
|
|
42204
|
+
citation: {
|
|
42205
|
+
url: note.url,
|
|
42206
|
+
...note.title ? { title: note.title } : {}
|
|
42207
|
+
}
|
|
42208
|
+
});
|
|
42209
|
+
}
|
|
42210
|
+
return events;
|
|
42211
|
+
}
|
|
42018
42212
|
if (type === "response.output_text.delta") {
|
|
42019
42213
|
const itemId = data.item_id;
|
|
42020
42214
|
const phase = itemId ? phaseByItem?.get(itemId) : void 0;
|
|
@@ -42335,10 +42529,6 @@ var XAIBatchAdapter = class {
|
|
|
42335
42529
|
throw new Error(`xAI batch create failed: ${JSON.stringify(createRes.body)}`);
|
|
42336
42530
|
const batch2 = createRes.body ?? {};
|
|
42337
42531
|
const batchId = batch2.batch_id ?? batch2.id;
|
|
42338
|
-
const _batchRequests = requests.map((r) => ({
|
|
42339
|
-
batch_request_id: r.customId,
|
|
42340
|
-
batch_request: { endpoint: "responses", body: r.body }
|
|
42341
|
-
}));
|
|
42342
42532
|
const addRes = await fetch2(this.buildAddRequestsRequest(batchId, requests));
|
|
42343
42533
|
if (addRes.status >= 400)
|
|
42344
42534
|
throw new Error(`xAI batch add requests failed: ${JSON.stringify(addRes.body)}`);
|
|
@@ -43853,7 +44043,8 @@ function makeStepState() {
|
|
|
43853
44043
|
stepToolCalls: [],
|
|
43854
44044
|
toolCallAccum: /* @__PURE__ */ new Map(),
|
|
43855
44045
|
stepUsage: emptyUsage(),
|
|
43856
|
-
stepFinishReason: "stop"
|
|
44046
|
+
stepFinishReason: "stop",
|
|
44047
|
+
stepCitations: /* @__PURE__ */ new Map()
|
|
43857
44048
|
};
|
|
43858
44049
|
}
|
|
43859
44050
|
function accumulateStreamEvent(event, state) {
|
|
@@ -43887,6 +44078,9 @@ function accumulateStreamEvent(event, state) {
|
|
|
43887
44078
|
}
|
|
43888
44079
|
return null;
|
|
43889
44080
|
}
|
|
44081
|
+
case "citation":
|
|
44082
|
+
state.stepCitations.set(event.citation.url, event.citation);
|
|
44083
|
+
return null;
|
|
43890
44084
|
case "usage":
|
|
43891
44085
|
state.stepUsage = event.usage;
|
|
43892
44086
|
return null;
|
|
@@ -43944,6 +44138,7 @@ function buildStepResponse(state, model, stepStart) {
|
|
|
43944
44138
|
toolCalls: state.stepToolCalls,
|
|
43945
44139
|
thinking: state.stepThinking || null,
|
|
43946
44140
|
media: [],
|
|
44141
|
+
...state.stepCitations.size ? { citations: [...state.stepCitations.values()] } : {},
|
|
43947
44142
|
latencyMs: stepLatency,
|
|
43948
44143
|
raw: null
|
|
43949
44144
|
};
|
|
@@ -44285,6 +44480,8 @@ var AgentLoop = class _AgentLoop {
|
|
|
44285
44480
|
// final LLM response (e.g. code-execution files produced during the run).
|
|
44286
44481
|
...last?.files ? { files: last.files } : {},
|
|
44287
44482
|
...last?.builtinToolCalls ? { builtinToolCalls: last.builtinToolCalls } : {},
|
|
44483
|
+
// Every step's sources, not just the final step's — see the accumulator.
|
|
44484
|
+
...args.citations.length ? { citations: args.citations } : {},
|
|
44288
44485
|
...last?.moderation ? { moderation: last.moderation } : {},
|
|
44289
44486
|
latencyMs: performance.now() - args.startPerf,
|
|
44290
44487
|
raw: args.raw
|
|
@@ -44351,6 +44548,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44351
44548
|
let stepCount = 0;
|
|
44352
44549
|
let toolCallCount = 0;
|
|
44353
44550
|
let lastResponse = null;
|
|
44551
|
+
const citationsByUrl = /* @__PURE__ */ new Map();
|
|
44354
44552
|
let reason = "done";
|
|
44355
44553
|
let errorMsg;
|
|
44356
44554
|
let caughtError;
|
|
@@ -44392,6 +44590,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44392
44590
|
const stepLatency = performance.now() - stepStart;
|
|
44393
44591
|
totalLlmTimeMs += stepLatency;
|
|
44394
44592
|
addUsage(totalUsage, lastResponse.usage);
|
|
44593
|
+
for (const cite of lastResponse.citations ?? []) citationsByUrl.set(cite.url, cite);
|
|
44395
44594
|
if (this._reflectRetry?.handles(lastResponse.finishReason)) {
|
|
44396
44595
|
const verdict = this._reflectRetry.recordFailure();
|
|
44397
44596
|
await this.hooks.emit("onWarning", {
|
|
@@ -44499,6 +44698,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44499
44698
|
totalUsage,
|
|
44500
44699
|
lastResponse,
|
|
44501
44700
|
media: lastResponse?.media ?? [],
|
|
44701
|
+
citations: [...citationsByUrl.values()],
|
|
44502
44702
|
raw: lastResponse?.raw ?? null
|
|
44503
44703
|
});
|
|
44504
44704
|
await this.settleRun({
|
|
@@ -44544,6 +44744,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44544
44744
|
let finalText = "";
|
|
44545
44745
|
let finalContent = [];
|
|
44546
44746
|
let lastResponse = null;
|
|
44747
|
+
const citationsByUrl = /* @__PURE__ */ new Map();
|
|
44547
44748
|
let reason = "done";
|
|
44548
44749
|
let errorMsg;
|
|
44549
44750
|
let caughtError;
|
|
@@ -44598,6 +44799,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44598
44799
|
finalContent = content;
|
|
44599
44800
|
finalText = state.stepText;
|
|
44600
44801
|
lastResponse = stepResponse;
|
|
44802
|
+
for (const cite of stepResponse.citations ?? []) citationsByUrl.set(cite.url, cite);
|
|
44601
44803
|
this._history.append(
|
|
44602
44804
|
{
|
|
44603
44805
|
...buildAssistantMessage(lastResponse, {
|
|
@@ -44681,6 +44883,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44681
44883
|
finalContent,
|
|
44682
44884
|
totalUsage,
|
|
44683
44885
|
lastResponse,
|
|
44886
|
+
citations: [...citationsByUrl.values()],
|
|
44684
44887
|
// The streaming path already emitted media as events, and never holds a
|
|
44685
44888
|
// raw provider payload.
|
|
44686
44889
|
media: [],
|
|
@@ -54674,37 +54877,26 @@ function listModels(opts = {}) {
|
|
|
54674
54877
|
var LIVE = {
|
|
54675
54878
|
openai: {
|
|
54676
54879
|
url: "https://api.openai.com/v1/models",
|
|
54677
|
-
headers: (k) => ({ authorization: `Bearer ${k}` }),
|
|
54678
54880
|
items: (b) => b.data ?? [],
|
|
54679
54881
|
id: (m) => m.id
|
|
54680
54882
|
},
|
|
54681
54883
|
openrouter: {
|
|
54682
54884
|
url: "https://openrouter.ai/api/v1/models",
|
|
54683
|
-
headers: (k) => ({ authorization: `Bearer ${k}` }),
|
|
54684
54885
|
items: (b) => b.data ?? [],
|
|
54685
54886
|
id: (m) => m.id
|
|
54686
54887
|
},
|
|
54687
54888
|
xai: {
|
|
54688
54889
|
url: "https://api.x.ai/v1/models",
|
|
54689
|
-
headers: (k) => ({ authorization: `Bearer ${k}` }),
|
|
54690
54890
|
items: (b) => b.data ?? [],
|
|
54691
54891
|
id: (m) => m.id
|
|
54692
54892
|
},
|
|
54693
54893
|
anthropic: {
|
|
54694
54894
|
url: "https://api.anthropic.com/v1/models",
|
|
54695
|
-
// Mirror the chat adapter: Anthropic rejects browser requests without the
|
|
54696
|
-
// explicit opt-in header, so the /models call must send it too.
|
|
54697
|
-
headers: (k) => ({
|
|
54698
|
-
"x-api-key": k,
|
|
54699
|
-
"anthropic-version": ANTHROPIC_API_VERSION,
|
|
54700
|
-
...isBrowser() ? { "anthropic-dangerous-direct-browser-access": "true" } : {}
|
|
54701
|
-
}),
|
|
54702
54895
|
items: (b) => b.data ?? [],
|
|
54703
54896
|
id: (m) => m.id
|
|
54704
54897
|
},
|
|
54705
54898
|
google: {
|
|
54706
54899
|
url: "https://generativelanguage.googleapis.com/v1beta/models",
|
|
54707
|
-
headers: (k) => ({ "x-goog-api-key": k }),
|
|
54708
54900
|
items: (b) => b.models ?? [],
|
|
54709
54901
|
id: (m) => m.name.replace(/^models\//, "")
|
|
54710
54902
|
}
|
|
@@ -54797,7 +54989,9 @@ async function fetchLiveBody(opts) {
|
|
|
54797
54989
|
const p = (async () => {
|
|
54798
54990
|
const built = buildFromSpec(
|
|
54799
54991
|
utilitySpec(`${opts.provider}/models.list`),
|
|
54800
|
-
|
|
54992
|
+
// Anthropic refuses a browser request without an explicit opt-in header,
|
|
54993
|
+
// and the spec asks for `browser` to decide whether to send it.
|
|
54994
|
+
{ browser: isBrowser() },
|
|
54801
54995
|
makeRegistry({}),
|
|
54802
54996
|
opts.provider,
|
|
54803
54997
|
void 0,
|
package/dist/index.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ export type { AssistantPhase, Role, MessageOrigin, ContentPart, TextPart, ImageP
|
|
|
49
49
|
export { isFunctionTool, isBuiltinTool } from './llm/types/tools';
|
|
50
50
|
export type { FunctionTool, BuiltinTool, McpToolParams, Tool, ToolChoice, JsonSchema } from './llm/types/tools';
|
|
51
51
|
export { emptyUsage } from './llm/types/response';
|
|
52
|
-
export type { BuiltinToolCall, CompletionResponse, FileOutput, FinishReason, KnownFinishReason, Usage, } from './llm/types/response';
|
|
52
|
+
export type { BuiltinToolCall, Citation, CompletionResponse, FileOutput, FinishReason, KnownFinishReason, Usage, } from './llm/types/response';
|
|
53
53
|
export type { FileStream, RetrievedFile } from './llm/files/retrieve';
|
|
54
54
|
export type { CacheConfig, NormalizedRequest, ProviderOptions, ReasoningContext, ThinkingConfig } from './llm/types/request';
|
|
55
55
|
export type { MediaStreamType, StreamEvent } from './llm/types/stream';
|
package/dist/index.js
CHANGED
|
@@ -30896,7 +30896,7 @@ var xai_create_default = {
|
|
|
30896
30896
|
var xai_addRequests_default = {
|
|
30897
30897
|
id: "xai/batch.addRequests",
|
|
30898
30898
|
extends: "xai/batch.base",
|
|
30899
|
-
_note: "Step 2 of submit
|
|
30899
|
+
_note: "Step 2 of submit: the batch opened in step 1 is filled here. batch_request is a TAGGED UNION whose variant name is the KEY -- {responses: <body>}, never {endpoint: 'responses', body: <body>}. Measured live 2026-08-28: the endpoint-field form is rejected 422 with 'unknown variant endpoint, expected one of chat_get_completion, responses, image_generation, image_edit, video_generation, video_extension'. That form had never been sent to xAI, here or in the TypeScript adapter this spec came from.",
|
|
30900
30900
|
envelope: {
|
|
30901
30901
|
method: "POST",
|
|
30902
30902
|
url: { $join: [{ $config: "baseURL" }, "/v1/batches/", { $: "batchId" }, "/requests"] },
|
|
@@ -30912,7 +30912,7 @@ var xai_addRequests_default = {
|
|
|
30912
30912
|
{
|
|
30913
30913
|
value: {
|
|
30914
30914
|
batch_request_id: { $: "@customId" },
|
|
30915
|
-
batch_request: {
|
|
30915
|
+
batch_request: { responses: { $: "@body" } }
|
|
30916
30916
|
}
|
|
30917
30917
|
}
|
|
30918
30918
|
]
|
|
@@ -31264,6 +31264,42 @@ var xai_upload_default = {
|
|
|
31264
31264
|
multipart: [{ name: "file", file: true }, { name: "purpose", value: "assistants" }]
|
|
31265
31265
|
};
|
|
31266
31266
|
|
|
31267
|
+
// src/wire/specs/files/xai.delete.json
|
|
31268
|
+
var xai_delete_default = {
|
|
31269
|
+
id: "xai/files.delete",
|
|
31270
|
+
extends: "openai/files.base",
|
|
31271
|
+
_note: "Same shape as OpenAI's, per xai.upload's note that the surface differs only in the upload purpose. baseURL is supplied by the adapter, so the path is all that is declared here.",
|
|
31272
|
+
envelope: {
|
|
31273
|
+
method: "DELETE",
|
|
31274
|
+
bodyKind: "none",
|
|
31275
|
+
url: { $join: [{ $config: "baseURL" }, "/v1/files/", { $: "remoteId" }] }
|
|
31276
|
+
}
|
|
31277
|
+
};
|
|
31278
|
+
|
|
31279
|
+
// src/wire/specs/files/xai.getInfo.json
|
|
31280
|
+
var xai_getInfo_default = {
|
|
31281
|
+
id: "xai/files.getInfo",
|
|
31282
|
+
extends: "openai/files.base",
|
|
31283
|
+
_note: "Same shape as OpenAI's, per xai.upload's note that the surface differs only in the upload purpose. baseURL is supplied by the adapter, so the path is all that is declared here.",
|
|
31284
|
+
envelope: {
|
|
31285
|
+
method: "GET",
|
|
31286
|
+
bodyKind: "none",
|
|
31287
|
+
url: { $join: [{ $config: "baseURL" }, "/v1/files/", { $: "remoteId" }] }
|
|
31288
|
+
}
|
|
31289
|
+
};
|
|
31290
|
+
|
|
31291
|
+
// src/wire/specs/files/xai.list.json
|
|
31292
|
+
var xai_list_default = {
|
|
31293
|
+
id: "xai/files.list",
|
|
31294
|
+
extends: "openai/files.base",
|
|
31295
|
+
_note: "Same shape as OpenAI's, per xai.upload's note that the surface differs only in the upload purpose. baseURL is supplied by the adapter, so the path is all that is declared here.",
|
|
31296
|
+
envelope: {
|
|
31297
|
+
method: "GET",
|
|
31298
|
+
bodyKind: "none",
|
|
31299
|
+
url: { $join: [{ $config: "baseURL" }, "/v1/files"] }
|
|
31300
|
+
}
|
|
31301
|
+
};
|
|
31302
|
+
|
|
31267
31303
|
// src/wire/specs/files/google.finishUpload.json
|
|
31268
31304
|
var google_finishUpload_default = {
|
|
31269
31305
|
id: "google/files.finishUpload",
|
|
@@ -31504,6 +31540,9 @@ var DELTAS = new Map(
|
|
|
31504
31540
|
google_getInfo_default,
|
|
31505
31541
|
google_list_default,
|
|
31506
31542
|
xai_upload_default,
|
|
31543
|
+
xai_delete_default,
|
|
31544
|
+
xai_getInfo_default,
|
|
31545
|
+
xai_list_default,
|
|
31507
31546
|
google_finishUpload_default,
|
|
31508
31547
|
videos_status_default,
|
|
31509
31548
|
videos_cancel_default,
|
|
@@ -32314,6 +32353,14 @@ var anthropic_default3 = {
|
|
|
32314
32353
|
value: {
|
|
32315
32354
|
$config: "apiVersion"
|
|
32316
32355
|
}
|
|
32356
|
+
},
|
|
32357
|
+
{
|
|
32358
|
+
name: "anthropic-dangerous-direct-browser-access",
|
|
32359
|
+
value: "true",
|
|
32360
|
+
_note: "Only in a browser, where the request is otherwise blocked by CORS. The chat path sends it (messages.ts) and so do the files specs; models.list lost it when this request moved to a spec, because the header lived in the helper's own table.",
|
|
32361
|
+
when: {
|
|
32362
|
+
truthy: "browser"
|
|
32363
|
+
}
|
|
32317
32364
|
}
|
|
32318
32365
|
]
|
|
32319
32366
|
}
|
|
@@ -36590,6 +36637,7 @@ var LLMClient = class {
|
|
|
36590
36637
|
let moderationReport;
|
|
36591
36638
|
const files = [];
|
|
36592
36639
|
const builtinToolCalls = [];
|
|
36640
|
+
const citationsByUrl = /* @__PURE__ */ new Map();
|
|
36593
36641
|
const fetchStream = this.fetchStreamFn;
|
|
36594
36642
|
const queueName = this.queueName;
|
|
36595
36643
|
const priority = this.priority;
|
|
@@ -36643,6 +36691,9 @@ var LLMClient = class {
|
|
|
36643
36691
|
case "file":
|
|
36644
36692
|
files.push(event.file);
|
|
36645
36693
|
break;
|
|
36694
|
+
case "citation":
|
|
36695
|
+
citationsByUrl.set(event.citation.url, event.citation);
|
|
36696
|
+
break;
|
|
36646
36697
|
case "builtin_tool_end":
|
|
36647
36698
|
builtinToolCalls.push({
|
|
36648
36699
|
tool: event.tool,
|
|
@@ -36677,6 +36728,7 @@ var LLMClient = class {
|
|
|
36677
36728
|
media: [],
|
|
36678
36729
|
...files.length ? { files } : {},
|
|
36679
36730
|
...builtinToolCalls.length ? { builtinToolCalls } : {},
|
|
36731
|
+
...citationsByUrl.size ? { citations: [...citationsByUrl.values()] } : {},
|
|
36680
36732
|
...moderationReport ? { moderation: moderationReport } : {},
|
|
36681
36733
|
latencyMs: performance.now() - start,
|
|
36682
36734
|
raw: null
|
|
@@ -38487,6 +38539,87 @@ function pinFor(model, pins) {
|
|
|
38487
38539
|
var ANTHROPIC_MESSAGE_PINS = anthropic_messages_default;
|
|
38488
38540
|
var GOOGLE_GENERATE_PINS = google_generate_default;
|
|
38489
38541
|
|
|
38542
|
+
// src/llm/providers/_shared/citations.ts
|
|
38543
|
+
function fromAnthropic(raw) {
|
|
38544
|
+
const out = [];
|
|
38545
|
+
for (const block of raw.content ?? []) {
|
|
38546
|
+
for (const cite of block.citations ?? []) {
|
|
38547
|
+
const url = cite.url;
|
|
38548
|
+
if (url) {
|
|
38549
|
+
out.push({
|
|
38550
|
+
url,
|
|
38551
|
+
...cite.title ? { title: cite.title } : {},
|
|
38552
|
+
// Anthropic is the only provider that reports the cited passage.
|
|
38553
|
+
...cite.cited_text ? { text: cite.cited_text } : {}
|
|
38554
|
+
});
|
|
38555
|
+
}
|
|
38556
|
+
}
|
|
38557
|
+
}
|
|
38558
|
+
return out;
|
|
38559
|
+
}
|
|
38560
|
+
function fromGoogle(raw) {
|
|
38561
|
+
const out = [];
|
|
38562
|
+
for (const candidate of raw.candidates ?? []) {
|
|
38563
|
+
const grounding = candidate.groundingMetadata ?? {};
|
|
38564
|
+
for (const chunk of grounding.groundingChunks ?? []) {
|
|
38565
|
+
const web = chunk.web ?? {};
|
|
38566
|
+
const uri = web.uri;
|
|
38567
|
+
if (uri) out.push({ url: uri, ...web.title ? { title: web.title } : {} });
|
|
38568
|
+
}
|
|
38569
|
+
}
|
|
38570
|
+
return out;
|
|
38571
|
+
}
|
|
38572
|
+
function fromResponses(raw) {
|
|
38573
|
+
const out = [];
|
|
38574
|
+
for (const item of raw.output ?? []) {
|
|
38575
|
+
for (const part of item.content ?? []) {
|
|
38576
|
+
for (const note of part.annotations ?? []) {
|
|
38577
|
+
if (note.type === "url_citation" && note.url) {
|
|
38578
|
+
out.push({
|
|
38579
|
+
url: note.url,
|
|
38580
|
+
...note.title ? { title: note.title } : {}
|
|
38581
|
+
});
|
|
38582
|
+
}
|
|
38583
|
+
}
|
|
38584
|
+
}
|
|
38585
|
+
}
|
|
38586
|
+
return out;
|
|
38587
|
+
}
|
|
38588
|
+
function fromCompletions(raw) {
|
|
38589
|
+
const out = [];
|
|
38590
|
+
const message = (raw.choices ?? [])[0]?.message ?? {};
|
|
38591
|
+
for (const note of message.annotations ?? []) {
|
|
38592
|
+
const detail = note.url_citation ?? note;
|
|
38593
|
+
if (note.type === "url_citation" && detail.url) {
|
|
38594
|
+
out.push({
|
|
38595
|
+
url: detail.url,
|
|
38596
|
+
...detail.title ? { title: detail.title } : {}
|
|
38597
|
+
});
|
|
38598
|
+
}
|
|
38599
|
+
}
|
|
38600
|
+
for (const url of raw.citations ?? []) {
|
|
38601
|
+
if (typeof url === "string") out.push({ url });
|
|
38602
|
+
}
|
|
38603
|
+
return out;
|
|
38604
|
+
}
|
|
38605
|
+
var READERS = {
|
|
38606
|
+
messages: fromAnthropic,
|
|
38607
|
+
generate: fromGoogle,
|
|
38608
|
+
responses: fromResponses,
|
|
38609
|
+
completions: fromCompletions
|
|
38610
|
+
// `interactions` is DELIBERATELY absent. Google's Interactions API returns a
|
|
38611
|
+
// `steps[]` machine, not `candidates[]`, so `fromGoogle` would read a key that
|
|
38612
|
+
// is never there -- and no recorded Interactions response carries grounding, so
|
|
38613
|
+
// there is nothing to write a reader against. Guessing the shape would produce
|
|
38614
|
+
// a reader that is confidently wrong and passes a test built from the same
|
|
38615
|
+
// guess. The adapter is wired anyway: when the shape is measured, only this
|
|
38616
|
+
// table changes.
|
|
38617
|
+
};
|
|
38618
|
+
function extractCitations(api, raw) {
|
|
38619
|
+
if (raw == null || typeof raw !== "object") return [];
|
|
38620
|
+
return READERS[api]?.(raw) ?? [];
|
|
38621
|
+
}
|
|
38622
|
+
|
|
38490
38623
|
// src/llm/providers/_shared/builtin-tools.ts
|
|
38491
38624
|
var NATIVE_TO_UNIFIED = {
|
|
38492
38625
|
// OpenAI / xAI Responses output-item types
|
|
@@ -38705,6 +38838,7 @@ var AnthropicAdapter = class {
|
|
|
38705
38838
|
model_context_window_exceeded: "length",
|
|
38706
38839
|
refusal: "content_filter"
|
|
38707
38840
|
});
|
|
38841
|
+
const citations = extractCitations("messages", raw);
|
|
38708
38842
|
return {
|
|
38709
38843
|
id: r.id,
|
|
38710
38844
|
model: r.model,
|
|
@@ -38714,6 +38848,7 @@ var AnthropicAdapter = class {
|
|
|
38714
38848
|
text: content.filter((p) => p.type === "text").map((p) => p.text).join(""),
|
|
38715
38849
|
toolCalls,
|
|
38716
38850
|
media: [],
|
|
38851
|
+
...citations.length ? { citations } : {},
|
|
38717
38852
|
...files.length ? { files } : {},
|
|
38718
38853
|
...builtinToolCalls.length ? { builtinToolCalls } : {},
|
|
38719
38854
|
thinking,
|
|
@@ -38741,6 +38876,20 @@ var AnthropicAdapter = class {
|
|
|
38741
38876
|
if (delta.type === "text_delta") return [{ type: "text", text: delta.text }];
|
|
38742
38877
|
if (delta.type === "thinking_delta")
|
|
38743
38878
|
return [{ type: "thinking", text: delta.thinking }];
|
|
38879
|
+
if (delta.type === "citations_delta") {
|
|
38880
|
+
const cite = delta.citation ?? {};
|
|
38881
|
+
const url = cite.url;
|
|
38882
|
+
return url ? [
|
|
38883
|
+
{
|
|
38884
|
+
type: "citation",
|
|
38885
|
+
citation: {
|
|
38886
|
+
url,
|
|
38887
|
+
...cite.title ? { title: cite.title } : {},
|
|
38888
|
+
...cite.cited_text ? { text: cite.cited_text } : {}
|
|
38889
|
+
}
|
|
38890
|
+
}
|
|
38891
|
+
] : [];
|
|
38892
|
+
}
|
|
38744
38893
|
if (delta.type === "input_json_delta") {
|
|
38745
38894
|
if (state.current) {
|
|
38746
38895
|
state.current.json += delta.partial_json ?? "";
|
|
@@ -39291,6 +39440,7 @@ var GoogleAdapter = class {
|
|
|
39291
39440
|
builtinToolCalls.push({ tool: "web_fetch", ...typeof url === "string" ? { url } : {} });
|
|
39292
39441
|
}
|
|
39293
39442
|
}
|
|
39443
|
+
const citations = extractCitations("generate", raw);
|
|
39294
39444
|
return {
|
|
39295
39445
|
// generateContent DOES return an id — `responseId`, at the top level. The
|
|
39296
39446
|
// fallback stays for older payloads, but minting one unconditionally made
|
|
@@ -39306,6 +39456,7 @@ var GoogleAdapter = class {
|
|
|
39306
39456
|
toolCalls,
|
|
39307
39457
|
thinking,
|
|
39308
39458
|
media,
|
|
39459
|
+
...citations.length ? { citations } : {},
|
|
39309
39460
|
...files.length ? { files } : {},
|
|
39310
39461
|
...builtinToolCalls.length ? { builtinToolCalls } : {},
|
|
39311
39462
|
latencyMs,
|
|
@@ -39390,6 +39541,18 @@ var GoogleAdapter = class {
|
|
|
39390
39541
|
events.push({ type: "tool_call_end", id: "" });
|
|
39391
39542
|
}
|
|
39392
39543
|
}
|
|
39544
|
+
for (const chunk of candidate.groundingMetadata?.groundingChunks ?? []) {
|
|
39545
|
+
const web = chunk.web ?? {};
|
|
39546
|
+
if (web.uri) {
|
|
39547
|
+
events.push({
|
|
39548
|
+
type: "citation",
|
|
39549
|
+
citation: {
|
|
39550
|
+
url: web.uri,
|
|
39551
|
+
...web.title ? { title: web.title } : {}
|
|
39552
|
+
}
|
|
39553
|
+
});
|
|
39554
|
+
}
|
|
39555
|
+
}
|
|
39393
39556
|
if (candidate.groundingMetadata && !state.webSearchEmitted) {
|
|
39394
39557
|
state.webSearchEmitted = true;
|
|
39395
39558
|
const q = candidate.groundingMetadata.webSearchQueries?.[0];
|
|
@@ -39612,6 +39775,7 @@ var GoogleInteractionsAdapter = class {
|
|
|
39612
39775
|
queued: "pending",
|
|
39613
39776
|
in_progress: "pending"
|
|
39614
39777
|
});
|
|
39778
|
+
const citations = extractCitations("interactions", raw);
|
|
39615
39779
|
return {
|
|
39616
39780
|
id: r.id ?? crypto.randomUUID(),
|
|
39617
39781
|
model: "",
|
|
@@ -39621,6 +39785,7 @@ var GoogleInteractionsAdapter = class {
|
|
|
39621
39785
|
text,
|
|
39622
39786
|
toolCalls,
|
|
39623
39787
|
media,
|
|
39788
|
+
...citations.length ? { citations } : {},
|
|
39624
39789
|
thinking,
|
|
39625
39790
|
latencyMs,
|
|
39626
39791
|
raw
|
|
@@ -40962,13 +41127,14 @@ var OpenAIAdapter = class {
|
|
|
40962
41127
|
content.push(parsed);
|
|
40963
41128
|
toolCalls.push(parsed);
|
|
40964
41129
|
}
|
|
40965
|
-
const finishReason = extractFinishReason(
|
|
40966
|
-
|
|
40967
|
-
|
|
40968
|
-
|
|
40969
|
-
);
|
|
41130
|
+
const finishReason = extractFinishReason(toolCalls.length > 0, choice.finish_reason, {
|
|
41131
|
+
tool_calls: "tool_use",
|
|
41132
|
+
length: "length",
|
|
41133
|
+
content_filter: "content_filter"
|
|
41134
|
+
});
|
|
40970
41135
|
const reasoningContent = message.reasoning_content ?? null;
|
|
40971
41136
|
const moderation = parseNativeModeration(r.moderation);
|
|
41137
|
+
const citations = extractCitations("completions", raw);
|
|
40972
41138
|
return {
|
|
40973
41139
|
id: r.id,
|
|
40974
41140
|
model: r.model,
|
|
@@ -40978,6 +41144,7 @@ var OpenAIAdapter = class {
|
|
|
40978
41144
|
text,
|
|
40979
41145
|
toolCalls,
|
|
40980
41146
|
media,
|
|
41147
|
+
...citations.length ? { citations } : {},
|
|
40981
41148
|
thinking: reasoningContent,
|
|
40982
41149
|
...moderation ? { moderation } : {},
|
|
40983
41150
|
latencyMs,
|
|
@@ -41010,6 +41177,18 @@ var OpenAIAdapter = class {
|
|
|
41010
41177
|
if (delta.content) {
|
|
41011
41178
|
events.push({ type: "text", text: delta.content });
|
|
41012
41179
|
}
|
|
41180
|
+
for (const note of delta.annotations ?? []) {
|
|
41181
|
+
const detail = note.url_citation ?? note;
|
|
41182
|
+
if (note.type === "url_citation" && detail.url) {
|
|
41183
|
+
events.push({
|
|
41184
|
+
type: "citation",
|
|
41185
|
+
citation: {
|
|
41186
|
+
url: detail.url,
|
|
41187
|
+
...detail.title ? { title: detail.title } : {}
|
|
41188
|
+
}
|
|
41189
|
+
});
|
|
41190
|
+
}
|
|
41191
|
+
}
|
|
41013
41192
|
const toolIdByIndex = state?.toolIdByIndex ?? /* @__PURE__ */ new Map();
|
|
41014
41193
|
const toolCalls = delta.tool_calls ?? [];
|
|
41015
41194
|
for (const tc of toolCalls) {
|
|
@@ -41920,6 +42099,7 @@ var OpenAIResponsesAdapter = class {
|
|
|
41920
42099
|
if (text && content.length === 0) content.push({ type: "text", text });
|
|
41921
42100
|
}
|
|
41922
42101
|
const moderation = parseNativeModeration(r.moderation);
|
|
42102
|
+
const citations = extractCitations("responses", raw);
|
|
41923
42103
|
return {
|
|
41924
42104
|
id: r.id,
|
|
41925
42105
|
model: r.model ?? "",
|
|
@@ -41930,6 +42110,7 @@ var OpenAIResponsesAdapter = class {
|
|
|
41930
42110
|
toolCalls,
|
|
41931
42111
|
thinking,
|
|
41932
42112
|
media,
|
|
42113
|
+
...citations.length ? { citations } : {},
|
|
41933
42114
|
...files.length ? { files } : {},
|
|
41934
42115
|
...builtinToolCalls.length ? { builtinToolCalls } : {},
|
|
41935
42116
|
...moderation ? { moderation } : {},
|
|
@@ -41942,6 +42123,19 @@ var OpenAIResponsesAdapter = class {
|
|
|
41942
42123
|
const data = sseJson(event);
|
|
41943
42124
|
const type = data.type;
|
|
41944
42125
|
const events = [];
|
|
42126
|
+
if (type === "response.output_text.annotation.added") {
|
|
42127
|
+
const note = data.annotation ?? {};
|
|
42128
|
+
if (note.type === "url_citation" && note.url) {
|
|
42129
|
+
events.push({
|
|
42130
|
+
type: "citation",
|
|
42131
|
+
citation: {
|
|
42132
|
+
url: note.url,
|
|
42133
|
+
...note.title ? { title: note.title } : {}
|
|
42134
|
+
}
|
|
42135
|
+
});
|
|
42136
|
+
}
|
|
42137
|
+
return events;
|
|
42138
|
+
}
|
|
41945
42139
|
if (type === "response.output_text.delta") {
|
|
41946
42140
|
const itemId = data.item_id;
|
|
41947
42141
|
const phase = itemId ? phaseByItem?.get(itemId) : void 0;
|
|
@@ -42262,10 +42456,6 @@ var XAIBatchAdapter = class {
|
|
|
42262
42456
|
throw new Error(`xAI batch create failed: ${JSON.stringify(createRes.body)}`);
|
|
42263
42457
|
const batch2 = createRes.body ?? {};
|
|
42264
42458
|
const batchId = batch2.batch_id ?? batch2.id;
|
|
42265
|
-
const _batchRequests = requests.map((r) => ({
|
|
42266
|
-
batch_request_id: r.customId,
|
|
42267
|
-
batch_request: { endpoint: "responses", body: r.body }
|
|
42268
|
-
}));
|
|
42269
42459
|
const addRes = await fetch2(this.buildAddRequestsRequest(batchId, requests));
|
|
42270
42460
|
if (addRes.status >= 400)
|
|
42271
42461
|
throw new Error(`xAI batch add requests failed: ${JSON.stringify(addRes.body)}`);
|
|
@@ -43780,7 +43970,8 @@ function makeStepState() {
|
|
|
43780
43970
|
stepToolCalls: [],
|
|
43781
43971
|
toolCallAccum: /* @__PURE__ */ new Map(),
|
|
43782
43972
|
stepUsage: emptyUsage(),
|
|
43783
|
-
stepFinishReason: "stop"
|
|
43973
|
+
stepFinishReason: "stop",
|
|
43974
|
+
stepCitations: /* @__PURE__ */ new Map()
|
|
43784
43975
|
};
|
|
43785
43976
|
}
|
|
43786
43977
|
function accumulateStreamEvent(event, state) {
|
|
@@ -43814,6 +44005,9 @@ function accumulateStreamEvent(event, state) {
|
|
|
43814
44005
|
}
|
|
43815
44006
|
return null;
|
|
43816
44007
|
}
|
|
44008
|
+
case "citation":
|
|
44009
|
+
state.stepCitations.set(event.citation.url, event.citation);
|
|
44010
|
+
return null;
|
|
43817
44011
|
case "usage":
|
|
43818
44012
|
state.stepUsage = event.usage;
|
|
43819
44013
|
return null;
|
|
@@ -43871,6 +44065,7 @@ function buildStepResponse(state, model, stepStart) {
|
|
|
43871
44065
|
toolCalls: state.stepToolCalls,
|
|
43872
44066
|
thinking: state.stepThinking || null,
|
|
43873
44067
|
media: [],
|
|
44068
|
+
...state.stepCitations.size ? { citations: [...state.stepCitations.values()] } : {},
|
|
43874
44069
|
latencyMs: stepLatency,
|
|
43875
44070
|
raw: null
|
|
43876
44071
|
};
|
|
@@ -44212,6 +44407,8 @@ var AgentLoop = class _AgentLoop {
|
|
|
44212
44407
|
// final LLM response (e.g. code-execution files produced during the run).
|
|
44213
44408
|
...last?.files ? { files: last.files } : {},
|
|
44214
44409
|
...last?.builtinToolCalls ? { builtinToolCalls: last.builtinToolCalls } : {},
|
|
44410
|
+
// Every step's sources, not just the final step's — see the accumulator.
|
|
44411
|
+
...args.citations.length ? { citations: args.citations } : {},
|
|
44215
44412
|
...last?.moderation ? { moderation: last.moderation } : {},
|
|
44216
44413
|
latencyMs: performance.now() - args.startPerf,
|
|
44217
44414
|
raw: args.raw
|
|
@@ -44278,6 +44475,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44278
44475
|
let stepCount = 0;
|
|
44279
44476
|
let toolCallCount = 0;
|
|
44280
44477
|
let lastResponse = null;
|
|
44478
|
+
const citationsByUrl = /* @__PURE__ */ new Map();
|
|
44281
44479
|
let reason = "done";
|
|
44282
44480
|
let errorMsg;
|
|
44283
44481
|
let caughtError;
|
|
@@ -44319,6 +44517,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44319
44517
|
const stepLatency = performance.now() - stepStart;
|
|
44320
44518
|
totalLlmTimeMs += stepLatency;
|
|
44321
44519
|
addUsage(totalUsage, lastResponse.usage);
|
|
44520
|
+
for (const cite of lastResponse.citations ?? []) citationsByUrl.set(cite.url, cite);
|
|
44322
44521
|
if (this._reflectRetry?.handles(lastResponse.finishReason)) {
|
|
44323
44522
|
const verdict = this._reflectRetry.recordFailure();
|
|
44324
44523
|
await this.hooks.emit("onWarning", {
|
|
@@ -44426,6 +44625,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44426
44625
|
totalUsage,
|
|
44427
44626
|
lastResponse,
|
|
44428
44627
|
media: lastResponse?.media ?? [],
|
|
44628
|
+
citations: [...citationsByUrl.values()],
|
|
44429
44629
|
raw: lastResponse?.raw ?? null
|
|
44430
44630
|
});
|
|
44431
44631
|
await this.settleRun({
|
|
@@ -44471,6 +44671,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44471
44671
|
let finalText = "";
|
|
44472
44672
|
let finalContent = [];
|
|
44473
44673
|
let lastResponse = null;
|
|
44674
|
+
const citationsByUrl = /* @__PURE__ */ new Map();
|
|
44474
44675
|
let reason = "done";
|
|
44475
44676
|
let errorMsg;
|
|
44476
44677
|
let caughtError;
|
|
@@ -44525,6 +44726,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44525
44726
|
finalContent = content;
|
|
44526
44727
|
finalText = state.stepText;
|
|
44527
44728
|
lastResponse = stepResponse;
|
|
44729
|
+
for (const cite of stepResponse.citations ?? []) citationsByUrl.set(cite.url, cite);
|
|
44528
44730
|
this._history.append(
|
|
44529
44731
|
{
|
|
44530
44732
|
...buildAssistantMessage(lastResponse, {
|
|
@@ -44608,6 +44810,7 @@ var AgentLoop = class _AgentLoop {
|
|
|
44608
44810
|
finalContent,
|
|
44609
44811
|
totalUsage,
|
|
44610
44812
|
lastResponse,
|
|
44813
|
+
citations: [...citationsByUrl.values()],
|
|
44611
44814
|
// The streaming path already emitted media as events, and never holds a
|
|
44612
44815
|
// raw provider payload.
|
|
44613
44816
|
media: [],
|
|
@@ -54601,37 +54804,26 @@ function listModels(opts = {}) {
|
|
|
54601
54804
|
var LIVE = {
|
|
54602
54805
|
openai: {
|
|
54603
54806
|
url: "https://api.openai.com/v1/models",
|
|
54604
|
-
headers: (k) => ({ authorization: `Bearer ${k}` }),
|
|
54605
54807
|
items: (b) => b.data ?? [],
|
|
54606
54808
|
id: (m) => m.id
|
|
54607
54809
|
},
|
|
54608
54810
|
openrouter: {
|
|
54609
54811
|
url: "https://openrouter.ai/api/v1/models",
|
|
54610
|
-
headers: (k) => ({ authorization: `Bearer ${k}` }),
|
|
54611
54812
|
items: (b) => b.data ?? [],
|
|
54612
54813
|
id: (m) => m.id
|
|
54613
54814
|
},
|
|
54614
54815
|
xai: {
|
|
54615
54816
|
url: "https://api.x.ai/v1/models",
|
|
54616
|
-
headers: (k) => ({ authorization: `Bearer ${k}` }),
|
|
54617
54817
|
items: (b) => b.data ?? [],
|
|
54618
54818
|
id: (m) => m.id
|
|
54619
54819
|
},
|
|
54620
54820
|
anthropic: {
|
|
54621
54821
|
url: "https://api.anthropic.com/v1/models",
|
|
54622
|
-
// Mirror the chat adapter: Anthropic rejects browser requests without the
|
|
54623
|
-
// explicit opt-in header, so the /models call must send it too.
|
|
54624
|
-
headers: (k) => ({
|
|
54625
|
-
"x-api-key": k,
|
|
54626
|
-
"anthropic-version": ANTHROPIC_API_VERSION,
|
|
54627
|
-
...isBrowser() ? { "anthropic-dangerous-direct-browser-access": "true" } : {}
|
|
54628
|
-
}),
|
|
54629
54822
|
items: (b) => b.data ?? [],
|
|
54630
54823
|
id: (m) => m.id
|
|
54631
54824
|
},
|
|
54632
54825
|
google: {
|
|
54633
54826
|
url: "https://generativelanguage.googleapis.com/v1beta/models",
|
|
54634
|
-
headers: (k) => ({ "x-goog-api-key": k }),
|
|
54635
54827
|
items: (b) => b.models ?? [],
|
|
54636
54828
|
id: (m) => m.name.replace(/^models\//, "")
|
|
54637
54829
|
}
|
|
@@ -54724,7 +54916,9 @@ async function fetchLiveBody(opts) {
|
|
|
54724
54916
|
const p = (async () => {
|
|
54725
54917
|
const built = buildFromSpec(
|
|
54726
54918
|
utilitySpec(`${opts.provider}/models.list`),
|
|
54727
|
-
|
|
54919
|
+
// Anthropic refuses a browser request without an explicit opt-in header,
|
|
54920
|
+
// and the spec asks for `browser` to decide whether to send it.
|
|
54921
|
+
{ browser: isBrowser() },
|
|
54728
54922
|
makeRegistry({}),
|
|
54729
54923
|
opts.provider,
|
|
54730
54924
|
void 0,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The sources an answer cited, read from four different wire shapes.
|
|
2
|
+
*
|
|
3
|
+
* `builtinToolCalls` already records what the model INVOKED — that it searched,
|
|
4
|
+
* and for what. This is the other half: what it ended up CITING. They are not
|
|
5
|
+
* the same list. A model can run three searches and cite one page, or open a
|
|
6
|
+
* page and cite nothing, and a caller rendering footnotes needs the second
|
|
7
|
+
* list, not the first.
|
|
8
|
+
*
|
|
9
|
+
* Until now this was only reachable by regexing `response.raw`, which is what
|
|
10
|
+
* the web-search example actually did. That is a per-consumer reimplementation
|
|
11
|
+
* of provider knowledge that belongs in the SDK.
|
|
12
|
+
*
|
|
13
|
+
* This function always returns an array. The RESPONSE FIELD it feeds is
|
|
14
|
+
* optional and omitted when empty, like the `files` / `builtinToolCalls` lines
|
|
15
|
+
* it sits beside -- R3 forbids adding a required field to a response type, so
|
|
16
|
+
* the Python port's always-present array is not available here. Callers read
|
|
17
|
+
* `response.citations ?? []`.
|
|
18
|
+
*/
|
|
19
|
+
import type { Citation } from '../../types/response';
|
|
20
|
+
/** Every source the answer cited, or `[]`.
|
|
21
|
+
*
|
|
22
|
+
* `api` is the wire surface (`'messages' | 'generate' | 'interactions' |
|
|
23
|
+
* 'responses' | 'completions'`). An unknown surface yields `[]` rather than
|
|
24
|
+
* throwing: a response we cannot read citations from is still a valid answer. */
|
|
25
|
+
export declare function extractCitations(api: string, raw: unknown): Citation[];
|
|
@@ -20,6 +20,16 @@ export interface CompletionResponse {
|
|
|
20
20
|
* search or code-execution run. A durable trail of what was called (the provider
|
|
21
21
|
* ran them server-side; nothing for the client to execute). Absent when none. */
|
|
22
22
|
builtinToolCalls?: BuiltinToolCall[];
|
|
23
|
+
/** Sources the answer cited, unified across providers.
|
|
24
|
+
*
|
|
25
|
+
* Distinct from `builtinToolCalls`, which records what the model INVOKED: a turn
|
|
26
|
+
* can run three searches and cite one page, or open a page and cite nothing.
|
|
27
|
+
* Rendering footnotes needs this list, not that one.
|
|
28
|
+
*
|
|
29
|
+
* Absent when the model cited nothing — R3: a response type grows by OPTIONAL
|
|
30
|
+
* fields only, so this cannot be the always-present array the Python port
|
|
31
|
+
* exposes. Read it as `response.citations ?? []`. */
|
|
32
|
+
citations?: Citation[];
|
|
23
33
|
/** Inline-moderation outcome, when the `moderation` request option was used.
|
|
24
34
|
* Report-only: present for observability; it never blocks the call. Absent when
|
|
25
35
|
* moderation was not requested. */
|
|
@@ -58,6 +68,19 @@ export interface FileOutput {
|
|
|
58
68
|
* Absent for providers that don't need extra context. */
|
|
59
69
|
ref?: Record<string, unknown>;
|
|
60
70
|
}
|
|
71
|
+
/** A source the answer cited.
|
|
72
|
+
*
|
|
73
|
+
* Four providers report these four different ways — Anthropic on the text block,
|
|
74
|
+
* Google in `groundingMetadata`, OpenAI as annotations, xAI as bare top-level URLs.
|
|
75
|
+
* The differences stop at the adapter. */
|
|
76
|
+
export interface Citation {
|
|
77
|
+
url: string;
|
|
78
|
+
/** Page title, when the provider supplies one. */
|
|
79
|
+
title?: string;
|
|
80
|
+
/** The passage the source supports. Only Anthropic reports this today; absent
|
|
81
|
+
* elsewhere rather than faked from the answer text. */
|
|
82
|
+
text?: string;
|
|
83
|
+
}
|
|
61
84
|
/** A hosted builtin tool the model invoked (provider-run), with its inputs/outputs. */
|
|
62
85
|
export interface BuiltinToolCall {
|
|
63
86
|
/** Unified tool name: `'web_search'` | `'code_interpreter'` | … (normalized from
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Universal streaming event types. */
|
|
2
2
|
import type { ModerationEntry } from '../moderation/types';
|
|
3
3
|
import type { AssistantPhase } from './messages';
|
|
4
|
-
import type { FileOutput, Usage } from './response';
|
|
4
|
+
import type { Citation, FileOutput, Usage } from './response';
|
|
5
5
|
export type MediaStreamType = 'image' | 'audio' | 'video';
|
|
6
6
|
export type StreamEvent =
|
|
7
7
|
/** `itemId` identifies WHICH output item a delta belongs to, when the provider reports
|
|
@@ -62,6 +62,19 @@ export type StreamEvent =
|
|
|
62
62
|
type: 'file';
|
|
63
63
|
file: FileOutput;
|
|
64
64
|
}
|
|
65
|
+
/** The answer cited a source. Emitted as the citation arrives, which is NOT
|
|
66
|
+
* when the search ran: a provider searches early and cites while it writes, so
|
|
67
|
+
* these interleave with `text` deltas. Distinct from `builtin_tool_end`, which
|
|
68
|
+
* reports the search itself.
|
|
69
|
+
*
|
|
70
|
+
* Measured shapes: Anthropic `citations_delta`, OpenAI/xAI Responses
|
|
71
|
+
* `response.output_text.annotation.added`, chat-completions `delta.annotations`,
|
|
72
|
+
* Google's populated `groundingMetadata` chunk. Also collected onto the streamed
|
|
73
|
+
* final response's `citations`, deduped by url. */
|
|
74
|
+
| {
|
|
75
|
+
type: 'citation';
|
|
76
|
+
citation: Citation;
|
|
77
|
+
}
|
|
65
78
|
/** A hosted (provider-run) builtin tool began executing server-side — e.g. the
|
|
66
79
|
* model started a web search or code-execution run. `tool` is the unified name
|
|
67
80
|
* (`'web_search'` | `'code_interpreter'` | …). Informational progress: unlike
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@combycode/llm-sdk",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Unified, pluggable AI SDK for accessing the LLMs of every major provider (Anthropic, OpenAI, Google, xAI, OpenRouter) through one API. Cross-environment: Node, Bun, and the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"prepublishOnly": "bun run build",
|
|
35
35
|
"test": "bun test",
|
|
36
36
|
"test:unit": "bun test tests/unit",
|
|
37
|
+
"test:coverage": "bun test tests/unit --coverage",
|
|
38
|
+
"coverage:gate": "bun run scripts/coverage-gate.ts",
|
|
37
39
|
"test:integration": "bun test tests/integration",
|
|
38
40
|
"test:helpers": "bun test tests/helpers",
|
|
39
41
|
"test:live": "bun test tests/live",
|