Merge remote-tracking branch 'upstream/main' into feat/image-outputs
This commit is contained in:
@@ -2,13 +2,36 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Switched the built-in `xiaomi` provider endpoint from Token Plan AMS (`https://token-plan-ams.xiaomimimo.com/anthropic`) to API billing (`https://api.xiaomimimo.com/anthropic`). `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users still on Token Plan must move to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var.
|
||||
## [0.73.1] - 2026-05-07
|
||||
|
||||
### Added
|
||||
|
||||
- Added Xiaomi MiMo Token Plan regional providers with per-region env vars: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), and `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`).
|
||||
- Added OAuth login flow metadata so clients can present interactive provider choices during login ([#4190](https://github.com/earendil-works/pi-mono/pull/4190) by [@mitsuhiko](https://github.com/mitsuhiko)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenAI Responses reasoning text streaming for LM Studio and other compatible providers that emit `response.reasoning_text.delta` events ([#4191](https://github.com/badlogic/pi-mono/pull/4191) by [@yaanfpv](https://github.com/yaanfpv)).
|
||||
- Fixed OpenAI Codex OAuth refresh failures writing directly to stderr while the TUI is active ([#4141](https://github.com/badlogic/pi-mono/issues/4141)).
|
||||
- Fixed OpenAI-compatible chat completion streams that interleave content and tool-call deltas in the same choice.
|
||||
- Fixed the Kimi K2 P6 model alias to normalize to `kimi-for-coding` ([#4218](https://github.com/earendil-works/pi-mono/issues/4218)).
|
||||
- Fixed OpenAI Codex Responses requests to send a non-empty system prompt ([#4184](https://github.com/earendil-works/pi-mono/issues/4184)).
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Switched the built-in `xiaomi` provider endpoint from Token Plan AMS (`https://token-plan-ams.xiaomimimo.com/anthropic`) to API billing (`https://api.xiaomimimo.com/anthropic`). `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users still on Token Plan must move to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)).
|
||||
|
||||
### Added
|
||||
|
||||
- Added Xiaomi MiMo Token Plan regional providers with per-region env vars: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), and `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`) ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)).
|
||||
- Added `registerSessionResourceCleanup()` and `cleanupSessionResources()` so providers can register cleanup hooks for session-scoped resources.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed generated OpenAI-compatible model metadata for Qwen 3.5/3.6 and MiniMax M2.7 to match models.dev and OpenCode Go ([#4110](https://github.com/badlogic/pi-mono/pull/4110) by [@jsynowiec](https://github.com/jsynowiec)).
|
||||
- Fixed Bedrock Converse thinking effort mapping to preserve native `xhigh` for Claude Opus 4.7.
|
||||
- Fixed OpenAI Codex Responses WebSocket transport to fall back to SSE when setup fails before streaming starts, and attach transport diagnostics to the assistant message ([#4133](https://github.com/badlogic/pi-mono/issues/4133)).
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
|
||||
@@ -386,6 +386,8 @@ All streaming events emitted during assistant message generation:
|
||||
| `done` | Stream complete | `reason`: Stop reason ("stop", "length", "toolUse"), `message`: Final assistant message |
|
||||
| `error` | Error occurred | `reason`: Error type ("error" or "aborted"), `error`: AssistantMessage with partial content |
|
||||
|
||||
Streaming events for different content blocks are not guaranteed to be contiguous. Providers may emit deltas for text, thinking, and tool calls in the same upstream chunk, and pi may surface corresponding events interleaved, for example `text_start`, `text_delta`, `toolcall_start`, `text_delta`, `toolcall_delta`. Consumers must use `contentIndex` to associate each delta/end event with its block and must not assume that a block's `*_start`/`*_delta`/`*_end` sequence is uninterrupted by events for other blocks.
|
||||
|
||||
## Image Input
|
||||
|
||||
Models with vision capabilities can process images. You can check if a model supports images via the `input` property. If you pass images to a non-vision model, they are silently ignored.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-ai",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -857,15 +857,17 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
||||
const kimiModels = data["kimi-for-coding"].models as Record<string, ModelsDevModel>;
|
||||
const hasCanonicalModel = Object.prototype.hasOwnProperty.call(kimiModels, "kimi-for-coding");
|
||||
|
||||
const kimiAliases = new Set(["k2p5", "k2p6"]);
|
||||
|
||||
for (const [modelId, model] of Object.entries(kimiModels)) {
|
||||
const m = model as ModelsDevModel;
|
||||
if (m.tool_call !== true) continue;
|
||||
// models.dev still exposes deprecated "k2p5" in some snapshots.
|
||||
// Normalize to the canonical model id and drop duplicates when canonical exists.
|
||||
if (modelId === "k2p5" && hasCanonicalModel) continue;
|
||||
// models.dev may expose versioned aliases (e.g. k2p5/k2p6).
|
||||
// Normalize aliases to the canonical model id and drop duplicates when canonical exists.
|
||||
if (kimiAliases.has(modelId) && hasCanonicalModel) continue;
|
||||
|
||||
const normalizedId = modelId === "k2p5" ? "kimi-for-coding" : modelId;
|
||||
const normalizedName = modelId === "k2p5" ? "Kimi For Coding" : m.name || normalizedId;
|
||||
const normalizedId = kimiAliases.has(modelId) ? "kimi-for-coding" : modelId;
|
||||
const normalizedName = kimiAliases.has(modelId) ? "Kimi For Coding" : m.name || normalizedId;
|
||||
|
||||
models.push({
|
||||
id: normalizedId,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
32
packages/ai/test/openai-codex-oauth.test.ts
Normal file
32
packages/ai/test/openai-codex-oauth.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { refreshOpenAICodexToken } from "../src/utils/oauth/openai-codex.js";
|
||||
|
||||
describe("OpenAI Codex OAuth", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("does not write token refresh failures to stderr", async () => {
|
||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (): Promise<Response> => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Could not validate your token. Please try signing in again.",
|
||||
type: "invalid_request_error",
|
||||
},
|
||||
}),
|
||||
{ status: 401, statusText: "Unauthorized", headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(refreshOpenAICodexToken("invalid-refresh-token")).rejects.toThrow(
|
||||
/OpenAI Codex token refresh failed \(401\).*Could not validate your token/,
|
||||
);
|
||||
expect(consoleError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -552,6 +552,238 @@ describe("openai-completions tool_choice", () => {
|
||||
expect(toolCall).not.toHaveProperty("partialArgs");
|
||||
});
|
||||
|
||||
it("accumulates mixed content, reasoning, and parallel tool call deltas independently", async () => {
|
||||
mockState.chunks = [
|
||||
{
|
||||
id: "chatcmpl-mixed-deltas",
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content: "answer 1",
|
||||
reasoning_content: "think 1",
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: "tc_read_initial",
|
||||
type: "function",
|
||||
function: { name: "read", arguments: '{"path":"README' },
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
id: "tc_grep_initial",
|
||||
type: "function",
|
||||
function: { name: "grep", arguments: '{"pattern":"TODO' },
|
||||
},
|
||||
{
|
||||
id: "tc_list_no_index",
|
||||
type: "function",
|
||||
function: { name: "list", arguments: '{"path":"packages' },
|
||||
},
|
||||
{
|
||||
id: "tc_write_no_index",
|
||||
type: "function",
|
||||
function: { name: "write", arguments: '{"path":"out' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chatcmpl-mixed-deltas",
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content: " answer 2",
|
||||
tool_calls: [
|
||||
{
|
||||
index: 1,
|
||||
id: "tc_grep_changed",
|
||||
type: "function",
|
||||
function: { arguments: '","path":"src' },
|
||||
},
|
||||
{
|
||||
id: "tc_write_no_index",
|
||||
type: "function",
|
||||
function: { arguments: '.txt","content":"ok"}' },
|
||||
},
|
||||
{
|
||||
id: "tc_list_no_index",
|
||||
type: "function",
|
||||
function: { arguments: '/ai"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chatcmpl-mixed-deltas",
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content: "\n",
|
||||
reasoning_content: " think 2",
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: "tc_read_changed",
|
||||
type: "function",
|
||||
function: { arguments: '.md"}' },
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
type: "function",
|
||||
function: { arguments: '"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: "tool_calls",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 8,
|
||||
prompt_tokens_details: { cached_tokens: 0 },
|
||||
completion_tokens_details: { reasoning_tokens: 2 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!;
|
||||
const model = { ...baseModel, api: "openai-completions" } as const;
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: "read",
|
||||
description: "Read a file",
|
||||
parameters: Type.Object({ path: Type.String() }),
|
||||
},
|
||||
{
|
||||
name: "grep",
|
||||
description: "Search a file",
|
||||
parameters: Type.Object({ pattern: Type.String(), path: Type.String() }),
|
||||
},
|
||||
{
|
||||
name: "list",
|
||||
description: "List a directory",
|
||||
parameters: Type.Object({ path: Type.String() }),
|
||||
},
|
||||
{
|
||||
name: "write",
|
||||
description: "Write a file",
|
||||
parameters: Type.Object({ path: Type.String(), content: Type.String() }),
|
||||
},
|
||||
];
|
||||
const s = streamSimple(
|
||||
model,
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Think, answer, and use tools.",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
tools,
|
||||
},
|
||||
{ apiKey: "test" },
|
||||
);
|
||||
|
||||
const eventTypes: string[] = [];
|
||||
const toolEventsByContentIndex = new Map<number, string[]>();
|
||||
for await (const event of s) {
|
||||
eventTypes.push(event.type);
|
||||
if (event.type === "toolcall_start" || event.type === "toolcall_delta" || event.type === "toolcall_end") {
|
||||
const events = toolEventsByContentIndex.get(event.contentIndex) ?? [];
|
||||
events.push(event.type);
|
||||
toolEventsByContentIndex.set(event.contentIndex, events);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await s.result();
|
||||
expect(response.stopReason).toBe("toolUse");
|
||||
expect(eventTypes.filter((type) => type === "text_start")).toHaveLength(1);
|
||||
expect(eventTypes.filter((type) => type === "text_delta")).toHaveLength(3);
|
||||
expect(eventTypes.filter((type) => type === "text_end")).toHaveLength(1);
|
||||
expect(eventTypes.filter((type) => type === "thinking_start")).toHaveLength(1);
|
||||
expect(eventTypes.filter((type) => type === "thinking_delta")).toHaveLength(2);
|
||||
expect(eventTypes.filter((type) => type === "thinking_end")).toHaveLength(1);
|
||||
expect(eventTypes.filter((type) => type === "toolcall_start")).toHaveLength(4);
|
||||
expect(eventTypes.filter((type) => type === "toolcall_delta")).toHaveLength(9);
|
||||
expect(eventTypes.filter((type) => type === "toolcall_end")).toHaveLength(4);
|
||||
expect(toolEventsByContentIndex.get(2)).toEqual([
|
||||
"toolcall_start",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_end",
|
||||
]);
|
||||
expect(toolEventsByContentIndex.get(3)).toEqual([
|
||||
"toolcall_start",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_end",
|
||||
]);
|
||||
expect(toolEventsByContentIndex.get(4)).toEqual([
|
||||
"toolcall_start",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_end",
|
||||
]);
|
||||
expect(toolEventsByContentIndex.get(5)).toEqual([
|
||||
"toolcall_start",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_end",
|
||||
]);
|
||||
|
||||
expect(response.content).toHaveLength(6);
|
||||
expect(response.content[0]).toEqual({ type: "text", text: "answer 1 answer 2\n" });
|
||||
expect(response.content[1]).toEqual({
|
||||
type: "thinking",
|
||||
thinking: "think 1 think 2",
|
||||
thinkingSignature: "reasoning_content",
|
||||
});
|
||||
const readCall = response.content[2];
|
||||
const grepCall = response.content[3];
|
||||
const listCall = response.content[4];
|
||||
const writeCall = response.content[5];
|
||||
expect(readCall.type).toBe("toolCall");
|
||||
expect(grepCall.type).toBe("toolCall");
|
||||
expect(listCall.type).toBe("toolCall");
|
||||
expect(writeCall.type).toBe("toolCall");
|
||||
if (
|
||||
readCall.type !== "toolCall" ||
|
||||
grepCall.type !== "toolCall" ||
|
||||
listCall.type !== "toolCall" ||
|
||||
writeCall.type !== "toolCall"
|
||||
) {
|
||||
throw new Error("Expected toolCall content");
|
||||
}
|
||||
expect(readCall.id).toBe("tc_read_initial");
|
||||
expect(readCall.name).toBe("read");
|
||||
expect(readCall.arguments).toEqual({ path: "README.md" });
|
||||
expect(readCall).not.toHaveProperty("streamIndex");
|
||||
expect(readCall).not.toHaveProperty("partialArgs");
|
||||
expect(grepCall.id).toBe("tc_grep_initial");
|
||||
expect(grepCall.name).toBe("grep");
|
||||
expect(grepCall.arguments).toEqual({ pattern: "TODO", path: "src" });
|
||||
expect(grepCall).not.toHaveProperty("streamIndex");
|
||||
expect(grepCall).not.toHaveProperty("partialArgs");
|
||||
expect(listCall.id).toBe("tc_list_no_index");
|
||||
expect(listCall.name).toBe("list");
|
||||
expect(listCall.arguments).toEqual({ path: "packages/ai" });
|
||||
expect(listCall).not.toHaveProperty("streamIndex");
|
||||
expect(listCall).not.toHaveProperty("partialArgs");
|
||||
expect(writeCall.id).toBe("tc_write_no_index");
|
||||
expect(writeCall.name).toBe("write");
|
||||
expect(writeCall.arguments).toEqual({ path: "out.txt", content: "ok" });
|
||||
expect(writeCall).not.toHaveProperty("streamIndex");
|
||||
expect(writeCall).not.toHaveProperty("partialArgs");
|
||||
});
|
||||
|
||||
it("does not double-count reasoning tokens in completion usage", async () => {
|
||||
mockState.chunks = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user