@codeworksh/aikit 0.1.0 → 0.2.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.
@@ -1,14 +0,0 @@
1
- import { Stream } from "./stream";
2
-
3
- type ProviderModule = {
4
- default: Stream.ProtocolProvider;
5
- };
6
-
7
- const providerModules = new Bun.Glob("providers/*/index.ts");
8
- const providerBaseUrl = new URL("./", import.meta.url);
9
-
10
- for (const path of providerModules.scanSync(import.meta.dir)) {
11
- const mod = (await import(new URL(path, providerBaseUrl).href)) as ProviderModule;
12
- if (!mod?.default) continue;
13
- Stream.registerProtocolProvider(mod.default, path);
14
- }
@@ -1,299 +0,0 @@
1
- import { NamedError } from "@codeworksh/utils";
2
- import { type Static, Type } from "@sinclair/typebox";
3
- import type { Message } from "../message/message";
4
- import { Model } from "../model/model";
5
- import type { AssistantMessageEventStream } from "../utils/eventstream";
6
- import type { Provider } from "./provider";
7
-
8
- export namespace Stream {
9
- export const ProtocolProviderNotFoundError = NamedError.create(
10
- "StreamProtocolProviderNotFoundError",
11
- Type.Object({
12
- protocol: Model.KnownProtocolSchema,
13
- }),
14
- );
15
- export type ProtocolProviderNotFoundError = InstanceType<typeof ProtocolProviderNotFoundError>;
16
-
17
- export const ProtocolMismatchError = NamedError.create(
18
- "StreamProtocolMismatchError",
19
- Type.Object({
20
- actual: Model.KnownProtocolSchema,
21
- expected: Model.KnownProtocolSchema,
22
- }),
23
- );
24
- export type ProtocolMismatchError = InstanceType<typeof ProtocolMismatchError>;
25
-
26
- export const ThinkingLevelEnum = {
27
- minimal: "minimal",
28
- low: "low",
29
- medium: "medium",
30
- high: "high",
31
- xhigh: "xhigh",
32
- } as const;
33
- export const ThinkingLevelSchema = Type.Union([
34
- Type.Literal(ThinkingLevelEnum.minimal),
35
- Type.Literal(ThinkingLevelEnum.low),
36
- Type.Literal(ThinkingLevelEnum.medium),
37
- Type.Literal(ThinkingLevelEnum.high),
38
- Type.Literal(ThinkingLevelEnum.xhigh),
39
- ]);
40
- export type ThinkingLevel = Static<typeof ThinkingLevelSchema>;
41
-
42
- export const ThinkingBudgetsSchema = Type.Object({
43
- minimal: Type.Optional(Type.Number()),
44
- low: Type.Optional(Type.Number()),
45
- medium: Type.Optional(Type.Number()),
46
- high: Type.Optional(Type.Number()),
47
- });
48
- export type ThinkingBudgets = Static<typeof ThinkingBudgetsSchema>;
49
-
50
- export const CacheRetentionSchema = Type.Union([Type.Literal("none"), Type.Literal("short"), Type.Literal("long")]);
51
- export type CacheRetention = Static<typeof CacheRetentionSchema>;
52
-
53
- export const TransportSchema = Type.Union([Type.Literal("sse"), Type.Literal("websocket"), Type.Literal("auto")]);
54
- export type Transport = Static<typeof TransportSchema>;
55
-
56
- export const OptionsSchema = Type.Object({
57
- temperature: Type.Optional(Type.Number()),
58
- maxTokens: Type.Optional(Type.Number()),
59
- signal: Type.Optional(Type.Unsafe<AbortSignal>()),
60
- apiKey: Type.Optional(Type.String()),
61
- transport: Type.Optional(TransportSchema),
62
- cacheRetention: Type.Optional(CacheRetentionSchema),
63
- sessionId: Type.Optional(Type.String()),
64
- onPayload: Type.Optional(
65
- Type.Unsafe<
66
- (
67
- payload: unknown,
68
- model: Model.TModel<Model.KnownProtocol>,
69
- ) => unknown | undefined | Promise<unknown | undefined>
70
- >(),
71
- ),
72
- headers: Type.Optional(Type.Record(Type.String(), Type.String())),
73
- maxRetryDelayMs: Type.Optional(Type.Number()),
74
- metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
75
- });
76
- export type Options = Static<typeof OptionsSchema>;
77
- export type ProviderOptions = Options & Record<string, unknown>;
78
-
79
- export const SimpleOptionsSchema = Type.Composite([
80
- OptionsSchema,
81
- Type.Object({
82
- reasoning: Type.Optional(ThinkingLevelSchema),
83
- thinkingBudgets: Type.Optional(ThinkingBudgetsSchema),
84
- }),
85
- ]);
86
- export type SimpleOptions = Static<typeof SimpleOptionsSchema>;
87
-
88
- // Contract:
89
- // - Must return an AssistantMessageEventStream.
90
- // - Once invoked, request/model/runtime failures should be encoded in the
91
- // returned stream, not thrown.
92
- // - Error termination must produce an AssistantMessage with stopReason
93
- // "error" or "aborted" and errorMessage, emitted via the stream protocol.
94
- export type StreamFunction<
95
- TProtocol extends Model.KnownProtocol = Model.KnownProtocol,
96
- TOptions extends Options = Options,
97
- > = (model: Model.TModel<TProtocol>, context: Message.Context, options?: TOptions) => AssistantMessageEventStream;
98
-
99
- export interface IStream<
100
- TProtocol extends Model.KnownProtocol = Model.KnownProtocol,
101
- S extends Options = Options,
102
- SS extends SimpleOptions = SimpleOptions,
103
- > {
104
- readonly provider: Provider.Info;
105
- readonly protocol: Model.KnownProtocol;
106
-
107
- stream: StreamFunction<TProtocol, S>;
108
- streamSimple: StreamFunction<TProtocol, SS>;
109
- }
110
-
111
- export function buildBaseOptions(model: Model.Value, options?: SimpleOptions, apiKey?: string): Options {
112
- return {
113
- temperature: options?.temperature,
114
- maxTokens: options?.maxTokens || Math.min(model.maxTokens, 32000),
115
- signal: options?.signal,
116
- apiKey: apiKey || options?.apiKey,
117
- transport: options?.transport,
118
- cacheRetention: options?.cacheRetention,
119
- sessionId: options?.sessionId,
120
- headers: options?.headers,
121
- onPayload: options?.onPayload,
122
- maxRetryDelayMs: options?.maxRetryDelayMs,
123
- metadata: options?.metadata,
124
- };
125
- }
126
-
127
- export function clampReasoning(effort: ThinkingLevel | undefined): Exclude<ThinkingLevel, "xhigh"> | undefined {
128
- return effort === ThinkingLevelEnum.xhigh ? ThinkingLevelEnum.high : effort;
129
- }
130
-
131
- export function adjustMaxTokensForThinking(
132
- baseMaxTokens: number,
133
- modelMaxTokens: number,
134
- reasoningLevel: ThinkingLevel,
135
- customBudgets?: ThinkingBudgets,
136
- ): { maxTokens: number; thinkingBudget: number } {
137
- const defaultBudgets: ThinkingBudgets = {
138
- minimal: 1024,
139
- low: 2048,
140
- medium: 8192,
141
- high: 16384,
142
- };
143
- const budgets = { ...defaultBudgets, ...customBudgets };
144
- const minOutputTokens = 1024;
145
- const level = clampReasoning(reasoningLevel)!;
146
- let thinkingBudget = budgets[level]!;
147
- const maxTokens = Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens);
148
-
149
- if (maxTokens <= thinkingBudget) {
150
- thinkingBudget = Math.max(0, maxTokens - minOutputTokens);
151
- }
152
-
153
- return { maxTokens, thinkingBudget };
154
- }
155
-
156
- export type ProtocolStreamFunction = (
157
- model: Model.Value,
158
- context: Message.Context,
159
- options?: Options,
160
- ) => AssistantMessageEventStream;
161
-
162
- export type ProtocolStreamSimpleFunction = (
163
- model: Model.Value,
164
- context: Message.Context,
165
- options?: SimpleOptions,
166
- ) => AssistantMessageEventStream;
167
-
168
- export interface ProtocolProvider<
169
- TProtocol extends Model.KnownProtocol = Model.KnownProtocol,
170
- TOptions extends Options = Options,
171
- > {
172
- protocol: TProtocol;
173
- stream: StreamFunction<TProtocol, TOptions>;
174
- streamSimple: StreamFunction<TProtocol, SimpleOptions>;
175
- }
176
-
177
- interface ProtocolProviderInternal {
178
- protocol: Model.KnownProtocol;
179
- stream: any;
180
- streamSimple: any;
181
- }
182
-
183
- type RegisteredProtocolProvider = {
184
- provider: ProtocolProviderInternal;
185
- sourceId?: string;
186
- };
187
-
188
- const protocolProviderRegistry = new Map<string, RegisteredProtocolProvider>();
189
-
190
- function wrapStream<TProtocol extends Model.KnownProtocol, TOptions extends Options>(
191
- protocol: TProtocol,
192
- stream: StreamFunction<TProtocol, TOptions>,
193
- ): ProtocolStreamFunction {
194
- return (model, context, options) => {
195
- if (model.protocol !== protocol) {
196
- throw new ProtocolMismatchError({
197
- actual: model.protocol,
198
- expected: protocol,
199
- });
200
- }
201
- return stream(model as Model.TModel<TProtocol>, context, options as TOptions);
202
- };
203
- }
204
-
205
- function wrapStreamSimple<TProtocol extends Model.KnownProtocol>(
206
- protocol: TProtocol,
207
- streamSimple: StreamFunction<TProtocol, SimpleOptions>,
208
- ): ProtocolStreamFunction {
209
- return (model, context, options) => {
210
- if (model.protocol !== protocol) {
211
- throw new ProtocolMismatchError({
212
- actual: model.protocol,
213
- expected: protocol,
214
- });
215
- }
216
- return streamSimple(model as Model.TModel<TProtocol>, context, options);
217
- };
218
- }
219
-
220
- export function registerProtocolProvider<TProtocol extends Model.KnownProtocol, TOptions extends Options>(
221
- provider: ProtocolProvider<TProtocol, TOptions>,
222
- sourceId?: string,
223
- ): void {
224
- protocolProviderRegistry.set(provider.protocol, {
225
- provider: {
226
- protocol: provider.protocol,
227
- stream: wrapStream(provider.protocol, provider.stream),
228
- streamSimple: wrapStreamSimple(provider.protocol, provider.streamSimple),
229
- },
230
- sourceId,
231
- });
232
- }
233
-
234
- export function getProtocolProvider(protocol: Model.KnownProtocol): ProtocolProviderInternal | undefined {
235
- return protocolProviderRegistry.get(protocol)?.provider;
236
- }
237
-
238
- export function resolveProtocolProvider(model: Model.Value): ProtocolProviderInternal {
239
- const provider = getProtocolProvider(model.protocol);
240
- if (!provider) {
241
- throw new ProtocolProviderNotFoundError({
242
- protocol: model.protocol,
243
- });
244
- }
245
- return provider;
246
- }
247
-
248
- export function stream(
249
- model: Model.Value,
250
- context: Message.Context,
251
- options?: Options,
252
- ): AssistantMessageEventStream {
253
- const provider = resolveProtocolProvider(model);
254
- return provider.stream(model, context, options);
255
- }
256
-
257
- export async function complete(
258
- model: Model.Value,
259
- context: Message.Context,
260
- options?: Options,
261
- ): Promise<Message.AssistantMessage> {
262
- const s = stream(model, context, options);
263
- return s.result();
264
- }
265
-
266
- export function streamSimple(
267
- model: Model.Value,
268
- context: Message.Context,
269
- options?: SimpleOptions,
270
- ): AssistantMessageEventStream {
271
- const provider = resolveProtocolProvider(model);
272
- return provider.streamSimple(model, context, options);
273
- }
274
-
275
- export async function completeSimple(
276
- model: Model.Value,
277
- context: Message.Context,
278
- options?: SimpleOptions,
279
- ): Promise<Message.AssistantMessage> {
280
- const s = streamSimple(model, context, options);
281
- return s.result();
282
- }
283
-
284
- export function getApiProviders(): ProtocolProviderInternal[] {
285
- return Array.from(protocolProviderRegistry.values(), (entry) => entry.provider);
286
- }
287
-
288
- export function unregisterProtocolProviders(sourceId: string): void {
289
- for (const [api, entry] of protocolProviderRegistry.entries()) {
290
- if (entry.sourceId === sourceId) {
291
- protocolProviderRegistry.delete(api);
292
- }
293
- }
294
- }
295
-
296
- export function clearProtocolProviders(): void {
297
- protocolProviderRegistry.clear();
298
- }
299
- }
package/src/stream.ts DELETED
@@ -1,72 +0,0 @@
1
- import "./provider/register";
2
-
3
- import type { Message } from "./message/message";
4
- import type { Model } from "./model/model";
5
- import { Stream } from "./provider/stream";
6
- import type { AssistantMessageEventStream } from "./utils/eventstream";
7
-
8
- /**
9
- * Stream assistant responses for a model and context.
10
- *
11
- * Example:
12
- *
13
- * ```ts
14
- * const s = stream(model, context, {
15
- * apiKey: process.env.ANTHROPIC_API_KEY,
16
- * });
17
- *
18
- * for await (const event of s) {
19
- * if (event.type === "text.delta") {
20
- * process.stdout.write(event.delta);
21
- * }
22
- * }
23
- *
24
- * const message = await s.result();
25
- * ```
26
- *
27
- * This export also includes convenience helpers:
28
- *
29
- * ```ts
30
- * const message = await stream.complete(model, context, options);
31
- * const simple = stream.simple(model, context, options);
32
- * const simpleMessage = await stream.completeSimple(model, context, options);
33
- * ```
34
- */
35
- type StreamCallable = {
36
- (model: Model.Value, context: Message.Context, options?: Stream.Options): AssistantMessageEventStream;
37
- complete: typeof Stream.complete;
38
- simple: typeof Stream.streamSimple;
39
- completeSimple: typeof Stream.completeSimple;
40
- resolveProtocolProvider: typeof Stream.resolveProtocolProvider;
41
- };
42
-
43
- const streamImpl = (
44
- model: Model.Value,
45
- context: Message.Context,
46
- options?: Stream.Options,
47
- ): AssistantMessageEventStream => Stream.stream(model, context, options);
48
-
49
- /**
50
- * Start a streaming assistant response for a model/context pair.
51
- *
52
- * This is the main streaming entrypoint.
53
- * Attached helpers:
54
- *
55
- * - `stream.complete(...)` resolves the final assistant message without manually iterating events.
56
- * - `stream.simple(...)` uses `Stream.SimpleOptions` for higher-level reasoning controls.
57
- * - `stream.completeSimple(...)` is the non-streaming counterpart to `stream.simple(...)`.
58
- * - `stream.resolveProtocolProvider(...)` exposes protocol resolution for advanced integrations.
59
- */
60
- export const stream = Object.assign(streamImpl, {
61
- complete: Stream.complete,
62
- simple: Stream.streamSimple,
63
- completeSimple: Stream.completeSimple,
64
- resolveProtocolProvider: Stream.resolveProtocolProvider,
65
- }) as StreamCallable;
66
-
67
- /** Resolve a streamed response directly to the final assistant message. */
68
- export const complete = Stream.complete;
69
- /** Start a stream using the simplified options surface. */
70
- export const streamSimple = Stream.streamSimple;
71
- /** Resolve a simplified stream request directly to the final assistant message. */
72
- export const completeSimple = Stream.completeSimple;
@@ -1,95 +0,0 @@
1
- /**
2
- * @description Defines the Generic event stream class for async iteration.
3
- */
4
-
5
- import type { Event } from "../event/event";
6
- import type { Message } from "../message/message";
7
-
8
- // Implements the async iterator pattern
9
- // LLM output are streams of events for consuming
10
- // This class helps with that by acting like buffer data structure to manage the streams
11
- // for producer and consumer
12
- export class EventStream<T, R = T> implements AsyncIterable<T> {
13
- private queue: T[] = [];
14
- private waiting: ((value: IteratorResult<T>) => void)[] = [];
15
- private done = false;
16
- private readonly finalResultPromise: Promise<R>;
17
- private resolveFinalResult!: (result: R) => void;
18
-
19
- constructor(
20
- private isComplete: (event: T) => boolean,
21
- private extractResult: (event: T) => R,
22
- ) {
23
- this.finalResultPromise = new Promise((resolve) => {
24
- this.resolveFinalResult = resolve;
25
- });
26
- }
27
-
28
- push(event: T): void {
29
- if (this.done) return;
30
-
31
- if (this.isComplete(event)) {
32
- this.done = true;
33
- this.resolveFinalResult(this.extractResult(event));
34
- }
35
-
36
- // Deliver to waiting consumer or queue it
37
- const waiter = this.waiting.shift();
38
- if (waiter) {
39
- waiter({ value: event, done: false });
40
- } else {
41
- this.queue.push(event);
42
- }
43
- }
44
-
45
- end(result?: R): void {
46
- this.done = true;
47
- if (result !== undefined) {
48
- this.resolveFinalResult(result);
49
- }
50
- // Notify all waiting consumers that we're done
51
- while (this.waiting.length > 0) {
52
- const waiter = this.waiting.shift()!;
53
- waiter({ value: undefined as any, done: true });
54
- }
55
- }
56
-
57
- async *[Symbol.asyncIterator](): AsyncIterator<T> {
58
- while (true) {
59
- if (this.queue.length > 0) {
60
- yield this.queue.shift()!;
61
- } else if (this.done) {
62
- return;
63
- } else {
64
- const result = await new Promise<IteratorResult<T>>((resolve) => this.waiting.push(resolve));
65
- if (result.done) return;
66
- yield result.value;
67
- }
68
- }
69
- }
70
-
71
- result(): Promise<R> {
72
- return this.finalResultPromise;
73
- }
74
- }
75
-
76
- export class AssistantMessageEventStream extends EventStream<Event.LLMMessageEvent, Message.AssistantMessage> {
77
- constructor() {
78
- super(
79
- (event) => event.type === "done" || event.type === "error",
80
- (event) => {
81
- if (event.type === "done") {
82
- return event.message;
83
- } else if (event.type === "error") {
84
- return event.error;
85
- }
86
- throw new Error("Unexpected event type for final result");
87
- },
88
- );
89
- }
90
- }
91
-
92
- /** Factory function for AssistantMessageEventStream (for use in extensions) */
93
- export function createAssistantMessageEventStream(): AssistantMessageEventStream {
94
- return new AssistantMessageEventStream();
95
- }
@@ -1,24 +0,0 @@
1
- import { type TUnsafe, Type } from "@sinclair/typebox";
2
-
3
- /**
4
- * Creates a string enum schema compatible with Google's API and other providers
5
- * that don't support anyOf/const patterns.
6
- *
7
- * @example
8
- * const OperationSchema = StringEnum(["add", "subtract", "multiply", "divide"], {
9
- * description: "The operation to perform"
10
- * });
11
- *
12
- * type Operation = Static<typeof OperationSchema>; // "add" | "subtract" | "multiply" | "divide"
13
- */
14
- export function StringEnum<T extends readonly string[]>(
15
- values: T,
16
- options?: { description?: string; default?: T[number] },
17
- ): TUnsafe<T[number]> {
18
- return Type.Unsafe<T[number]>({
19
- type: "string",
20
- enum: values as any,
21
- ...(options?.description && { description: options.description }),
22
- ...(options?.default && { default: options.default }),
23
- });
24
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * @description Helper function to parse partial jsons during LLM streaming.
3
- */
4
-
5
- import { parse as partialParse } from "partial-json";
6
-
7
- /**
8
- * Attempts to parse potentially incomplete JSON during streaming.
9
- * Always returns a valid object, even if the JSON is incomplete.
10
- *
11
- * @param partialJson The partial JSON string from streaming
12
- * @returns Parsed object or empty object if parsing fails
13
- */
14
- export function parseStreamingJson<T = any>(partialJson: string | undefined): T {
15
- if (!partialJson || partialJson.trim() === "") {
16
- return {} as T;
17
- }
18
-
19
- // Try standard parsing first (fastest for complete JSON)
20
- try {
21
- return JSON.parse(partialJson) as T;
22
- } catch {
23
- // Try partial-json for incomplete JSON
24
- try {
25
- const result = partialParse(partialJson);
26
- return (result ?? {}) as T;
27
- } catch {
28
- // If all parsing fails, return empty object
29
- return {} as T;
30
- }
31
- }
32
- }
@@ -1,121 +0,0 @@
1
- import type { Message } from "../message/message";
2
-
3
- /**
4
- * Regex patterns to detect context overflow errors from different providers.
5
- *
6
- * These patterns match error messages returned when the input exceeds
7
- * the model's context window.
8
- *
9
- * Provider-specific patterns (with example error messages):
10
- *
11
- * - Anthropic: "prompt is too long: 213462 tokens > 200000 maximum"
12
- * - OpenAI: "Your input exceeds the context window of this model"
13
- * - Google: "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)"
14
- * - xAI: "This model's maximum prompt length is 131072 but the request contains 537812 tokens"
15
- * - Groq: "Please reduce the length of the messages or completion"
16
- * - OpenRouter: "This endpoint's maximum context length is X tokens. However, you requested about Y tokens"
17
- * - llama.cpp: "the request exceeds the available context size, try increasing it"
18
- * - LM Studio: "tokens to keep from the initial prompt is greater than the context length"
19
- * - GitHub Copilot: "prompt token count of X exceeds the limit of Y"
20
- * - MiniMax: "invalid params, context window exceeds limit"
21
- * - Kimi For Coding: "Your request exceeded model token limit: X (requested: Y)"
22
- * - Cerebras: Returns "400/413 status code (no body)" - handled separately below
23
- * - Mistral: Returns "400/413 status code (no body)" - handled separately below
24
- * - z.ai: Does NOT error, accepts overflow silently - handled via usage.input > contextWindow
25
- * - Ollama: Silently truncates input - not detectable via error message
26
- */
27
- const OVERFLOW_PATTERNS = [
28
- /prompt is too long/i, // Anthropic
29
- /input is too long for requested model/i, // Amazon Bedrock
30
- /exceeds the context window/i, // OpenAI (Completions & Responses API)
31
- /input token count.*exceeds the maximum/i, // Google (Gemini)
32
- /maximum prompt length is \d+/i, // xAI (Grok)
33
- /reduce the length of the messages/i, // Groq
34
- /maximum context length is \d+ tokens/i, // OpenRouter (all backends)
35
- /exceeds the limit of \d+/i, // GitHub Copilot
36
- /exceeds the available context size/i, // llama.cpp server
37
- /greater than the context length/i, // LM Studio
38
- /context window exceeds limit/i, // MiniMax
39
- /exceeded model token limit/i, // Kimi For Coding
40
- /context[_ ]length[_ ]exceeded/i, // Generic fallback
41
- /too many tokens/i, // Generic fallback
42
- /token limit exceeded/i, // Generic fallback
43
- ];
44
-
45
- /**
46
- * Check if an assistant message represents a context overflow error.
47
- *
48
- * This handles two cases:
49
- * 1. Error-based overflow: Most providers return stopReason "error" with a
50
- * specific error message pattern.
51
- * 2. Silent overflow: Some providers accept overflow requests and return
52
- * successfully. For these, we check if usage.input exceeds the context window.
53
- *
54
- * ## Reliability by Provider
55
- *
56
- * **Reliable detection (returns error with detectable message):**
57
- * - Anthropic: "prompt is too long: X tokens > Y maximum"
58
- * - OpenAI (Completions & Responses): "exceeds the context window"
59
- * - Google Gemini: "input token count exceeds the maximum"
60
- * - xAI (Grok): "maximum prompt length is X but request contains Y"
61
- * - Groq: "reduce the length of the messages"
62
- * - Cerebras: 400/413 status code (no body)
63
- * - Mistral: 400/413 status code (no body)
64
- * - OpenRouter (all backends): "maximum context length is X tokens"
65
- * - llama.cpp: "exceeds the available context size"
66
- * - LM Studio: "greater than the context length"
67
- * - Kimi For Coding: "exceeded model token limit: X (requested: Y)"
68
- *
69
- * **Unreliable detection:**
70
- * - z.ai: Sometimes accepts overflow silently (detectable via usage.input > contextWindow),
71
- * sometimes returns rate limit errors. Pass contextWindow param to detect silent overflow.
72
- * - Ollama: Silently truncates input without error. Cannot be detected via this function.
73
- * The response will have usage.input < expected, but we don't know the expected value.
74
- *
75
- * ## Custom Providers
76
- *
77
- * If you've added custom models via settings.json, this function may not detect
78
- * overflow errors from those providers. To add support:
79
- *
80
- * 1. Send a request that exceeds the model's context window
81
- * 2. Check the errorMessage in the response
82
- * 3. Create a regex pattern that matches the error
83
- * 4. The pattern should be added to OVERFLOW_PATTERNS in this file, or
84
- * check the errorMessage yourself before calling this function
85
- *
86
- * @param message - The assistant message to check
87
- * @param contextWindow - Optional context window size for detecting silent overflow (z.ai)
88
- * @returns true if the message indicates a context overflow
89
- */
90
- export function isContextOverflow(message: Message.AssistantMessage, contextWindow?: number): boolean {
91
- // Case 1: Check error message patterns
92
- if (message.stopReason === "error" && message.errorMessage) {
93
- // Check known patterns
94
- if (OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage!))) {
95
- return true;
96
- }
97
-
98
- // Cerebras and Mistral return 400/413 with no body for context overflow
99
- // Note: 429 is rate limiting (requests/tokens per time), NOT context overflow
100
- if (/^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message.errorMessage)) {
101
- return true;
102
- }
103
- }
104
-
105
- // Case 2: Silent overflow (z.ai style) - successful but usage exceeds context
106
- if (contextWindow && message.stopReason === "stop") {
107
- const inputTokens = message.usage.input + message.usage.cacheRead;
108
- if (inputTokens > contextWindow) {
109
- return true;
110
- }
111
- }
112
-
113
- return false;
114
- }
115
-
116
- /**
117
- * Get the overflow patterns for testing purposes.
118
- */
119
- export function getOverflowPatterns(): RegExp[] {
120
- return [...OVERFLOW_PATTERNS];
121
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Removes unpaired Unicode surrogate characters from a string.
3
- *
4
- * Unpaired surrogates (high surrogates 0xD800-0xDBFF without matching low surrogates 0xDC00-0xDFFF,
5
- * or vice versa) cause JSON serialization errors in many API providers.
6
- *
7
- * Valid emoji and other characters outside the Basic Multilingual Plane use properly paired
8
- * surrogates and will NOT be affected by this function.
9
- *
10
- * @param text - The text to sanitize
11
- * @returns The sanitized text with unpaired surrogates removed
12
- *
13
- * @example
14
- * // Valid emoji (properly paired surrogates) are preserved
15
- * sanitizeSurrogates("Hello 🙈 World") // => "Hello 🙈 World"
16
- *
17
- * // Unpaired high surrogate is removed
18
- * const unpaired = String.fromCharCode(0xD83D); // high surrogate without low
19
- * sanitizeSurrogates(`Text ${unpaired} here`) // => "Text here"
20
- */
21
- export function sanitizeSurrogates(text: string): string {
22
- // Replace unpaired high surrogates (0xD800-0xDBFF not followed by low surrogate)
23
- // Replace unpaired low surrogates (0xDC00-0xDFFF not preceded by high surrogate)
24
- return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
25
- }