@combycode/llm-sdk 3.1.0 → 3.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 +282 -0
- package/dist/agent/loop-step-state.d.ts +5 -1
- package/dist/catalog/catalog.d.ts +12 -0
- package/dist/index.browser.js +3980 -1165
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3980 -1165
- package/dist/llm/client-internal.d.ts +3 -1
- package/dist/llm/client.d.ts +10 -0
- package/dist/llm/providers/_shared/citations.d.ts +25 -0
- package/dist/llm/providers/anthropic/messages.d.ts +32 -7
- package/dist/llm/providers/anthropic/response-registry.d.ts +2 -0
- package/dist/llm/providers/anthropic/stream-registry.d.ts +2 -0
- package/dist/llm/providers/google/generate.d.ts +7 -8
- package/dist/llm/providers/google/interactions-registry.d.ts +2 -0
- package/dist/llm/providers/google/interactions-stream-registry.d.ts +2 -0
- package/dist/llm/providers/google/interactions.d.ts +7 -5
- package/dist/llm/providers/google/response-registry.d.ts +2 -0
- package/dist/llm/providers/google/stream-registry.d.ts +2 -0
- package/dist/llm/providers/openai/completions.d.ts +21 -3
- package/dist/llm/providers/openai/response-registry.d.ts +2 -0
- package/dist/llm/providers/openai/responses-registry.d.ts +2 -0
- package/dist/llm/providers/openai/responses-stream-registry.d.ts +2 -0
- package/dist/llm/providers/openai/responses.d.ts +31 -3
- package/dist/llm/providers/openai/stream-registry.d.ts +2 -0
- package/dist/llm/providers/openrouter/completions.d.ts +11 -7
- package/dist/llm/providers/openrouter/response-registry.d.ts +2 -0
- package/dist/llm/providers/openrouter/stream-registry.d.ts +2 -0
- package/dist/llm/providers/response-registries.d.ts +5 -0
- package/dist/llm/providers/xai/completions.d.ts +1 -4
- package/dist/llm/providers/xai/responses-registry.d.ts +2 -0
- package/dist/llm/providers/xai/responses.d.ts +12 -0
- package/dist/llm/providers/xai/stream-registry.d.ts +2 -0
- package/dist/llm/types/response.d.ts +23 -0
- package/dist/llm/types/stream.d.ts +14 -1
- package/dist/plugins/context-guard/facts.d.ts +9 -0
- package/dist/plugins/context-guard/tools.d.ts +3 -0
- package/dist/plugins/context-guard/types.d.ts +5 -0
- package/dist/util/audio-mime.d.ts +16 -0
- package/dist/util/compare.d.ts +13 -0
- package/dist/wire/interpreter.d.ts +2 -0
- package/dist/wire/response-interpreter.d.ts +156 -0
- package/dist/wire/response-specs.d.ts +7 -0
- package/dist/wire/stream-interpreter.d.ts +94 -0
- package/dist/wire/stream-specs.d.ts +8 -0
- package/package.json +6 -4
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** Build a unified response from a provider body, driven by a spec.
|
|
2
|
+
*
|
|
3
|
+
* The request side has been spec-driven since 3.0.0; the parse side is seven
|
|
4
|
+
* hand-written `parseResponse` implementations doing the same four things in
|
|
5
|
+
* four different spellings. This is the other half.
|
|
6
|
+
*
|
|
7
|
+
* -- what is reused, and why that matters ----------------------------------
|
|
8
|
+
* Everything except classification. `evalTemplate`, `evalCond`, `$`, `$map`,
|
|
9
|
+
* `$call`, `$table`, `$join`, `$when` and `$default` come from the request
|
|
10
|
+
* interpreter unchanged, because that evaluator never cared what the root
|
|
11
|
+
* object was: it resolves paths against `ctx.req`, and nothing in it is
|
|
12
|
+
* request-shaped. Handing it a response body costs nothing, and means the
|
|
13
|
+
* Python port inherits the whole evaluator it has already transposed.
|
|
14
|
+
*
|
|
15
|
+
* -- the one genuinely new thing -------------------------------------------
|
|
16
|
+
* Requests map a tree onto another tree. Responses must first CLASSIFY: every
|
|
17
|
+
* provider returns a heterogeneous array (Anthropic `content[]`, OpenAI
|
|
18
|
+
* `output[]`, Google `parts[]`) whose elements are discriminated by a type
|
|
19
|
+
* field and fan out to different destinations. `$map` cannot express that,
|
|
20
|
+
* because one element may need to land in TWO places at once.
|
|
21
|
+
*
|
|
22
|
+
* Hence `collect`: walk an array, switch on a discriminator, emit into named
|
|
23
|
+
* accumulators. `emit: ['content', 'toolCalls']` places the SAME object
|
|
24
|
+
* reference in both, which is what the hand-written adapters do today
|
|
25
|
+
* (`content.push(tc); toolCalls.push(tc)`) and is load-bearing: a consumer that
|
|
26
|
+
* mutates `response.toolCalls[0]` sees it in `response.content` too.
|
|
27
|
+
*
|
|
28
|
+
* -- the root object -------------------------------------------------------
|
|
29
|
+
* Paths resolve against `{ raw, out }`, in BOTH phases:
|
|
30
|
+
*
|
|
31
|
+
* { "$": "raw.stop_reason" } the provider body
|
|
32
|
+
* { "$map": "out.content" } what has been collected so far
|
|
33
|
+
* { "$": "@text" } the block currently being classified
|
|
34
|
+
*
|
|
35
|
+
* `out` is readable during collection on purpose: Anthropic attaches a tool
|
|
36
|
+
* result to the builtin call it belongs to by matching `tool_use_id` against
|
|
37
|
+
* calls already collected, and that is a lookup into `out`, not into `raw`.
|
|
38
|
+
*/
|
|
39
|
+
import { type Cond, type Json, type Registry } from './interpreter';
|
|
40
|
+
/** How an accumulator behaves. Declared up front so that "absent" and "empty"
|
|
41
|
+
* are a decision in the spec rather than an accident of what the body held:
|
|
42
|
+
* `content` is an array even when empty, `files` is absent unless the turn
|
|
43
|
+
* produced one, and a response type grows by OPTIONAL fields only. */
|
|
44
|
+
export interface AccumulatorDecl {
|
|
45
|
+
kind: 'array' | 'scalar';
|
|
46
|
+
/** Arrays: drop the key entirely when nothing landed in it. */
|
|
47
|
+
omitEmpty?: boolean;
|
|
48
|
+
/** Scalars: the value when nothing was emitted. `thinking` is null, not absent. */
|
|
49
|
+
default?: Json;
|
|
50
|
+
/** Working state, never assembled into the result.
|
|
51
|
+
*
|
|
52
|
+
* OpenAI's `program` items must carry the reasoning items that preceded them
|
|
53
|
+
* in the same output -- the program cannot be sent back without them -- so
|
|
54
|
+
* those items are collected as they go and read by a later block. They are
|
|
55
|
+
* scaffolding for the build, not a field of the response. */
|
|
56
|
+
internal?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface EmitRule {
|
|
59
|
+
/** Accumulator name, or several. Several means ONE evaluated value placed in
|
|
60
|
+
* each -- the same reference, not copies. Omitted only with `effect`. */
|
|
61
|
+
emit?: string | string[];
|
|
62
|
+
/** `push` appends one value (default); `concat` splices an evaluated ARRAY in,
|
|
63
|
+
* for a block that yields several (one Anthropic code-execution result can
|
|
64
|
+
* carry more than one output file); `scalar` assigns, last write wins. */
|
|
65
|
+
mode?: 'push' | 'concat' | 'scalar';
|
|
66
|
+
/** Extra guard beyond the discriminator match. */
|
|
67
|
+
when?: Cond;
|
|
68
|
+
as?: Json;
|
|
69
|
+
/** A named effect run for this block INSTEAD of emitting.
|
|
70
|
+
*
|
|
71
|
+
* Some blocks modify what is already collected rather than adding to it:
|
|
72
|
+
* Anthropic's `*_tool_result` attaches its stdout to the `server_tool_use`
|
|
73
|
+
* it belongs to, matched on `tool_use_id`. That is a lookup into `out` and a
|
|
74
|
+
* write to an object already in it, which no amount of emitting expresses.
|
|
75
|
+
* The effect receives the block as `ctx.item` and the accumulators at
|
|
76
|
+
* `ctx.req.out`. */
|
|
77
|
+
effect?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface CollectRule {
|
|
80
|
+
/** Path to the array to walk, e.g. `raw.content`. A missing or non-array value
|
|
81
|
+
* is not an error: a response with no content is a normal response. */
|
|
82
|
+
from: string;
|
|
83
|
+
/** Field on each element that selects the case, e.g. `type`.
|
|
84
|
+
*
|
|
85
|
+
* OPTIONAL, because not every provider discriminates by a field. OpenAI's
|
|
86
|
+
* `tool_calls` is homogeneous -- every element is a tool call -- and Google's
|
|
87
|
+
* `parts[]` discriminates by WHICH KEY EXISTS (`text` vs `functionCall` vs
|
|
88
|
+
* `inlineData`) rather than by a type tag. With no `match`, every element
|
|
89
|
+
* takes `default`, whose rules carry their own `when`. */
|
|
90
|
+
match?: string;
|
|
91
|
+
cases?: Record<string, EmitRule | EmitRule[]>;
|
|
92
|
+
/** Elements matching no case. Omitted means ignore them, which is the right
|
|
93
|
+
* default: providers add block types continuously, and an unknown one must
|
|
94
|
+
* not break the whole parse. */
|
|
95
|
+
default?: EmitRule | EmitRule[];
|
|
96
|
+
}
|
|
97
|
+
export interface ResponseFieldRule {
|
|
98
|
+
to: string;
|
|
99
|
+
/** Path in the root object. Mutually exclusive with `value`. */
|
|
100
|
+
from?: string;
|
|
101
|
+
value?: Json;
|
|
102
|
+
when?: Cond;
|
|
103
|
+
/** Used when `from` resolves to undefined. */
|
|
104
|
+
default?: Json;
|
|
105
|
+
}
|
|
106
|
+
export interface ResponseSpec {
|
|
107
|
+
id: string;
|
|
108
|
+
extends?: string;
|
|
109
|
+
accumulators?: Record<string, AccumulatorDecl>;
|
|
110
|
+
fields?: ResponseFieldRule[];
|
|
111
|
+
/** Emits that are not driven by walking an array, run BEFORE `collect`.
|
|
112
|
+
*
|
|
113
|
+
* OpenAI puts the assistant's text at `message.content` and its spoken audio
|
|
114
|
+
* at `message.audio` -- two single values, not elements of anything -- and
|
|
115
|
+
* both must sit in `content` ahead of the tool calls, because order inside
|
|
116
|
+
* `content` is what a consumer renders. */
|
|
117
|
+
seed?: EmitRule[];
|
|
118
|
+
collect?: CollectRule[];
|
|
119
|
+
/** Emits run AFTER `collect`, for what can only be decided once everything is
|
|
120
|
+
* in. OpenAI falls back to the `output_text` convenience field, but only when
|
|
121
|
+
* no message item produced text AND nothing else landed in `content` -- a
|
|
122
|
+
* condition that does not exist until the walk is over. */
|
|
123
|
+
finalize?: EmitRule[];
|
|
124
|
+
/** Computed last, so they can read everything `collect` produced. A derived
|
|
125
|
+
* value overrides an accumulator of the same name. */
|
|
126
|
+
derive?: Record<string, Json>;
|
|
127
|
+
tables?: Record<string, Record<string, Json>>;
|
|
128
|
+
_note?: string;
|
|
129
|
+
}
|
|
130
|
+
/** Flatten a spec's `extends` chain.
|
|
131
|
+
*
|
|
132
|
+
* OpenRouter is Chat Completions plus one rule: a `url_citation` annotation is
|
|
133
|
+
* the only signal that its `:online` search ran. Copying the OpenAI spec to add
|
|
134
|
+
* that rule would leave two files to keep in step, and they would diverge the
|
|
135
|
+
* first time only one was edited -- which is the whole argument for specs.
|
|
136
|
+
*
|
|
137
|
+
* Merge rules, one line each:
|
|
138
|
+
* accumulators / derive / tables merge by key, child wins
|
|
139
|
+
* fields merge by `to`, child replaces, new append
|
|
140
|
+
* seed / collect APPEND, parent first
|
|
141
|
+
*
|
|
142
|
+
* Append rather than merge for the ordered ones, because their order is their
|
|
143
|
+
* meaning: a child adding to `content` adds AFTER what the parent put there.
|
|
144
|
+
*/
|
|
145
|
+
export declare function resolveResponseSpec(id: string, byId: Map<string, ResponseSpec>, seen?: Set<string>): ResponseSpec;
|
|
146
|
+
/** The accumulators, initialised from their declarations. */
|
|
147
|
+
export declare function initAccumulators(decls: Record<string, AccumulatorDecl> | undefined): Record<string, unknown>;
|
|
148
|
+
export declare function emitInto(out: Record<string, unknown>, rule: EmitRule, value: unknown, specId: string): void;
|
|
149
|
+
export interface BuildResponseOptions {
|
|
150
|
+
/** Merged in before `fields`, for values the spec cannot know: `latencyMs`,
|
|
151
|
+
* and `raw` itself. */
|
|
152
|
+
extra?: Record<string, unknown>;
|
|
153
|
+
/** Adapter config, for `$config`. */
|
|
154
|
+
config?: Record<string, unknown>;
|
|
155
|
+
}
|
|
156
|
+
export declare function buildResponse(spec: ResponseSpec, raw: unknown, reg: Registry, opts?: BuildResponseOptions): Record<string, unknown>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ResponseSpec } from './response-interpreter';
|
|
2
|
+
export declare const RESPONSE_SPECS: Map<string, ResponseSpec>;
|
|
3
|
+
/** The spec id for a corpus/runtime target key, e.g. `openai/completions`. */
|
|
4
|
+
export declare const responseSpecId: (target: string) => string;
|
|
5
|
+
/** The spec FLATTENED: `extends` is walked here, so no caller ever sees a delta
|
|
6
|
+
* and mistakes it for the whole thing. */
|
|
7
|
+
export declare function getResponseSpec(id: string): ResponseSpec;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Turn a provider's SSE events into unified stream events, driven by a spec.
|
|
2
|
+
*
|
|
3
|
+
* The buffered interpreter gets the whole body at once and builds one object.
|
|
4
|
+
* A stream arrives in fragments, and the parse is a small state machine: an
|
|
5
|
+
* Anthropic `server_tool_use` accumulates its input JSON across several deltas
|
|
6
|
+
* before it can be paired with the `*_tool_result` that completes it; OpenAI
|
|
7
|
+
* correlates tool-call fragments by index because only the first carries an id;
|
|
8
|
+
* Google emits its hosted-tool events once per stream and has to remember that.
|
|
9
|
+
*
|
|
10
|
+
* -- how little of this is actually new ------------------------------------
|
|
11
|
+
* Measured across the five hand-written parsers: 518 lines of code, of which
|
|
12
|
+
* 38 touch state. The other 93% is dispatch and mapping -- the same thing the
|
|
13
|
+
* buffered specs express -- so this driver is the buffered one with two
|
|
14
|
+
* differences:
|
|
15
|
+
*
|
|
16
|
+
* 1. `out` is created ONCE for the stream, not per call, so an accumulator
|
|
17
|
+
* is how the state machine remembers.
|
|
18
|
+
* 2. One reserved accumulator, `events`, is drained and returned after each
|
|
19
|
+
* SSE event. Emitting a unified event means emitting into it.
|
|
20
|
+
*
|
|
21
|
+
* Everything else -- `EmitRule`, `emit`/`mode`/`when`/`as`/`effect`, the whole
|
|
22
|
+
* evaluator -- is shared verbatim.
|
|
23
|
+
*
|
|
24
|
+
* -- the root object -------------------------------------------------------
|
|
25
|
+
* Paths resolve against `{ raw, out, event }`:
|
|
26
|
+
*
|
|
27
|
+
* { "$": "raw.delta.text" } the parsed event payload
|
|
28
|
+
* { "$": "out.current.tool" } state carried across events
|
|
29
|
+
* { "eq": ["event.name", "ping"] } the SSE envelope, for keep-alives
|
|
30
|
+
*
|
|
31
|
+
* `event.data` is the payload as it arrived, so a spec can match a sentinel
|
|
32
|
+
* like `[DONE]` that is not JSON at all.
|
|
33
|
+
*/
|
|
34
|
+
import { type AccumulatorDecl, type EmitRule } from './response-interpreter';
|
|
35
|
+
import { type Cond, type Json, type Registry } from './interpreter';
|
|
36
|
+
/** The accumulator every stream spec emits into. Declared by the driver, not by
|
|
37
|
+
* the spec, because a stream that cannot emit is not a stream. */
|
|
38
|
+
export declare const EVENTS = "events";
|
|
39
|
+
export interface EventRule {
|
|
40
|
+
/** Guard on the whole event, before any discrimination. */
|
|
41
|
+
when?: Cond;
|
|
42
|
+
/** Handle this rule and then stop: no later rule sees the event.
|
|
43
|
+
*
|
|
44
|
+
* Two shapes need it. Anthropic's `ping` keep-alives mean nothing and must
|
|
45
|
+
* not fall through to the type switch, so the rule has no body at all.
|
|
46
|
+
* OpenAI's moderation and usage-only chunks emit and THEN return early,
|
|
47
|
+
* which is why `stop` fires after the body rather than before it. */
|
|
48
|
+
stop?: boolean;
|
|
49
|
+
/** Walk an array inside the payload and apply this rule to EACH element,
|
|
50
|
+
* with the element as `ctx.item` so `@`-paths address it.
|
|
51
|
+
*
|
|
52
|
+
* Google sends `candidates[0].content.parts[]` on every chunk, and each part
|
|
53
|
+
* is a different kind of thing decided by which key it has -- the same shape
|
|
54
|
+
* `collect` handles on the buffered side. Without this the whole loop would
|
|
55
|
+
* collapse into one effect, which is code where it could be data. */
|
|
56
|
+
each?: string;
|
|
57
|
+
/** Field of the parsed payload (or of the current element, under `each`) that
|
|
58
|
+
* selects the case, e.g. `type`.
|
|
59
|
+
*
|
|
60
|
+
* A LIST means "whichever of these is present", first defined wins. Google's
|
|
61
|
+
* Interactions stream discriminates on `event_type ?? type`, and writing that
|
|
62
|
+
* as two rules would fire both whenever the first was present. */
|
|
63
|
+
match?: string | string[];
|
|
64
|
+
cases?: Record<string, EmitRule | EmitRule[]>;
|
|
65
|
+
/** Payloads matching no case. Omitted means ignore them, which is right:
|
|
66
|
+
* providers add event types continuously and an unknown one must not break
|
|
67
|
+
* the stream. */
|
|
68
|
+
default?: EmitRule | EmitRule[];
|
|
69
|
+
}
|
|
70
|
+
export interface StreamSpec {
|
|
71
|
+
id: string;
|
|
72
|
+
extends?: string;
|
|
73
|
+
/** Carried for the LIFETIME OF THE STREAM. This is the state machine's memory:
|
|
74
|
+
* the open tool call, the map of ids awaiting their result, the emit-once
|
|
75
|
+
* flags. `events` is added by the driver and must not be declared here. */
|
|
76
|
+
state?: Record<string, AccumulatorDecl>;
|
|
77
|
+
/** Applied in order to each SSE event. */
|
|
78
|
+
on?: EventRule[];
|
|
79
|
+
tables?: Record<string, Record<string, Json>>;
|
|
80
|
+
_note?: string;
|
|
81
|
+
}
|
|
82
|
+
/** One SSE event as the transport delivers it. Structural on purpose: `wire`
|
|
83
|
+
* does not import from `network`. */
|
|
84
|
+
export interface StreamInput {
|
|
85
|
+
event?: string;
|
|
86
|
+
data: string;
|
|
87
|
+
}
|
|
88
|
+
export interface StreamBuildOptions {
|
|
89
|
+
/** Adapter config, for `$config`. */
|
|
90
|
+
config?: Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
/** Build a stream parser: call the returned function per SSE event, and it
|
|
93
|
+
* returns the unified events that event produced (often none). */
|
|
94
|
+
export declare function createStreamBuilder(spec: StreamSpec, reg: Registry, opts?: StreamBuildOptions): (event: StreamInput) => unknown[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StreamSpec } from './stream-interpreter';
|
|
2
|
+
export declare const STREAM_SPECS: Map<string, StreamSpec>;
|
|
3
|
+
/** The stream spec id for a corpus/runtime target key. */
|
|
4
|
+
export declare const streamSpecId: (target: string) => string;
|
|
5
|
+
/** Flattened: `extends` is walked here, so no caller sees a delta and mistakes
|
|
6
|
+
* it for the whole thing. Merge rules mirror the response side -- `state` by
|
|
7
|
+
* key, `on` appended parent-first, since rule order is rule meaning. */
|
|
8
|
+
export declare function getStreamSpec(id: string, seen?: Set<string>): StreamSpec;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@combycode/llm-sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.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",
|
|
@@ -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",
|
|
@@ -44,11 +46,11 @@
|
|
|
44
46
|
"check": "biome check src tests",
|
|
45
47
|
"check:fix": "biome check --write src tests",
|
|
46
48
|
"gen:registry": "bun run scripts/gen-wire-registry.ts",
|
|
47
|
-
"gate": "bun run scripts/gate.ts",
|
|
49
|
+
"gate": "bun run scripts/gate.ts --config .quality-gate/config.json",
|
|
48
50
|
"gate:selftest": "node ../../quality-gate/selftest.mjs",
|
|
49
51
|
"record:responses": "bun run scripts/record-responses.ts",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
+
"gate:snapshot": "bun run scripts/gate.ts --config .quality-gate/config.json --only api-snapshot --update",
|
|
53
|
+
"derive:shapes": "bun run scripts/derive-response-shapes.ts"
|
|
52
54
|
},
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@biomejs/biome": "^2.4.13",
|