Merge remote-tracking branch 'upstream/main' into feat/image-outputs

This commit is contained in:
Cristina Poncela Cubeiro
2026-05-07 16:46:17 +02:00
63 changed files with 2774 additions and 777 deletions

View File

@@ -38,6 +38,8 @@ export type {
OAuthProviderId,
OAuthProviderInfo,
OAuthProviderInterface,
OAuthSelectOption,
OAuthSelectPrompt,
} from "./utils/oauth/types.js";
export * from "./utils/overflow.js";
export * from "./utils/typebox-helpers.js";

View File

@@ -5819,24 +5819,6 @@ export const MODELS = {
} satisfies Model<"openai-completions">,
},
"kimi-coding": {
"k2p6": {
id: "k2p6",
name: "Kimi K2.6",
api: "anthropic-messages",
provider: "kimi-coding",
baseUrl: "https://api.kimi.com/coding",
headers: {"User-Agent":"KimiCLI/1.5"},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 32768,
} satisfies Model<"anthropic-messages">,
"kimi-for-coding": {
id: "kimi-for-coding",
name: "Kimi For Coding",
@@ -7604,9 +7586,9 @@ export const MODELS = {
"big-pickle": {
id: "big-pickle",
name: "Big Pickle",
api: "anthropic-messages",
api: "openai-completions",
provider: "opencode",
baseUrl: "https://opencode.ai/zen",
baseUrl: "https://opencode.ai/zen/v1",
reasoning: true,
input: ["text"],
cost: {
@@ -7617,7 +7599,7 @@ export const MODELS = {
},
contextWindow: 200000,
maxTokens: 128000,
} satisfies Model<"anthropic-messages">,
} satisfies Model<"openai-completions">,
"claude-haiku-4-5": {
id: "claude-haiku-4-5",
name: "Claude Haiku 4.5",
@@ -7872,9 +7854,9 @@ export const MODELS = {
thinkingLevelMap: {"off":null},
input: ["text", "image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
input: 0.05,
output: 0.4,
cacheRead: 0.005,
cacheWrite: 0,
},
contextWindow: 400000,
@@ -8360,55 +8342,21 @@ export const MODELS = {
} satisfies Model<"openai-completions">,
"kimi-k2.6": {
id: "kimi-k2.6",
name: "Kimi K2.6 (3x limits)",
name: "Kimi K2.6",
api: "openai-completions",
provider: "opencode-go",
baseUrl: "https://opencode.ai/zen/go/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.32,
output: 1.34,
cacheRead: 0.054,
input: 0.95,
output: 4,
cacheRead: 0.16,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"mimo-v2-omni": {
id: "mimo-v2-omni",
name: "MiMo V2 Omni",
api: "openai-completions",
provider: "opencode-go",
baseUrl: "https://opencode.ai/zen/go/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.4,
output: 2,
cacheRead: 0.08,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"mimo-v2-pro": {
id: "mimo-v2-pro",
name: "MiMo V2 Pro",
api: "openai-completions",
provider: "opencode-go",
baseUrl: "https://opencode.ai/zen/go/v1",
reasoning: true,
input: ["text"],
cost: {
input: 1,
output: 3,
cacheRead: 0.2,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"mimo-v2.5": {
id: "mimo-v2.5",
name: "MiMo V2.5",
@@ -8549,23 +8497,6 @@ export const MODELS = {
contextWindow: 131072,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"allenai/olmo-3.1-32b-instruct": {
id: "allenai/olmo-3.1-32b-instruct",
name: "AllenAI: Olmo 3.1 32B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.19999999999999998,
output: 0.6,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 65536,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"amazon/nova-2-lite-v1": {
id: "amazon/nova-2-lite-v1",
name: "Amazon: Nova 2 Lite",
@@ -8700,7 +8631,7 @@ export const MODELS = {
cacheWrite: 3.75,
},
contextWindow: 200000,
maxTokens: 128000,
maxTokens: 64000,
} satisfies Model<"openai-completions">,
"anthropic/claude-3.7-sonnet:thinking": {
id: "anthropic/claude-3.7-sonnet:thinking",
@@ -8977,6 +8908,23 @@ export const MODELS = {
contextWindow: 2000000,
maxTokens: 30000,
} satisfies Model<"openai-completions">,
"baidu/cobuddy:free": {
id: "baidu/cobuddy:free",
name: "Baidu Qianfan: CoBuddy (free)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"baidu/ernie-4.5-21b-a3b": {
id: "baidu/ernie-4.5-21b-a3b",
name: "Baidu: ERNIE 4.5 21B A3B",
@@ -9207,8 +9155,8 @@ export const MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.21,
output: 0.7899999999999999,
input: 0.27,
output: 0.95,
cacheRead: 0.13,
cacheWrite: 0,
},
@@ -9318,7 +9266,7 @@ export const MODELS = {
cacheRead: 0.024999999999999998,
cacheWrite: 0.08333333333333334,
},
contextWindow: 1000000,
contextWindow: 1048576,
maxTokens: 8192,
} satisfies Model<"openai-completions">,
"google/gemini-2.0-flash-lite-001": {
@@ -9695,23 +9643,6 @@ export const MODELS = {
contextWindow: 256000,
maxTokens: 80000,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-8b-instruct": {
id: "meta-llama/llama-3-8b-instruct",
name: "Meta: Llama 3 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.03,
output: 0.04,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-70b-instruct": {
id: "meta-llama/llama-3.1-70b-instruct",
name: "Meta: Llama 3.1 70B Instruct",
@@ -10103,6 +10034,23 @@ export const MODELS = {
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-medium-3-5": {
id: "mistralai/mistral-medium-3-5",
name: "Mistral: Mistral Medium 3.5",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1.5,
output: 7.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-medium-3.1": {
id: "mistralai/mistral-medium-3.1",
name: "Mistral: Mistral Medium 3.1",
@@ -10333,13 +10281,13 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.74,
output: 3.49,
cacheRead: 0.14,
input: 0.75,
output: 3.5,
cacheRead: 0.15,
cacheWrite: 0,
},
contextWindow: 262142,
maxTokens: 262142,
contextWindow: 262144,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"nex-agi/deepseek-v3.1-nex-n1": {
id: "nex-agi/deepseek-v3.1-nex-n1",
@@ -11254,6 +11202,23 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"openai/gpt-chat-latest": {
id: "openai/gpt-chat-latest",
name: "OpenAI: GPT Chat Latest",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 5,
output: 30,
cacheRead: 0.5,
cacheWrite: 0,
},
contextWindow: 400000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"openai/gpt-oss-120b": {
id: "openai/gpt-oss-120b",
name: "OpenAI: gpt-oss-120b",
@@ -11807,13 +11772,13 @@ export const MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.08,
output: 0.28,
input: 0.09,
output: 0.44999999999999996,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 40960,
maxTokens: 16384,
maxTokens: 20000,
} satisfies Model<"openai-completions">,
"qwen/qwen3-30b-a3b-instruct-2507": {
id: "qwen/qwen3-30b-a3b-instruct-2507",
@@ -11943,7 +11908,7 @@ export const MODELS = {
reasoning: false,
input: ["text"],
cost: {
input: 0.12,
input: 0.11,
output: 0.7999999999999999,
cacheRead: 0.07,
cacheWrite: 0,
@@ -12232,13 +12197,13 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.1625,
output: 1.3,
cacheRead: 0,
input: 0.14,
output: 1,
cacheRead: 0.049999999999999996,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 65536,
maxTokens: 81920,
} satisfies Model<"openai-completions">,
"qwen/qwen3.5-397b-a17b": {
id: "qwen/qwen3.5-397b-a17b",
@@ -12266,13 +12231,13 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.09999999999999999,
input: 0.04,
output: 0.15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
maxTokens: 81920,
} satisfies Model<"openai-completions">,
"qwen/qwen3.5-flash-02-23": {
id: "qwen/qwen3.5-flash-02-23",
@@ -12342,6 +12307,23 @@ export const MODELS = {
contextWindow: 262144,
maxTokens: 81920,
} satisfies Model<"openai-completions">,
"qwen/qwen3.6-35b-a3b": {
id: "qwen/qwen3.6-35b-a3b",
name: "Qwen: Qwen3.6 35B A3B",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.15,
output: 1,
cacheRead: 0.049999999999999996,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"qwen/qwen3.6-flash": {
id: "qwen/qwen3.6-flash",
name: "Qwen: Qwen3.6 Flash",
@@ -12986,7 +12968,7 @@ export const MODELS = {
cacheWrite: 0,
},
contextWindow: 202752,
maxTokens: 16384,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"z-ai/glm-5-turbo": {
id: "z-ai/glm-5-turbo",
@@ -13133,13 +13115,13 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.74,
output: 3.49,
cacheRead: 0.14,
input: 0.75,
output: 3.5,
cacheRead: 0.15,
cacheWrite: 0,
},
contextWindow: 262142,
maxTokens: 262142,
contextWindow: 262144,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"~openai/gpt-latest": {
id: "~openai/gpt-latest",
@@ -14573,23 +14555,6 @@ export const MODELS = {
contextWindow: 131072,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"moonshotai/kimi-k2-0905": {
id: "moonshotai/kimi-k2-0905",
name: "Kimi K2 0905",
api: "anthropic-messages",
provider: "vercel-ai-gateway",
baseUrl: "https://ai-gateway.vercel.sh",
reasoning: false,
input: ["text"],
cost: {
input: 0.6,
output: 2.5,
cacheRead: 0.3,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 128000,
} satisfies Model<"anthropic-messages">,
"moonshotai/kimi-k2-thinking": {
id: "moonshotai/kimi-k2-thinking",
name: "Kimi K2 Thinking",
@@ -15546,8 +15511,8 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 2,
output: 6,
input: 1.25,
output: 2.5,
cacheRead: 0.19999999999999998,
cacheWrite: 0,
},
@@ -15563,8 +15528,8 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 2,
output: 6,
input: 1.25,
output: 2.5,
cacheRead: 0.19999999999999998,
cacheWrite: 0,
},
@@ -15580,8 +15545,8 @@ export const MODELS = {
reasoning: false,
input: ["text", "image"],
cost: {
input: 2,
output: 6,
input: 1.25,
output: 2.5,
cacheRead: 0.19999999999999998,
cacheWrite: 0,
},
@@ -15597,8 +15562,8 @@ export const MODELS = {
reasoning: false,
input: ["text", "image"],
cost: {
input: 2,
output: 6,
input: 1.25,
output: 2.5,
cacheRead: 0.19999999999999998,
cacheWrite: 0,
},
@@ -15614,8 +15579,8 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 2,
output: 6,
input: 1.25,
output: 2.5,
cacheRead: 0.19999999999999998,
cacheWrite: 0,
},
@@ -15631,8 +15596,8 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 2,
output: 6,
input: 1.25,
output: 2.5,
cacheRead: 0.19999999999999998,
cacheWrite: 0,
},
@@ -16405,8 +16370,8 @@ export const MODELS = {
cacheRead: 0.01,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 64000,
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"anthropic-messages">,
"mimo-v2-omni": {
id: "mimo-v2-omni",
@@ -16422,8 +16387,8 @@ export const MODELS = {
cacheRead: 0.08,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 128000,
contextWindow: 262144,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"mimo-v2-pro": {
id: "mimo-v2-pro",
@@ -16439,8 +16404,8 @@ export const MODELS = {
cacheRead: 0.2,
cacheWrite: 0,
},
contextWindow: 1000000,
maxTokens: 128000,
contextWindow: 1048576,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"mimo-v2.5": {
id: "mimo-v2.5",
@@ -16449,7 +16414,7 @@ export const MODELS = {
provider: "xiaomi",
baseUrl: "https://api.xiaomimimo.com/anthropic",
reasoning: true,
input: ["text"],
input: ["text", "image"],
cost: {
input: 0.4,
output: 2,
@@ -16466,7 +16431,7 @@ export const MODELS = {
provider: "xiaomi",
baseUrl: "https://api.xiaomimimo.com/anthropic",
reasoning: true,
input: ["text", "image"],
input: ["text"],
cost: {
input: 1,
output: 3,
@@ -16492,8 +16457,8 @@ export const MODELS = {
cacheRead: 0.01,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 64000,
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"anthropic-messages">,
"mimo-v2-omni": {
id: "mimo-v2-omni",
@@ -16509,8 +16474,8 @@ export const MODELS = {
cacheRead: 0.08,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 128000,
contextWindow: 262144,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"mimo-v2-pro": {
id: "mimo-v2-pro",
@@ -16526,8 +16491,8 @@ export const MODELS = {
cacheRead: 0.2,
cacheWrite: 0,
},
contextWindow: 1000000,
maxTokens: 128000,
contextWindow: 1048576,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"mimo-v2.5": {
id: "mimo-v2.5",
@@ -16536,7 +16501,7 @@ export const MODELS = {
provider: "xiaomi-token-plan-ams",
baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic",
reasoning: true,
input: ["text"],
input: ["text", "image"],
cost: {
input: 0.4,
output: 2,
@@ -16553,7 +16518,7 @@ export const MODELS = {
provider: "xiaomi-token-plan-ams",
baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic",
reasoning: true,
input: ["text", "image"],
input: ["text"],
cost: {
input: 1,
output: 3,
@@ -16579,8 +16544,8 @@ export const MODELS = {
cacheRead: 0.01,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 64000,
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"anthropic-messages">,
"mimo-v2-omni": {
id: "mimo-v2-omni",
@@ -16596,8 +16561,8 @@ export const MODELS = {
cacheRead: 0.08,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 128000,
contextWindow: 262144,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"mimo-v2-pro": {
id: "mimo-v2-pro",
@@ -16613,8 +16578,8 @@ export const MODELS = {
cacheRead: 0.2,
cacheWrite: 0,
},
contextWindow: 1000000,
maxTokens: 128000,
contextWindow: 1048576,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"mimo-v2.5": {
id: "mimo-v2.5",
@@ -16623,7 +16588,7 @@ export const MODELS = {
provider: "xiaomi-token-plan-cn",
baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic",
reasoning: true,
input: ["text"],
input: ["text", "image"],
cost: {
input: 0.4,
output: 2,
@@ -16640,7 +16605,7 @@ export const MODELS = {
provider: "xiaomi-token-plan-cn",
baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic",
reasoning: true,
input: ["text", "image"],
input: ["text"],
cost: {
input: 1,
output: 3,
@@ -16666,8 +16631,8 @@ export const MODELS = {
cacheRead: 0.01,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 64000,
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"anthropic-messages">,
"mimo-v2-omni": {
id: "mimo-v2-omni",
@@ -16683,8 +16648,8 @@ export const MODELS = {
cacheRead: 0.08,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 128000,
contextWindow: 262144,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"mimo-v2-pro": {
id: "mimo-v2-pro",
@@ -16700,8 +16665,8 @@ export const MODELS = {
cacheRead: 0.2,
cacheWrite: 0,
},
contextWindow: 1000000,
maxTokens: 128000,
contextWindow: 1048576,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"mimo-v2.5": {
id: "mimo-v2.5",
@@ -16710,7 +16675,7 @@ export const MODELS = {
provider: "xiaomi-token-plan-sgp",
baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic",
reasoning: true,
input: ["text"],
input: ["text", "image"],
cost: {
input: 0.4,
output: 2,
@@ -16727,7 +16692,7 @@ export const MODELS = {
provider: "xiaomi-token-plan-sgp",
baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic",
reasoning: true,
input: ["text", "image"],
input: ["text"],
cost: {
input: 1,
output: 3,
@@ -16813,7 +16778,7 @@ export const MODELS = {
} satisfies Model<"openai-completions">,
"glm-5v-turbo": {
id: "glm-5v-turbo",
name: "glm-5v-turbo",
name: "GLM-5V-Turbo",
api: "openai-completions",
provider: "zai",
baseUrl: "https://api.z.ai/api/coding/paas/v4",

View File

@@ -352,7 +352,7 @@ function buildRequestBody(
model: model.id,
store: false,
stream: true,
instructions: context.systemPrompt,
instructions: context.systemPrompt || "You are a helpful assistant.",
input: messages,
text: { verbosity: options?.textVerbosity || "low" },
include: ["reasoning.encrypted_content"],

View File

@@ -160,45 +160,104 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
partialArgs?: string;
streamIndex?: number;
}
type StreamingBlock = TextContent | ThinkingContent | StreamingToolCallBlock;
type StreamingToolCallDelta = NonNullable<ChatCompletionChunk.Choice.Delta["tool_calls"]>[number];
let currentBlock: TextContent | ThinkingContent | StreamingToolCallBlock | null = null;
const blocks = output.content;
const getContentIndex = (block: typeof currentBlock) => (block ? blocks.indexOf(block) : -1);
const currentContentIndex = () => getContentIndex(currentBlock);
const finishCurrentBlock = (block?: typeof currentBlock) => {
if (block) {
const contentIndex = getContentIndex(block);
if (contentIndex === -1) {
return;
}
if (block.type === "text") {
stream.push({
type: "text_end",
contentIndex,
content: block.text,
partial: output,
});
} else if (block.type === "thinking") {
stream.push({
type: "thinking_end",
contentIndex,
content: block.thinking,
partial: output,
});
} else if (block.type === "toolCall") {
block.arguments = parseStreamingJson(block.partialArgs);
// Finalize in-place and strip the scratch buffers so replay only
// carries parsed arguments.
delete block.partialArgs;
delete block.streamIndex;
stream.push({
type: "toolcall_end",
contentIndex,
toolCall: block,
partial: output,
});
}
let textBlock: TextContent | null = null;
let thinkingBlock: ThinkingContent | null = null;
const toolCallBlocksByIndex = new Map<number, StreamingToolCallBlock>();
const toolCallBlocksById = new Map<string, StreamingToolCallBlock>();
const blocks = output.content as StreamingBlock[];
const getContentIndex = (block: StreamingBlock) => blocks.indexOf(block);
const finishBlock = (block: StreamingBlock) => {
const contentIndex = getContentIndex(block);
if (contentIndex === -1) {
return;
}
if (block.type === "text") {
stream.push({
type: "text_end",
contentIndex,
content: block.text,
partial: output,
});
} else if (block.type === "thinking") {
stream.push({
type: "thinking_end",
contentIndex,
content: block.thinking,
partial: output,
});
} else if (block.type === "toolCall") {
block.arguments = parseStreamingJson(block.partialArgs);
// Finalize in-place and strip the scratch buffers so replay only
// carries parsed arguments.
delete block.partialArgs;
delete block.streamIndex;
stream.push({
type: "toolcall_end",
contentIndex,
toolCall: block,
partial: output,
});
}
};
const ensureTextBlock = () => {
if (!textBlock) {
textBlock = { type: "text", text: "" };
blocks.push(textBlock);
stream.push({ type: "text_start", contentIndex: getContentIndex(textBlock), partial: output });
}
return textBlock;
};
const ensureThinkingBlock = (thinkingSignature: string) => {
if (!thinkingBlock) {
thinkingBlock = {
type: "thinking",
thinking: "",
thinkingSignature,
};
blocks.push(thinkingBlock);
stream.push({ type: "thinking_start", contentIndex: getContentIndex(thinkingBlock), partial: output });
}
return thinkingBlock;
};
const ensureToolCallBlock = (toolCall: StreamingToolCallDelta) => {
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
let block = streamIndex !== undefined ? toolCallBlocksByIndex.get(streamIndex) : undefined;
if (!block && toolCall.id) {
block = toolCallBlocksById.get(toolCall.id);
}
if (!block) {
block = {
type: "toolCall",
id: toolCall.id || "",
name: toolCall.function?.name || "",
arguments: {},
partialArgs: "",
streamIndex,
};
if (streamIndex !== undefined) {
toolCallBlocksByIndex.set(streamIndex, block);
}
if (toolCall.id) {
toolCallBlocksById.set(toolCall.id, block);
}
blocks.push(block);
stream.push({
type: "toolcall_start",
contentIndex: getContentIndex(block),
partial: output,
});
}
if (streamIndex !== undefined && block.streamIndex === undefined) {
block.streamIndex = streamIndex;
toolCallBlocksByIndex.set(streamIndex, block);
}
if (toolCall.id) {
toolCallBlocksById.set(toolCall.id, block);
}
return block;
};
for await (const chunk of openaiStream) {
@@ -237,22 +296,14 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
choice.delta.content !== undefined &&
choice.delta.content.length > 0
) {
if (!currentBlock || currentBlock.type !== "text") {
finishCurrentBlock(currentBlock);
currentBlock = { type: "text", text: "" };
output.content.push(currentBlock);
stream.push({ type: "text_start", contentIndex: currentContentIndex(), partial: output });
}
if (currentBlock.type === "text") {
currentBlock.text += choice.delta.content;
stream.push({
type: "text_delta",
contentIndex: currentContentIndex(),
delta: choice.delta.content,
partial: output,
});
}
const block = ensureTextBlock();
block.text += choice.delta.content;
stream.push({
type: "text_delta",
contentIndex: getContentIndex(block),
delta: choice.delta.content,
partial: output,
});
}
// Some endpoints return reasoning in reasoning_content (llama.cpp),
@@ -260,38 +311,24 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
// Use the first non-empty reasoning field to avoid duplication
// (e.g., chutes.ai returns both reasoning_content and reasoning with same content)
const reasoningFields = ["reasoning_content", "reasoning", "reasoning_text"];
const deltaFields = choice.delta as Record<string, unknown>;
let foundReasoningField: string | null = null;
for (const field of reasoningFields) {
if (
(choice.delta as any)[field] !== null &&
(choice.delta as any)[field] !== undefined &&
(choice.delta as any)[field].length > 0
) {
if (!foundReasoningField) {
foundReasoningField = field;
break;
}
const value = deltaFields[field];
if (typeof value === "string" && value.length > 0) {
foundReasoningField = field;
break;
}
}
if (foundReasoningField) {
if (!currentBlock || currentBlock.type !== "thinking") {
finishCurrentBlock(currentBlock);
currentBlock = {
type: "thinking",
thinking: "",
thinkingSignature: foundReasoningField,
};
output.content.push(currentBlock);
stream.push({ type: "thinking_start", contentIndex: currentContentIndex(), partial: output });
}
if (currentBlock.type === "thinking") {
const delta = (choice.delta as any)[foundReasoningField];
currentBlock.thinking += delta;
const delta = deltaFields[foundReasoningField];
if (typeof delta === "string" && delta.length > 0) {
const block = ensureThinkingBlock(foundReasoningField);
block.thinking += delta;
stream.push({
type: "thinking_delta",
contentIndex: currentContentIndex(),
contentIndex: getContentIndex(block),
delta,
partial: output,
});
@@ -300,52 +337,27 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
if (choice?.delta?.tool_calls) {
for (const toolCall of choice.delta.tool_calls) {
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
const sameToolCall =
currentBlock?.type === "toolCall" &&
((streamIndex !== undefined && currentBlock.streamIndex === streamIndex) ||
(streamIndex === undefined && toolCall.id && currentBlock.id === toolCall.id));
if (!sameToolCall) {
finishCurrentBlock(currentBlock);
currentBlock = {
type: "toolCall",
id: toolCall.id || "",
name: toolCall.function?.name || "",
arguments: {},
partialArgs: "",
streamIndex,
};
output.content.push(currentBlock);
stream.push({
type: "toolcall_start",
contentIndex: getContentIndex(currentBlock),
partial: output,
});
const block = ensureToolCallBlock(toolCall);
if (!block.id && toolCall.id) {
block.id = toolCall.id;
toolCallBlocksById.set(toolCall.id, block);
}
if (!block.name && toolCall.function?.name) {
block.name = toolCall.function.name;
}
const currentToolCallBlock = currentBlock?.type === "toolCall" ? currentBlock : null;
if (currentToolCallBlock) {
if (!currentToolCallBlock.id && toolCall.id) currentToolCallBlock.id = toolCall.id;
if (!currentToolCallBlock.name && toolCall.function?.name) {
currentToolCallBlock.name = toolCall.function.name;
}
if (currentToolCallBlock.streamIndex === undefined && streamIndex !== undefined) {
currentToolCallBlock.streamIndex = streamIndex;
}
let delta = "";
if (toolCall.function?.arguments) {
delta = toolCall.function.arguments;
currentToolCallBlock.partialArgs += toolCall.function.arguments;
currentToolCallBlock.arguments = parseStreamingJson(currentToolCallBlock.partialArgs);
}
stream.push({
type: "toolcall_delta",
contentIndex: getContentIndex(currentToolCallBlock),
delta,
partial: output,
});
let delta = "";
if (toolCall.function?.arguments) {
delta = toolCall.function.arguments;
block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments;
block.arguments = parseStreamingJson(block.partialArgs);
}
stream.push({
type: "toolcall_delta",
contentIndex: getContentIndex(block),
delta,
partial: output,
});
}
}
@@ -365,7 +377,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
}
}
finishCurrentBlock(currentBlock);
for (const block of blocks) {
finishBlock(block);
}
if (options?.signal?.aborted) {
throw new Error("Request was aborted");
}

View File

@@ -354,6 +354,16 @@ export async function processResponsesStream<TApi extends Api>(
});
}
}
} else if (event.type === "response.reasoning_text.delta") {
if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
currentBlock.thinking += event.delta;
stream.push({
type: "thinking_delta",
contentIndex: blockIndex(),
delta: event.delta,
partial: output,
});
}
} else if (event.type === "response.content_part.added") {
if (currentItem?.type === "message") {
currentItem.content = currentItem.content || [];
@@ -429,7 +439,9 @@ export async function processResponsesStream<TApi extends Api>(
const item = event.item;
if (item.type === "reasoning" && currentBlock?.type === "thinking") {
currentBlock.thinking = item.summary?.map((s) => s.text).join("\n\n") || "";
const summaryText = item.summary?.map((s) => s.text).join("\n\n") || "";
const contentText = item.content?.map((c) => c.text).join("\n\n") || "";
currentBlock.thinking = summaryText || contentText || currentBlock.thinking;
currentBlock.thinkingSignature = JSON.stringify(item);
stream.push({
type: "thinking_end",

View File

@@ -30,7 +30,7 @@ const SCOPE = "openid profile email offline_access";
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
type TokenSuccess = { type: "success"; access: string; refresh: string; expires: number };
type TokenFailure = { type: "failed" };
type TokenFailure = { type: "failed"; message: string; status?: number };
type TokenResult = TokenSuccess | TokenFailure;
type JwtPayload = {
@@ -108,8 +108,11 @@ async function exchangeAuthorizationCode(
if (!response.ok) {
const text = await response.text().catch(() => "");
console.error("[openai-codex] code->token failed:", response.status, text);
return { type: "failed" };
return {
type: "failed",
status: response.status,
message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`,
};
}
const json = (await response.json()) as {
@@ -119,8 +122,10 @@ async function exchangeAuthorizationCode(
};
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
console.error("[openai-codex] token response missing fields:", json);
return { type: "failed" };
return {
type: "failed",
message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`,
};
}
return {
@@ -145,8 +150,11 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
if (!response.ok) {
const text = await response.text().catch(() => "");
console.error("[openai-codex] Token refresh failed:", response.status, text);
return { type: "failed" };
return {
type: "failed",
status: response.status,
message: `OpenAI Codex token refresh failed (${response.status}): ${text || response.statusText}`,
};
}
const json = (await response.json()) as {
@@ -156,8 +164,10 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
};
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
console.error("[openai-codex] Token refresh response missing fields:", json);
return { type: "failed" };
return {
type: "failed",
message: `OpenAI Codex token refresh response missing fields: ${JSON.stringify(json)}`,
};
}
return {
@@ -167,8 +177,10 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
expires: Date.now() + json.expires_in * 1000,
};
} catch (error) {
console.error("[openai-codex] Token refresh error:", error);
return { type: "failed" };
return {
type: "failed",
message: `OpenAI Codex token refresh error: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
@@ -258,12 +270,7 @@ function startLocalOAuthServer(state: string): Promise<OAuthServerInfo> {
waitForCode: () => waitForCodePromise,
});
})
.on("error", (err: NodeJS.ErrnoException) => {
console.error(
`[openai-codex] Failed to bind http://${CALLBACK_HOST}:1455 (`,
err.code,
") Falling back to manual paste.",
);
.on("error", (_err: NodeJS.ErrnoException) => {
settleWait?.(null);
resolve({
close: () => {
@@ -386,7 +393,7 @@ export async function loginOpenAICodex(options: {
const tokenResult = await exchangeAuthorizationCode(code, verifier);
if (tokenResult.type !== "success") {
throw new Error("Token exchange failed");
throw new Error(tokenResult.message);
}
const accountId = getAccountId(tokenResult.access);
@@ -411,7 +418,7 @@ export async function loginOpenAICodex(options: {
export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredentials> {
const result = await refreshAccessToken(refreshToken);
if (result.type !== "success") {
throw new Error("Failed to refresh OpenAI Codex token");
throw new Error(result.message);
}
const accountId = getAccountId(result.access);

View File

@@ -23,11 +23,23 @@ export type OAuthAuthInfo = {
instructions?: string;
};
export type OAuthSelectOption = {
id: string;
label: string;
};
export type OAuthSelectPrompt = {
message: string;
options: OAuthSelectOption[];
};
export interface OAuthLoginCallbacks {
onAuth: (info: OAuthAuthInfo) => void;
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
onProgress?: (message: string) => void;
onManualCodeInput?: () => Promise<string>;
/** Show an interactive selector and return the selected option id, or undefined on cancel. */
onSelect?: (prompt: OAuthSelectPrompt) => Promise<string | undefined>;
signal?: AbortSignal;
}