@combycode/llm-sdk 2.2.2 → 3.0.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 (95) hide show
  1. package/CHANGELOG.md +555 -0
  2. package/MIGRATION.md +130 -0
  3. package/dist/agent/loop-internals.d.ts +4 -0
  4. package/dist/agent/loop.d.ts +35 -0
  5. package/dist/bus/hook-bus.d.ts +13 -3
  6. package/dist/bus/hook-map.d.ts +24 -0
  7. package/dist/{llm/providers → catalog}/builtin-tools.d.ts +1 -1
  8. package/dist/{plugins/model-catalog → catalog}/catalog.d.ts +19 -0
  9. package/dist/helpers/client-pool.d.ts +1 -1
  10. package/dist/helpers/client-resolver.d.ts +22 -3
  11. package/dist/helpers/count-tokens.d.ts +1 -1
  12. package/dist/helpers/engine.d.ts +39 -6
  13. package/dist/helpers/mcp.d.ts +6 -1
  14. package/dist/helpers/models.d.ts +1 -1
  15. package/dist/helpers/one-shot.d.ts +2 -2
  16. package/dist/helpers/select-model.d.ts +1 -1
  17. package/dist/index.browser.js +17611 -4247
  18. package/dist/index.d.ts +5 -5
  19. package/dist/index.js +17611 -4247
  20. package/dist/llm/client-config.d.ts +9 -1
  21. package/dist/llm/client-internal.d.ts +11 -0
  22. package/dist/llm/client.d.ts +6 -0
  23. package/dist/llm/providers/_shared/sse.d.ts +19 -0
  24. package/dist/llm/providers/anthropic/batch.d.ts +13 -2
  25. package/dist/llm/providers/anthropic/constants.d.ts +8 -43
  26. package/dist/llm/providers/anthropic/files.d.ts +13 -2
  27. package/dist/llm/providers/anthropic/messages.d.ts +18 -1
  28. package/dist/llm/providers/google/batch.d.ts +14 -1
  29. package/dist/llm/providers/google/constants.d.ts +6 -15
  30. package/dist/llm/providers/google/embeddings.d.ts +8 -1
  31. package/dist/llm/providers/google/files.d.ts +34 -1
  32. package/dist/llm/providers/google/generate.d.ts +15 -1
  33. package/dist/llm/providers/google/interactions.d.ts +5 -1
  34. package/dist/llm/providers/google/media.d.ts +45 -4
  35. package/dist/llm/providers/google/realtime.d.ts +15 -2
  36. package/dist/llm/providers/openai/batch.d.ts +19 -2
  37. package/dist/llm/providers/openai/completions.d.ts +13 -1
  38. package/dist/llm/providers/openai/embeddings.d.ts +13 -1
  39. package/dist/llm/providers/openai/files.d.ts +13 -2
  40. package/dist/llm/providers/openai/media.d.ts +31 -2
  41. package/dist/llm/providers/openai/moderations.d.ts +11 -1
  42. package/dist/llm/providers/openai/realtime.d.ts +18 -2
  43. package/dist/llm/providers/openai/responses.d.ts +10 -2
  44. package/dist/llm/providers/openai/transcription.d.ts +7 -1
  45. package/dist/llm/providers/openrouter/completions.d.ts +5 -3
  46. package/dist/llm/providers/openrouter/embeddings.d.ts +3 -0
  47. package/dist/llm/providers/openrouter/media.d.ts +14 -4
  48. package/dist/llm/providers/openrouter/responses.d.ts +5 -3
  49. package/dist/llm/providers/xai/batch.d.ts +13 -2
  50. package/dist/llm/providers/xai/completions.d.ts +5 -3
  51. package/dist/llm/providers/xai/files.d.ts +13 -2
  52. package/dist/llm/providers/xai/media.d.ts +28 -10
  53. package/dist/llm/providers/xai/responses.d.ts +5 -3
  54. package/dist/llm/response-shape.d.ts +96 -0
  55. package/dist/llm/server-state.d.ts +1 -1
  56. package/dist/llm/types/options.d.ts +2 -2
  57. package/dist/llm/types/provider.d.ts +6 -0
  58. package/dist/llm/types/request.d.ts +49 -1
  59. package/dist/llm/wire-multipart.d.ts +26 -0
  60. package/dist/llm/wire-transforms.d.ts +28 -0
  61. package/dist/plugins/context-measurer/counter/count-api.d.ts +40 -6
  62. package/dist/plugins/context-measurer/counter/heuristic.d.ts +1 -1
  63. package/dist/plugins/context-measurer/counter/hybrid.d.ts +34 -1
  64. package/dist/plugins/context-measurer/counter/tiktoken.d.ts +7 -0
  65. package/dist/plugins/context-measurer/measurer.d.ts +1 -1
  66. package/dist/plugins/cost-collector/collector.d.ts +1 -1
  67. package/dist/plugins/cost-collector/cost-collector-internal.d.ts +1 -1
  68. package/dist/plugins/cost-collector/cost-collector-types.d.ts +1 -1
  69. package/dist/plugins/files/registry.d.ts +1 -1
  70. package/dist/plugins/files/strategy.d.ts +1 -1
  71. package/dist/plugins/internal-tools/registry.d.ts +1 -1
  72. package/dist/plugins/internal-tools/runner/types.d.ts +1 -1
  73. package/dist/plugins/mcp/sampling.d.ts +23 -1
  74. package/dist/plugins/mcp/transport-http.d.ts +13 -7
  75. package/dist/plugins/mcp/wire-rules.d.ts +21 -0
  76. package/dist/plugins/media/output.d.ts +1 -1
  77. package/dist/plugins/retrieval/document-file.d.ts +15 -0
  78. package/dist/plugins/retrieval/hosted-google.d.ts +8 -3
  79. package/dist/plugins/retrieval/hosted-openai.d.ts +9 -1
  80. package/dist/plugins/retrieval/hosted-xai.d.ts +8 -2
  81. package/dist/plugins/telemetry/telemetry.d.ts +2 -133
  82. package/dist/plugins/telemetry/types.d.ts +139 -0
  83. package/dist/util/hash.d.ts +8 -0
  84. package/dist/{plugins/media → util}/source-image.d.ts +1 -1
  85. package/dist/wire/chat-specs.d.ts +32 -0
  86. package/dist/wire/inherit.d.ts +43 -0
  87. package/dist/wire/interpreter.d.ts +279 -0
  88. package/dist/wire/mcp-specs.d.ts +13 -0
  89. package/dist/wire/media-specs.d.ts +20 -0
  90. package/dist/wire/pins.d.ts +34 -0
  91. package/dist/wire/registry.d.ts +20 -0
  92. package/dist/wire/retrieval-specs.d.ts +14 -0
  93. package/dist/wire/service-specs.d.ts +18 -0
  94. package/dist/wire/utility-specs.d.ts +10 -0
  95. package/package.json +6 -3
@@ -10,7 +10,8 @@
10
10
  * An in-memory store backs the sandbox sidebar; `toOtlpTraces()` shapes spans
11
11
  * into OTLP-compatible JSON for a real OTel exporter to forward. */
12
12
  import type { HookBus } from '../../bus/hook-bus';
13
- import type { HookName } from '../../bus/hook-map';
13
+ export type { Span, SpanKind, TelemetryAdapterOptions, TelemetryEvent, TelemetryMetrics, TelemetryResource, TraceEvent, TraceEventType, TraceFilter, TraceHandler, } from './types';
14
+ import type { Span, TelemetryAdapterOptions, TelemetryEvent, TelemetryMetrics, TelemetryResource, TraceFilter, TraceHandler } from './types';
14
15
  /** Derive a conformant hex id from one of our readable ids. `bytes` is 16 for a trace
15
16
  * id, 8 for a span id.
16
17
  *
@@ -22,81 +23,6 @@ export declare function toOtlpId(input: string, bytes: 8 | 16): string;
22
23
  * `intValue` carrying a STRING, which is how OTLP/JSON encodes 64-bit integers; send
23
24
  * them as plain strings instead and no backend can sum them. */
24
25
  export declare function toOtlpValue(value: unknown): Record<string, unknown>;
25
- export type SpanKind = 'llm' | 'http' | 'media' | 'agent' | 'tool' | 'mcp' | 'other';
26
- export interface Span {
27
- traceId: string;
28
- spanId: string;
29
- /** The span this one runs under. Without it every span is a sibling and a backend
30
- * draws a flat list instead of a tree — so a run reads as "9 things happened", not
31
- * "a turn, which called a tool, which asked a second model".
32
- *
33
- * Resolved in this order: the innermost container span still open on this trace
34
- * (`agent.run` / `tool.call`), else the app's span from a supplied `traceparent`,
35
- * else none — this span is the root. */
36
- parentSpanId?: string;
37
- name: string;
38
- kind: SpanKind;
39
- startTime: number;
40
- endTime?: number;
41
- durationMs?: number;
42
- status: 'unset' | 'ok' | 'error';
43
- attributes: Record<string, unknown>;
44
- }
45
- /** What kind of work an event describes. `message` is conversation content, which is not
46
- * a span — it is the thing you want in a debug store and NOT in your metrics backend,
47
- * which is exactly why it filters separately. */
48
- export type TraceEventType = 'agent' | 'tool' | 'llm' | 'http' | 'mcp' | 'media' | 'message' | 'other';
49
- /** One piece of work, carrying enough of the tree that a consumer can push it straight
50
- * into their own tracer without reconstructing anything. */
51
- export interface TraceEvent {
52
- type: TraceEventType;
53
- /** The app's trace when it supplied a `traceparent`, else ours. */
54
- traceId: string;
55
- spanId: string;
56
- /** Already resolved past anything this subscriber filtered out — see `survivingParent`. */
57
- parentSpanId?: string;
58
- /** The conventional name (`chat gpt-5.4-nano`, `execute_tool search`). */
59
- name: string;
60
- startTime: number;
61
- endTime?: number;
62
- durationMs?: number;
63
- status: 'unset' | 'ok' | 'error';
64
- attributes: Record<string, unknown>;
65
- }
66
- /** Declarative on purpose, rather than a predicate: knowing the types up front lets a
67
- * filtered-out event cost nothing, where a predicate would force us to build the payload
68
- * just to let the caller throw it away. */
69
- export interface TraceFilter {
70
- types?: readonly TraceEventType[];
71
- }
72
- export type TraceHandler = (event: TraceEvent) => void;
73
- export interface TelemetryEvent {
74
- seq: number;
75
- time: number;
76
- name: HookName;
77
- category: string;
78
- traceId?: string;
79
- ctx: unknown;
80
- }
81
- export interface TelemetryMetrics {
82
- requests: number;
83
- errors: number;
84
- retries: number;
85
- rateLimitHits: number;
86
- completions: number;
87
- mediaGenerated: number;
88
- costUsd: number;
89
- inputTokens: number;
90
- outputTokens: number;
91
- inFlight: number;
92
- queueDepth: number;
93
- latency: {
94
- count: number;
95
- min: number;
96
- max: number;
97
- avg: number;
98
- };
99
- }
100
26
  /** Parse a W3C `traceparent`: `00-<32 hex trace>-<16 hex span>-<flags>`.
101
27
  * Returns null for anything malformed or for the all-zero ids the spec forbids —
102
28
  * a bad header must not silently reroute telemetry into a garbage trace. */
@@ -104,63 +30,6 @@ export declare function parseTraceparent(value: string | undefined): {
104
30
  traceId: string;
105
31
  spanId: string;
106
32
  } | null;
107
- /** OpenTelemetry Resource — identifies the SERVICE producing this telemetry, so
108
- * a shared backend can separate streams from different apps and attribute cost
109
- * per service (`sum by service.name`). Stamped on every span/metric/log. */
110
- export interface TelemetryResource {
111
- /** Primary grouping key, e.g. "billing-api". OTel default: "unknown_service". */
112
- serviceName: string;
113
- /** Optional namespace/group, e.g. "prod" or a team. */
114
- serviceNamespace?: string;
115
- /** Unique instance (pod/host/process); a good default is the engine sessionId. */
116
- serviceInstanceId?: string;
117
- serviceVersion?: string;
118
- /** Arbitrary resource attributes (deployment.environment, cloud.region, …). */
119
- attributes?: Record<string, string>;
120
- }
121
- export interface TelemetryAdapterOptions {
122
- /** Cap on retained events (ring buffer). Default 2000. */
123
- maxEvents?: number;
124
- /** Service identity stamped on all exported telemetry. */
125
- resource?: TelemetryResource;
126
- /** Whether provider error TEXT may be stored in telemetry. Default `true`
127
- * (unchanged behaviour, and the same default as the OpenAI Agents SDK's
128
- * `trace_include_sensitive_data`).
129
- *
130
- * A provider's `error.message` / `error.raw` can echo request content back —
131
- * a moderation refusal quotes the prompt, a validation error names the offending
132
- * field and value. URLs and headers are always redacted regardless; this switch
133
- * governs the free-text payload. Set `false` when telemetry leaves your trust
134
- * boundary (a shared collector, a vendor APM) and the message is replaced by a
135
- * fixed `[redacted]` string while name/code/status are kept for triage. */
136
- includeSensitiveData?: boolean;
137
- /** Which event types to hand to `onTrace`. Omitted → everything.
138
- *
139
- * Filtering SPLICES the tree rather than punching holes in it: drop `http` and the
140
- * spans under it re-parent to the nearest surviving ancestor. Dropping without that
141
- * leaves orphans, and a backend draws an orphan as a second root — worse than not
142
- * filtering at all. */
143
- types?: readonly TraceEventType[];
144
- /** Whether conversation content rides along on `message` events. Default `'none'`:
145
- * prompts and completions are the debugging gold AND the PII, so sending them is a
146
- * decision to make on purpose rather than inherit. `'full'` adds the Opt-In
147
- * `gen_ai.input.messages` / `gen_ai.output.messages` attributes; `'none'` still
148
- * reports the shape (counts and sizes), which is enough to spot a runaway prompt. */
149
- content?: 'none' | 'full';
150
- /** Fraction of TRACES to emit, 0..1. Default 1.
151
- *
152
- * Per trace, never per span: sampling spans independently shreds every tree it touches
153
- * — a tool call with no run, a model call with no tool. The decision is a hash of the
154
- * trace id, so it is stable across processes and two services sharing a trace agree
155
- * without coordinating.
156
- *
157
- * This is HEAD sampling: the choice is made when the trace first appears, before we
158
- * know whether it ends in an error. Keeping all errors needs tail sampling, which
159
- * needs buffering; do that in your collector, which is built for it. */
160
- sample?: number;
161
- /** Convenience for the common case of a single sink — same as calling `onTrace`. */
162
- onTrace?: TraceHandler;
163
- }
164
33
  export declare class TelemetryAdapter {
165
34
  readonly events: TelemetryEvent[];
166
35
  readonly spans: Span[];
@@ -0,0 +1,139 @@
1
+ /** Telemetry types.
2
+ *
3
+ * Split out of `telemetry.ts` so the adapter file is implementation and this one
4
+ * is contract — matching how the rest of the codebase is laid out, and making
5
+ * the public surface of the plugin readable without paging through 1,200 lines.
6
+ */
7
+ import type { HookName } from '../../bus/hook-map';
8
+ export type SpanKind = 'llm' | 'http' | 'media' | 'agent' | 'tool' | 'mcp' | 'other';
9
+ export interface Span {
10
+ traceId: string;
11
+ spanId: string;
12
+ /** The span this one runs under. Without it every span is a sibling and a backend
13
+ * draws a flat list instead of a tree — so a run reads as "9 things happened", not
14
+ * "a turn, which called a tool, which asked a second model".
15
+ *
16
+ * Resolved in this order: the innermost container span still open on this trace
17
+ * (`agent.run` / `tool.call`), else the app's span from a supplied `traceparent`,
18
+ * else none — this span is the root. */
19
+ parentSpanId?: string;
20
+ name: string;
21
+ kind: SpanKind;
22
+ startTime: number;
23
+ endTime?: number;
24
+ durationMs?: number;
25
+ status: 'unset' | 'ok' | 'error';
26
+ attributes: Record<string, unknown>;
27
+ }
28
+ /** What kind of work an event describes. `message` is conversation content, which is not
29
+ * a span — it is the thing you want in a debug store and NOT in your metrics backend,
30
+ * which is exactly why it filters separately. */
31
+ export type TraceEventType = 'agent' | 'tool' | 'llm' | 'http' | 'mcp' | 'media' | 'message' | 'other';
32
+ /** One piece of work, carrying enough of the tree that a consumer can push it straight
33
+ * into their own tracer without reconstructing anything. */
34
+ export interface TraceEvent {
35
+ type: TraceEventType;
36
+ /** The app's trace when it supplied a `traceparent`, else ours. */
37
+ traceId: string;
38
+ spanId: string;
39
+ /** Already resolved past anything this subscriber filtered out — see `survivingParent`. */
40
+ parentSpanId?: string;
41
+ /** The conventional name (`chat gpt-5.4-nano`, `execute_tool search`). */
42
+ name: string;
43
+ startTime: number;
44
+ endTime?: number;
45
+ durationMs?: number;
46
+ status: 'unset' | 'ok' | 'error';
47
+ attributes: Record<string, unknown>;
48
+ }
49
+ /** Declarative on purpose, rather than a predicate: knowing the types up front lets a
50
+ * filtered-out event cost nothing, where a predicate would force us to build the payload
51
+ * just to let the caller throw it away. */
52
+ export interface TraceFilter {
53
+ types?: readonly TraceEventType[];
54
+ }
55
+ export type TraceHandler = (event: TraceEvent) => void;
56
+ export interface TelemetryEvent {
57
+ seq: number;
58
+ time: number;
59
+ name: HookName;
60
+ category: string;
61
+ traceId?: string;
62
+ ctx: unknown;
63
+ }
64
+ export interface TelemetryMetrics {
65
+ requests: number;
66
+ errors: number;
67
+ retries: number;
68
+ rateLimitHits: number;
69
+ completions: number;
70
+ mediaGenerated: number;
71
+ costUsd: number;
72
+ inputTokens: number;
73
+ outputTokens: number;
74
+ inFlight: number;
75
+ queueDepth: number;
76
+ latency: {
77
+ count: number;
78
+ min: number;
79
+ max: number;
80
+ avg: number;
81
+ };
82
+ }
83
+ /** OpenTelemetry Resource — identifies the SERVICE producing this telemetry, so
84
+ * a shared backend can separate streams from different apps and attribute cost
85
+ * per service (`sum by service.name`). Stamped on every span/metric/log. */
86
+ export interface TelemetryResource {
87
+ /** Primary grouping key, e.g. "billing-api". OTel default: "unknown_service". */
88
+ serviceName: string;
89
+ /** Optional namespace/group, e.g. "prod" or a team. */
90
+ serviceNamespace?: string;
91
+ /** Unique instance (pod/host/process); a good default is the engine sessionId. */
92
+ serviceInstanceId?: string;
93
+ serviceVersion?: string;
94
+ /** Arbitrary resource attributes (deployment.environment, cloud.region, …). */
95
+ attributes?: Record<string, string>;
96
+ }
97
+ export interface TelemetryAdapterOptions {
98
+ /** Cap on retained events (ring buffer). Default 2000. */
99
+ maxEvents?: number;
100
+ /** Service identity stamped on all exported telemetry. */
101
+ resource?: TelemetryResource;
102
+ /** Whether provider error TEXT may be stored in telemetry. Default `true`
103
+ * (unchanged behaviour, and the same default as the OpenAI Agents SDK's
104
+ * `trace_include_sensitive_data`).
105
+ *
106
+ * A provider's `error.message` / `error.raw` can echo request content back —
107
+ * a moderation refusal quotes the prompt, a validation error names the offending
108
+ * field and value. URLs and headers are always redacted regardless; this switch
109
+ * governs the free-text payload. Set `false` when telemetry leaves your trust
110
+ * boundary (a shared collector, a vendor APM) and the message is replaced by a
111
+ * fixed `[redacted]` string while name/code/status are kept for triage. */
112
+ includeSensitiveData?: boolean;
113
+ /** Which event types to hand to `onTrace`. Omitted → everything.
114
+ *
115
+ * Filtering SPLICES the tree rather than punching holes in it: drop `http` and the
116
+ * spans under it re-parent to the nearest surviving ancestor. Dropping without that
117
+ * leaves orphans, and a backend draws an orphan as a second root — worse than not
118
+ * filtering at all. */
119
+ types?: readonly TraceEventType[];
120
+ /** Whether conversation content rides along on `message` events. Default `'none'`:
121
+ * prompts and completions are the debugging gold AND the PII, so sending them is a
122
+ * decision to make on purpose rather than inherit. `'full'` adds the Opt-In
123
+ * `gen_ai.input.messages` / `gen_ai.output.messages` attributes; `'none'` still
124
+ * reports the shape (counts and sizes), which is enough to spot a runaway prompt. */
125
+ content?: 'none' | 'full';
126
+ /** Fraction of TRACES to emit, 0..1. Default 1.
127
+ *
128
+ * Per trace, never per span: sampling spans independently shreds every tree it touches
129
+ * — a tool call with no run, a model call with no tool. The decision is a hash of the
130
+ * trace id, so it is stable across processes and two services sharing a trace agree
131
+ * without coordinating.
132
+ *
133
+ * This is HEAD sampling: the choice is made when the trace first appears, before we
134
+ * know whether it ends in an error. Keeping all errors needs tail sampling, which
135
+ * needs buffering; do that in your collector, which is built for it. */
136
+ sample?: number;
137
+ /** Convenience for the common case of a single sink — same as calling `onTrace`. */
138
+ onTrace?: TraceHandler;
139
+ }
@@ -0,0 +1,8 @@
1
+ /** FNV-1a, 32-bit. Deterministic, synchronous and dependency-free — used where a
2
+ * stable short id has to be derived from content rather than from a clock.
3
+ *
4
+ * Not cryptographic. Collisions are acceptable for naming and bucketing; do not
5
+ * use it for integrity or security. */
6
+ export declare function fnv1a32(input: string): number;
7
+ /** Same value as 8 lowercase hex characters. */
8
+ export declare function fnv1a32Hex(input: string): string;
@@ -5,7 +5,7 @@
5
5
  * - OpenAI: `{ image_url }` (data-URL) or `{ file_id }`
6
6
  * - xAI: `{ url }` (data-URL) or `{ file_id }`
7
7
  * - Google: `inline_data {mime_type,data}` or `file_data {file_uri}` */
8
- import type { DataSource } from '../../llm/types/messages';
8
+ import type { DataSource } from '../llm/types/messages';
9
9
  export interface NormalizedImageRef {
10
10
  /** Raw base64 (no `data:` prefix), when inline. */
11
11
  base64?: string;
@@ -0,0 +1,32 @@
1
+ /** The wire specs the RUNTIME loads: chat only, resolved and memoised.
2
+ *
3
+ * Deliberately not `registry.ts`. That index imports all 71 specs — every media,
4
+ * realtime, files and batch spec included — so an adapter importing it would pull
5
+ * the whole set into every bundle whether or not anything reads them. Measured on
6
+ * the 2.3.0 build: interpreter + inherit + transforms is 18 KB minified, the nine
7
+ * chat specs add 15 KB, and all 71 specs add 41 KB. Chat-only is +2.7% on the
8
+ * bundle; everything is +4.8% for specs nothing executes yet.
9
+ *
10
+ * `registry.ts` stays as the complete index for the tests and the ports. This is
11
+ * the runtime's subset, and it grows a family at a time as each adapter is
12
+ * migrated — so what ships is always what runs.
13
+ *
14
+ * Chains are resolved once per id and cached: resolution walks `extends` and
15
+ * merges deltas, which is pure setup work and has no business happening per
16
+ * request.
17
+ */
18
+ import type { WireSpec } from './interpreter';
19
+ /** The spec for `id`, with its inheritance chain already applied.
20
+ *
21
+ * Throws on an unknown id rather than falling back to something plausible: a
22
+ * silently-substituted spec is a wrong request sent confidently, which is the
23
+ * exact failure the specs exist to end. Callers pick the fallback themselves —
24
+ * see each adapter's DEFAULT_SPEC. */
25
+ export declare function chatSpec(id: string): WireSpec;
26
+ /** Whether a spec id is one the runtime can build. Lets an adapter fall back to
27
+ * its default instead of throwing when a catalog pin names a spec from a family
28
+ * that is not migrated yet. */
29
+ export declare const isChatSpec: (id: string | undefined) => id is string;
30
+ /** Ids the runtime carries — asserted by the tests so this list and the shipped
31
+ * spec files cannot drift apart unnoticed. */
32
+ export declare const CHAT_SPEC_IDS: readonly string[];
@@ -0,0 +1,43 @@
1
+ /** Spec inheritance: a spec version extends the previous one and overrides only
2
+ * what changed.
3
+ *
4
+ * This replaces the `variants` section. Instead of asking "which shape does this
5
+ * model id take?" at request time — which needed version arithmetic, and which
6
+ * is currently WRONG for `claude-opus-4-20250514` (the date suffix parses as the
7
+ * minor version, so a 4.0 model resolves to the 4.6+ shape) — the catalog pins a
8
+ * model to a spec id, and the spec chain carries the differences as deltas.
9
+ *
10
+ * Merge rules, in one sentence each:
11
+ * - `fields` are keyed by `to` — child replaces, `remove:true` deletes, new ones append
12
+ * - `blocks` are keyed by `name` — same, with `before`/`after` for explicit placement
13
+ * - `tables` merge per table, per key
14
+ * - `overlays`/`envelope.headers` are keyed and replaced
15
+ * - everything scalar: child wins
16
+ *
17
+ * Block ORDER is load-bearing (proved by the mutation suite), so appended blocks
18
+ * land at the end unless the delta says otherwise.
19
+ */
20
+ import type { WireSpec } from './interpreter';
21
+ export interface Deltas {
22
+ extends?: string;
23
+ /** Rules to delete from the inherited spec. */
24
+ removeFields?: string[];
25
+ removeBlocks?: string[];
26
+ /** Explicit placement for an appended block. */
27
+ placeBlocks?: Record<string, {
28
+ before?: string;
29
+ after?: string;
30
+ }>;
31
+ }
32
+ export type SpecDelta = Partial<WireSpec> & Deltas & {
33
+ id: string;
34
+ };
35
+ export declare function applyDelta(base: WireSpec, delta: SpecDelta): WireSpec;
36
+ /** Resolve a spec id to its fully flattened form by walking `extends`. */
37
+ export declare function resolveSpec(id: string, byId: Map<string, SpecDelta>, seen?: Set<string>): WireSpec;
38
+ /** Catalog pin: model id -> spec id. `default` is what an unknown model gets. */
39
+ export interface PinTable {
40
+ default: string;
41
+ models: Record<string, string>;
42
+ }
43
+ export declare function specForModel(model: string, pins: PinTable): string;
@@ -0,0 +1,279 @@
1
+ /** Wire-spec interpreter — prototype for report 037 / 3.0.0.
2
+ *
3
+ * Turns a declarative wire spec + a NormalizedRequest into the same
4
+ * ProviderHttpRequest the hand-written `buildRequest` produces today.
5
+ *
6
+ * The point is NOT to eliminate code. It is to move the per-provider and
7
+ * per-MODEL knowledge — field names, shapes, enum values, which variant a
8
+ * model takes — out of imperative code and into reviewable data that the
9
+ * update pipeline can diff and that all three language ports can share.
10
+ *
11
+ * Structural work (turning unified messages into provider content parts) stays
12
+ * as named code in the registry. See `transforms.ts`.
13
+ */
14
+ export type Json = unknown;
15
+ /** A condition evaluated against the request + resolved model variants. */
16
+ export type Cond = {
17
+ defined: string;
18
+ } | {
19
+ truthy: string;
20
+ } | {
21
+ eq: [string, Json];
22
+ } | {
23
+ ne: [string, Json];
24
+ } | {
25
+ variant: string;
26
+ } | {
27
+ flavor: string | string[];
28
+ } | {
29
+ pred: string;
30
+ }
31
+ /** Truthiness of a field on the current $map item. */
32
+ | {
33
+ itemTruthy: string;
34
+ }
35
+ /** The current $map item equals this value (items are scalars here). */
36
+ | {
37
+ itemEq: Json;
38
+ } | {
39
+ isLast: true;
40
+ } | {
41
+ isFunctionTool: true;
42
+ } | {
43
+ builtin: string;
44
+ }
45
+ /** `req.tools` contains a builtin of this type. */
46
+ | {
47
+ hasTool: string;
48
+ }
49
+ /** `req.tools` contains at least one function tool. */
50
+ | {
51
+ hasFunctionTool: true;
52
+ }
53
+ /** Array at `path` contains `value`. */
54
+ | {
55
+ includes: [string, Json];
56
+ } | {
57
+ not: Cond;
58
+ } | {
59
+ all: Cond[];
60
+ } | {
61
+ any: Cond[];
62
+ };
63
+ export interface FieldRule {
64
+ /** Dotted path into NormalizedRequest. */
65
+ from: string;
66
+ /** Dotted path into the body. */
67
+ to: string;
68
+ /** Default when the source is absent. Emits the field even if unset. */
69
+ default?: Json;
70
+ /** Extra gate on top of the presence check. */
71
+ when?: Cond;
72
+ /** Presence test: `defined` (!== undefined) or `truthy`. Default `defined`. */
73
+ presence?: 'defined' | 'truthy';
74
+ /** Named value table to map the source value through. */
75
+ table?: string;
76
+ /** Value used when the table has no entry (rather than dropping the field). */
77
+ tableDefault?: Json;
78
+ /** Named transform applied to the source value. */
79
+ call?: string;
80
+ }
81
+ export interface BlockRule {
82
+ /** Documentation handle; also used in diff output. */
83
+ name: string;
84
+ when?: Cond;
85
+ /** Dotted target path. Omit to merge the result into the body root. */
86
+ to?: string;
87
+ /** Deep-merge into whatever is already at `to` (output_config case). */
88
+ merge?: boolean;
89
+ /** Value template. */
90
+ template?: Json;
91
+ /** Named builder invoked with (req, ctx) instead of a template. */
92
+ call?: string;
93
+ /** Named cross-field effects run after the block is written. */
94
+ effects?: string[];
95
+ }
96
+ export interface WireSpec {
97
+ id: string;
98
+ provider: string;
99
+ api: string;
100
+ /** Adapter flavor, for specs shared by several providers (openai|xai|openrouter). */
101
+ flavors?: string[];
102
+ envelope?: {
103
+ path?: Json;
104
+ /** Full URL template. Non-chat adapters (media, files, batch) address an
105
+ * absolute URL rather than a path under a shared base. */
106
+ url?: Json;
107
+ method?: string;
108
+ /** `json` (default), `multipart`, or `none` for GET/DELETE with no body.
109
+ * Multipart matters: a FormData body JSON-stringifies to `{}`, so comparing
110
+ * it as JSON would pass vacuously no matter what the fields are. */
111
+ /** `json` (default), `multipart`, `none` for a bodyless GET/DELETE, or
112
+ * `raw` when the body is caller-supplied BYTES the spec cannot describe —
113
+ * a file being streamed to an upload session. The spec still owns the URL,
114
+ * method and headers; only the payload comes from outside. */
115
+ bodyKind?: 'json' | 'multipart' | 'none' | 'raw' | 'form';
116
+ /** Headers in declaration order. An entry with `spread` merges an evaluated
117
+ * OBJECT of headers instead of setting one, which is what a caller-supplied
118
+ * header map or a resolved auth bundle is. Order is the whole point: it is
119
+ * what decides whether a configured `accept` overrides the default one or the
120
+ * other way round, and that was previously a property of which spread came
121
+ * later in a hand-written object literal. */
122
+ headers?: {
123
+ name?: string;
124
+ value?: Json;
125
+ when?: Cond;
126
+ spread?: Json;
127
+ }[];
128
+ /** Query parameters, appended to `url` (or `path`) in declaration order.
129
+ *
130
+ * Splicing them into the URL with `$join` works only while every parameter is
131
+ * present: `$join` propagates an omitted part, so one absent `pageToken` takes
132
+ * the whole URL with it. Declaring them separately lets a parameter drop out
133
+ * on its own, and puts the encoding in ONE place — the hand-written adapters
134
+ * disagreed about whether to call `encodeURIComponent`, which is how a page
135
+ * token with a `+` in it silently paged from the wrong place. */
136
+ query?: {
137
+ name: string;
138
+ value: Json;
139
+ when?: Cond;
140
+ }[];
141
+ /** How query values are escaped. `component` (default) percent-escapes
142
+ * everything, including a space as `%20`. `form` uses the
143
+ * application/x-www-form-urlencoded rules, where a space is `+` — which is
144
+ * what RFC 6749 prescribes for an OAuth authorization request, and what its
145
+ * servers are used to receiving. Both decode to the same string; they are not
146
+ * the same bytes, and a signature over the request would notice. */
147
+ queryEncoding?: 'component' | 'form';
148
+ };
149
+ /** Model-id → variant flags. The migration target is a catalog pin; the
150
+ * `idMatch` form is what today's regex helpers do, expressed as data.
151
+ * `fn` is the escape hatch for rules a pattern cannot express (version
152
+ * arithmetic) — every use of it is a finding, not a feature. */
153
+ variants?: {
154
+ flag: string;
155
+ idMatch?: string;
156
+ fn?: string;
157
+ unless?: string;
158
+ note?: string;
159
+ }[];
160
+ /** Value tables referenced by `table:` and `$table`. */
161
+ tables?: Record<string, Record<string, Json>>;
162
+ /** Fields we deliberately never send, with the reason. */
163
+ unsupported?: {
164
+ from: string;
165
+ reason: string;
166
+ }[];
167
+ fields?: FieldRule[];
168
+ blocks?: BlockRule[];
169
+ /** Multipart form fields, in order, when `envelope.bodyKind` is 'multipart'. */
170
+ multipart?: {
171
+ name: string;
172
+ value?: Json;
173
+ file?: boolean;
174
+ when?: Cond;
175
+ /** Emit ONE field per array element instead of a single array-valued
176
+ * field. Real forms use repeated keys for lists — OpenAI's transcription
177
+ * takes `languages[]` once per language — and a single field holding an
178
+ * array is a different request the server will not accept. */
179
+ repeat?: boolean;
180
+ }[];
181
+ /** Non-HTTP surfaces. A realtime session is not one request: it is a
182
+ * connection descriptor plus a sequence of outbound frames, so those are
183
+ * named operations rather than a single envelope+body. */
184
+ operations?: Record<string, OperationRule>;
185
+ /** Per-flavor patches applied after the blocks. This mirrors what the xai and
186
+ * openrouter adapters already do today: call the base builder, then patch the
187
+ * result. Expressing it as ops keeps the shared spec authoritative. */
188
+ overlays?: Record<string, {
189
+ ops: OverlayOp[];
190
+ }>;
191
+ }
192
+ export interface OverlayOp {
193
+ op: 'rename' | 'delete' | 'set' | 'mergeFrom' | 'call';
194
+ /** rename/delete: body path. mergeFrom: request path. */
195
+ from?: string;
196
+ /** rename/set: body path. */
197
+ to?: string;
198
+ value?: Json;
199
+ call?: string;
200
+ when?: Cond;
201
+ }
202
+ export interface Ctx {
203
+ req: any;
204
+ spec: WireSpec;
205
+ flavor: string;
206
+ /** Adapter-level configuration (baseURL, apiKey, ...) referenced by `$config`.
207
+ * Keeps the URL declarative rather than pushing it into a named transform. */
208
+ config: Record<string, unknown>;
209
+ variants: Set<string>;
210
+ body: Record<string, unknown>;
211
+ /** Per-item scope while inside a $map. */
212
+ item?: {
213
+ value: any;
214
+ index: number;
215
+ isLast: boolean;
216
+ };
217
+ /** Collected multipart fields, when the spec declares a multipart body. */
218
+ multipart?: MultipartField[];
219
+ }
220
+ export interface MultipartField {
221
+ name: string;
222
+ kind: 'file' | 'value';
223
+ value?: Json;
224
+ }
225
+ export type Transform = (value: any, ctx: Ctx) => Json;
226
+ export type Builder = (ctx: Ctx) => Json;
227
+ export type Predicate = (ctx: Ctx) => boolean;
228
+ export type Effect = (ctx: Ctx) => void;
229
+ export interface Registry {
230
+ transforms: Record<string, Transform>;
231
+ builders: Record<string, Builder>;
232
+ predicates: Record<string, Predicate>;
233
+ effects: Record<string, Effect>;
234
+ }
235
+ export declare function getPath(root: any, path: string): any;
236
+ export declare function evalCond(cond: Cond | undefined, ctx: Ctx, reg: Registry): boolean;
237
+ export declare function resolveVariants(spec: WireSpec, model: string, reg: Registry): Set<string>;
238
+ export interface BuiltRequest {
239
+ body: Record<string, unknown>;
240
+ headers?: Record<string, string>;
241
+ path?: string;
242
+ url?: string;
243
+ method?: string;
244
+ /** The body is caller-supplied bytes (bodyKind 'raw'). */
245
+ rawBody?: boolean;
246
+ /** Present instead of a JSON body when bodyKind is 'multipart'. */
247
+ multipart?: MultipartField[];
248
+ /** The body is form-urlencoded: `body` holds the FIELDS, and the caller encodes
249
+ * them. Same split as multipart — the spec says what the form carries, the
250
+ * runtime does the encoding, and the frozen fixture stays readable as fields
251
+ * rather than as one percent-escaped string. */
252
+ formBody?: boolean;
253
+ /** True when the spec declares the request carries no body at all. */
254
+ noBody?: boolean;
255
+ }
256
+ export declare function buildFromSpec(spec: WireSpec, req: any, reg: Registry, flavor?: string,
257
+ /** Coverage hook: called with every rule that actually fired. */
258
+ onUse?: (kind: 'field' | 'block' | 'header' | 'variant' | 'overlay', name: string) => void,
259
+ /** Adapter config exposed to `$config`. */
260
+ config?: Record<string, unknown>): BuiltRequest;
261
+ export interface OperationRule {
262
+ /** Connection descriptor (realtime `connect`). */
263
+ url?: Json;
264
+ protocols?: Json;
265
+ /** Outbound frames, in order. A frame whose template evaluates away is skipped. */
266
+ frames?: {
267
+ name?: string;
268
+ when?: Cond;
269
+ template: Json;
270
+ }[];
271
+ }
272
+ export interface BuiltConnection {
273
+ url: string;
274
+ protocols?: string[];
275
+ }
276
+ /** Build the connection descriptor for an operation (realtime `connect`). */
277
+ export declare function buildConnection(spec: WireSpec, operation: string, input: any, reg: Registry, config?: Record<string, unknown>): BuiltConnection;
278
+ /** Build the outbound frames for an operation (realtime `open` / `send`). */
279
+ export declare function buildFrames(spec: WireSpec, operation: string, input: any, reg: Registry, config?: Record<string, unknown>): Json[];