@combycode/llm-sdk 2.3.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +429 -0
- package/MIGRATION.md +130 -0
- package/dist/bus/hook-bus.d.ts +13 -3
- package/dist/bus/hook-map.d.ts +24 -0
- package/dist/catalog/catalog.d.ts +23 -26
- package/dist/helpers/client-resolver.d.ts +21 -2
- package/dist/helpers/count-tokens.d.ts +1 -1
- package/dist/helpers/engine.d.ts +27 -5
- package/dist/helpers/select-model.d.ts +35 -0
- package/dist/index.browser.js +17394 -4392
- package/dist/index.d.ts +3 -3
- package/dist/index.js +17394 -4392
- package/dist/llm/client-config.d.ts +8 -0
- package/dist/llm/client.d.ts +5 -0
- package/dist/llm/providers/anthropic/batch.d.ts +13 -2
- package/dist/llm/providers/anthropic/constants.d.ts +8 -43
- package/dist/llm/providers/anthropic/files.d.ts +13 -2
- package/dist/llm/providers/anthropic/messages.d.ts +18 -1
- package/dist/llm/providers/google/batch.d.ts +14 -1
- package/dist/llm/providers/google/constants.d.ts +6 -15
- package/dist/llm/providers/google/embeddings.d.ts +8 -1
- package/dist/llm/providers/google/files.d.ts +19 -1
- package/dist/llm/providers/google/generate.d.ts +15 -1
- package/dist/llm/providers/google/interactions.d.ts +5 -1
- package/dist/llm/providers/google/media.d.ts +26 -3
- package/dist/llm/providers/openai/batch.d.ts +19 -2
- package/dist/llm/providers/openai/completions.d.ts +13 -1
- package/dist/llm/providers/openai/embeddings.d.ts +13 -1
- package/dist/llm/providers/openai/files.d.ts +13 -2
- package/dist/llm/providers/openai/media.d.ts +21 -1
- package/dist/llm/providers/openai/moderations.d.ts +11 -1
- package/dist/llm/providers/openai/realtime.d.ts +3 -0
- package/dist/llm/providers/openai/responses.d.ts +10 -2
- package/dist/llm/providers/openai/transcription.d.ts +7 -1
- package/dist/llm/providers/openrouter/completions.d.ts +5 -3
- package/dist/llm/providers/openrouter/embeddings.d.ts +3 -0
- package/dist/llm/providers/openrouter/media.d.ts +14 -4
- package/dist/llm/providers/openrouter/responses.d.ts +5 -3
- package/dist/llm/providers/xai/batch.d.ts +13 -2
- package/dist/llm/providers/xai/completions.d.ts +5 -3
- package/dist/llm/providers/xai/files.d.ts +13 -2
- package/dist/llm/providers/xai/media.d.ts +28 -10
- package/dist/llm/providers/xai/responses.d.ts +5 -3
- package/dist/llm/response-shape.d.ts +96 -0
- package/dist/llm/types/provider.d.ts +11 -0
- package/dist/llm/types/request.d.ts +5 -6
- package/dist/llm/wire-multipart.d.ts +26 -0
- package/dist/{wire/transforms.d.ts → llm/wire-transforms.d.ts} +11 -5
- package/dist/plugins/context-measurer/counter/count-api.d.ts +39 -5
- package/dist/plugins/context-measurer/counter/hybrid.d.ts +33 -0
- package/dist/plugins/context-measurer/counter/tiktoken.d.ts +7 -0
- package/dist/plugins/mcp/transport-http.d.ts +13 -7
- package/dist/plugins/mcp/wire-rules.d.ts +21 -0
- package/dist/plugins/retrieval/document-file.d.ts +15 -0
- package/dist/plugins/retrieval/hosted-google.d.ts +8 -3
- package/dist/plugins/retrieval/hosted-openai.d.ts +9 -1
- package/dist/plugins/retrieval/hosted-xai.d.ts +8 -2
- package/dist/wire/chat-specs.d.ts +32 -0
- package/dist/wire/inherit.d.ts +0 -4
- package/dist/wire/interpreter.d.ts +84 -1
- package/dist/wire/mcp-specs.d.ts +13 -0
- package/dist/wire/media-specs.d.ts +20 -0
- package/dist/wire/pins.d.ts +34 -0
- package/dist/wire/registry.d.ts +6 -4
- package/dist/wire/retrieval-specs.d.ts +14 -0
- package/dist/wire/service-specs.d.ts +18 -0
- package/dist/wire/utility-specs.d.ts +10 -0
- package/package.json +4 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* No model returns everything: speaker labels and word timings live on different
|
|
16
16
|
* models. `keywords` / `languages` are `gpt-transcribe`-only and 400 elsewhere. */
|
|
17
17
|
import type { TranscriptLanguage, TranscriptSegment, TranscriptWord } from '../../types/audio';
|
|
18
|
-
import type { EngineFetch } from '../../../network/types';
|
|
18
|
+
import type { EngineFetch, HttpRequest } from '../../../network/types';
|
|
19
19
|
export interface OpenAITranscriptionAdapterConfig {
|
|
20
20
|
apiKey: string;
|
|
21
21
|
baseURL?: string;
|
|
@@ -52,6 +52,12 @@ export declare class OpenAITranscriptionAdapter {
|
|
|
52
52
|
private readonly apiKey;
|
|
53
53
|
private readonly baseURL;
|
|
54
54
|
constructor(config: OpenAITranscriptionAdapterConfig);
|
|
55
|
+
/** Named code these specs need. */
|
|
56
|
+
private readonly wireRegistry;
|
|
57
|
+
/** The multipart spec names the FIELDS; the audio bytes come from the request.
|
|
58
|
+
* `wordTimestamps` with `diarization` is rejected before this point: they select
|
|
59
|
+
* different response formats and no model serves both. */
|
|
60
|
+
buildTranscribeRequest(req: TranscriptionRequest): HttpRequest;
|
|
55
61
|
transcribe(req: TranscriptionRequest, fetch: EngineFetch): Promise<OpenAITranscriptionResult>;
|
|
56
62
|
}
|
|
57
63
|
export declare function parseTranscription(body: unknown): OpenAITranscriptionResult;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** OpenRouter provider adapter — OpenAI-compatible with extensions. */
|
|
2
2
|
import type { SSEEvent } from '../../../network/types';
|
|
3
|
-
import type { ProviderAdapter
|
|
4
|
-
import type { NormalizedRequest } from '../../types/request';
|
|
3
|
+
import type { ProviderAdapter } from '../../types/provider';
|
|
5
4
|
import type { CompletionResponse } from '../../types/response';
|
|
6
5
|
import type { StreamEvent } from '../../types/stream';
|
|
7
6
|
import { OpenAIAdapter } from '../openai/completions';
|
|
@@ -14,7 +13,10 @@ export declare class OpenRouterAdapter extends OpenAIAdapter {
|
|
|
14
13
|
constructor(config: OpenRouterAdapterConfig);
|
|
15
14
|
baseURL(): string;
|
|
16
15
|
completionPath(): string;
|
|
17
|
-
buildRequest(
|
|
16
|
+
/** Everything this class used to do to `super.buildRequest()` — the max_tokens
|
|
17
|
+
* rename, the reasoning strip, the tier remap, the routing passthrough — is the
|
|
18
|
+
* `openrouter` overlay in the shared spec. Naming the flavor IS the override now. */
|
|
19
|
+
protected readonly wireFlavor: string;
|
|
18
20
|
parseResponse(raw: unknown, latencyMs: number): CompletionResponse;
|
|
19
21
|
/** Stateful — emit a single `web_search` builtin-tool pair the first time
|
|
20
22
|
* `url_citation` annotations appear in the stream (the `:online` search signal). */
|
|
@@ -4,4 +4,7 @@ export declare class OpenRouterEmbeddingAdapter extends OpenAIEmbeddingAdapter {
|
|
|
4
4
|
readonly name: string;
|
|
5
5
|
constructor(config: OpenAIEmbeddingAdapterConfig);
|
|
6
6
|
protected embeddingsPath(): string;
|
|
7
|
+
/** The whole OpenRouter delta is the URL, so the spec that carries it is the
|
|
8
|
+
* whole override. */
|
|
9
|
+
protected specId(): string;
|
|
7
10
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* image (and audio) generation go through `POST /api/v1/chat/completions` with
|
|
3
3
|
* a `modalities` field; output comes back on `message.images[]` /
|
|
4
4
|
* `message.audio`. Cost is the provider-reported `usage.cost`. */
|
|
5
|
-
import type { EngineFetch } from '../../../network/types';
|
|
5
|
+
import type { EngineFetch, HttpRequest } from '../../../network/types';
|
|
6
6
|
import type { AudioGenRequest, ImageEditRequest, ImageGenRequest, MediaCapabilities, MediaProviderAdapter, RawMediaResult } from '../../../plugins/media/types';
|
|
7
7
|
export interface OpenRouterMediaAdapterConfig {
|
|
8
8
|
apiKey: string;
|
|
@@ -14,12 +14,22 @@ export declare class OpenRouterMediaAdapter implements MediaProviderAdapter {
|
|
|
14
14
|
private readonly baseURL;
|
|
15
15
|
constructor(config: OpenRouterMediaAdapterConfig);
|
|
16
16
|
capabilities(): MediaCapabilities;
|
|
17
|
-
|
|
17
|
+
/** Named code the specs cannot express as data — the data-URL and image_config. */
|
|
18
|
+
private readonly wireRegistry;
|
|
19
|
+
/** Build one request from its spec, then add the engine metadata. */
|
|
20
|
+
private fromSpec;
|
|
21
|
+
/** Text-to-image. OpenRouter has no image endpoint: images come back from
|
|
22
|
+
* chat/completions with `modalities: ['image','text']`. */
|
|
23
|
+
buildImageRequest(req: ImageGenRequest, model?: string): HttpRequest;
|
|
24
|
+
/** Image-to-image: the same call with the source image as a second content part. */
|
|
25
|
+
buildEditImageRequest(req: ImageEditRequest, model?: string): HttpRequest;
|
|
26
|
+
/** Audio out, again through chat/completions. */
|
|
27
|
+
buildAudioRequest(req: AudioGenRequest, model?: string): HttpRequest;
|
|
28
|
+
/** Exposed for the wire registry: `image_config` from normalised params. */
|
|
29
|
+
imageConfig(params: ImageGenRequest['params']): Record<string, unknown>;
|
|
18
30
|
generateImage(req: ImageGenRequest, fetch: EngineFetch): Promise<RawMediaResult[]>;
|
|
19
31
|
editImage(req: ImageEditRequest, fetch: EngineFetch): Promise<RawMediaResult[]>;
|
|
20
32
|
generateAudio(req: AudioGenRequest, fetch: EngineFetch): Promise<RawMediaResult>;
|
|
21
|
-
/** image_config from normalized params (aspect_ratio / image_size / strength). */
|
|
22
|
-
private imageConfig;
|
|
23
33
|
private chat;
|
|
24
34
|
private chatImage;
|
|
25
35
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/** OpenRouter Responses API adapter.
|
|
2
2
|
* Drop-in replacement for OpenAI Responses API at openrouter.ai/api/v1/responses.
|
|
3
3
|
* Stateless: no previous_response_id support (beta limitation). */
|
|
4
|
-
import type { ProviderAdapter
|
|
5
|
-
import type { NormalizedRequest } from '../../types/request';
|
|
4
|
+
import type { ProviderAdapter } from '../../types/provider';
|
|
6
5
|
import { OpenAIResponsesAdapter } from '../openai/responses';
|
|
7
6
|
export interface OpenRouterResponsesAdapterConfig {
|
|
8
7
|
apiKey: string;
|
|
@@ -13,5 +12,8 @@ export declare class OpenRouterResponsesAdapter extends OpenAIResponsesAdapter {
|
|
|
13
12
|
constructor(config: OpenRouterResponsesAdapterConfig);
|
|
14
13
|
baseURL(): string;
|
|
15
14
|
completionPath(): string;
|
|
16
|
-
buildRequest(
|
|
15
|
+
/** Everything this class used to do to `super.buildRequest()` — the max_tokens
|
|
16
|
+
* rename, the reasoning strip, the tier remap, the routing passthrough — is the
|
|
17
|
+
* `openrouter` overlay in the shared spec. Naming the flavor IS the override now. */
|
|
18
|
+
protected readonly wireFlavor: string;
|
|
17
19
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** xAI batch adapter — create batch, add requests, poll, get results.
|
|
2
2
|
* All HTTP flows through the injected EngineFetch (NetworkEngine queue). */
|
|
3
|
-
import type { EngineFetch } from '../../../network/types';
|
|
3
|
+
import type { EngineFetch, HttpRequest } from '../../../network/types';
|
|
4
4
|
import type { BatchProviderAdapter, BatchRequest, BatchResult, BatchStatus } from '../../../plugins/batch/types';
|
|
5
5
|
export interface XAIBatchAdapterConfig {
|
|
6
6
|
apiKey: string;
|
|
@@ -11,7 +11,18 @@ export declare class XAIBatchAdapter implements BatchProviderAdapter {
|
|
|
11
11
|
private readonly apiKey;
|
|
12
12
|
private readonly baseURL;
|
|
13
13
|
constructor(config: XAIBatchAdapterConfig);
|
|
14
|
-
|
|
14
|
+
/** Batch rules need no adapter handles: the request list is mapped by the spec. */
|
|
15
|
+
private readonly wireRegistry;
|
|
16
|
+
/** Build one batch request from its spec, then add the engine metadata.
|
|
17
|
+
*
|
|
18
|
+
* `bodyKind: none` in a spec means no body at all: the interpreter reports that
|
|
19
|
+
* as `noBody`, and the engine wants the field simply absent. */
|
|
20
|
+
private fromSpec;
|
|
21
|
+
buildCreateRequest(requests: BatchRequest[]): HttpRequest;
|
|
22
|
+
buildAddRequestsRequest(batchId: string, requests: BatchRequest[]): HttpRequest;
|
|
23
|
+
buildStatusRequest(batchId: string): HttpRequest;
|
|
24
|
+
buildCancelRequest(batchId: string): HttpRequest;
|
|
25
|
+
buildResultsRequest(batchId: string): HttpRequest;
|
|
15
26
|
submit(requests: BatchRequest[], fetch: EngineFetch): Promise<string>;
|
|
16
27
|
getStatus(batchId: string, fetch: EngineFetch): Promise<BatchStatus>;
|
|
17
28
|
getResults(batchId: string, fetch: EngineFetch): Promise<BatchResult[]>;
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* - Returns reasoning_content in message (plain text, unlike OpenAI which hides it)
|
|
6
6
|
*/
|
|
7
7
|
import type { SSEEvent } from '../../../network/types';
|
|
8
|
-
import type { ProviderAdapter
|
|
9
|
-
import type { NormalizedRequest } from '../../types/request';
|
|
8
|
+
import type { ProviderAdapter } from '../../types/provider';
|
|
10
9
|
import type { CompletionResponse } from '../../types/response';
|
|
11
10
|
import type { StreamEvent } from '../../types/stream';
|
|
12
11
|
import { OpenAIAdapter, type OpenAIStreamState } from '../openai/completions';
|
|
@@ -18,7 +17,10 @@ export declare class XAIAdapter extends OpenAIAdapter {
|
|
|
18
17
|
readonly name: ProviderAdapter['name'];
|
|
19
18
|
constructor(config: XAIAdapterConfig);
|
|
20
19
|
baseURL(): string;
|
|
21
|
-
buildRequest(
|
|
20
|
+
/** Everything this class used to do to `super.buildRequest()` — the max_tokens
|
|
21
|
+
* rename, the reasoning strip, the tier remap, the routing passthrough — is the
|
|
22
|
+
* `xai` overlay in the shared spec. Naming the flavor IS the override now. */
|
|
23
|
+
protected readonly wireFlavor: string;
|
|
22
24
|
parseResponse(raw: unknown, latencyMs: number): CompletionResponse;
|
|
23
25
|
parseStreamEvent(event: SSEEvent, state?: OpenAIStreamState): StreamEvent[];
|
|
24
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** xAI file adapter — POST /v1/files (purpose=assistants).
|
|
2
2
|
* All HTTP flows through the injected EngineFetch (NetworkEngine queue). */
|
|
3
|
-
import type { EngineFetch } from '../../../network/types';
|
|
3
|
+
import type { EngineFetch, HttpRequest } from '../../../network/types';
|
|
4
4
|
import type { FileAttachment } from '../../../plugins/files/attachment';
|
|
5
5
|
import type { FileProviderAdapter, FileUploadResult, RemoteFileInfo } from '../../../plugins/files/provider-adapter';
|
|
6
6
|
export interface XAIFileAdapterConfig {
|
|
@@ -15,7 +15,18 @@ export declare class XAIFileAdapter implements FileProviderAdapter {
|
|
|
15
15
|
private readonly apiKey;
|
|
16
16
|
private readonly baseURL;
|
|
17
17
|
constructor(config: XAIFileAdapterConfig);
|
|
18
|
-
|
|
18
|
+
/** File rules need no adapter handles. */
|
|
19
|
+
private readonly wireRegistry;
|
|
20
|
+
/** Build one file request from its spec, then add the engine metadata.
|
|
21
|
+
*
|
|
22
|
+
* A multipart spec describes the FIELDS but not the bytes, so an upload passes
|
|
23
|
+
* its attachment in and the descriptor is filled here. `bodyKind: none` arrives
|
|
24
|
+
* as `noBody`; the engine wants the field simply absent. */
|
|
25
|
+
private fromSpec;
|
|
26
|
+
buildUploadRequest(file: FileAttachment, data: Uint8Array): Promise<HttpRequest>;
|
|
27
|
+
buildDeleteRequest(remoteId: string): Promise<HttpRequest>;
|
|
28
|
+
buildGetInfoRequest(remoteId: string): Promise<HttpRequest>;
|
|
29
|
+
buildListRequest(): Promise<HttpRequest>;
|
|
19
30
|
upload(file: FileAttachment, fetch: EngineFetch): Promise<FileUploadResult>;
|
|
20
31
|
delete(remoteId: string, fetch: EngineFetch): Promise<void>;
|
|
21
32
|
getInfo(remoteId: string, fetch: EngineFetch): Promise<RemoteFileInfo | null>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** xAI media adapter — images, TTS, video. All HTTP through EngineFetch. */
|
|
2
|
-
import type { EngineFetch } from '../../../network/types';
|
|
2
|
+
import type { EngineFetch, HttpRequest } from '../../../network/types';
|
|
3
3
|
import type { AudioGenRequest, ImageEditRequest, ImageGenRequest, MediaCapabilities, MediaProviderAdapter, RawMediaResult, VideoGenRequest, VideoStatus } from '../../../plugins/media/types';
|
|
4
4
|
export interface XAIMediaAdapterConfig {
|
|
5
5
|
apiKey: string;
|
|
@@ -11,7 +11,33 @@ export declare class XAIMediaAdapter implements MediaProviderAdapter {
|
|
|
11
11
|
private readonly baseURL;
|
|
12
12
|
constructor(config: XAIMediaAdapterConfig);
|
|
13
13
|
capabilities(): MediaCapabilities;
|
|
14
|
-
|
|
14
|
+
/** Named code the specs cannot express as data — image/video source refs. */
|
|
15
|
+
private readonly wireRegistry;
|
|
16
|
+
/** Build one request from its spec, then add the engine metadata.
|
|
17
|
+
*
|
|
18
|
+
* `provider`, `model` and `responseType` are routing and decoding concerns that
|
|
19
|
+
* never reach xAI, so the specs do not model them. */
|
|
20
|
+
private fromSpec;
|
|
21
|
+
/** Poll a video job. All three xAI video endpoints return a request_id that is
|
|
22
|
+
* polled here. */
|
|
23
|
+
buildVideoStatusRequest(operationId: string): HttpRequest;
|
|
24
|
+
buildVideoCancelRequest(operationId: string): HttpRequest;
|
|
25
|
+
/** Fetch bytes from a URL xAI put in its own response - a generated image or a
|
|
26
|
+
* finished video. The URL is an input because xAI chose it; the auth headers
|
|
27
|
+
* are still ours, which is why it is a spec and not a bare fetch. */
|
|
28
|
+
buildDownloadRequest(downloadUrl: string, model?: string): HttpRequest;
|
|
29
|
+
/** Text-to-image. */
|
|
30
|
+
buildImageRequest(req: ImageGenRequest, model?: string): HttpRequest;
|
|
31
|
+
/** Image-to-image edit — JSON with a data-URL or file_id, no multipart, no mask. */
|
|
32
|
+
buildEditImageRequest(req: ImageEditRequest, model?: string): HttpRequest;
|
|
33
|
+
/** TTS. arraybuffer because the response is audio bytes. */
|
|
34
|
+
buildAudioRequest(req: AudioGenRequest, model?: string): HttpRequest;
|
|
35
|
+
/** Video submission, routed by input and mode:
|
|
36
|
+
* no sourceVideo -> /v1/videos/generations
|
|
37
|
+
* sourceVideo + videoMode extend -> /v1/videos/extensions (duration only)
|
|
38
|
+
* sourceVideo + videoMode edit -> /v1/videos/edits (prompt + video)
|
|
39
|
+
* Three endpoints with three different field sets, so three specs. */
|
|
40
|
+
buildVideoRequest(req: VideoGenRequest, model?: string): HttpRequest;
|
|
15
41
|
generateImage(req: ImageGenRequest, fetch: EngineFetch): Promise<RawMediaResult[]>;
|
|
16
42
|
/** Image-to-image edit via `/v1/images/edits` (JSON; base64 data-URL or
|
|
17
43
|
* file_id, no multipart, no mask). */
|
|
@@ -21,14 +47,6 @@ export declare class XAIMediaAdapter implements MediaProviderAdapter {
|
|
|
21
47
|
private parseImages;
|
|
22
48
|
generateAudio(req: AudioGenRequest, fetch: EngineFetch): Promise<RawMediaResult>;
|
|
23
49
|
submitVideo(req: VideoGenRequest, fetch: EngineFetch): Promise<string>;
|
|
24
|
-
/** Route a video request to the right xAI endpoint by input + mode:
|
|
25
|
-
* - no `sourceVideo` → `/v1/videos/generations` (text/image-to-video)
|
|
26
|
-
* - `sourceVideo` + `videoMode:'extend'` (default) → `/v1/videos/extensions`
|
|
27
|
-
* — continues from the last frame; takes `duration`, NOT aspect/resolution.
|
|
28
|
-
* - `sourceVideo` + `videoMode:'edit'` → `/v1/videos/edits` — prompt + video
|
|
29
|
-
* only (no duration/aspect/resolution).
|
|
30
|
-
* All three return a `request_id` polled via the same status endpoint. */
|
|
31
|
-
private buildVideoSubmit;
|
|
32
50
|
getVideoStatus(operationId: string, fetch: EngineFetch): Promise<VideoStatus>;
|
|
33
51
|
downloadVideo(operationId: string, fetch: EngineFetch): Promise<RawMediaResult>;
|
|
34
52
|
cancelVideo(operationId: string, fetch: EngineFetch): Promise<void>;
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* - Reasoning automatic for reasoning models (no effort param needed)
|
|
6
6
|
* - Encrypted reasoning via include: ["reasoning.encrypted_content"]
|
|
7
7
|
*/
|
|
8
|
-
import type { ProviderAdapter
|
|
9
|
-
import type { NormalizedRequest } from '../../types/request';
|
|
8
|
+
import type { ProviderAdapter } from '../../types/provider';
|
|
10
9
|
import type { FileOutput } from '../../types/response';
|
|
11
10
|
import { OpenAIResponsesAdapter } from '../openai/responses';
|
|
12
11
|
export interface XAIResponsesAdapterConfig {
|
|
@@ -17,7 +16,10 @@ export declare class XAIResponsesAdapter extends OpenAIResponsesAdapter {
|
|
|
17
16
|
readonly name: ProviderAdapter['name'];
|
|
18
17
|
constructor(config: XAIResponsesAdapterConfig);
|
|
19
18
|
baseURL(): string;
|
|
20
|
-
buildRequest(
|
|
19
|
+
/** Everything this class used to do to `super.buildRequest()` — the max_tokens
|
|
20
|
+
* rename, the reasoning strip, the tier remap, the routing passthrough — is the
|
|
21
|
+
* `xai` overlay in the shared spec. Naming the flavor IS the override now. */
|
|
22
|
+
protected readonly wireFlavor: string;
|
|
21
23
|
/** xAI embeds code-execution files inline in the `logs` payload — extend the base
|
|
22
24
|
* extraction (which handles OpenAI-style annotations / image URLs) with the xAI shape. */
|
|
23
25
|
protected filesFromOutputItem(item: Record<string, unknown>): FileOutput[];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Runtime shape check — tell me when a provider's response stops looking like the
|
|
2
|
+
* one we learned to read.
|
|
3
|
+
*
|
|
4
|
+
* Parsing is the half of the library with the least warning before a failure. A
|
|
5
|
+
* request that goes wrong comes back as a 400. A RESPONSE that goes wrong comes
|
|
6
|
+
* back as a 200 with a field we do not read: the parse succeeds, the number is
|
|
7
|
+
* `undefined`, and the first sign is a cost dashboard that quietly reports zero
|
|
8
|
+
* or a tool call that never arrives. `google/generate` shipped for months
|
|
9
|
+
* discarding a `responseId` that had been there all along, under a comment saying
|
|
10
|
+
* it did not exist.
|
|
11
|
+
*
|
|
12
|
+
* So this compares the body against a description of the shape we understand:
|
|
13
|
+
*
|
|
14
|
+
* unknown path a field we have never seen. Either the provider added it —
|
|
15
|
+
* possibly the one carrying something we now want — or we are
|
|
16
|
+
* talking to something that is not the API we think.
|
|
17
|
+
* missing path a field present in EVERY recording is absent from this one.
|
|
18
|
+
* That is the shape of a rename, and a rename is what silently
|
|
19
|
+
* turns a token count into `undefined`.
|
|
20
|
+
* unknown value a discriminator (`type`, `role`, `finish_reason`, …) carries
|
|
21
|
+
* a value we do not branch on. A new content-block type is the
|
|
22
|
+
* most expensive kind of drift there is, because the content
|
|
23
|
+
* is simply dropped and nothing errors.
|
|
24
|
+
*
|
|
25
|
+
* OFF by default, and it never changes what is parsed: it only emits `onWarning`.
|
|
26
|
+
* A check that could alter a response would be a new way to break one.
|
|
27
|
+
*
|
|
28
|
+
* The descriptions in `response-shapes.json` are DERIVED, not hand-written —
|
|
29
|
+
* `scripts/derive-response-shapes.ts` builds them from the recorded corpus, and a
|
|
30
|
+
* test asserts every recorded body produces no unknown paths. That is what keeps
|
|
31
|
+
* the description honest: it cannot drift from real responses without a test going
|
|
32
|
+
* red, and when a provider does add a field, re-recording surfaces it as a
|
|
33
|
+
* decision rather than as a silent difference.
|
|
34
|
+
*/
|
|
35
|
+
import type { HookBus } from '../bus/hook-bus';
|
|
36
|
+
import type { SSEEvent } from '../network/types';
|
|
37
|
+
export interface ShapeDecl {
|
|
38
|
+
/** Every path seen across the recordings for this target. */
|
|
39
|
+
known: string[];
|
|
40
|
+
/** Paths present in EVERY recording — absence is worth a warning. */
|
|
41
|
+
expected: string[];
|
|
42
|
+
/** Discriminator path → the values we have actually seen. */
|
|
43
|
+
values?: Record<string, string[]>;
|
|
44
|
+
}
|
|
45
|
+
export interface ShapeSet {
|
|
46
|
+
/** The non-streaming body. */
|
|
47
|
+
response?: ShapeDecl;
|
|
48
|
+
/** One declaration PER SSE event type, keyed by the SSE `event:` name when the
|
|
49
|
+
* provider sends one and by the payload's own `type` otherwise.
|
|
50
|
+
*
|
|
51
|
+
* Pooling every event type into one declaration was the first attempt, and it
|
|
52
|
+
* cost the missing-field check entirely: `message_start` and
|
|
53
|
+
* `content_block_delta` share almost no fields, so the intersection across a
|
|
54
|
+
* whole stream came to a single path and nothing could ever be reported
|
|
55
|
+
* absent. Per type, "this event always carries usage" becomes a statement
|
|
56
|
+
* worth making. */
|
|
57
|
+
stream?: Record<string, ShapeDecl>;
|
|
58
|
+
}
|
|
59
|
+
/** How an SSE event names its own kind. The SSE `event:` line wins — Anthropic
|
|
60
|
+
* routes on it, and its payload `type` merely repeats it — then the payload's
|
|
61
|
+
* `type`, then a single bucket for providers that discriminate on neither
|
|
62
|
+
* (chat-completions chunks are all one shape). */
|
|
63
|
+
export declare function streamEventKey(event: SSEEvent, data: unknown): string;
|
|
64
|
+
export type ShapeBook = Record<string, ShapeSet>;
|
|
65
|
+
export interface ShapeFindings {
|
|
66
|
+
unknown: string[];
|
|
67
|
+
missing: string[];
|
|
68
|
+
/** `[path, value]` for a discriminator carrying something new. */
|
|
69
|
+
unknownValues: Array<[string, string]>;
|
|
70
|
+
}
|
|
71
|
+
/** Collect the paths of a value. Array indices collapse to `[]` — otherwise a
|
|
72
|
+
* three-element list would describe a different shape than a four-element one. */
|
|
73
|
+
export declare function pathsOf(value: unknown, prefix?: string, out?: Set<string>, depth?: number): Set<string>;
|
|
74
|
+
/** Collect `path → value` for the discriminator keys only. */
|
|
75
|
+
export declare function discriminatorsOf(value: unknown, prefix?: string, out?: Array<[string, string]>, depth?: number): Array<[string, string]>;
|
|
76
|
+
/** Compare one body against a declaration. Pure — the caller decides what to do. */
|
|
77
|
+
export declare function checkShape(body: unknown, decl: ShapeDecl): ShapeFindings;
|
|
78
|
+
/** Per-client checker. Holds what it has already reported, because the alternative
|
|
79
|
+
* is the same warning on every request for the rest of the process — which is how
|
|
80
|
+
* a diagnostic gets muted by the person reading it. */
|
|
81
|
+
export declare class ResponseShapeChecker {
|
|
82
|
+
private readonly hooks;
|
|
83
|
+
private readonly provider;
|
|
84
|
+
private readonly api;
|
|
85
|
+
private readonly book;
|
|
86
|
+
private readonly reported;
|
|
87
|
+
constructor(hooks: HookBus, provider: string, api: string, book: ShapeBook);
|
|
88
|
+
/** The declaration for this client, or undefined when nothing was recorded for
|
|
89
|
+
* it — in which case the check stays silent rather than calling every field
|
|
90
|
+
* unknown. */
|
|
91
|
+
private get set();
|
|
92
|
+
checkResponse(body: unknown): void;
|
|
93
|
+
checkStreamEvent(event: SSEEvent): void;
|
|
94
|
+
private report;
|
|
95
|
+
private warn;
|
|
96
|
+
}
|
|
@@ -6,6 +6,12 @@ import type { NormalizedRequest } from './request';
|
|
|
6
6
|
import type { CompletionResponse } from './response';
|
|
7
7
|
import type { StreamEvent } from './stream';
|
|
8
8
|
export type ProviderName = 'anthropic' | 'openai' | 'google' | 'xai' | 'openrouter';
|
|
9
|
+
/** The same five names at runtime. A `"vendor/model"` prefix can only be read as
|
|
10
|
+
* a provider if it IS one — OpenRouter's own ids are all `vendor/model`, so
|
|
11
|
+
* without this check `openai/gpt-5.4-nano` on OpenRouter parses as the provider
|
|
12
|
+
* `openai`, and `qwen/qwen3` parses as a provider named `qwen`. */
|
|
13
|
+
export declare const PROVIDER_NAMES: readonly ["anthropic", "openai", "google", "xai", "openrouter"];
|
|
14
|
+
export declare function isProviderName(value: string): value is ProviderName;
|
|
9
15
|
export type ApiType = 'completions' | 'responses' | 'messages' | 'interactions' | 'generate';
|
|
10
16
|
export interface ProviderConfig {
|
|
11
17
|
provider: ProviderName;
|
|
@@ -18,6 +24,11 @@ export interface ProviderHttpRequest {
|
|
|
18
24
|
/** Override of the default completion path. Used by providers that route
|
|
19
25
|
* per-API or per-modality. */
|
|
20
26
|
path?: string;
|
|
27
|
+
/** What the build deliberately left out, and why — a hosted tool this provider
|
|
28
|
+
* refuses to run beside the attached content, for instance. The client emits
|
|
29
|
+
* each as `onWarning`, because dropping a capability the caller asked for and
|
|
30
|
+
* saying nothing is how a missing feature gets mistaken for a working one. */
|
|
31
|
+
notes?: string[];
|
|
21
32
|
}
|
|
22
33
|
export interface ProviderAdapter {
|
|
23
34
|
readonly name: ProviderName;
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* and system are fixed at construction. The LLMClient internally builds
|
|
5
5
|
* this `NormalizedRequest` from (input, options, this.model, this.system). */
|
|
6
6
|
import type { ModerationRequest } from '../moderation/types';
|
|
7
|
-
import type { ModelWire } from '../../catalog/catalog';
|
|
8
7
|
import type { AudioOptions } from './audio';
|
|
9
8
|
import type { Message } from './messages';
|
|
10
9
|
import type { ServiceTier } from './tiers';
|
|
@@ -92,11 +91,11 @@ export interface NormalizedRequest {
|
|
|
92
91
|
serviceTier?: ServiceTier;
|
|
93
92
|
moderation?: ModerationRequest;
|
|
94
93
|
providerOptions?: ProviderOptions;
|
|
95
|
-
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
|
|
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;
|
|
100
99
|
audio?: AudioOptions;
|
|
101
100
|
outputModalities?: Array<'text' | 'audio'>;
|
|
102
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;
|
|
@@ -8,14 +8,20 @@
|
|
|
8
8
|
* structural message/content transformation, schema-shape rules, and one
|
|
9
9
|
* variant rule that is arithmetic rather than a pattern.
|
|
10
10
|
*/
|
|
11
|
-
import type { Registry } from '
|
|
11
|
+
import type { Registry } from '../wire/interpreter';
|
|
12
12
|
/** Adapters whose private message builders we reuse. Instantiated once; the
|
|
13
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. */
|
|
14
20
|
export interface AdapterHandles {
|
|
15
|
-
anthropic
|
|
16
|
-
google
|
|
17
|
-
openaiResponses
|
|
18
|
-
openaiCompletions
|
|
21
|
+
anthropic?: any;
|
|
22
|
+
google?: any;
|
|
23
|
+
openaiResponses?: any;
|
|
24
|
+
openaiCompletions?: any;
|
|
19
25
|
googleInteractions?: any;
|
|
20
26
|
openrouterMedia?: any;
|
|
21
27
|
}
|
|
@@ -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 {
|
|
4
|
+
import type { EngineFetch } from '../../../network/types';
|
|
5
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
|
|
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
|
|
23
|
+
constructor(apiKey: string, fetchFn: EngineFetch, baseURL?: string);
|
|
24
24
|
countText(model: string, text: string): Promise<number>;
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
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>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Message } from '../../../llm/types/messages';
|
|
3
3
|
import type { TokenCountContext, TokenCounter, LearnInput } from '../../../agent/types';
|
|
4
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;
|