File size: 11,665 Bytes
4e23b01 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | import type { Message, StreamedMessagePart, VideoURLPart } from './message';
import type { Tool } from './tool';
import type { TokenUsage } from './usage';
export type JsonSchemaObject = Record<string, unknown>;
export interface JsonObjectResponseFormat {
readonly type: 'json_object';
}
export interface JsonSchemaResponseFormat {
readonly type: 'json_schema';
readonly jsonSchema: {
readonly name: string;
readonly schema: JsonSchemaObject;
readonly strict?: boolean | undefined;
readonly description?: string | undefined;
};
}
export type ResponseFormat = JsonObjectResponseFormat | JsonSchemaResponseFormat;
/**
* Thinking effort passed to {@link ChatProvider.withThinking}.
*
* `'off'` and `'on'` are the only reserved values: `'off'` disables thinking,
* and `'on'` is the on-signal for boolean models (models that do not declare
* `support_efforts`). Everything else is a model-declared effort (e.g.
* `"low"`, `"high"`, `"max"`) carried as an open string. The type collapses to
* `string` at runtime; it exists purely as a semantic marker that a value is
* expected to be `'off'`, `'on'`, or a model-declared effort.
*
* Provider adapters receive an already-resolved effort. Adapters with a native
* effort field pass concrete strings through to their upstream API; model
* compatibility and fallback are resolved before this boundary.
*/
export type ThinkingEffort = 'off' | 'on' | (string & {});
/**
* Optional context passed to {@link ChatProvider.withMaxCompletionTokens} so a
* provider can tighten the caller-supplied cap to its own transport
* constraints.
*/
export interface MaxCompletionTokensOptions {
/**
* Tokens already consumed by the current context (API-reported input +
* output of the latest completed step). Chat-completions providers use it
* to size the cap to the remaining context window.
*/
readonly usedContextTokens?: number;
/** Model context-window size in tokens (`max_context_size`). */
readonly maxContextTokens?: number;
}
/**
* Normalized finish-reason signal indicating why a generation stopped.
*
* Each provider's native stop value is mapped to one of these, and the
* unmapped original string is preserved in `rawFinishReason` as an escape
* hatch. `null` means the provider did not emit a finish_reason (e.g. the
* stream was cut off before the final event).
*
* - `'completed'`: normal completion (OpenAI `'stop'`, Anthropic
* `'end_turn'` / `'stop_sequence'`, Gemini `'STOP'`).
* - `'tool_calls'`: generation paused so the caller can dispatch tool
* calls and feed their results back. Note that the OpenAI Responses API
* and Google GenAI report `'completed'` here; only the Chat
* Completions–style providers and Anthropic surface a dedicated value.
* - `'truncated'`: token budget exhausted (OpenAI `'length'`, Anthropic
* `'max_tokens'`, Gemini `'MAX_TOKENS'`, Responses `'max_output_tokens'`).
* - `'filtered'`: content filter or safety policy blocked the response.
* - `'paused'`: Anthropic-specific `'pause_turn'`.
* - `'other'`: recognized non-null reason that does not fit the categories
* above.
*/
export type FinishReason =
| 'completed'
| 'tool_calls'
| 'truncated'
| 'filtered'
| 'paused'
| 'other';
/**
* An async-iterable stream of message parts produced by a single LLM response.
*
* Consumers iterate over the stream with `for await..of` to receive
* {@link StreamedMessagePart} chunks. After the iteration completes, the
* {@link id}, {@link usage}, {@link finishReason}, and
* {@link rawFinishReason} properties reflect the final values reported by
* the provider.
*/
export interface StreamedMessage {
[Symbol.asyncIterator](): AsyncIterator<StreamedMessagePart>;
/** Provider-assigned response identifier, or `null` if not available. */
readonly id: string | null;
/** Token usage statistics, populated after the stream completes. */
readonly usage: TokenUsage | null;
/**
* Normalized finish reason, populated after the stream completes.
*
* `null` if the provider did not emit a finish_reason (for example, the
* stream was interrupted before the final event arrived).
*/
readonly finishReason: FinishReason | null;
/**
* Raw provider-specific finish_reason string, preserved verbatim as an
* escape hatch for callers that need the original wire value.
*
* `null` if the provider did not emit a finish_reason.
*/
readonly rawFinishReason: string | null;
/**
* Provider trace identifier read from the `x-trace-id` response header
* (Kimi/KFC only). Available as soon as the response headers arrive —
* before the stream body is drained — so hosts can attribute even an
* interrupted stream to its server-side request. `null` when the provider
* does not report one.
*/
readonly traceId?: string | null;
}
/**
* Options that can be forwarded to a single {@link ChatProvider.generate} call.
*/
export interface ProviderRequestAuth {
/** Bearer/API token resolved for this specific provider request. */
apiKey?: string;
/** Request-scoped headers. These override constructor-level default headers. */
headers?: Record<string, string>;
}
export interface GenerateOptions {
/**
* An {@link AbortSignal} that, when aborted, requests cancellation of the
* in-flight generate call. Providers that accept a signal will forward it
* to their underlying HTTP client; the generate loop in
* {@link generate | generate()} also checks the signal between streamed
* parts.
*/
signal?: AbortSignal;
/**
* Request-scoped provider auth. Hosts should resolve this immediately before
* each request/retry so providers never retain mutable credential state.
*/
auth?: ProviderRequestAuth;
/**
* Optional model-output format constraint. Providers map this to their native
* structured-output field when supported.
*/
responseFormat?: ResponseFormat;
/**
* Host-side instrumentation hook fired immediately before invoking the
* provider adapter's generate call.
*/
onRequestStart?: () => void;
/**
* Host-side instrumentation hook fired by the provider adapter immediately
* before it dispatches the network request to the upstream API. The window
* between {@link onRequestStart} and this hook is in-process request-building
* time (message serialization, param assembly) spent by the client; the
* window between this hook and the first streamed part is network + server
* time. Splitting time-to-first-token across this boundary lets hosts
* attribute latency to the client vs. the API server.
*/
onRequestSent?: () => void;
/**
* Host-side instrumentation hook fired as soon as the provider response
* headers arrive (before the stream body is drained), carrying the
* provider trace identifier from the `x-trace-id` header, or `null` when
* the provider does not report one. Firing early lets hosts attribute a
* stream that is cancelled mid-flight to its server-side request.
*/
onTraceId?: (traceId: string | null) => void;
/**
* Host-side instrumentation hook fired after the provider stream is fully
* drained, before post-processing the assembled response. Receives the
* {@link StreamDecodeStats} accounting accumulated across the stream when at
* least one part was streamed, or `undefined` for an empty stream.
*/
onStreamEnd?: (stats?: StreamDecodeStats) => void;
}
/**
* Decode-phase accounting for a single streamed generation. Splits the window
* from the first streamed part to stream end into the time spent waiting on the
* provider for the next part (server + network) versus the time spent
* processing each part in-process (deep copy, host callbacks, part merging).
*
* Because both buckets are wall-clock measured on the single JS thread, a
* stop-the-world GC pause that lands while awaiting the next part is counted in
* {@link serverDecodeMs}; a non-trivial {@link clientConsumeMs} share is the
* unambiguous signal that the host's per-part processing is throttling decode.
*/
export interface StreamDecodeStats {
/** Cumulative time spent awaiting the next streamed part (server + network). */
readonly serverDecodeMs: number;
/** Cumulative time spent processing streamed parts in-process (client). */
readonly clientConsumeMs: number;
}
/**
* In-memory video bytes for providers that require an uploaded file
* reference instead of an inline data URL.
*/
export interface VideoUploadInput {
readonly data: Uint8Array;
readonly mimeType: string;
readonly filename?: string | undefined;
}
/**
* Unified interface for an LLM chat provider.
*
* Each provider implementation (Kimi, OpenAI, Anthropic, Google GenAI, etc.)
* converts the common {@link Message} / {@link Tool} types into the
* provider-specific wire format, streams back a {@link StreamedMessage}, and
* exposes configuration helpers such as {@link withThinking}.
*/
export interface ChatProvider {
/** Short identifier for the provider backend (e.g. `"kimi"`, `"anthropic"`). */
readonly name: string;
/** Model name passed to the upstream API (e.g. `"moonshot-v1-auto"`). */
readonly modelName: string;
/** Current thinking effort, or `null` if thinking is not configured. */
readonly thinkingEffort: ThinkingEffort | null;
/**
* The effective completion-token cap this instance will send on the wire,
* derived from its generation kwargs — covering constructor defaults (e.g.
* Anthropic's required `max_tokens`), direct kwargs configuration, and
* {@link withMaxCompletionTokens} (after any implementation-side clamping:
* remaining context window, transport ceilings, model-default resolution).
* `undefined` when the instance sends no cap. Read by hosts that record
* the outbound request, so the recorded value matches what the provider
* actually sends.
*/
readonly maxCompletionTokens?: number;
/**
* Send a conversation to the LLM and return a streamed response.
*
* @param systemPrompt - System-level instruction prepended to the request.
* @param tools - Tool definitions the model may invoke.
* @param history - The conversation history (user, assistant, tool messages).
* @param options - Optional per-call settings such as an {@link AbortSignal}.
*/
generate(
systemPrompt: string,
tools: Tool[],
history: Message[],
options?: GenerateOptions,
): Promise<StreamedMessage>;
/** Return a shallow copy of this provider with the given thinking effort. */
withThinking(effort: ThinkingEffort): ChatProvider;
/**
* Return a shallow copy of this provider with the per-request completion
* budget clamped to `maxCompletionTokens`. Optional because not every
* backend benefits from a client-computed cap.
*
* When `options` are provided, implementations may further tighten the cap
* based on their own transport constraints — e.g. chat-completions
* endpoints size the cap to the remaining context window
* (`maxContextTokens - usedContextTokens`) and/or clamp to a fixed ceiling.
*
* Implementations MUST NOT mutate or replace internal HTTP clients on the
* returned clone — the clone is expected to share transport state with the
* original. See `KimiChatProvider._clone()` for the rationale.
*/
withMaxCompletionTokens?(
maxCompletionTokens: number,
options?: MaxCompletionTokensOptions,
): ChatProvider;
/** Upload a video and return a content part that can be sent to this provider. */
uploadVideo?(input: string | VideoUploadInput, options?: GenerateOptions): Promise<VideoURLPart>;
}
|