@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +282 -0
  2. package/dist/agent/loop-step-state.d.ts +5 -1
  3. package/dist/catalog/catalog.d.ts +12 -0
  4. package/dist/index.browser.js +3980 -1165
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +3980 -1165
  7. package/dist/llm/client-internal.d.ts +3 -1
  8. package/dist/llm/client.d.ts +10 -0
  9. package/dist/llm/providers/_shared/citations.d.ts +25 -0
  10. package/dist/llm/providers/anthropic/messages.d.ts +32 -7
  11. package/dist/llm/providers/anthropic/response-registry.d.ts +2 -0
  12. package/dist/llm/providers/anthropic/stream-registry.d.ts +2 -0
  13. package/dist/llm/providers/google/generate.d.ts +7 -8
  14. package/dist/llm/providers/google/interactions-registry.d.ts +2 -0
  15. package/dist/llm/providers/google/interactions-stream-registry.d.ts +2 -0
  16. package/dist/llm/providers/google/interactions.d.ts +7 -5
  17. package/dist/llm/providers/google/response-registry.d.ts +2 -0
  18. package/dist/llm/providers/google/stream-registry.d.ts +2 -0
  19. package/dist/llm/providers/openai/completions.d.ts +21 -3
  20. package/dist/llm/providers/openai/response-registry.d.ts +2 -0
  21. package/dist/llm/providers/openai/responses-registry.d.ts +2 -0
  22. package/dist/llm/providers/openai/responses-stream-registry.d.ts +2 -0
  23. package/dist/llm/providers/openai/responses.d.ts +31 -3
  24. package/dist/llm/providers/openai/stream-registry.d.ts +2 -0
  25. package/dist/llm/providers/openrouter/completions.d.ts +11 -7
  26. package/dist/llm/providers/openrouter/response-registry.d.ts +2 -0
  27. package/dist/llm/providers/openrouter/stream-registry.d.ts +2 -0
  28. package/dist/llm/providers/response-registries.d.ts +5 -0
  29. package/dist/llm/providers/xai/completions.d.ts +1 -4
  30. package/dist/llm/providers/xai/responses-registry.d.ts +2 -0
  31. package/dist/llm/providers/xai/responses.d.ts +12 -0
  32. package/dist/llm/providers/xai/stream-registry.d.ts +2 -0
  33. package/dist/llm/types/response.d.ts +23 -0
  34. package/dist/llm/types/stream.d.ts +14 -1
  35. package/dist/plugins/context-guard/facts.d.ts +9 -0
  36. package/dist/plugins/context-guard/tools.d.ts +3 -0
  37. package/dist/plugins/context-guard/types.d.ts +5 -0
  38. package/dist/util/audio-mime.d.ts +16 -0
  39. package/dist/util/compare.d.ts +13 -0
  40. package/dist/wire/interpreter.d.ts +2 -0
  41. package/dist/wire/response-interpreter.d.ts +156 -0
  42. package/dist/wire/response-specs.d.ts +7 -0
  43. package/dist/wire/stream-interpreter.d.ts +94 -0
  44. package/dist/wire/stream-specs.d.ts +8 -0
  45. 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.1.0",
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
- "derive:shapes": "bun run scripts/derive-response-shapes.ts",
51
- "gate:snapshot": "bun run scripts/gate.ts --only api-snapshot --update"
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",