@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
@@ -8,6 +8,49 @@ import type { AudioOptions } from './audio';
8
8
  import type { Message } from './messages';
9
9
  import type { ServiceTier } from './tiers';
10
10
  import type { Tool, ToolChoice } from './tools';
11
+ /** Provider-specific request options that have no unified equivalent.
12
+ *
13
+ * This was `Record<string, unknown>` — the one untyped hole in the request, and
14
+ * therefore the one place a typo produced silence rather than an error:
15
+ * `promtCacheOptions` type-checked and was simply never sent.
16
+ *
17
+ * Every key below is one an adapter actually reads; the list is derived from
18
+ * the read sites, not invented. The index signature stays so a caller can still
19
+ * pass something the SDK does not know about yet — a provider ships a parameter
20
+ * before we model it, and refusing it would make the escape hatch useless. What
21
+ * changed is that the keys we DO know are checked and discoverable.
22
+ *
23
+ * Keys are grouped by the provider that consumes them; sending one to a
24
+ * different provider is ignored, not an error. */
25
+ export interface ProviderOptions {
26
+ /** Forwarded as the `anthropic-user-profile-id` header: identifies the end
27
+ * user a request acts on behalf of. Needs the account-level
28
+ * `user-profiles` beta. */
29
+ userProfileId?: string;
30
+ /** Native moderation policy, sent alongside the `moderation` request field. */
31
+ moderationPolicy?: Record<string, unknown>;
32
+ /** `prompt_cache_options` — OpenAI-only prompt-cache controls. */
33
+ promptCacheOptions?: Record<string, unknown>;
34
+ /** `reasoning.mode` on the Responses API. */
35
+ reasoningMode?: 'standard' | 'pro';
36
+ /** Overrides `generationConfig.responseModalities`, e.g. for image or audio
37
+ * generation. Wins over the modality implied by `outputModalities`. */
38
+ responseModalities?: string[];
39
+ /** `generationConfig.speechConfig` — voice selection for audio output. */
40
+ speechConfig?: Record<string, unknown>;
41
+ /** `generationConfig.imageConfig` — aspect ratio / size for image output. */
42
+ imageConfig?: Record<string, unknown>;
43
+ /** `generationConfig.translationConfig`. */
44
+ translationConfig?: Record<string, unknown>;
45
+ /** Name of a cached-content handle to reuse. Forwarded only when it is a
46
+ * non-empty string. */
47
+ cachedContent?: string;
48
+ /** Routing options merged into the request body (provider order, transforms,
49
+ * and the rest of OpenRouter's routing surface). */
50
+ openrouter?: Record<string, unknown>;
51
+ /** Anything the SDK does not model yet. Adapters ignore what they do not read. */
52
+ [key: string]: unknown;
53
+ }
11
54
  export interface NormalizedRequest {
12
55
  /** From LLMClientConfig.model — fixed at construction. */
13
56
  model: string;
@@ -47,7 +90,12 @@ export interface NormalizedRequest {
47
90
  cache?: CacheConfig;
48
91
  serviceTier?: ServiceTier;
49
92
  moderation?: ModerationRequest;
50
- providerOptions?: Record<string, unknown>;
93
+ providerOptions?: ProviderOptions;
94
+ /** Which wire spec builds this request, from the catalog's `ModelInfo.wireSpec`
95
+ * and resolved by `LLMClient`. Absent when the engine runs without a catalog or
96
+ * the model is not catalogued, in which case the adapter derives the spec from
97
+ * the model id — the same fallback `wire` has. */
98
+ wireSpec?: string;
51
99
  audio?: AudioOptions;
52
100
  outputModalities?: Array<'text' | 'audio'>;
53
101
  previousResponseId?: string;
@@ -0,0 +1,26 @@
1
+ /** Turn a spec's multipart DESCRIPTOR into a real FormData.
2
+ *
3
+ * A spec can say that a request is multipart and which fields it carries, but not
4
+ * what the bytes are — those come from a `FileAttachment` the caller holds. So the
5
+ * interpreter emits `{ name, kind: 'file' | 'value', value? }` and this fills in
6
+ * the one part it cannot: the file itself.
7
+ *
8
+ * Kept out of `src/wire/` deliberately. The wire layer has no outbound imports and
9
+ * no notion of an attachment; this is the seam where spec data meets the caller's
10
+ * bytes, which makes it llm-layer glue rather than part of the interpreter.
11
+ */
12
+ import type { MultipartField } from './../wire/interpreter';
13
+ export interface MultipartFile {
14
+ /** The bytes, already read. */
15
+ data: Uint8Array;
16
+ filename: string;
17
+ mimeType: string;
18
+ }
19
+ /** Build the FormData a multipart spec describes.
20
+ *
21
+ * Field ORDER follows the spec, because multipart is an ordered format and some
22
+ * servers care. A `file` field with no file supplied is an error rather than an
23
+ * omission: a silently fileless upload would be accepted by the type checker and
24
+ * rejected by the provider, which is the exact failure mode the specs exist to
25
+ * remove. */
26
+ export declare function toFormData(fields: MultipartField[], file: MultipartFile): FormData;
@@ -0,0 +1,28 @@
1
+ /** The named-code registry the wire specs delegate to.
2
+ *
3
+ * Everything here is bound to the REAL library internals, never reimplemented —
4
+ * if the interpreter's output matches the adapter's, it is because the spec
5
+ * drove the same code, not because I wrote a second copy that happens to agree.
6
+ *
7
+ * What lands in this file is the honest answer to "what cannot be data":
8
+ * structural message/content transformation, schema-shape rules, and one
9
+ * variant rule that is arithmetic rather than a pattern.
10
+ */
11
+ import type { Registry } from '../wire/interpreter';
12
+ /** Adapters whose private message builders we reuse. Instantiated once; the
13
+ * builders are pure with respect to the request. */
14
+ /** The hand-written adapter methods the specs cannot express as data.
15
+ *
16
+ * All optional: a registry built by ONE adapter to drive its own spec carries only
17
+ * its own handle, and each transform below is reached only from that provider's
18
+ * spec. Requiring the full set would force every adapter to import every other
19
+ * adapter just to build its own request. */
20
+ export interface AdapterHandles {
21
+ anthropic?: any;
22
+ google?: any;
23
+ openaiResponses?: any;
24
+ openaiCompletions?: any;
25
+ googleInteractions?: any;
26
+ openrouterMedia?: any;
27
+ }
28
+ export declare function makeRegistry(a: AdapterHandles): Registry;
@@ -1,14 +1,14 @@
1
1
  /** Count API adapter — exact token counting via provider endpoints. */
2
2
  import type { Message } from '../../../llm/types/messages';
3
3
  import type { TokenCountContext, TokenCounter, LearnInput } from '../../../agent/types';
4
- import type { FetchFn } from '../../../network/types';
5
- import type { ModelCatalog } from '../../model-catalog/catalog';
4
+ import type { EngineFetch } from '../../../network/types';
5
+ import type { ModelCatalog } from '../../../catalog/catalog';
6
6
  /** Anthropic count endpoint: POST /v1/messages/count_tokens */
7
7
  export declare class AnthropicCountApi {
8
8
  private readonly apiKey;
9
9
  private readonly fetchFn;
10
10
  private readonly baseURL;
11
- constructor(apiKey: string, fetchFn?: FetchFn, baseURL?: string);
11
+ constructor(apiKey: string, fetchFn: EngineFetch, baseURL?: string);
12
12
  countMessages(model: string, messages: Array<{
13
13
  role: string;
14
14
  content: unknown;
@@ -20,18 +20,52 @@ export declare class GoogleCountApi {
20
20
  private readonly apiKey;
21
21
  private readonly fetchFn;
22
22
  private readonly baseURL;
23
- constructor(apiKey: string, fetchFn?: FetchFn, baseURL?: string);
23
+ constructor(apiKey: string, fetchFn: EngineFetch, baseURL?: string);
24
24
  countText(model: string, text: string): Promise<number>;
25
25
  }
26
- /** TokenCounter backed by Anthropic/Google count APIs. Falls back to heuristic
27
- * for fast estimates and unknown providers. */
26
+ /** xAI tokenizer: POST /v1/tokenize-text
27
+ *
28
+ * Their own SDK reaches this over gRPC (`xai_api.Tokenize/TokenizeText`), which
29
+ * is why it looked for a while like exact counting on xAI would cost a protobuf
30
+ * dependency. It does not: the REST host answers the same call with the same
31
+ * token list, so this is one more spec-built request and the library stays
32
+ * zero-dependency.
33
+ *
34
+ * What it counts is TEXT, not a chat request. Anthropic's and Google's endpoints
35
+ * take the message array a completion would send, so their answer matches what
36
+ * the completion is billed for; this one tokenizes the string you hand it, so it
37
+ * is exact for that string and excludes the framing the chat template adds
38
+ * around it. Still the right answer to "how many tokens is this content", and
39
+ * vastly better than four-chars-per-token — on one Cyrillic line the heuristic
40
+ * says 9 where the tokenizer says 19.
41
+ *
42
+ * The response names the list `token_ids` (the proto calls it `tokens`); the
43
+ * count is its length. */
44
+ export declare class XAICountApi {
45
+ private readonly apiKey;
46
+ private readonly fetchFn;
47
+ private readonly baseURL;
48
+ constructor(apiKey: string, fetchFn: EngineFetch, baseURL?: string);
49
+ countText(model: string, text: string): Promise<number>;
50
+ }
51
+ /** TokenCounter backed by Anthropic/Google/xAI count APIs. Falls back to
52
+ * heuristic for fast estimates and unknown providers. */
28
53
  export declare class CountApiCounter implements TokenCounter {
29
54
  private readonly providers;
30
55
  private heuristic;
56
+ private readonly catalog;
31
57
  constructor(catalog: ModelCatalog | null, providers?: {
32
58
  anthropic?: AnthropicCountApi;
33
59
  google?: GoogleCountApi;
60
+ xai?: XAICountApi;
34
61
  });
62
+ /** The id to SEND. `ctx.model` is our canonical slug — `claude-haiku-4.5` —
63
+ * which is not what the provider answers to: its callable id is the dated
64
+ * `claude-haiku-4-5-20251001`. The chat path translates through the catalog;
65
+ * this one did not, so the moment a model's slug and api id differed the count
66
+ * endpoint returned 404. Nothing noticed while the strategy was never
67
+ * selected. */
68
+ private apiModel;
35
69
  estimate(text: string, ctx?: TokenCountContext): number;
36
70
  estimateMessage(msg: Message, ctx?: TokenCountContext): number;
37
71
  measure(text: string, ctx?: TokenCountContext): Promise<number>;
@@ -1,7 +1,7 @@
1
1
  /** Heuristic token counter — chars-per-token with optional calibration. */
2
2
  import type { Message } from '../../../llm/types/messages';
3
3
  import type { TokenCountContext, TokenCounter, LearnInput } from '../../../agent/types';
4
- import type { ModelCatalog } from '../../model-catalog/catalog';
4
+ import type { ModelCatalog } from '../../../catalog/catalog';
5
5
  import type { CalibrationStore } from '../types';
6
6
  /** Count chars across a message's content parts. */
7
7
  export declare function messageChars(msg: Message): number;
@@ -1,7 +1,8 @@
1
1
  /** HybridTokenCounter — selects strategy per model based on catalog config. */
2
2
  import type { Message } from '../../../llm/types/messages';
3
3
  import type { TokenCountContext, TokenCounter, LearnInput } from '../../../agent/types';
4
- import type { ModelCatalog } from '../../model-catalog/catalog';
4
+ import type { ModelCatalog } from '../../../catalog/catalog';
5
+ import type { EngineFetch } from '../../../network/types';
5
6
  import type { CalibrationStore } from '../types';
6
7
  export interface HybridCounterConfig {
7
8
  catalog?: ModelCatalog;
@@ -9,7 +10,14 @@ export interface HybridCounterConfig {
9
10
  countApiKeys?: {
10
11
  anthropic?: string;
11
12
  google?: string;
13
+ xai?: string;
12
14
  };
15
+ /** Required to use the exact count APIs: they are HTTP calls, and every HTTP
16
+ * call in this library goes through the engine's fetch. Without it the exact
17
+ * strategies are unavailable and counting falls back to the heuristic — which
18
+ * is said out loud rather than done quietly, because a silent downgrade from
19
+ * exact to estimated is invisible in the only place it matters: the number. */
20
+ fetch?: EngineFetch;
13
21
  }
14
22
  /**
15
23
  * HybridTokenCounter routes based on catalog's tokenizer.strategy:
@@ -24,6 +32,8 @@ export declare class HybridTokenCounter implements TokenCounter {
24
32
  * counter. See CONSTITUTION.md standing decisions (2026-08-08). */
25
33
  private _tiktoken?;
26
34
  private countApi;
35
+ /** Said once per counter, not once per call. */
36
+ private warnedNoTiktoken;
27
37
  private readonly _config;
28
38
  constructor(config: HybridCounterConfig);
29
39
  warmCache(): Promise<void>;
@@ -31,6 +41,29 @@ export declare class HybridTokenCounter implements TokenCounter {
31
41
  estimateMessage(msg: Message, ctx?: TokenCountContext): number;
32
42
  measure(text: string, ctx?: TokenCountContext): Promise<number>;
33
43
  measureMessage(msg: Message, ctx?: TokenCountContext): Promise<number>;
44
+ /** Run the chosen strategy, falling back to the heuristic if — and ONLY if —
45
+ * the optional `tiktoken` peer is not installed.
46
+ *
47
+ * The catalog can name `tiktoken` for a model without the consumer having
48
+ * installed it: it is an optional PEER dependency precisely so that most
49
+ * people do not carry its 5.6 MB of wasm. Without this, marking OpenAI models
50
+ * as exactly-countable would turn a number into a thrown error for everyone
51
+ * who did not opt in — and the guide has always promised the opposite
52
+ * ("without it everything still works").
53
+ *
54
+ * Only THAT error is caught. A network failure inside the count API, or a
55
+ * genuine tokenizer fault, still surfaces: silently answering with an estimate
56
+ * when an exact count was asked for and was possible is how a wrong number
57
+ * gets believed. */
58
+ private withoutOptionalPeer;
34
59
  learn(input: LearnInput): void;
60
+ /** Which strategy a context resolves to, without running it.
61
+ *
62
+ * Public because a caller has to be able to ASK. `countTokens()` records a
63
+ * zero-cost ledger entry for a count-API call, and it was deciding that from
64
+ * the provider and the presence of a key — that is intent, not evidence. It now
65
+ * asks what actually ran, so the ledger cannot claim a provider call that never
66
+ * left the process. */
67
+ strategyNameFor(ctx?: TokenCountContext): 'tiktoken' | 'count_api' | 'heuristic';
35
68
  private strategyFor;
36
69
  }
@@ -6,8 +6,15 @@
6
6
  */
7
7
  import type { Message } from '../../../llm/types/messages';
8
8
  import type { TokenCountContext, TokenCounter, LearnInput } from '../../../agent/types';
9
+ /** Marks the one error a caller is allowed to treat as "use something else". */
10
+ export declare const TIKTOKEN_MISSING: unique symbol;
9
11
  /** Build the error thrown when the optional peer is missing. Exported for tests; not public API. */
10
12
  export declare function tiktokenUnavailableError(cause: unknown): Error;
13
+ /** Is this the "tiktoken is not installed" error, rather than any other failure?
14
+ *
15
+ * Matched on the marker below rather than on the message text, so rewording the
16
+ * message cannot silently turn a graceful fallback into a thrown error. */
17
+ export declare function isTiktokenUnavailable(err: unknown): boolean;
11
18
  export declare class TiktokenCounter implements TokenCounter {
12
19
  private encodings;
13
20
  estimate(text: string, ctx?: TokenCountContext): number;
@@ -4,7 +4,7 @@ import type { HookBus } from '../../bus/hook-bus';
4
4
  import type { Message } from '../../llm/types/messages';
5
5
  import type { TokenCounter } from '../../agent/types';
6
6
  import type { ConversationHistory } from '../../agent/history';
7
- import type { ModelCatalog } from '../model-catalog/catalog';
7
+ import type { ModelCatalog } from '../../catalog/catalog';
8
8
  import type { Persistence } from '../persistence/types';
9
9
  import type { CalibrationStore, ContextThresholds, CalibrationConfig } from './types';
10
10
  export interface ContextMeasurerConfig {
@@ -2,7 +2,7 @@
2
2
  * pricing or provider-reported totals, emits onCostEntry/onBudgetWarning/
3
3
  * onBudgetExceeded. */
4
4
  import type { CostEntry } from '../../bus/hook-map';
5
- import type { ModelCatalog } from '../model-catalog/catalog';
5
+ import type { ModelCatalog } from '../../catalog/catalog';
6
6
  import type { Budget, CostCollectorConfig, CostFilter, CostSummary } from './cost-collector-types';
7
7
  export declare class CostCollector {
8
8
  private ledger;
@@ -1,7 +1,7 @@
1
1
  /** CostCollector internals — pure cost-math + filtering helpers, split out of
2
2
  * the class so each is independently testable. */
3
3
  import type { CostEntry } from '../../bus/hook-map';
4
- import type { ModelCatalog } from '../model-catalog/catalog';
4
+ import type { ModelCatalog } from '../../catalog/catalog';
5
5
  import type { CostFilter, CostSummary } from './cost-collector-types';
6
6
  /** Pull provider-reported cost evidence out of a raw response body. */
7
7
  export declare function extractProviderCost(provider: string, raw: unknown): Record<string, unknown>;
@@ -1,6 +1,6 @@
1
1
  /** CostCollector public types: config, budgets, filters, summaries. */
2
2
  import type { HookBus } from '../../bus/hook-bus';
3
- import type { ModelCatalog } from '../model-catalog/catalog';
3
+ import type { ModelCatalog } from '../../catalog/catalog';
4
4
  export interface CostCollectorConfig {
5
5
  hooks: HookBus;
6
6
  catalog: ModelCatalog;
@@ -3,7 +3,7 @@
3
3
  * parts (provider_ref / inline base64 / url). */
4
4
  import type { HookBus } from '../../bus/hook-bus';
5
5
  import type { EngineFetch } from '../../network/types';
6
- import type { ModelCatalog } from '../model-catalog/catalog';
6
+ import type { ModelCatalog } from '../../catalog/catalog';
7
7
  import { FileAttachment, type FileContent } from './attachment';
8
8
  import type { FileProviderAdapter, RemoteFileInfo } from './provider-adapter';
9
9
  import { type FileStrategy } from './strategy';
@@ -1,5 +1,5 @@
1
1
  /** FileStrategy — pluggable decision maker for how to attach files. */
2
- import type { ModelInfo } from '../model-catalog/catalog';
2
+ import type { ModelInfo } from '../../catalog/catalog';
3
3
  import type { FileAttachment } from './attachment';
4
4
  export interface FileStrategyContext {
5
5
  file: FileAttachment;
@@ -1,6 +1,6 @@
1
1
  /** ToolRegistry — unified access across multiple backends with caching, search, filtering. */
2
2
  import type { InternalTool, ToolBackend, ToolFilter, SearchOptions } from './types';
3
- import type { ModelCatalog } from '../model-catalog/catalog';
3
+ import type { ModelCatalog } from '../../catalog/catalog';
4
4
  export declare class ToolRegistry {
5
5
  private backends;
6
6
  private cache;
@@ -3,7 +3,7 @@ import type { HookBus } from '../../../bus/hook-bus';
3
3
  import type { LLMClientConfig } from '../../../llm/client-config';
4
4
  import type { ProviderName } from '../../../llm/types/provider';
5
5
  import type { JsonSchema } from '../../../llm/types/tools';
6
- import type { ModelCatalog } from '../../model-catalog/catalog';
6
+ import type { ModelCatalog } from '../../../catalog/catalog';
7
7
  import type { ToolRegistry } from '../registry';
8
8
  import type { CompatFile, ModelPreference } from '../types';
9
9
  import type { TokenCounter } from '../../../agent/types';
@@ -4,6 +4,7 @@
4
4
  * custom handler or a model id to auto-wire. */
5
5
  import type { EngineHandle } from '../../helpers/engine';
6
6
  import type { ProviderName } from '../../llm/types/provider';
7
+ import type { Message } from '../../llm/types/messages';
7
8
  import type { McpCreateMessageParams, McpCreateMessageResult } from './types';
8
9
  export type McpSamplingHandler = (params: McpCreateMessageParams) => Promise<McpCreateMessageResult>;
9
10
  /** Auto-wire sampling to our LLM. */
@@ -13,5 +14,26 @@ export interface McpSamplingViaLLM {
13
14
  engine?: EngineHandle;
14
15
  }
15
16
  export type McpSamplingConfig = McpSamplingHandler | McpSamplingViaLLM;
17
+ /** The one thing this module needs from the ergonomic layer: run a completion.
18
+ *
19
+ * Taken as a parameter rather than imported, because `plugins` importing
20
+ * `helpers` closed a cycle (`helpers` already imports most of `plugins`). The
21
+ * public `samplingHandler` lives in `helpers/mcp` and supplies `complete`; the
22
+ * mapping between MCP's message shape and ours stays here, where it belongs. */
23
+ export type McpCompleteFn = (args: {
24
+ model: string;
25
+ provider?: ProviderName;
26
+ engine?: EngineHandle;
27
+ system?: string;
28
+ prompt: Message[];
29
+ maxTokens?: number;
30
+ temperature?: number;
31
+ }) => Promise<{
32
+ text: string;
33
+ response: {
34
+ model: string;
35
+ finishReason: string;
36
+ };
37
+ }>;
16
38
  /** Build a sampling handler: pass-through a custom function, or auto-wire a model. */
17
- export declare function samplingHandler(config: McpSamplingConfig): McpSamplingHandler;
39
+ export declare function samplingHandlerWith(complete: McpCompleteFn, config: McpSamplingConfig): McpSamplingHandler;
@@ -62,13 +62,19 @@ export declare class HttpTransport extends BaseJsonRpcTransport implements McpTr
62
62
  * two. The eventual JSON-RPC response has no pending entry to settle — its only meaning is "the
63
63
  * stream ended", which is reported through `onEnd`. */
64
64
  sendLongLivedRequest(method: string, params?: unknown, onEnd?: (error?: unknown) => void): Promise<number>;
65
- private headers;
66
- /** Modern-era routing headers: `Mcp-Method` on every request, plus `Mcp-Name` carrying the
67
- * method's subject (tool name / prompt name / resource URI) so a gateway can route and
68
- * authorize without parsing the body. No-op on the handshake wire. */
69
- private routingHeaders;
70
- /** Base headers + any OAuth bearer + per-call extras. */
71
- private authedHeaders;
65
+ /** The shared registry plus the two rules only MCP has. */
66
+ private readonly wireRegistry;
67
+ /** Build one MCP request from its spec.
68
+ *
69
+ * Everything that varies — the negotiated era, the session, the declared
70
+ * protocol version, a resolved bearer, the resumption cursor is passed IN, so
71
+ * the spec decides which headers those facts produce. That decision used to be
72
+ * spread across three private helpers and the order in which their results were
73
+ * spread into an object literal.
74
+ *
75
+ * `provider` / `model` route and queue the call inside the NetworkEngine and are
76
+ * not part of the wire, so they wrap the spec's output. */
77
+ private request0;
72
78
  private post;
73
79
  }
74
80
  /** Extract the JSON-RPC response matching `id` from a JSON or SSE body. */
@@ -0,0 +1,21 @@
1
+ /** The two MCP rules a spec cannot express as data, and where they live.
2
+ *
3
+ * Every other named spec rule sits in `src/llm/wire-transforms.ts`, but these
4
+ * cannot: `llm -> plugins` is a forbidden edge (the layer test names it), and MCP
5
+ * is a plugin. So the transport composes its own registry from the shared one
6
+ * rather than the shared one reaching down into MCP.
7
+ *
8
+ * Both are genuinely code rather than data:
9
+ *
10
+ * `mcpModern` — era is set only AFTER discovery succeeds, so a request has to be
11
+ * judged by the version it DECLARES as well. Keying on era alone left the
12
+ * `server/discover` probe itself half-modern, which a modern server rejects.
13
+ *
14
+ * `mcpNameHeader` — the subject lives under a different param per method (`name`
15
+ * for tools/call and prompts/get, `uri` for resources/read), so this is a lookup
16
+ * followed by a read at the key that lookup returned. A template can express a
17
+ * fixed path, not a computed one.
18
+ */
19
+ import type { Registry } from '../../wire/interpreter';
20
+ /** Add the MCP rules to a base registry, leaving the base untouched. */
21
+ export declare function mcpWireRegistry(base: Registry): Registry;
@@ -7,7 +7,7 @@
7
7
  * by core LLM adapters + an onCompletion subscriber, NOT this class. */
8
8
  import type { HookBus } from '../../bus/hook-bus';
9
9
  import type { EngineFetch } from '../../network/types';
10
- import type { ModelCatalog } from '../model-catalog/catalog';
10
+ import type { ModelCatalog } from '../../catalog/catalog';
11
11
  import { type AudioGenRequest, type ImageEditRequest, type ImageGenRequest, type MediaOutputConfig, type MediaProviderAdapter, type MediaResult, type MediaStore, type VideoGenRequest } from './types';
12
12
  export interface MediaOutputInit {
13
13
  hooks: HookBus;
@@ -0,0 +1,15 @@
1
+ /** The upload part for a document: its bytes, and a name for them.
2
+ *
3
+ * All three hosted backends upload a document the same way — the text as a
4
+ * `text/plain` file — and all three had their own copy of this, which is how they
5
+ * came to disagree about nothing yet still had to be fixed three times.
6
+ *
7
+ * The fallback name is derived from the CONTENT. It used to be a random UUID,
8
+ * which made the request unreproducible: it could not be asserted in a test,
9
+ * frozen in a fixture, or matched against a log, and a retried upload arrived
10
+ * under a different name every time. A content hash keeps a retry idempotent
11
+ * while still separating two different documents.
12
+ */
13
+ import type { MultipartFile } from '../../llm/wire-multipart';
14
+ import type { DocumentSource } from './types';
15
+ export declare function documentFile(source: DocumentSource): MultipartFile;
@@ -38,9 +38,14 @@ export declare class HostedGoogleRetrievalBackend implements RetrievalBackend {
38
38
  private readonly fetch;
39
39
  private readonly baseURL;
40
40
  constructor(config: HostedGoogleRetrievalConfig);
41
- private authHeaders;
42
- /** Auth headers for multipart/form-data file upload (no content-type override). */
43
- private authHeadersNoContentType;
41
+ /** File-search rules need no adapter handles. */
42
+ private readonly wireRegistry;
43
+ /** Build one file-search request from its spec, then add the engine metadata.
44
+ *
45
+ * `provider` / `model` / `responseType` route and queue the call inside the
46
+ * NetworkEngine; they are not part of the wire, so they wrap the spec's output
47
+ * rather than being described by it. */
48
+ private request;
44
49
  createCorpus(opts: CreateCorpusOptions): Promise<CorpusRef>;
45
50
  addDocument(corpus: CorpusRef, source: DocumentSource, opts?: AddDocumentOptions): Promise<DocumentRef>;
46
51
  /** Poll a long-running Operation until done: true.
@@ -29,7 +29,15 @@ export declare class HostedOpenAIRetrievalBackend implements RetrievalBackend {
29
29
  private readonly fetch;
30
30
  private readonly baseURL;
31
31
  constructor(config: HostedOpenAIRetrievalConfig);
32
- private bearer;
32
+ /** Vector-store rules need no adapter handles. */
33
+ private readonly wireRegistry;
34
+ /** Build one vector-store request from its spec, then add the engine metadata.
35
+ *
36
+ * `provider` / `model` / `responseType` are how the NetworkEngine routes and
37
+ * queues the call — they are not part of the wire, so they are wrapped around
38
+ * the spec's output rather than described by it. A multipart spec names the
39
+ * fields but not the bytes, so an upload passes its file in here. */
40
+ private request;
33
41
  createCorpus(opts: CreateCorpusOptions): Promise<CorpusRef>;
34
42
  addDocument(corpus: CorpusRef, source: DocumentSource, opts?: AddDocumentOptions): Promise<DocumentRef>;
35
43
  indexStatus(corpus: CorpusRef): Promise<IndexStatus>;
@@ -41,8 +41,14 @@ export declare class HostedXaiRetrievalBackend implements RetrievalBackend {
41
41
  private readonly baseURL;
42
42
  private readonly managementBaseURL;
43
43
  constructor(config: HostedXaiRetrievalConfig);
44
- private stdBearer;
45
- private mgmtBearer;
44
+ /** Collection rules need no adapter handles. */
45
+ private readonly wireRegistry;
46
+ /** Build one collections request from its spec, then add the engine metadata.
47
+ *
48
+ * BOTH planes are handed to every spec: which host and which key a call uses is
49
+ * a property of the ENDPOINT, so the spec decides it rather than the caller
50
+ * picking a bearer helper and hoping it matches the URL it typed. */
51
+ private request;
46
52
  createCorpus(opts: CreateCorpusOptions): Promise<CorpusRef>;
47
53
  addDocument(corpus: CorpusRef, source: DocumentSource, opts?: AddDocumentOptions): Promise<DocumentRef>;
48
54
  indexStatus(corpus: CorpusRef): Promise<IndexStatus>;