merge: sync upstream pi-mono main into sproutclaw
Some checks failed
CI / build-check-test (push) Has been cancelled

Merge upstream/main (195 commits) while keeping sproutclaw-specific
README, webui workflow docs, and restored upstream .pi prompt templates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-05-22 20:05:31 +08:00
642 changed files with 18785 additions and 26436 deletions

View File

@@ -6,7 +6,7 @@ import type {
SimpleStreamOptions,
StreamFunction,
StreamOptions,
} from "./types.js";
} from "./types.ts";
export type ApiStreamFunction = (
model: Model<Api>,

View File

@@ -1,4 +1,4 @@
import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.js";
import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts";
export const bedrockProviderModule = {
streamBedrock,

View File

@@ -2,8 +2,8 @@
import { createInterface } from "node:readline";
import { existsSync, readFileSync, writeFileSync } from "fs";
import { getOAuthProvider, getOAuthProviders } from "./utils/oauth/index.js";
import type { OAuthCredentials, OAuthProviderId } from "./utils/oauth/types.js";
import { getOAuthProvider, getOAuthProviders } from "./utils/oauth/index.ts";
import type { OAuthCredentials, OAuthProviderId } from "./utils/oauth/types.ts";
const AUTH_FILE = "auth.json";
const PROVIDERS = getOAuthProviders();

View File

@@ -1,4 +1,4 @@
// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui)
// NEVER convert to top-level imports - breaks browser/Vite builds
let _existsSync: typeof import("node:fs").existsSync | null = null;
let _homedir: typeof import("node:os").homedir | null = null;
let _join: typeof import("node:path").join | null = null;
@@ -23,7 +23,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
});
}
import type { KnownProvider } from "./types.js";
import type { KnownProvider } from "./types.ts";
let _procEnvCache: Map<string, string> | null = null;

View File

@@ -1,7 +1,7 @@
// This file is auto-generated by scripts/generate-image-models.ts
// Do not edit manually - run 'npm run generate-image-models' to update
import type { ImagesApi, ImagesModel } from "./types.js";
import type { ImagesApi, ImagesModel } from "./types.ts";
export const IMAGE_MODELS = {
"openrouter": {
@@ -230,6 +230,171 @@ export const IMAGE_MODELS = {
"cacheWrite": 0
}
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v3": {
id: "recraft/recraft-v3",
name: "Recraft: Recraft V3",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v4": {
id: "recraft/recraft-v4",
name: "Recraft: Recraft V4",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v4-pro": {
id: "recraft/recraft-v4-pro",
name: "Recraft: Recraft V4 Pro",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v4-pro-vector": {
id: "recraft/recraft-v4-pro-vector",
name: "Recraft: Recraft V4 Pro Vector",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v4-vector": {
id: "recraft/recraft-v4-vector",
name: "Recraft: Recraft V4 Vector",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v4.1": {
id: "recraft/recraft-v4.1",
name: "Recraft: Recraft V4.1",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v4.1-pro": {
id: "recraft/recraft-v4.1-pro",
name: "Recraft: Recraft V4.1 Pro",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v4.1-pro-vector": {
id: "recraft/recraft-v4.1-pro-vector",
name: "Recraft: Recraft V4.1 Pro Vector",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v4.1-utility": {
id: "recraft/recraft-v4.1-utility",
name: "Recraft: Recraft V4.1 Utility",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v4.1-utility-pro": {
id: "recraft/recraft-v4.1-utility-pro",
name: "Recraft: Recraft V4.1 Utility Pro",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"recraft/recraft-v4.1-vector": {
id: "recraft/recraft-v4.1-vector",
name: "Recraft: Recraft V4.1 Vector",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
"sourceful/riverflow-v2-fast": {
id: "sourceful/riverflow-v2-fast",
name: "Sourceful: Riverflow V2 Fast",
@@ -305,5 +470,20 @@ export const IMAGE_MODELS = {
"cacheWrite": 0
}
} satisfies ImagesModel<"openrouter-images">,
"x-ai/grok-imagine-image-quality": {
id: "x-ai/grok-imagine-image-quality",
name: "xAI: Grok Imagine Image Quality",
api: "openrouter-images",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
input: ["text", "image"],
output: ["image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
} satisfies ImagesModel<"openrouter-images">,
},
} as const satisfies Record<string, Record<string, ImagesModel<ImagesApi>>>;

View File

@@ -1,5 +1,5 @@
import { IMAGE_MODELS } from "./image-models.generated.js";
import type { ImagesApi, ImagesModel, KnownImagesProvider } from "./types.js";
import { IMAGE_MODELS } from "./image-models.generated.ts";
import type { ImagesApi, ImagesModel, KnownImagesProvider } from "./types.ts";
const imageModelRegistry: Map<string, Map<string, ImagesModel<ImagesApi>>> = new Map();

View File

@@ -1,4 +1,4 @@
import type { AssistantImages, ImagesApi, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "./types.js";
import type { AssistantImages, ImagesApi, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "./types.ts";
export type ImagesApiFunction = (
model: ImagesModel<ImagesApi>,

View File

@@ -1,7 +1,7 @@
import "./providers/images/register-builtins.js";
import "./providers/images/register-builtins.ts";
import { getImagesApiProvider } from "./images-api-registry.js";
import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ProviderImagesOptions } from "./types.js";
import { getImagesApiProvider } from "./images-api-registry.ts";
import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ProviderImagesOptions } from "./types.ts";
function resolveImagesApiProvider(api: ImagesApi) {
const provider = getImagesApiProvider(api);

View File

@@ -1,34 +1,34 @@
export type { Static, TSchema } from "typebox";
export { Type } from "typebox";
export * from "./api-registry.js";
export * from "./env-api-keys.js";
export * from "./image-models.js";
export * from "./images.js";
export * from "./images-api-registry.js";
export * from "./models.js";
export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.js";
export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.js";
export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.js";
export * from "./providers/faux.js";
export type { GoogleOptions } from "./providers/google.js";
export type { GoogleThinkingLevel } from "./providers/google-shared.js";
export type { GoogleVertexOptions } from "./providers/google-vertex.js";
export * from "./providers/images/register-builtins.js";
export type { MistralOptions } from "./providers/mistral.js";
export * from "./api-registry.ts";
export * from "./env-api-keys.ts";
export * from "./image-models.ts";
export * from "./images.ts";
export * from "./images-api-registry.ts";
export * from "./models.ts";
export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.ts";
export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.ts";
export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.ts";
export * from "./providers/faux.ts";
export type { GoogleOptions } from "./providers/google.ts";
export type { GoogleThinkingLevel } from "./providers/google-shared.ts";
export type { GoogleVertexOptions } from "./providers/google-vertex.ts";
export * from "./providers/images/register-builtins.ts";
export type { MistralOptions } from "./providers/mistral.ts";
export type {
OpenAICodexResponsesOptions,
OpenAICodexWebSocketDebugStats,
} from "./providers/openai-codex-responses.js";
export type { OpenAICompletionsOptions } from "./providers/openai-completions.js";
export type { OpenAIResponsesOptions } from "./providers/openai-responses.js";
export * from "./providers/register-builtins.js";
export * from "./session-resources.js";
export * from "./stream.js";
export * from "./types.js";
export * from "./utils/diagnostics.js";
export * from "./utils/event-stream.js";
export * from "./utils/json-parse.js";
} from "./providers/openai-codex-responses.ts";
export type { OpenAICompletionsOptions } from "./providers/openai-completions.ts";
export type { OpenAIResponsesOptions } from "./providers/openai-responses.ts";
export * from "./providers/register-builtins.ts";
export * from "./session-resources.ts";
export * from "./stream.ts";
export * from "./types.ts";
export * from "./utils/diagnostics.ts";
export * from "./utils/event-stream.ts";
export * from "./utils/json-parse.ts";
export type {
OAuthAuthInfo,
OAuthCredentials,
@@ -40,7 +40,7 @@ export type {
OAuthProviderInterface,
OAuthSelectOption,
OAuthSelectPrompt,
} from "./utils/oauth/types.js";
export * from "./utils/overflow.js";
export * from "./utils/typebox-helpers.js";
export * from "./utils/validation.js";
} from "./utils/oauth/types.ts";
export * from "./utils/overflow.ts";
export * from "./utils/typebox-helpers.ts";
export * from "./utils/validation.ts";

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { MODELS } from "./models.generated.js";
import type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from "./types.js";
import { MODELS } from "./models.generated.ts";
import type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from "./types.ts";
const modelRegistry: Map<string, Map<string, Model<Api>>> = new Map();

View File

@@ -1 +1 @@
export * from "./utils/oauth/index.js";
export * from "./utils/oauth/index.ts";

View File

@@ -20,8 +20,9 @@ import {
type ToolConfiguration,
ToolResultStatus,
} from "@aws-sdk/client-bedrock-runtime";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import type { DocumentType } from "@smithy/types";
import { calculateCost } from "../models.js";
import { calculateCost } from "../models.ts";
import type {
Api,
AssistantMessage,
@@ -39,12 +40,13 @@ import type {
Tool,
ToolCall,
ToolResultMessage,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.js";
import { transformMessages } from "./transform-messages.js";
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { parseStreamingJson } from "../utils/json-parse.ts";
import { createHttpProxyAgentsForTarget } from "../utils/node-http-proxy.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.ts";
import { transformMessages } from "./transform-messages.ts";
export type BedrockThinkingDisplay = "summarized" | "omitted";
@@ -156,30 +158,15 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt
};
}
if (
process.env.HTTP_PROXY ||
process.env.HTTPS_PROXY ||
process.env.NO_PROXY ||
process.env.http_proxy ||
process.env.https_proxy ||
process.env.no_proxy
) {
const nodeHttpHandler = await import("@smithy/node-http-handler");
const proxyAgent = await import("proxy-agent");
const agent = new proxyAgent.ProxyAgent();
const proxyAgents = createHttpProxyAgentsForTarget(model.baseUrl);
if (proxyAgents) {
// Bedrock runtime uses NodeHttp2Handler by default since v3.798.0, which is based
// on `http2` module and has no support for http agent.
// Use NodeHttpHandler to support http agent.
config.requestHandler = new nodeHttpHandler.NodeHttpHandler({
httpAgent: agent,
httpsAgent: agent,
});
// Use NodeHttpHandler to support HTTP(S) proxy agents.
config.requestHandler = new NodeHttpHandler(proxyAgents);
} else if (process.env.AWS_BEDROCK_FORCE_HTTP1 === "1") {
// Some custom endpoints require HTTP/1.1 instead of HTTP/2
const nodeHttpHandler = await import("@smithy/node-http-handler");
config.requestHandler = new nodeHttpHandler.NodeHttpHandler();
config.requestHandler = new NodeHttpHandler();
}
} else {
// Non-Node environment (browser): fall back to us-east-1 since
@@ -196,12 +183,13 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt
try {
const client = new BedrockRuntimeClient(config);
const cacheRetention = resolveCacheRetention(options.cacheRetention);
const inferenceMaxTokens = options.maxTokens ?? (isAnthropicClaudeModel(model) ? model.maxTokens : undefined);
let commandInput = {
modelId: model.id,
messages: convertMessages(context, model, cacheRetention),
system: buildSystemPrompt(context.systemPrompt, model, cacheRetention),
inferenceConfig: {
...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }),
...(inferenceMaxTokens !== undefined && { maxTokens: inferenceMaxTokens }),
...(options.temperature !== undefined && { temperature: options.temperature }),
},
toolConfig: convertToolConfig(context.tools, options.toolChoice),
@@ -327,8 +315,10 @@ export const streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", Simp
} satisfies BedrockOptions);
}
// Undefined means the caller did not request an output cap; let the helper use the model cap.
// Do not coerce to 0 here, or the thinking budget would become the entire maxTokens value.
const adjusted = adjustMaxTokensForThinking(
base.maxTokens || 0,
base.maxTokens,
model.maxTokens,
options.reasoning,
options.thinkingBudgets,
@@ -631,24 +621,31 @@ function convertMessages(
const m = transformedMessages[i];
switch (m.role) {
case "user":
case "user": {
const content: ContentBlock[] = [];
if (typeof m.content === "string") {
content.push({ text: sanitizeSurrogates(m.content) });
} else {
for (const c of m.content) {
switch (c.type) {
case "text":
content.push({ text: sanitizeSurrogates(c.text) });
break;
case "image":
content.push({ image: createImageBlock(c.mimeType, c.data) });
break;
default:
continue;
}
}
}
if (content.length === 0) continue;
result.push({
role: ConversationRole.USER,
content:
typeof m.content === "string"
? [{ text: sanitizeSurrogates(m.content) }]
: m.content.map((c) => {
switch (c.type) {
case "text":
return { text: sanitizeSurrogates(c.text) };
case "image":
return { image: createImageBlock(c.mimeType, c.data) };
default:
throw new Error("Unknown user content type");
}
}),
content,
});
break;
}
case "assistant": {
// Skip assistant messages with empty content (e.g., from aborted requests)
// Bedrock rejects messages with empty content arrays
@@ -699,7 +696,7 @@ function convertMessages(
}
break;
default:
throw new Error("Unknown assistant content type");
continue;
}
}
// Skip if all content blocks were filtered out
@@ -758,7 +755,7 @@ function convertMessages(
break;
}
default:
throw new Error("Unknown message role");
continue;
}
}

View File

@@ -6,8 +6,8 @@ import type {
MessageParam,
RawMessageStreamEvent,
} from "@anthropic-ai/sdk/resources/messages.js";
import { getEnvApiKey } from "../env-api-keys.js";
import { calculateCost } from "../models.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { calculateCost } from "../models.ts";
import type {
AnthropicMessagesCompat,
Api,
@@ -26,16 +26,16 @@ import type {
Tool,
ToolCall,
ToolResultMessage,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { headersToRecord } from "../utils/headers.js";
import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { resolveCloudflareBaseUrl } from "./cloudflare.js";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.js";
import { transformMessages } from "./transform-messages.js";
import { resolveCloudflareBaseUrl } from "./cloudflare.ts";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts";
import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.ts";
import { transformMessages } from "./transform-messages.ts";
/**
* Resolve cache retention preference.
@@ -165,9 +165,16 @@ const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14
const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
function getAnthropicCompat(model: Model<"anthropic-messages">): Required<AnthropicMessagesCompat> {
// Auto-detect session affinity and cache control support from provider
const isFireworks = model.provider === "fireworks";
const isCloudflareAiGatewayAnthropic =
model.provider === "cloudflare-ai-gateway" && model.baseUrl.includes("anthropic");
return {
supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,
supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? !isFireworks,
supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? !isFireworks,
sendSessionAffinityHeaders:
model.compat?.sendSessionAffinityHeaders ?? !!(isFireworks || isCloudflareAiGatewayAnthropic),
supportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? !isFireworks,
};
}
@@ -463,6 +470,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
});
}
const cacheRetention = options?.cacheRetention ?? resolveCacheRetention();
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
const created = createClient(
model,
apiKey,
@@ -470,6 +480,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
shouldUseFineGrainedToolStreamingBeta(model, context),
options?.headers,
copilotDynamicHeaders,
cacheSessionId,
);
client = created.client;
isOAuth = created.isOAuthToken;
@@ -740,8 +751,10 @@ export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleS
} satisfies AnthropicOptions);
}
// Undefined means the caller did not request an output cap; let the helper use the model cap.
// Do not coerce to 0 here, or the thinking budget would become the entire max_tokens value.
const adjusted = adjustMaxTokensForThinking(
base.maxTokens || 0,
base.maxTokens,
model.maxTokens,
options.reasoning,
options.thinkingBudgets,
@@ -766,6 +779,7 @@ function createClient(
useFineGrainedToolStreamingBeta: boolean,
optionsHeaders?: Record<string, string>,
dynamicHeaders?: Record<string, string>,
sessionId?: string,
): { client: Anthropic; isOAuthToken: boolean } {
// Adaptive thinking models (Opus 4.6, Sonnet 4.6) have interleaved thinking built-in.
// The beta header is deprecated on Opus 4.6 and redundant on Sonnet 4.6, so skip it.
@@ -847,8 +861,11 @@ function createClient(
}
// API key auth
const sessionAffinityHeaders: Record<string, string | null> =
sessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { "x-session-affinity": sessionId } : {};
const client = new Anthropic({
apiKey,
authToken: null,
baseURL: model.baseUrl,
dangerouslyAllowBrowser: true,
defaultHeaders: mergeHeaders(
@@ -857,6 +874,7 @@ function createClient(
"anthropic-dangerous-direct-browser-access": "true",
...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
},
sessionAffinityHeaders,
model.headers,
optionsHeaders,
),
@@ -875,7 +893,7 @@ function buildParams(
const params: MessageCreateParamsStreaming = {
model: model.id,
messages: convertMessages(context.messages, model, isOAuthToken, cacheControl),
max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
max_tokens: options?.maxTokens ?? model.maxTokens,
stream: true,
};
@@ -912,11 +930,12 @@ function buildParams(
}
if (context.tools && context.tools.length > 0) {
const compat = getAnthropicCompat(model);
params.tools = convertTools(
context.tools,
isOAuthToken,
getAnthropicCompat(model).supportsEagerToolInputStreaming,
cacheControl,
compat.supportsEagerToolInputStreaming,
compat.supportsCacheControlOnTools ? cacheControl : undefined,
);
}

View File

@@ -1,7 +1,7 @@
import { AzureOpenAI } from "openai";
import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
import { getEnvApiKey } from "../env-api-keys.js";
import { clampThinkingLevel } from "../models.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { clampThinkingLevel } from "../models.ts";
import type {
Api,
AssistantMessage,
@@ -10,11 +10,12 @@ import type {
SimpleStreamOptions,
StreamFunction,
StreamOptions,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { headersToRecord } from "../utils/headers.js";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
import { buildBaseOptions } from "./simple-options.js";
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts";
import { buildBaseOptions } from "./simple-options.ts";
const DEFAULT_AZURE_API_VERSION = "v1";
const AZURE_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode", "azure-openai-responses"]);
@@ -40,6 +41,22 @@ function resolveDeploymentName(model: Model<"azure-openai-responses">, options?:
return mappedDeployment || model.id;
}
function formatAzureOpenAIError(error: unknown): string {
if (error instanceof Error) {
const status = (error as Error & { status?: unknown }).status;
const statusCode = typeof status === "number" ? status : undefined;
if (statusCode !== undefined) {
return `Azure OpenAI API error (${statusCode}): ${error.message}`;
}
return error.message;
}
try {
return JSON.stringify(error);
} catch {
return String(error);
}
}
// Azure OpenAI Responses-specific options
export interface AzureOpenAIResponsesOptions extends StreamOptions {
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
@@ -119,7 +136,7 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
delete (block as { partialJson?: string }).partialJson;
}
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
output.errorMessage = formatAzureOpenAIError(error);
stream.push({ type: "error", reason: output.stopReason, error: output });
stream.end();
}
@@ -245,7 +262,7 @@ function buildParams(
model: deploymentName,
input: messages,
stream: true,
prompt_cache_key: options?.sessionId,
prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId),
};
if (options?.maxTokens) {

View File

@@ -1,4 +1,4 @@
import type { Api, Model } from "../types.js";
import type { Api, Model } from "../types.ts";
/** Workers AI direct endpoint. */
export const CLOUDFLARE_WORKERS_AI_BASE_URL =

View File

@@ -1,4 +1,4 @@
import { registerApiProvider, unregisterApiProviders } from "../api-registry.js";
import { registerApiProvider, unregisterApiProviders } from "../api-registry.ts";
import type {
AssistantMessage,
AssistantMessageEventStream,
@@ -14,8 +14,8 @@ import type {
ToolCall,
ToolResultMessage,
Usage,
} from "../types.js";
import { createAssistantMessageEventStream } from "../utils/event-stream.js";
} from "../types.ts";
import { createAssistantMessageEventStream } from "../utils/event-stream.ts";
const DEFAULT_API = "faux";
const DEFAULT_PROVIDER = "faux";

View File

@@ -1,4 +1,4 @@
import type { Message } from "../types.js";
import type { Message } from "../types.ts";
// Copilot expects X-Initiator to indicate whether the request is user-initiated
// or agent-initiated (e.g. follow-up after assistant/tool messages).

View File

@@ -3,9 +3,9 @@
*/
import { type Content, FinishReason, FunctionCallingConfigMode, type Part } from "@google/genai";
import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from "../types.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { transformMessages } from "./transform-messages.js";
import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from "../types.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { transformMessages } from "./transform-messages.ts";
type GoogleApiType = "google-generative-ai" | "google-vertex";

View File

@@ -7,7 +7,7 @@ import {
type ThinkingConfig,
ThinkingLevel,
} from "@google/genai";
import { calculateCost, clampThinkingLevel } from "../models.js";
import { calculateCost, clampThinkingLevel } from "../models.ts";
import type {
Api,
AssistantMessage,
@@ -21,10 +21,10 @@ import type {
ThinkingBudgets,
ThinkingContent,
ToolCall,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import type { GoogleThinkingLevel } from "./google-shared.js";
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import type { GoogleThinkingLevel } from "./google-shared.ts";
import {
convertMessages,
convertTools,
@@ -32,8 +32,8 @@ import {
mapStopReason,
mapToolChoice,
retainThoughtSignature,
} from "./google-shared.js";
import { buildBaseOptions } from "./simple-options.js";
} from "./google-shared.ts";
import { buildBaseOptions } from "./simple-options.ts";
export interface GoogleVertexOptions extends StreamOptions {
toolChoice?: "auto" | "none" | "any";

View File

@@ -4,8 +4,8 @@ import {
GoogleGenAI,
type ThinkingConfig,
} from "@google/genai";
import { getEnvApiKey } from "../env-api-keys.js";
import { calculateCost, clampThinkingLevel } from "../models.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { calculateCost, clampThinkingLevel } from "../models.ts";
import type {
Api,
AssistantMessage,
@@ -19,10 +19,10 @@ import type {
ThinkingContent,
ThinkingLevel,
ToolCall,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import type { GoogleThinkingLevel } from "./google-shared.js";
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import type { GoogleThinkingLevel } from "./google-shared.ts";
import {
convertMessages,
convertTools,
@@ -30,8 +30,8 @@ import {
mapStopReason,
mapToolChoice,
retainThoughtSignature,
} from "./google-shared.js";
import { buildBaseOptions } from "./simple-options.js";
} from "./google-shared.ts";
import { buildBaseOptions } from "./simple-options.ts";
export interface GoogleOptions extends StreamOptions {
toolChoice?: "auto" | "none" | "any";

View File

@@ -6,7 +6,7 @@ import type {
ChatCompletionContentPartText,
ChatCompletionCreateParamsNonStreaming,
} from "openai/resources/chat/completions.js";
import { getEnvApiKey } from "../../env-api-keys.js";
import { getEnvApiKey } from "../../env-api-keys.ts";
import type {
AssistantImages,
ImageContent,
@@ -15,9 +15,9 @@ import type {
ImagesModel,
ImagesOptions,
TextContent,
} from "../../types.js";
import { headersToRecord } from "../../utils/headers.js";
import { sanitizeSurrogates } from "../../utils/sanitize-unicode.js";
} from "../../types.ts";
import { headersToRecord } from "../../utils/headers.ts";
import { sanitizeSurrogates } from "../../utils/sanitize-unicode.ts";
interface OpenRouterGeneratedImage {
image_url?: string | { url?: string };

View File

@@ -1,6 +1,6 @@
import { registerImagesApiProvider } from "../../images-api-registry.js";
import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.js";
import type { generateImagesOpenRouter as generateImagesOpenRouterFunction } from "./openrouter.js";
import { registerImagesApiProvider } from "../../images-api-registry.ts";
import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.ts";
import type { generateImagesOpenRouter as generateImagesOpenRouterFunction } from "./openrouter.ts";
interface OpenRouterImagesProviderModule {
generateImagesOpenRouter: typeof generateImagesOpenRouterFunction;
@@ -21,7 +21,7 @@ function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, erro
}
function loadOpenRouterImagesProviderModule(): Promise<OpenRouterImagesProviderModule> {
openRouterImagesProviderModulePromise ||= import("./openrouter.js").then(
openRouterImagesProviderModulePromise ||= import("./openrouter.ts").then(
(module) => module as OpenRouterImagesProviderModule,
);
return openRouterImagesProviderModulePromise;

View File

@@ -6,8 +6,8 @@ import type {
ContentChunk,
FunctionTool,
} from "@mistralai/mistralai/models/components";
import { getEnvApiKey } from "../env-api-keys.js";
import { calculateCost, clampThinkingLevel } from "../models.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { calculateCost, clampThinkingLevel } from "../models.ts";
import type {
AssistantMessage,
Context,
@@ -21,13 +21,13 @@ import type {
ThinkingContent,
Tool,
ToolCall,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { shortHash } from "../utils/hash.js";
import { parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { buildBaseOptions } from "./simple-options.js";
import { transformMessages } from "./transform-messages.js";
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { shortHash } from "../utils/hash.ts";
import { parseStreamingJson } from "../utils/json-parse.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { buildBaseOptions } from "./simple-options.ts";
import { transformMessages } from "./transform-messages.ts";
const MISTRAL_TOOL_CALL_ID_LENGTH = 9;
const MAX_MISTRAL_ERROR_BODY_CHARS = 4000;

View File

@@ -6,7 +6,7 @@ import type {
ResponseStreamEvent,
} from "openai/resources/responses/responses.js";
// NEVER convert to top-level runtime imports - breaks browser/Vite builds (web-ui)
// NEVER convert to top-level runtime imports - breaks browser/Vite builds
let _os: typeof NodeOs | null = null;
type DynamicImport = (specifier: string) => Promise<unknown>;
@@ -20,9 +20,9 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
});
}
import { getEnvApiKey } from "../env-api-keys.js";
import { clampThinkingLevel } from "../models.js";
import { registerSessionResourceCleanup } from "../session-resources.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { clampThinkingLevel } from "../models.ts";
import { registerSessionResourceCleanup } from "../session-resources.ts";
import type {
Api,
AssistantMessage,
@@ -32,16 +32,17 @@ import type {
StreamFunction,
StreamOptions,
Usage,
} from "../types.js";
} from "../types.ts";
import {
appendAssistantMessageDiagnostic,
createAssistantMessageDiagnostic,
formatThrownValue,
} from "../utils/diagnostics.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { headersToRecord } from "../utils/headers.js";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
import { buildBaseOptions } from "./simple-options.js";
} from "../utils/diagnostics.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts";
import { buildBaseOptions } from "./simple-options.ts";
// ============================================================================
// Configuration
@@ -254,7 +255,29 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
const errorText = await response.text();
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
const delayMs = BASE_DELAY_MS * 2 ** attempt;
let delayMs = BASE_DELAY_MS * 2 ** attempt;
const retryAfterMs = response.headers.get("retry-after-ms");
if (retryAfterMs !== null) {
const millis = Number(retryAfterMs);
if (Number.isFinite(millis)) {
delayMs = Math.max(0, millis);
}
} else {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) {
delayMs = Math.max(0, seconds * 1000);
} else {
const date = Date.parse(retryAfter);
if (!Number.isNaN(date)) {
delayMs = Math.max(0, date - Date.now());
}
}
}
}
await sleep(delayMs, options?.signal);
continue;
}
@@ -356,7 +379,7 @@ function buildRequestBody(
input: messages,
text: { verbosity: options?.textVerbosity || "low" },
include: ["reasoning.encrypted_content"],
prompt_cache_key: options?.sessionId,
prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId),
tool_choice: "auto",
parallel_tool_calls: true,
};
@@ -711,7 +734,35 @@ type WebSocketConstructor = new (
protocols?: string | string[] | { headers?: Record<string, string> },
) => WebSocketLike;
function getWebSocketConstructor(): WebSocketConstructor | null {
let _cachedWebsocket: WebSocketConstructor | null = null;
async function getWebSocketConstructor(): Promise<WebSocketConstructor | null> {
if (_cachedWebsocket) return _cachedWebsocket;
// bun doesn't respect http proxy envs, ref: https://github.com/oven-sh/bun/issues/15489
// TODO: remove this when bun supports proxy envs in websocket.
if (
process?.versions?.bun &&
(process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy)
) {
const m = await dynamicImport("proxy-from-env");
const getProxyForUrl = (m as { getProxyForUrl: (url: string | object | URL) => string }).getProxyForUrl;
_cachedWebsocket = class extends WebSocket {
constructor(url: string | URL, options?: string | string[] | Record<string, unknown>) {
let _opts: Record<string, unknown> = {};
if (Array.isArray(options) || typeof options === "string") {
_opts = { protocols: options };
} else {
_opts = { ...options };
}
const proxy = getProxyForUrl(url.toString().replace(/^wss:/, "https:").replace(/^ws:/, "http:"));
super(url, { ..._opts, ...(proxy ? { proxy } : {}) } as any);
}
};
return _cachedWebsocket;
}
const ctor = (globalThis as { WebSocket?: unknown }).WebSocket;
if (typeof ctor !== "function") return null;
return ctor as unknown as WebSocketConstructor;
@@ -760,7 +811,7 @@ function scheduleSessionWebSocketExpiry(sessionId: string, entry: CachedWebSocke
}
async function connectWebSocket(url: string, headers: Headers, signal?: AbortSignal): Promise<WebSocketLike> {
const WebSocketCtor = getWebSocketConstructor();
const WebSocketCtor = await getWebSocketConstructor();
if (!WebSocketCtor) {
throw new Error("WebSocket transport is not available in this runtime");
}

View File

@@ -10,8 +10,8 @@ import type {
ChatCompletionSystemMessageParam,
ChatCompletionToolMessageParam,
} from "openai/resources/chat/completions.js";
import { getEnvApiKey } from "../env-api-keys.js";
import { calculateCost, clampThinkingLevel } from "../models.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { calculateCost, clampThinkingLevel } from "../models.ts";
import type {
AssistantMessage,
CacheRetention,
@@ -29,15 +29,16 @@ import type {
Tool,
ToolCall,
ToolResultMessage,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { headersToRecord } from "../utils/headers.js";
import { parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
import { buildBaseOptions } from "./simple-options.js";
import { transformMessages } from "./transform-messages.js";
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { parseStreamingJson } from "../utils/json-parse.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { buildBaseOptions } from "./simple-options.ts";
import { transformMessages } from "./transform-messages.ts";
/**
* Check if conversation messages contain tool calls or tool results.
@@ -165,6 +166,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
let textBlock: TextContent | null = null;
let thinkingBlock: ThinkingContent | null = null;
let hasFinishReason = false;
const toolCallBlocksByIndex = new Map<number, StreamingToolCallBlock>();
const toolCallBlocksById = new Map<string, StreamingToolCallBlock>();
const blocks = output.content as StreamingBlock[];
@@ -288,6 +290,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
if (finishReasonResult.errorMessage) {
output.errorMessage = finishReasonResult.errorMessage;
}
hasFinishReason = true;
}
if (choice.delta) {
@@ -324,7 +327,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
if (foundReasoningField) {
const delta = deltaFields[foundReasoningField];
if (typeof delta === "string" && delta.length > 0) {
const block = ensureThinkingBlock(foundReasoningField);
const thinkingSignature =
model.provider === "opencode-go" && foundReasoningField === "reasoning"
? "reasoning_content"
: foundReasoningField;
const block = ensureThinkingBlock(thinkingSignature);
block.thinking += delta;
stream.push({
type: "thinking_delta",
@@ -390,6 +397,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
if (output.stopReason === "error") {
throw new Error(output.errorMessage || "Provider returned an error stop reason");
}
if (!hasFinishReason) {
throw new Error("Stream ended without finish_reason");
}
stream.push({ type: "done", reason: output.stopReason, message: output });
stream.end();
@@ -507,7 +517,7 @@ function buildParams(
prompt_cache_key:
(model.baseUrl.includes("api.openai.com") && cacheRetention !== "none") ||
(cacheRetention === "long" && compat.supportsLongCacheRetention)
? options?.sessionId
? clampOpenAIPromptCacheKey(options?.sessionId)
: undefined,
prompt_cache_retention: cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined,
};
@@ -839,7 +849,10 @@ export function convertMessages(
}
// Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
const signature = nonEmptyThinkingBlocks[0].thinkingSignature;
let signature = nonEmptyThinkingBlocks[0].thinkingSignature;
if (model.provider === "opencode-go" && signature === "reasoning") {
signature = "reasoning_content";
}
if (signature && signature.length > 0) {
(assistantMsg as any)[signature] = nonEmptyThinkingBlocks.map((block) => block.thinking).join("\n");
}
@@ -997,17 +1010,17 @@ function parseChunkUsage(
model: Model<"openai-completions">,
): AssistantMessage["usage"] {
const promptTokens = rawUsage.prompt_tokens || 0;
const reportedCachedTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
const cacheReadTokens = rawUsage.prompt_tokens_details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
// Normalize to pi-ai semantics:
// - cacheRead: hits from cache created by previous requests only
// - cacheWrite: tokens written to cache in this request
// Some OpenAI-compatible providers (observed on OpenRouter) report cached_tokens
// as (previous hits + current writes). In that case, remove cacheWrite from cacheRead.
const cacheReadTokens =
cacheWriteTokens > 0 ? Math.max(0, reportedCachedTokens - cacheWriteTokens) : reportedCachedTokens;
// Follow documented OpenAI/OpenRouter semantics: cached_tokens is cache-read
// tokens (hits). OpenAI does not document or emit cache_write_tokens, but
// OpenRouter-compatible providers can include it as a separate write count.
// OpenRouter's own provider/tests affirm the separate mapping:
// https://github.com/OpenRouterTeam/ai-sdk-provider/pull/409
// Do not subtract writes from cached_tokens, otherwise spec-compliant
// providers are under-reported. DS4 mirrors this contract too:
// https://github.com/antirez/ds4/pull/29
const input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
// OpenAI completion_tokens already includes reasoning_tokens.
const outputTokens = rawUsage.completion_tokens || 0;

View File

@@ -0,0 +1,8 @@
export const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
export function clampOpenAIPromptCacheKey(key: string | undefined): string | undefined {
if (key === undefined) return undefined;
const chars = Array.from(key);
if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH) return key;
return chars.slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join("");
}

View File

@@ -12,7 +12,7 @@ import type {
ResponseReasoningItem,
ResponseStreamEvent,
} from "openai/resources/responses/responses.js";
import { calculateCost } from "../models.js";
import { calculateCost } from "../models.ts";
import type {
Api,
AssistantMessage,
@@ -26,12 +26,12 @@ import type {
Tool,
ToolCall,
Usage,
} from "../types.js";
import type { AssistantMessageEventStream } from "../utils/event-stream.js";
import { shortHash } from "../utils/hash.js";
import { parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { transformMessages } from "./transform-messages.js";
} from "../types.ts";
import type { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { shortHash } from "../utils/hash.ts";
import { parseStreamingJson } from "../utils/json-parse.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { transformMessages } from "./transform-messages.ts";
// =============================================================================
// Utilities

View File

@@ -1,7 +1,7 @@
import OpenAI from "openai";
import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
import { getEnvApiKey } from "../env-api-keys.js";
import { clampThinkingLevel } from "../models.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { clampThinkingLevel } from "../models.ts";
import type {
Api,
AssistantMessage,
@@ -13,13 +13,14 @@ import type {
StreamFunction,
StreamOptions,
Usage,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { headersToRecord } from "../utils/headers.js";
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
import { buildBaseOptions } from "./simple-options.js";
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts";
import { buildBaseOptions } from "./simple-options.ts";
const OPENAI_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
@@ -51,6 +52,22 @@ function getPromptCacheRetention(
return cacheRetention === "long" && compat.supportsLongCacheRetention ? "24h" : undefined;
}
function formatOpenAIResponsesError(error: unknown): string {
if (error instanceof Error) {
const status = (error as Error & { status?: unknown }).status;
const statusCode = typeof status === "number" ? status : undefined;
if (statusCode !== undefined) {
return `OpenAI API error (${statusCode}): ${error.message}`;
}
return error.message;
}
try {
return JSON.stringify(error);
} catch {
return String(error);
}
}
// OpenAI Responses-specific options
export interface OpenAIResponsesOptions extends StreamOptions {
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
@@ -130,7 +147,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes
delete (block as { partialJson?: string }).partialJson;
}
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
output.errorMessage = formatOpenAIResponsesError(error);
stream.push({ type: "error", reason: output.stopReason, error: output });
stream.end();
}
@@ -224,7 +241,7 @@ function buildParams(model: Model<"openai-responses">, context: Context, options
model: model.id,
input: messages,
stream: true,
prompt_cache_key: cacheRetention === "none" ? undefined : options?.sessionId,
prompt_cache_key: cacheRetention === "none" ? undefined : clampOpenAIPromptCacheKey(options?.sessionId),
prompt_cache_retention: getPromptCacheRetention(compat, cacheRetention),
store: false,
};

View File

@@ -1,4 +1,4 @@
import { clearApiProviders, registerApiProvider } from "../api-registry.js";
import { clearApiProviders, registerApiProvider } from "../api-registry.ts";
import type {
Api,
AssistantMessage,
@@ -8,17 +8,17 @@ import type {
SimpleStreamOptions,
StreamFunction,
StreamOptions,
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import type { BedrockOptions } from "./amazon-bedrock.js";
import type { AnthropicOptions } from "./anthropic.js";
import type { AzureOpenAIResponsesOptions } from "./azure-openai-responses.js";
import type { GoogleOptions } from "./google.js";
import type { GoogleVertexOptions } from "./google-vertex.js";
import type { MistralOptions } from "./mistral.js";
import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.js";
import type { OpenAICompletionsOptions } from "./openai-completions.js";
import type { OpenAIResponsesOptions } from "./openai-responses.js";
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import type { BedrockOptions } from "./amazon-bedrock.ts";
import type { AnthropicOptions } from "./anthropic.ts";
import type { AzureOpenAIResponsesOptions } from "./azure-openai-responses.ts";
import type { GoogleOptions } from "./google.ts";
import type { GoogleVertexOptions } from "./google-vertex.ts";
import type { MistralOptions } from "./mistral.ts";
import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.ts";
import type { OpenAICompletionsOptions } from "./openai-completions.ts";
import type { OpenAIResponsesOptions } from "./openai-responses.ts";
interface LazyProviderModule<
TApi extends Api,
@@ -86,7 +86,10 @@ interface BedrockProviderModule {
) => AsyncIterable<AssistantMessageEvent>;
}
const importNodeOnlyProvider = (specifier: string): Promise<unknown> => import(specifier);
const importNodeOnlyProvider = (specifier: string): Promise<unknown> => {
const runtimeSpecifier = import.meta.url.endsWith(".js") ? specifier.replace(/\.ts$/, ".js") : specifier;
return import(runtimeSpecifier);
};
let anthropicProviderModulePromise:
| Promise<LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions>>
@@ -203,7 +206,7 @@ function createLazySimpleStream<
function loadAnthropicProviderModule(): Promise<
LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions>
> {
anthropicProviderModulePromise ||= import("./anthropic.js").then((module) => {
anthropicProviderModulePromise ||= import("./anthropic.ts").then((module) => {
const provider = module as AnthropicProviderModule;
return {
stream: provider.streamAnthropic,
@@ -216,7 +219,7 @@ function loadAnthropicProviderModule(): Promise<
function loadAzureOpenAIResponsesProviderModule(): Promise<
LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions>
> {
azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.js").then((module) => {
azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.ts").then((module) => {
const provider = module as AzureOpenAIResponsesProviderModule;
return {
stream: provider.streamAzureOpenAIResponses,
@@ -229,7 +232,7 @@ function loadAzureOpenAIResponsesProviderModule(): Promise<
function loadGoogleProviderModule(): Promise<
LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions>
> {
googleProviderModulePromise ||= import("./google.js").then((module) => {
googleProviderModulePromise ||= import("./google.ts").then((module) => {
const provider = module as GoogleProviderModule;
return {
stream: provider.streamGoogle,
@@ -242,7 +245,7 @@ function loadGoogleProviderModule(): Promise<
function loadGoogleVertexProviderModule(): Promise<
LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions>
> {
googleVertexProviderModulePromise ||= import("./google-vertex.js").then((module) => {
googleVertexProviderModulePromise ||= import("./google-vertex.ts").then((module) => {
const provider = module as GoogleVertexProviderModule;
return {
stream: provider.streamGoogleVertex,
@@ -255,7 +258,7 @@ function loadGoogleVertexProviderModule(): Promise<
function loadMistralProviderModule(): Promise<
LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions>
> {
mistralProviderModulePromise ||= import("./mistral.js").then((module) => {
mistralProviderModulePromise ||= import("./mistral.ts").then((module) => {
const provider = module as MistralProviderModule;
return {
stream: provider.streamMistral,
@@ -268,7 +271,7 @@ function loadMistralProviderModule(): Promise<
function loadOpenAICodexResponsesProviderModule(): Promise<
LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions>
> {
openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.js").then((module) => {
openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.ts").then((module) => {
const provider = module as OpenAICodexResponsesProviderModule;
return {
stream: provider.streamOpenAICodexResponses,
@@ -281,7 +284,7 @@ function loadOpenAICodexResponsesProviderModule(): Promise<
function loadOpenAICompletionsProviderModule(): Promise<
LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions>
> {
openAICompletionsProviderModulePromise ||= import("./openai-completions.js").then((module) => {
openAICompletionsProviderModulePromise ||= import("./openai-completions.ts").then((module) => {
const provider = module as OpenAICompletionsProviderModule;
return {
stream: provider.streamOpenAICompletions,
@@ -294,7 +297,7 @@ function loadOpenAICompletionsProviderModule(): Promise<
function loadOpenAIResponsesProviderModule(): Promise<
LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions>
> {
openAIResponsesProviderModulePromise ||= import("./openai-responses.js").then((module) => {
openAIResponsesProviderModulePromise ||= import("./openai-responses.ts").then((module) => {
const provider = module as OpenAIResponsesProviderModule;
return {
stream: provider.streamOpenAIResponses,
@@ -310,7 +313,7 @@ function loadBedrockProviderModule(): Promise<
if (bedrockProviderModuleOverride) {
return Promise.resolve(bedrockProviderModuleOverride);
}
bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.js").then((module) => {
bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.ts").then((module) => {
const provider = module as BedrockProviderModule;
return {
stream: provider.streamBedrock,

View File

@@ -1,9 +1,9 @@
import type { Api, Model, SimpleStreamOptions, StreamOptions, ThinkingBudgets, ThinkingLevel } from "../types.js";
import type { Api, Model, SimpleStreamOptions, StreamOptions, ThinkingBudgets, ThinkingLevel } from "../types.ts";
export function buildBaseOptions(model: Model<Api>, options?: SimpleStreamOptions, apiKey?: string): StreamOptions {
export function buildBaseOptions(_model: Model<Api>, options?: SimpleStreamOptions, apiKey?: string): StreamOptions {
return {
temperature: options?.temperature,
maxTokens: options?.maxTokens ?? (model.maxTokens > 0 ? Math.min(model.maxTokens, 32000) : undefined),
maxTokens: options?.maxTokens,
signal: options?.signal,
apiKey: apiKey || options?.apiKey,
transport: options?.transport,
@@ -24,7 +24,8 @@ export function clampReasoning(effort: ThinkingLevel | undefined): Exclude<Think
}
export function adjustMaxTokensForThinking(
baseMaxTokens: number,
// Undefined means no explicit caller cap. Use the model cap and fit thinking inside it.
baseMaxTokens: number | undefined,
modelMaxTokens: number,
reasoningLevel: ThinkingLevel,
customBudgets?: ThinkingBudgets,
@@ -40,7 +41,8 @@ export function adjustMaxTokensForThinking(
const minOutputTokens = 1024;
const level = clampReasoning(reasoningLevel)!;
let thinkingBudget = budgets[level]!;
const maxTokens = Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens);
const maxTokens =
baseMaxTokens === undefined ? modelMaxTokens : Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens);
if (maxTokens <= thinkingBudget) {
thinkingBudget = Math.max(0, maxTokens - minOutputTokens);

View File

@@ -7,7 +7,7 @@ import type {
TextContent,
ToolCall,
ToolResultMessage,
} from "../types.js";
} from "../types.ts";
const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";

View File

@@ -1,6 +1,6 @@
import "./providers/register-builtins.js";
import "./providers/register-builtins.ts";
import { getApiProvider } from "./api-registry.js";
import { getApiProvider } from "./api-registry.ts";
import type {
Api,
AssistantMessage,
@@ -10,9 +10,9 @@ import type {
ProviderStreamOptions,
SimpleStreamOptions,
StreamOptions,
} from "./types.js";
} from "./types.ts";
export { getEnvApiKey } from "./env-api-keys.js";
export { getEnvApiKey } from "./env-api-keys.ts";
function resolveApiProvider(api: Api) {
const provider = getApiProvider(api);

View File

@@ -1,7 +1,7 @@
import type { AssistantMessageDiagnostic } from "./utils/diagnostics.js";
import type { AssistantMessageEventStream } from "./utils/event-stream.js";
import type { AssistantMessageDiagnostic } from "./utils/diagnostics.ts";
import type { AssistantMessageEventStream } from "./utils/event-stream.ts";
export type { AssistantMessageEventStream } from "./utils/event-stream.js";
export type { AssistantMessageEventStream } from "./utils/event-stream.ts";
export type KnownApi =
| "openai-completions"
@@ -419,6 +419,22 @@ export interface AnthropicMessagesCompat {
supportsEagerToolInputStreaming?: boolean;
/** Whether the provider supports Anthropic long cache retention (`cache_control.ttl: "1h"`). Default: true. */
supportsLongCacheRetention?: boolean;
/**
* Whether to send the `x-session-affinity` header from `options.sessionId`
* when caching is enabled. Required for providers like Fireworks that use
* session affinity for prompt cache routing (requests to the same replica
* maximize cache hits).
* Default: false.
*/
sendSessionAffinityHeaders?: boolean;
/**
* Whether the provider supports Anthropic-style `cache_control` markers on
* tool definitions. When false, `cache_control` is omitted from tool params.
* Some Anthropic-compatible providers (e.g., Fireworks) do not support this
* field on tools and may reject or ignore it.
* Default: true.
*/
supportsCacheControlOnTools?: boolean;
}
/**

View File

@@ -1,4 +1,4 @@
import type { AssistantMessage, AssistantMessageEvent } from "../types.js";
import type { AssistantMessage, AssistantMessageEvent } from "../types.ts";
// Generic event stream class for async iteration
export class EventStream<T, R = T> implements AsyncIterable<T> {
@@ -7,11 +7,12 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
private done = false;
private finalResultPromise: Promise<R>;
private resolveFinalResult!: (result: R) => void;
private isComplete: (event: T) => boolean;
private extractResult: (event: T) => R;
constructor(
private isComplete: (event: T) => boolean,
private extractResult: (event: T) => R,
) {
constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) {
this.isComplete = isComplete;
this.extractResult = extractResult;
this.finalResultPromise = new Promise((resolve) => {
this.resolveFinalResult = resolve;
});

View File

@@ -0,0 +1,123 @@
import type { Agent as HttpAgent } from "node:http";
import type { Agent as HttpsAgent } from "node:https";
import { HttpProxyAgent } from "http-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent";
const DEFAULT_PROXY_PORTS: Record<string, number> = {
ftp: 21,
gopher: 70,
http: 80,
https: 443,
ws: 80,
wss: 443,
};
export interface NodeHttpProxyAgents {
httpAgent: HttpAgent;
httpsAgent: HttpsAgent;
}
export const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE =
"Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL.";
function getProxyEnv(key: string): string {
return process.env[key.toLowerCase()] || process.env[key.toUpperCase()] || "";
}
function parseProxyTargetUrl(targetUrl: string | URL): URL | undefined {
if (targetUrl instanceof URL) {
return targetUrl;
}
try {
return new URL(targetUrl);
} catch {
return undefined;
}
}
function shouldProxyHostname(hostname: string, port: number): boolean {
const noProxy = getProxyEnv("no_proxy").toLowerCase();
if (!noProxy) {
return true;
}
if (noProxy === "*") {
return false;
}
return noProxy.split(/[,\s]/).every((proxy) => {
if (!proxy) {
return true;
}
const parsedProxy = proxy.match(/^(.+):(\d+)$/);
let proxyHostname = parsedProxy ? parsedProxy[1] : proxy;
const proxyPort = parsedProxy ? Number.parseInt(parsedProxy[2]!, 10) : 0;
if (proxyPort && proxyPort !== port) {
return true;
}
if (!/^[.*]/.test(proxyHostname)) {
return hostname !== proxyHostname;
}
if (proxyHostname.startsWith("*")) {
proxyHostname = proxyHostname.slice(1);
}
return !hostname.endsWith(proxyHostname);
});
}
function getProxyForUrl(targetUrl: string | URL): string {
const parsedUrl = parseProxyTargetUrl(targetUrl);
if (!parsedUrl?.protocol || !parsedUrl.host) {
return "";
}
const protocol = parsedUrl.protocol.split(":", 1)[0]!;
const hostname = parsedUrl.host.replace(/:\d*$/, "");
const port = Number.parseInt(parsedUrl.port, 10) || DEFAULT_PROXY_PORTS[protocol] || 0;
if (!shouldProxyHostname(hostname, port)) {
return "";
}
let proxy = getProxyEnv(`${protocol}_proxy`) || getProxyEnv("all_proxy");
if (proxy && !proxy.includes("://")) {
proxy = `${protocol}://${proxy}`;
}
return proxy;
}
export function resolveHttpProxyUrlForTarget(targetUrl: string | URL): URL | undefined {
const proxy = getProxyForUrl(targetUrl);
if (!proxy) {
return undefined;
}
let proxyUrl: URL;
try {
proxyUrl = new URL(proxy);
} catch (error) {
throw new Error(
`Invalid proxy URL ${JSON.stringify(proxy)}: ${error instanceof Error ? error.message : String(error)}`,
);
}
if (proxyUrl.protocol !== "http:" && proxyUrl.protocol !== "https:") {
throw new Error(`${UNSUPPORTED_PROXY_PROTOCOL_MESSAGE} Got ${proxyUrl.protocol}`);
}
return proxyUrl;
}
export function createHttpProxyAgentsForTarget(targetUrl: string | URL): NodeHttpProxyAgents | undefined {
const proxyUrl = resolveHttpProxyUrlForTarget(targetUrl);
if (!proxyUrl) {
return undefined;
}
return {
httpAgent: new HttpProxyAgent(proxyUrl),
httpsAgent: new HttpsProxyAgent(proxyUrl) as unknown as HttpsAgent,
};
}

View File

@@ -6,9 +6,9 @@
*/
import type { Server } from "node:http";
import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js";
import { generatePKCE } from "./pkce.js";
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.js";
import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts";
import { generatePKCE } from "./pkce.ts";
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.ts";
type CallbackServerInfo = {
server: Server;

View File

@@ -2,9 +2,9 @@
* GitHub Copilot OAuth flow
*/
import { getModels } from "../../models.js";
import type { Api, Model } from "../../types.js";
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.js";
import { getModels } from "../../models.ts";
import type { Api, Model } from "../../types.ts";
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.ts";
type CopilotCredentials = OAuthCredentials & {
enterpriseUrl?: string;

View File

@@ -8,7 +8,7 @@
*/
// Anthropic
export { anthropicOAuthProvider, loginAnthropic, refreshAnthropicToken } from "./anthropic.js";
export { anthropicOAuthProvider, loginAnthropic, refreshAnthropicToken } from "./anthropic.ts";
// GitHub Copilot
export {
getGitHubCopilotBaseUrl,
@@ -16,20 +16,20 @@ export {
loginGitHubCopilot,
normalizeDomain,
refreshGitHubCopilotToken,
} from "./github-copilot.js";
} from "./github-copilot.ts";
// OpenAI Codex (ChatGPT OAuth)
export { loginOpenAICodex, openaiCodexOAuthProvider, refreshOpenAICodexToken } from "./openai-codex.js";
export { loginOpenAICodex, openaiCodexOAuthProvider, refreshOpenAICodexToken } from "./openai-codex.ts";
export * from "./types.js";
export * from "./types.ts";
// ============================================================================
// Provider Registry
// ============================================================================
import { anthropicOAuthProvider } from "./anthropic.js";
import { githubCopilotOAuthProvider } from "./github-copilot.js";
import { openaiCodexOAuthProvider } from "./openai-codex.js";
import type { OAuthCredentials, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types.js";
import { anthropicOAuthProvider } from "./anthropic.ts";
import { githubCopilotOAuthProvider } from "./github-copilot.ts";
import { openaiCodexOAuthProvider } from "./openai-codex.ts";
import type { OAuthCredentials, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types.ts";
const BUILT_IN_OAUTH_PROVIDERS: OAuthProviderInterface[] = [
anthropicOAuthProvider,

View File

@@ -5,7 +5,7 @@
* It is only intended for CLI use, not browser environments.
*/
// NEVER convert to top-level imports - breaks browser/Vite builds (web-ui)
// NEVER convert to top-level imports - breaks browser/Vite builds
let _randomBytes: typeof import("node:crypto").randomBytes | null = null;
let _http: typeof import("node:http") | null = null;
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
@@ -17,9 +17,9 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
});
}
import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.js";
import { generatePKCE } from "./pkce.js";
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.js";
import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts";
import { generatePKCE } from "./pkce.ts";
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.ts";
const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1";
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";

View File

@@ -1,4 +1,4 @@
import type { Api, Model } from "../../types.js";
import type { Api, Model } from "../../types.ts";
export type OAuthCredentials = {
refresh: string;

View File

@@ -1,4 +1,4 @@
import type { AssistantMessage } from "../types.js";
import type { AssistantMessage } from "../types.ts";
/**
* Regex patterns to detect context overflow errors from different providers.
@@ -11,6 +11,7 @@ import type { AssistantMessage } from "../types.js";
* - Anthropic: "prompt is too long: 213462 tokens > 200000 maximum"
* - Anthropic: "413 {\"error\":{\"type\":\"request_too_large\",\"message\":\"Request exceeds the maximum size\"}}"
* - OpenAI: "Your input exceeds the context window of this model"
* - OpenAI/LiteLLM: "Requested token count exceeds the model's maximum context length of 131072 tokens"
* - Google: "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)"
* - xAI: "This model's maximum prompt length is 131072 but the request contains 537812 tokens"
* - Groq: "Please reduce the length of the messages or completion"
@@ -34,6 +35,7 @@ const OVERFLOW_PATTERNS = [
/request_too_large/i, // Anthropic request byte-size overflow (HTTP 413)
/input is too long for requested model/i, // Amazon Bedrock
/exceeds the context window/i, // OpenAI (Completions & Responses API)
/exceeds (?:the )?(?:model'?s )?maximum context length of [\d,]+ tokens?/i, // OpenAI-compatible proxies (LiteLLM)
/input token count.*exceeds the maximum/i, // Google (Gemini)
/maximum prompt length is \d+/i, // xAI (Grok)
/reduce the length of the messages/i, // Groq
@@ -81,7 +83,7 @@ const NON_OVERFLOW_PATTERNS = [
*
* **Reliable detection (returns error with detectable message):**
* - Anthropic: "prompt is too long: X tokens > Y maximum" or "request_too_large"
* - OpenAI (Completions & Responses): "exceeds the context window"
* - OpenAI (Completions & Responses): "exceeds the context window" or "exceeds the model's maximum context length of X tokens"
* - Google Gemini: "input token count exceeds the maximum"
* - xAI (Grok): "maximum prompt length is X but request contains Y"
* - Groq: "reduce the length of the messages"

View File

@@ -1,7 +1,7 @@
import { Compile } from "typebox/compile";
import type { TLocalizedValidationError } from "typebox/error";
import { Value } from "typebox/value";
import type { Tool, ToolCall } from "../types.js";
import type { Tool, ToolCall } from "../types.ts";
const validatorCache = new WeakMap<object, ReturnType<typeof Compile>>();
const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");