merge: sync upstream pi-mono main into sproutclaw
Some checks failed
CI / build-check-test (push) Has been cancelled
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:
@@ -6,7 +6,7 @@ import type {
|
||||
SimpleStreamOptions,
|
||||
StreamFunction,
|
||||
StreamOptions,
|
||||
} from "./types.js";
|
||||
} from "./types.ts";
|
||||
|
||||
export type ApiStreamFunction = (
|
||||
model: Model<Api>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.js";
|
||||
import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts";
|
||||
|
||||
export const bedrockProviderModule = {
|
||||
streamBedrock,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>>>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from "./utils/oauth/index.js";
|
||||
export * from "./utils/oauth/index.ts";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
8
packages/ai/src/providers/openai-prompt-cache.ts
Normal file
8
packages/ai/src/providers/openai-prompt-cache.ts
Normal 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("");
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
123
packages/ai/src/utils/node-http-proxy.ts
Normal file
123
packages/ai/src/utils/node-http-proxy.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Api, Model } from "../../types.js";
|
||||
import type { Api, Model } from "../../types.ts";
|
||||
|
||||
export type OAuthCredentials = {
|
||||
refresh: string;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user