@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
package/MIGRATION.md CHANGED
@@ -1,3 +1,133 @@
1
+ # Migrating to 3.0.0
2
+
3
+ **Almost certainly no source changes.** One type member was removed, and it was one the SDK
4
+ set for its own adapters to read — not something an application was ever expected to write.
5
+
6
+ ## `ModelInfo.wire`, `NormalizedRequest.wire` and the `ModelWire` type are gone
7
+
8
+ `wire` carried per-model traits (`{ thinking, topK }`) that told an adapter which shape a model
9
+ takes. `ModelInfo.wireSpec` — which names the wire spec that builds the request — now carries the
10
+ same knowledge, and it carries it exactly once.
11
+
12
+ That duplication was the point of removing it. Two representations of one fact drift apart, and
13
+ this library has already shipped two bugs from precisely that: 2.2.1 sent Anthropic the retired
14
+ `thinking` shape, and 2.2.2 sent Gemini tool schemas on the wrong field. Keeping `wire` alongside
15
+ `wireSpec` would have been the same mistake with better tests.
16
+
17
+ **What to do:** nothing, unless you read `.wire` off a catalog entry. If you did:
18
+
19
+ ```ts
20
+ // before
21
+ const shape = catalog.get('anthropic', model)?.wire?.thinking; // 'adaptive' | 'budgeted'
22
+
23
+ // after — the pin names the spec, and the spec defines the shape
24
+ const spec = catalog.get('anthropic', model)?.wireSpec; // 'anthropic/messages@4.7'
25
+ ```
26
+
27
+ If you were SETTING `wire` on a custom catalog entry to steer an adapter, set `wireSpec` instead:
28
+
29
+ ```ts
30
+ catalog.set('anthropic', 'my-model', { pricing: {}, wireSpec: 'anthropic/messages@4.1' });
31
+ ```
32
+
33
+ **Behaviour is unchanged.** Every catalogued model produces the byte-identical request it produced
34
+ in 2.3.0 — checked on every CI run against a corpus frozen from the 2.3.0 tag: 290 subjects across
35
+ 23 request shapes, on both the pinned and the id-derived route.
36
+
37
+ ## The catalog is loaded by default
38
+
39
+ `createEngine()` and `LLMClient` now start with the bundled provider catalogs instead of an empty
40
+ one. **Almost certainly no source change** — and if you were already passing `catalog: 'defaults'`,
41
+ that still works and now says the same thing twice.
42
+
43
+ What changes if you were NOT passing it: prices become known, token counts can use the exact
44
+ strategies, and requests are built from the model's wire-spec pin rather than from a rule over its
45
+ id. All three were falling back before, silently.
46
+
47
+ ```ts
48
+ // before — an empty catalog unless you asked
49
+ const engine = createEngine({ apiKeys }); // no pricing, no pins
50
+ const engine = createEngine({ apiKeys, catalog: 'defaults' }); // the data
51
+
52
+ // after — the data, unless you opt out
53
+ const engine = createEngine({ apiKeys }); // the data
54
+ const engine = createEngine({ apiKeys, catalog: false }); // no entries, on purpose
55
+ ```
56
+
57
+ If you relied on an empty catalog — to be certain no bundled price was used, say — pass
58
+ `catalog: false` (or `'empty'`).
59
+
60
+ ## The token-count APIs need the engine's fetch
61
+
62
+ `AnthropicCountApi` and `GoogleCountApi` used to default their second argument to
63
+ `globalThis.fetch`. That default is gone: the fetch is required, and it is an `EngineFetch` — the
64
+ same request-object fetch every other adapter takes — rather than a WHATWG `(url, init)` one.
65
+
66
+ **Almost certainly no source change.** `countTokens()` and `HybridTokenCounter` build these for you,
67
+ and `countTokens()` passes `engine.fetch`. You only touch this if you construct one directly:
68
+
69
+ ```ts
70
+ // before — went around the NetworkEngine entirely
71
+ const api = new AnthropicCountApi(apiKey);
72
+
73
+ // after
74
+ const api = new AnthropicCountApi(apiKey, engine.fetch);
75
+ ```
76
+
77
+ If you build a `HybridTokenCounter` yourself and want the exact count APIs, pass `fetch`:
78
+
79
+ ```ts
80
+ new HybridTokenCounter({ catalog, countApiKeys, fetch: engine.fetch });
81
+ ```
82
+
83
+ Without it the exact strategies are unavailable and counting falls back to the heuristic, with a
84
+ warning — rather than quietly calling the provider outside the queue, the rate limiter, the retry
85
+ policy and the telemetry, which is what the old default did.
86
+
87
+ ## `hooks.onAny` receives one event instead of `(name, ctx)`
88
+
89
+ Only affects code that subscribes to the WHOLE event stream. `hooks.on('onCompletion', h)` — the
90
+ named subscription — is unchanged.
91
+
92
+ ```ts
93
+ // before
94
+ hooks.onAny((name, ctx) => {
95
+ if (name === 'onCompletion') {
96
+ const c = ctx as { response?: { usage?: { inputTokens?: number } } };
97
+ record(c.response?.usage?.inputTokens ?? 0);
98
+ }
99
+ });
100
+
101
+ // after — `event.type` narrows `event.ctx`, so the cast is gone
102
+ hooks.onAny((event) => {
103
+ if (event.type === 'onCompletion') {
104
+ record(event.ctx.response.usage?.inputTokens ?? 0);
105
+ }
106
+ });
107
+ ```
108
+
109
+ The old shape forced every subscriber to cast, and a cast keeps compiling after the field it names
110
+ is renamed — which for a usage or cost field is a metric that silently reads zero. `HookEvent` is
111
+ derived from `HookMap`, so a hook added later becomes a variant your `switch` is told about.
112
+
113
+ ## Google requests carry the API key in a header, not the URL
114
+
115
+ Behaviour, not signature: no source change, and no key of yours moves. Twelve Google endpoints —
116
+ files, batch, media, embeddings, count — used to append `?key=…` to the URL; they now send
117
+ `x-goog-api-key`. Nothing to update unless something in your infrastructure reads the key OUT of
118
+ the URL: an allowlist matching on the query string, a log scrubber written against `key=`, or a
119
+ proxy that routes on it. Those stop seeing it, which was the point — a URL travels through logs,
120
+ proxies and error reports that a header does not.
121
+
122
+ ## Nothing else was removed
123
+
124
+ The band helpers that went with it — `anthropicThinkingShape`, `anthropicAcceptsTopK`,
125
+ `googleUsesThinkingBudget`, and the thinking-budget tables — were never exported from the package
126
+ entry point, and the `exports` map has always blocked deep imports, so no application could reach
127
+ them. They now live as data in `src/wire/pins/`, which is what the Python and Rust ports read.
128
+
129
+ ---
130
+
1
131
  # Migrating to 2.0.0
2
132
 
3
133
  **Most codebases need no source changes.** The point of this library is that provider churn is our
@@ -20,6 +20,10 @@ export type RunTrace = TraceContext & {
20
20
  sessionId: string;
21
21
  requestId: string;
22
22
  };
23
+ /** Why a run ended. Written inline in both `complete()` and `stream()` and in
24
+ * `finalizeRun`'s argument list, which is three places to keep in step; named
25
+ * so adding a reason is one edit. */
26
+ export type RunEndReason = 'done' | 'stopped' | 'error' | 'guardrail' | 'max_steps';
23
27
  /** Create a fresh StepState for the start of a streaming step. */
24
28
  export declare function makeStepState(): StepState;
25
29
  /** Accumulate one SSE StreamEvent into StepState.
@@ -94,6 +94,41 @@ export declare class AgentLoop {
94
94
  removeTool(name: string): void;
95
95
  toolNames(): string[];
96
96
  stop(): void;
97
+ /** Record a thrown error onto the run: emit `onRunError` and return the parts
98
+ * the caller assigns. Both paths report `phase: 'llm_call'`. */
99
+ private recordRunError;
100
+ /** The run's final text.
101
+ *
102
+ * `finalAnswerText` strips `phase: 'commentary'` parts: a codex-family model
103
+ * narrates before it answers and `response.text` concatenates both, so an
104
+ * agent's final output used to include its own thinking-out-loud. Falls back
105
+ * to `.text` when the content carries no text parts, and is identical to
106
+ * `.text` for every model that reports no phase.
107
+ *
108
+ * The streaming path passes what it accumulated; the non-streaming path lets
109
+ * the last response decide. Both share the guardrail / max-steps overrides. */
110
+ private resolveFinalText;
111
+ /** Compose the run's CompletionResponse. `media` and `raw` are parameters
112
+ * because the two paths genuinely differ: the streaming path has already
113
+ * emitted media as events and never holds a raw provider payload. */
114
+ private buildFinalResponse;
115
+ /** Close out a run: write the report, then surface a failure.
116
+ *
117
+ * The order matters and is the reason this is one function rather than two
118
+ * calls at each site — a failed run must still emit its metrics and hooks
119
+ * before the error propagates, or a crash silently loses the run's telemetry.
120
+ * A failed run throws rather than returning empty text, matching the raw
121
+ * client. */
122
+ private settleRun;
123
+ /** Per-step options for the underlying LLM call: loop defaults, then the
124
+ * caller's overrides, then the run's trace.
125
+ *
126
+ * `complete()` and `stream()` duplicated this verbatim, differing only in the
127
+ * name of the local holding the composed system prompt. That is a bad place
128
+ * for a copy: the ctx block below is what stops one conversation arriving at
129
+ * the collector as several unrelated traces, and a fix applied to one path
130
+ * would have left the other silently splitting. */
131
+ private buildStepOptions;
97
132
  complete(input: string | ContentPart[] | Message[], options?: ExecuteOptions): Promise<CompletionResponse>;
98
133
  /** Run `complete` with a JSON Schema enforced via `structured`, then
99
134
  * JSON.parse the response text. Tool calls are still allowed within the
@@ -12,9 +12,19 @@
12
12
  * swallow — emitters need to know if a critical handler (like ContextGuard
13
13
  * abort) failed. Plugins that should never break the request must catch
14
14
  * their own errors. */
15
- import type { HookHandler, HookMap, HookName } from './hook-map';
16
- /** Catch-all handler: receives the event name + context for EVERY emit. */
17
- export type AnyHookHandler = (name: HookName, ctx: unknown) => void | Promise<void>;
15
+ import type { HookEvent, HookHandler, HookMap, HookName } from './hook-map';
16
+ /** Catch-all handler: receives EVERY emit as one discriminated union.
17
+ *
18
+ * It used to receive `(name, ctx: unknown)`, which pushed the type back onto the
19
+ * subscriber — and the SDK's own telemetry adapter, the only consumer, answered
20
+ * the way anyone would: `ctx as Record<string, unknown>`, then a cast per field —
21
+ * 26 of them in one method.
22
+ * Renaming a context field left those reads compiling and silently `undefined`,
23
+ * which for the token and cost fields means a metric that quietly goes to zero.
24
+ *
25
+ * With `HookEvent`, `e.type` narrows `e.ctx` and those casts stop being possible
26
+ * to write. */
27
+ export type AnyHookHandler = (event: HookEvent) => void | Promise<void>;
18
28
  export declare class HookBus {
19
29
  private handlers;
20
30
  private anyHandlers;
@@ -621,3 +621,27 @@ export interface HookMap {
621
621
  }
622
622
  export type HookName = keyof HookMap;
623
623
  export type HookHandler<K extends HookName> = (ctx: HookMap[K]) => void | Promise<void>;
624
+ /** One event, as a value. `HookMap` types a SUBSCRIPTION (`on('onCompletion', h)`
625
+ * knows its own ctx); this types the STREAM, where the name is not known until
626
+ * runtime and the payload has to travel with it.
627
+ *
628
+ * Derived from `HookMap` rather than written out, so the 51 variants cannot drift
629
+ * from the 51 hooks: adding an entry above adds a variant here, and a consumer
630
+ * switching exhaustively over `type` stops compiling until it handles the new one.
631
+ *
632
+ * hooks.onAny((e) => {
633
+ * if (e.type === 'onCompletion') e.ctx.response.usage; // narrowed
634
+ * });
635
+ *
636
+ * The payload stays nested under `ctx` instead of being spread onto the event.
637
+ * Spreading would collide with the contexts that already carry their own `type`
638
+ * field, and would copy an object on every emit — including the per-chunk ones. */
639
+ export type HookEventOf<K extends HookName> = {
640
+ readonly type: K;
641
+ readonly ctx: HookMap[K];
642
+ };
643
+ /** The SDK's whole event stream as a single discriminated union — the portable
644
+ * shape: a Rust enum, a Python tagged union, a TS union, all from one catalog. */
645
+ export type HookEvent = {
646
+ [K in HookName]: HookEventOf<K>;
647
+ }[HookName];
@@ -14,5 +14,5 @@
14
14
  * - web_fetch → anthropic (web_fetch_20260318), google (urlContext).
15
15
  * OpenAI has no separate fetch tool (its web_search does page-open); xAI /
16
16
  * openrouter expose none. */
17
- import type { ProviderName } from '../types/provider';
17
+ import type { ProviderName } from '../llm/types/provider';
18
18
  export declare const PROVIDER_BUILTIN_TOOLS: Record<ProviderName, readonly string[]>;
@@ -94,6 +94,17 @@ export interface ModelInfo {
94
94
  /** Other callable ids that resolve to this model (dated snapshots, the bare
95
95
  * callable form). Indexed for lookup + accepted as model strings. */
96
96
  aliases?: string[];
97
+ /** Which wire spec builds this model's requests, e.g. `anthropic/messages@4.7`.
98
+ *
99
+ * The specs live in `src/wire/specs` and are the artifact the Python and Rust
100
+ * ports consume, so the pin is what lets all three agree on a model without
101
+ * each re-deriving it from the model id.
102
+ *
103
+ * Today the pin is carried and validated but the hand-written adapters still
104
+ * build requests, reading `wire` above. Driving them from the pinned spec is
105
+ * the 3.0.0 step, at which point `wire` goes away — the spec already encodes
106
+ * the same knowledge, which is why a test asserts the two agree. */
107
+ wireSpec?: string;
97
108
  /** Model role/modality: chat | code | image | video | tts | stt | embedding | … */
98
109
  type?: string;
99
110
  /** Content kinds the model ACCEPTS as input: text | image | audio | video |
@@ -156,6 +167,14 @@ export declare class ModelCatalog {
156
167
  isStateModelBound(provider: string, model: string): boolean;
157
168
  list(provider?: string): ModelInfo[];
158
169
  load(data: Record<string, unknown>): void;
170
+ /** A catalog with every bundled provider entry already loaded.
171
+ *
172
+ * This is what the engine and the client build when nobody says otherwise. A
173
+ * fresh instance each time rather than a shared one: the catalog is mutable
174
+ * (`set()` is public and examples use it), so sharing would let one engine's
175
+ * edit reach another's request. Indexing all 427 entries costs about a
176
+ * millisecond, against a network call. */
177
+ static withProviderDefaults(): ModelCatalog;
159
178
  /** Load every provider's `catalog.json` shipped with the SDK. Synchronous —
160
179
  * the JSON files are bundled via static import so no I/O at runtime. */
161
180
  loadProviderDefaults(): void;
@@ -3,7 +3,7 @@
3
3
  * ClientResolver and (in the legacy SDK) by InternalToolRunner. */
4
4
  import { LLMClient } from '../llm/client';
5
5
  import type { LLMClientConfig } from '../llm/client-config';
6
- import type { ModelCatalog } from '../plugins/model-catalog/catalog';
6
+ import type { ModelCatalog } from '../catalog/catalog';
7
7
  export declare class ClientPool {
8
8
  private readonly catalog?;
9
9
  private clients;
@@ -9,7 +9,7 @@ import type { LLMClientConfig } from '../llm/client-config';
9
9
  import type { HookBus } from '../bus/hook-bus';
10
10
  import type { ProviderName } from '../llm/types/provider';
11
11
  import type { ServiceTier } from '../llm/types/tiers';
12
- import type { ModelCatalog } from '../plugins/model-catalog/catalog';
12
+ import type { ModelCatalog } from '../catalog/catalog';
13
13
  import type { EngineFetch, EngineFetchStream } from '../network/types';
14
14
  export interface ClientResolverConfig {
15
15
  /** provider → API key. Providers absent from this map can't be resolved. */
@@ -46,8 +46,27 @@ export declare class ClientResolver {
46
46
  export declare function parseModelId(modelId: string): [ProviderName, string];
47
47
  export declare function isNamespacedModelId(modelId: string): boolean;
48
48
  /** Resolve a model + optional provider to a concrete { provider, model }.
49
- * A namespaced id ("provider/model") yields its own provider; a bare model
50
- * requires an explicit `provider`. `label` names the caller in the error. */
49
+ *
50
+ * An EXPLICIT provider always wins. It used to lose to the model's prefix, and
51
+ * that is not a preference — every OpenRouter model id is `vendor/model`, so
52
+ * `createLLM({ provider: 'openrouter', model: 'openai/gpt-5.4-nano' })` resolved
53
+ * to the provider `openai` and sent the **OpenRouter key to api.openai.com**.
54
+ * Ids whose vendor is not one of our five (`qwen/qwen3`) fared differently and no
55
+ * better: the prefix was cast to a ProviderName and failed later as "no default
56
+ * adapter for provider 'qwen'".
57
+ *
58
+ * With a provider given, a leading `<provider>/` on the model is redundant and is
59
+ * stripped — `openrouter` + `openrouter/openai/gpt-5.4-nano` is the catalog's own
60
+ * slug form and means the OpenRouter model `openai/gpt-5.4-nano`.
61
+ *
62
+ * Without one, the `provider/model` prefix is still the documented sugar, and it
63
+ * stays permissive about the prefix on purpose: the pricing paths (`estimate`,
64
+ * `estimator`) resolve models that are catalogued under a provider nobody can
65
+ * CALL — a private deployment, a test fixture — and rejecting those would break
66
+ * costing a model you never send. A prefix that is not callable fails where it
67
+ * matters, in the adapter factory, naming the provider it could not build.
68
+ *
69
+ * `label` names the caller in the error. */
51
70
  export declare function resolveModel(model: string, provider: ProviderName | undefined, label: string): {
52
71
  provider: ProviderName;
53
72
  model: string;
@@ -17,7 +17,7 @@ export interface CountTokensOptions {
17
17
  provider?: ProviderName;
18
18
  /** Text or messages to count. */
19
19
  input: string | Message[];
20
- /** Key for the exact count-API path (Anthropic/Google). Falls back to engine.apiKeys. */
20
+ /** Key for the exact count-API path (Anthropic, Google, xAI). Falls back to engine.apiKeys. */
21
21
  apiKey?: string;
22
22
  /** Use the precise counter where available (default true). false = sync estimate. */
23
23
  exact?: boolean;
@@ -17,13 +17,15 @@
17
17
  * // llm.client uses engine.fetch + engine.hooks automatically. */
18
18
  import { AgentBus } from '../bus/agent-bus';
19
19
  import { HookBus } from '../bus/hook-bus';
20
+ import type { LLMClient } from '../llm/client';
21
+ import { type CreateLLMOptions } from './llm';
20
22
  import type { ProviderName } from '../llm/types/provider';
21
23
  import { NetworkEngine, type QueueSettings } from '../network/engine';
22
24
  import type { RetryPolicyOverride } from '../network/queue-state-config';
23
25
  import type { EngineConnect, EngineFetch, EngineFetchStream, FetchFn } from '../network/types';
24
26
  import { Cache } from '../plugins/cache/cache';
25
27
  import { CostCollector } from '../plugins/cost-collector/collector';
26
- import { ModelCatalog } from '../plugins/model-catalog/catalog';
28
+ import { ModelCatalog } from '../catalog/catalog';
27
29
  import { TelemetryAdapter, type TelemetryAdapterOptions } from '../plugins/telemetry/telemetry';
28
30
  import type { Persistence } from '../plugins/persistence/types';
29
31
  export interface EngineHandle {
@@ -63,6 +65,18 @@ export interface EngineHandle {
63
65
  * createMediaOutput, complete) read these to wire LLM clients without
64
66
  * the caller passing apiKey explicitly. */
65
67
  apiKeys: Partial<Record<ProviderName, string>>;
68
+ /** Whether clients built from this engine check response shapes. Read by
69
+ * `createLLM`; see `checkResponseShapes` on the options. */
70
+ checkResponseShapes: boolean;
71
+ /** Build an LLMClient bound to this engine.
72
+ *
73
+ * Exists so lower layers can obtain a client without importing the helpers
74
+ * layer: `plugins/internal-tools` needs one for LLM-backed tools, and
75
+ * importing `createLLM` directly made `plugins` depend on `helpers` while
76
+ * `helpers` already depended on `plugins` — a cycle that a Rust crate split
77
+ * cannot express. The engine is something those plugins already hold, so it
78
+ * is the natural place to hand the capability down. */
79
+ createClient(options: Omit<CreateLLMOptions, 'engine'>): LLMClient;
66
80
  /** Tear down all owned plugins. */
67
81
  destroy(): void;
68
82
  }
@@ -89,12 +103,31 @@ export interface EngineConfig {
89
103
  /** Custom low-level fetch transport — forwarded to the NetworkEngine's queue
90
104
  * (so retry/rate-limit/hooks still apply). Defaults to globalThis.fetch. */
91
105
  fetch?: FetchFn;
92
- /** Catalog wiring. Pass:
93
- * - `true` / 'defaults' load every bundled provider catalog.json
106
+ /** Warn when a provider's response stops looking like the one we learned to
107
+ * read: a field never seen before, a field that was always present and is now
108
+ * absent, or a discriminator carrying a value nothing branches on.
109
+ *
110
+ * OFF by default. It never changes what is parsed — it only emits `onWarning`,
111
+ * so subscribe with `hooks.on('onWarning', …)` and look for codes starting
112
+ * `response_shape_`. Each distinct finding is reported ONCE per client.
113
+ *
114
+ * Worth turning on in staging and in your test suite: response drift is the
115
+ * failure this library gives you the least warning about, because a renamed
116
+ * field still parses — into `undefined`. */
117
+ checkResponseShapes?: boolean;
118
+ /** Catalog wiring. **Defaults to the bundled provider catalogs.**
119
+ *
120
+ * The catalog is what the adapters read per model: which wire spec builds the
121
+ * request, what the model costs, which tokenizer counts it. Starting empty
122
+ * meant every one of those silently fell back — the id-derived spec, an
123
+ * unknown price, an estimated token count — and nothing said so. The data is
124
+ * statically imported either way, so leaving it unloaded saved no bytes.
125
+ *
126
+ * - undefined (default) / `true` / 'defaults' → every bundled catalog.json
94
127
  * - existing ModelCatalog instance → use as-is
95
- * - `{ entries: {...} }` → build empty + load() the entries
96
- * - undefined empty catalog */
97
- catalog?: ModelCatalog | true | 'defaults' | {
128
+ * - `{ entries: {...} }` → the given entries only
129
+ * - `false` / 'empty' no entries. Everything falls back; say so on purpose. */
130
+ catalog?: ModelCatalog | boolean | 'defaults' | 'empty' | {
98
131
  entries: Record<string, unknown>;
99
132
  };
100
133
  /** Per-provider API keys. Helpers consult this when no apiKey is passed
@@ -11,7 +11,12 @@ import type { AgentTool } from '../agent/types';
11
11
  import { McpClient } from '../plugins/mcp/client';
12
12
  import { type McpAuthProvider } from '../plugins/mcp/oauth';
13
13
  import type { SsrfGuardOptions } from '../plugins/mcp/url-guard';
14
- import { type McpSamplingConfig } from '../plugins/mcp/sampling';
14
+ import { type McpSamplingConfig, type McpSamplingHandler } from '../plugins/mcp/sampling';
15
+ /** Build a sampling handler that fulfils an MCP server's `sampling/createMessage`
16
+ * with our own engine. Thin wiring: the MCP-shape mapping lives in
17
+ * `plugins/mcp/sampling`, which cannot import this layer without closing a
18
+ * dependency cycle, so the completion function is passed down instead. */
19
+ export declare function samplingHandler(config: McpSamplingConfig): McpSamplingHandler;
15
20
  import { type McpElicitRequestParams, type McpElicitResult, type McpInitializeResult, type McpRoot, type McpServerConfig } from '../plugins/mcp/types';
16
21
  import { type EngineHandle } from './engine';
17
22
  export interface ConnectMcpOptions {
@@ -9,7 +9,7 @@
9
9
  * `{ raw: true }` for bare id strings. Results are cached IN MEMORY for 24h
10
10
  * (override with `refresh: true`). */
11
11
  import type { ProviderName } from '../llm/types/provider';
12
- import type { ModelInfo } from '../plugins/model-catalog/catalog';
12
+ import type { ModelInfo } from '../catalog/catalog';
13
13
  import { type EngineHandle } from './engine';
14
14
  /** Curated local catalog (the main answer). */
15
15
  export declare function listModels(opts?: {
@@ -15,7 +15,7 @@
15
15
  * LLMClient.complete. Either way the helper destroys its created client
16
16
  * before returning so callers don't leak. */
17
17
  import type { AgentTool } from '../agent/types';
18
- import type { CacheConfig, ThinkingConfig } from '../llm/types/request';
18
+ import type { CacheConfig, ProviderOptions, ThinkingConfig } from '../llm/types/request';
19
19
  import type { LLMClientConfig } from '../llm/client-config';
20
20
  import type { AudioOptions } from '../llm/types/audio';
21
21
  import type { ContentPart, Message } from '../llm/types/messages';
@@ -84,7 +84,7 @@ export interface CompleteOptions {
84
84
  /** Optional engine to use. Falls back to coreRegistry default. */
85
85
  engine?: EngineHandle;
86
86
  /** Provider-specific request options (e.g. `{ openrouter: { models: [...] } }`). */
87
- providerOptions?: Record<string, unknown>;
87
+ providerOptions?: ProviderOptions;
88
88
  /** Extra LLMClient options. */
89
89
  client?: Partial<Omit<LLMClientConfig, 'provider' | 'model' | 'apiKey'>>;
90
90
  /** When set, `estimate()` runs BEFORE the request is sent. If the cost for
@@ -11,7 +11,7 @@
11
11
  * Availability-aware: only considers providers with a configured API key.
12
12
  * Ranks cheapest-first (tiebreak: newest version); select() returns the single
13
13
  * best, selectModels() the ranked list. Thresholds + custom tags are overridable. */
14
- import type { ModelInfo } from '../plugins/model-catalog/catalog';
14
+ import type { ModelInfo } from '../catalog/catalog';
15
15
  import type { ProviderName } from '../llm/types/provider';
16
16
  import { type EngineHandle } from './engine';
17
17
  export interface SelectPrefs {