diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index cf50ffdea..43cb73c57 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -11,6 +11,8 @@ body: Keep this short. If it doesn't fit on one screen, it's too long. Write in your own voice. + **Important:** before reporting an issue in core, please validate first with `pi -ne` that this is not caused by an extension you loaded. + - type: textarea id: description attributes: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24f0f825e..d2c79b9fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ If you use an agent, run it from the `pi-mono` root directory so it picks up `AG All issues and PRs from new contributors are auto-closed by default. -Issues submitted Friday through Sunday are not reviewed. If something is urgent, ask on Discord: https://discord.com/invite/3cU7Bz4UPx +Issues submitted Friday through Sunday are not guaranteed to be reviewed. If something is urgent, ask on Discord: https://discord.com/invite/3cU7Bz4UPx Maintainers review auto-closed issues daily and reopen worthwhile ones. Issues that do not meet the quality bar below will not be reopened or receive a reply. @@ -76,9 +76,9 @@ Ask on [Discord](https://discord.com/invite/nKXTsAcmbT). pi receives more issues than the maintainers can responsibly review in real time. Many reports do not meet the quality bar in this guide or do not follow CONTRIBUTING.md. Some are slung at the repository mindlessly via an agent instead of being reviewed and shaped by the person submitting them. Auto-closing creates a buffer so maintainers can review the tracker on their own schedule and reopen the issues that meet the quality bar. -### Why are weekend issues not reviewed? +### Why are weekend issues lower priority? -Maintainers need uninterrupted time away from the issue tracker. Issues submitted Friday through Sunday are auto-closed and are not part of the Monday review queue. If a problem is urgent, ask on Discord and include the short version, a repro, and the relevant logs. +We triage the tracker during working hours. That means more issues can accumulate over the weekend. Anything submitted Friday through Sunday may be missed or given lower priority in the Monday review queue. If a problem is urgent, ask on Discord and include the short version, a repro, and the relevant logs. ### Why do some issues get no reply? diff --git a/package-lock.json b/package-lock.json index b32c59fed..4ee5233f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2337,6 +2337,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript/native-preview": { "version": "7.0.0-dev.20260120.1", "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260120.1.tgz", @@ -4700,7 +4707,6 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6170,10 +6176,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.1", + "version": "0.79.3", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.1", + "@earendil-works/pi-ai": "^0.79.3", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -6207,7 +6213,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.1", + "version": "0.79.3", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -6252,12 +6258,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.1", + "version": "0.79.3", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.1", - "@earendil-works/pi-ai": "^0.79.1", - "@earendil-works/pi-tui": "^0.79.1", + "@earendil-works/pi-agent-core": "^0.79.3", + "@earendil-works/pi-ai": "^0.79.3", + "@earendil-works/pi-tui": "^0.79.3", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -6269,6 +6275,7 @@ "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", + "semver": "7.8.0", "typebox": "1.1.38", "undici": "8.3.0", "yaml": "2.9.0" @@ -6283,6 +6290,7 @@ "@types/ms": "2.1.0", "@types/node": "24.12.4", "@types/proper-lockfile": "4.1.4", + "@types/semver": "7.7.1", "shx": "0.4.0", "typescript": "5.9.3", "vitest": "3.2.4" @@ -6296,32 +6304,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.1", + "version": "0.79.3", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.1" + "version": "0.79.3" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.1", + "version": "0.79.3", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.1", + "version": "1.9.3", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.1", + "version": "0.79.3", "dependencies": { "ms": "2.1.3" }, @@ -6357,7 +6365,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.1", + "version": "0.79.3", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 287f07c30..f176ad06b 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## [0.79.3] - 2026-06-13 + +## [0.79.2] - 2026-06-12 + ### Fixed - Fixed late tool progress callbacks after tool settlement to be ignored instead of emitting stale `tool_execution_update` events ([#5573](https://github.com/earendil-works/pi/issues/5573)). diff --git a/packages/agent/package.json b/packages/agent/package.json index afd0368cb..b8a17c299 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.1", + "version": "0.79.3", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.1", + "@earendil-works/pi-ai": "^0.79.3", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 93fc9a802..71da18c39 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,11 +2,28 @@ ## [Unreleased] -- When Amazon Bedrock rejects an unsupported data retention mode, the error now links the AWS data retention documentation ([#5561](https://github.com/earendil-works/pi/pull/5561) by [@unexge](https://github.com/unexge)). +### Fixed + +- Fixed OpenCode/OpenCode Go completion models that reject `prompt_cache_retention` to omit long-retention cache fields when `cacheRetention` is `long` ([#5702](https://github.com/earendil-works/pi/issues/5702)). + +## [0.79.3] - 2026-06-13 ### Fixed +- Restored OpenAI GPT-5.4/GPT-5.5 and OpenAI Codex GPT-5.4/GPT-5.4 mini/GPT-5.5 context window metadata to the observed 272k-token Codex backend limit, avoiding a billing hazard from sending prompts above Codex's accepted limit (reported by [@trethore](https://github.com/trethore)). + +## [0.79.2] - 2026-06-12 + +### Added + +- Added AWS data retention documentation links to Amazon Bedrock unsupported data retention mode validation errors ([#5561](https://github.com/earendil-works/pi/pull/5561) by [@unexge](https://github.com/unexge)). + +### Fixed + +- Fixed OpenAI-compatible context overflow detection for parenthesized `maximum context length (N)` errors ([#5677](https://github.com/earendil-works/pi/issues/5677)). +- Fixed OpenAI GPT-5.4/GPT-5.5 and OpenAI Codex GPT-5.4/GPT-5.4 mini/GPT-5.5 context window metadata to match current OpenAI limits ([#5644](https://github.com/earendil-works/pi/issues/5644)). - Increased the OpenAI Codex Responses SSE response-header timeout to 20 seconds to reduce false-positive stalls while retaining the bounded wait introduced for zero-event hangs ([#4945](https://github.com/earendil-works/pi/issues/4945)). +- Fixed Anthropic refusal stops to preserve provider `stop_details` explanations in error messages ([#5666](https://github.com/earendil-works/pi/pull/5666) by [@rwachtler](https://github.com/rwachtler)). - Fixed Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). ## [0.79.1] - 2026-06-09 diff --git a/packages/ai/package.json b/packages/ai/package.json index 9a54de2fb..806d19cac 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.1", + "version": "0.79.3", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 5e5fff94d..63c54a56c 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -187,6 +187,15 @@ const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([ "gpt-5.5", ]); +const OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS = new Set([ + "opencode:deepseek-v4-flash", + "opencode:deepseek-v4-pro", + "opencode:kimi-k2.5", + "opencode:kimi-k2.6", + "opencode:minimax-m2.7", + "opencode-go:kimi-k2.6", +]); + function mergeThinkingLevelMap(model: Model, map: NonNullable["thinkingLevelMap"]>): void { model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map }; } @@ -372,6 +381,10 @@ function normalizeNvidiaModelId(modelId: string): string { return modelId.toLowerCase().replaceAll("_", "."); } +function roundCost(value: number): number { + return Number(value.toFixed(6)); +} + async function fetchNvidiaNimModelIds(): Promise> { try { console.log("Fetching models from NVIDIA NIM API..."); @@ -417,10 +430,10 @@ async function fetchOpenRouterModels(): Promise[]> { } // Convert pricing from $/token to $/million tokens - const inputCost = parseFloat(model.pricing?.prompt || "0") * 1_000_000; - const outputCost = parseFloat(model.pricing?.completion || "0") * 1_000_000; - const cacheReadCost = parseFloat(model.pricing?.input_cache_read || "0") * 1_000_000; - const cacheWriteCost = parseFloat(model.pricing?.input_cache_write || "0") * 1_000_000; + const inputCost = roundCost(parseFloat(model.pricing?.prompt || "0") * 1_000_000); + const outputCost = roundCost(parseFloat(model.pricing?.completion || "0") * 1_000_000); + const cacheReadCost = roundCost(parseFloat(model.pricing?.input_cache_read || "0") * 1_000_000); + const cacheWriteCost = roundCost(parseFloat(model.pricing?.input_cache_write || "0") * 1_000_000); const normalizedModel: Model = { id: modelKey, @@ -476,10 +489,10 @@ async function fetchAiGatewayModels(): Promise[]> { input.push("image"); } - const inputCost = toNumber(model.pricing?.input) * 1_000_000; - const outputCost = toNumber(model.pricing?.output) * 1_000_000; - const cacheReadCost = toNumber(model.pricing?.input_cache_read) * 1_000_000; - const cacheWriteCost = toNumber(model.pricing?.input_cache_write) * 1_000_000; + const inputCost = roundCost(toNumber(model.pricing?.input) * 1_000_000); + const outputCost = roundCost(toNumber(model.pricing?.output) * 1_000_000); + const cacheReadCost = roundCost(toNumber(model.pricing?.input_cache_read) * 1_000_000); + const cacheWriteCost = roundCost(toNumber(model.pricing?.input_cache_write) * 1_000_000); models.push({ id: model.id, @@ -1066,6 +1079,13 @@ async function loadModelsDevData(): Promise[]> { if (api === "openai-completions") { compat = { ...(compat ?? {}), maxTokensField: "max_tokens" }; + if ( + OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS.has( + `${variant.provider}:${modelId}`, + ) + ) { + compat = { ...compat, supportsLongCacheRetention: false }; + } } models.push({ diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 57da60bca..35295e5dd 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -357,7 +357,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 11, @@ -497,7 +497,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -1389,7 +1389,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -1878,7 +1878,7 @@ export const MODELS = { baseUrl: "https://api.anthropic.com", compat: {"forceAdaptiveThinking":true}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -2919,30 +2919,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.25, - output: 0.69, + input: 0.35, + output: 0.75, cacheRead: 0, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "llama3.1-8b": { - id: "llama3.1-8b", - name: "Llama 3.1 8B", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 8000, + maxTokens: 40960, } satisfies Model<"openai-completions">, "zai-glm-4.7": { id: "zai-glm-4.7", @@ -2950,7 +2933,7 @@ export const MODELS = { api: "openai-completions", provider: "cerebras", baseUrl: "https://api.cerebras.ai/v1", - reasoning: false, + reasoning: true, input: ["text"], cost: { input: 2.25, @@ -2959,7 +2942,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 40000, + maxTokens: 40960, } satisfies Model<"openai-completions">, }, "cloudflare-ai-gateway": { @@ -3073,7 +3056,7 @@ export const MODELS = { baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", compat: {"forceAdaptiveThinking":true}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -3722,6 +3705,24 @@ export const MODELS = { contextWindow: 262144, maxTokens: 256000, } satisfies Model<"openai-completions">, + "@cf/moonshotai/kimi-k2.7-code": { + id: "@cf/moonshotai/kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, "@cf/nvidia/nemotron-3-120b-a12b": { id: "@cf/nvidia/nemotron-3-120b-a12b", name: "Nemotron 3 Super 120B", @@ -3944,24 +3945,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 32768, } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/kimi-k2p5": { - id: "accounts/fireworks/models/kimi-k2p5", - name: "Kimi K2.5", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/kimi-k2p6": { id: "accounts/fireworks/models/kimi-k2p6", name: "Kimi K2.6", @@ -3980,23 +3963,23 @@ export const MODELS = { contextWindow: 262000, maxTokens: 262000, } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/minimax-m2p5": { - id: "accounts/fireworks/models/minimax-m2p5", - name: "MiniMax-M2.5", + "accounts/fireworks/models/kimi-k2p7-code": { + id: "accounts/fireworks/models/kimi-k2p7-code", + name: "Kimi K2.7 Code", api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, - input: ["text"], + input: ["text", "image"], cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, + input: 0.95, + output: 4, + cacheRead: 0.19, cacheWrite: 0, }, - contextWindow: 196608, - maxTokens: 196608, + contextWindow: 262000, + maxTokens: 262000, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/minimax-m2p7": { id: "accounts/fireworks/models/minimax-m2p7", @@ -4016,9 +3999,27 @@ export const MODELS = { contextWindow: 196608, maxTokens: 196608, } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/qwen3p6-plus": { - id: "accounts/fireworks/models/qwen3p6-plus", - name: "Qwen 3.6 Plus", + "accounts/fireworks/models/minimax-m3": { + id: "accounts/fireworks/models/minimax-m3", + name: "MiniMax-M3", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 512000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/qwen3p7-plus": { + id: "accounts/fireworks/models/qwen3p7-plus", + name: "Qwen 3.7 Plus", api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", @@ -4026,9 +4027,9 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.5, - output: 3, - cacheRead: 0.1, + input: 0.4, + output: 1.6, + cacheRead: 0.08, cacheWrite: 0, }, contextWindow: 262144, @@ -4088,8 +4089,45 @@ export const MODELS = { contextWindow: 262000, maxTokens: 262000, } satisfies Model<"anthropic-messages">, + "accounts/fireworks/routers/kimi-k2p7-code-fast": { + id: "accounts/fireworks/routers/kimi-k2p7-code-fast", + name: "Kimi K2.7 Code Fast", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.38, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, }, "github-copilot": { + "claude-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, "claude-haiku-4.5": { id: "claude-haiku-4.5", name: "Claude Haiku 4.5 (latest)", @@ -5022,77 +5060,9 @@ export const MODELS = { } satisfies Model<"google-vertex">, }, "groq": { - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.75, - output: 0.99, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "gemma2-9b-it": { - id: "gemma2-9b-it", - name: "Gemma 2 9B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "groq/compound": { - id: "groq/compound", - name: "Compound", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "groq/compound-mini": { - id: "groq/compound-mini", - name: "Compound Mini", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, "llama-3.1-8b-instant": { id: "llama-3.1-8b-instant", - name: "Llama 3.1 8B Instant", + name: "Llama 3.1 8B", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5109,7 +5079,7 @@ export const MODELS = { } satisfies Model<"openai-completions">, "llama-3.3-70b-versatile": { id: "llama-3.3-70b-versatile", - name: "Llama 3.3 70B Versatile", + name: "Llama 3.3 70B", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5124,60 +5094,9 @@ export const MODELS = { contextWindow: 131072, maxTokens: 32768, } satisfies Model<"openai-completions">, - "llama3-70b-8192": { - id: "llama3-70b-8192", - name: "Llama 3 70B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.59, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "llama3-8b-8192": { - id: "llama3-8b-8192", - name: "Llama 3 8B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.05, - output: 0.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-maverick-17b-128e-instruct": { - id: "meta-llama/llama-4-maverick-17b-128e-instruct", - name: "Llama 4 Maverick 17B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, "meta-llama/llama-4-scout-17b-16e-instruct": { id: "meta-llama/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B", + name: "Llama 4 Scout 17B 16E", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5192,57 +5111,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 8192, } satisfies Model<"openai-completions">, - "mistral-saba-24b": { - id: "mistral-saba-24b", - name: "Mistral Saba 24B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.79, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-instruct-0905": { - id: "moonshotai/kimi-k2-instruct-0905", - name: "Kimi K2 Instruct 0905", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "openai/gpt-oss-120b": { id: "openai/gpt-oss-120b", name: "GPT OSS 120B", @@ -5294,26 +5162,9 @@ export const MODELS = { contextWindow: 131072, maxTokens: 65536, } satisfies Model<"openai-completions">, - "qwen-qwq-32b": { - id: "qwen-qwq-32b", - name: "Qwen QwQ 32B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.29, - output: 0.39, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "qwen/qwen3-32b": { id: "qwen/qwen3-32b", - name: "Qwen3 32B", + name: "Qwen3-32B", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5729,6 +5580,24 @@ export const MODELS = { } satisfies Model<"openai-completions">, }, "kimi-coding": { + "k2p7": { + id: "k2p7", + name: "Kimi K2.7 Code", + 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", @@ -6511,6 +6380,24 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, }, "moonshotai-cn": { "kimi-k2-0711-preview": { @@ -6763,8 +6650,8 @@ export const MODELS = { baseUrl: "https://integrate.api.nvidia.com/v1", headers: {"NVCF-POLL-SECONDS":"3600"}, compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], + reasoning: true, + input: ["text", "image"], cost: { input: 0, output: 0, @@ -6793,44 +6680,6 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, - "nvidia/llama-3.3-nemotron-super-49b-v1": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1", - name: "Llama 3.3 Nemotron Super 49B v1", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - name: "Llama 3.3 Nemotron Super 49B v1.5", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "nvidia/nemotron-3-nano-30b-a3b": { id: "nvidia/nemotron-3-nano-30b-a3b", name: "nemotron-3-nano-30b-a3b", @@ -6926,6 +6775,25 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131072, } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT-OSS-120B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"openai-completions">, "openai/gpt-oss-20b": { id: "openai/gpt-oss-20b", name: "GPT OSS 20B", @@ -6945,25 +6813,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 32768, } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-480b-a35b-instruct": { - id: "qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 66536, - } satisfies Model<"openai-completions">, "qwen/qwen3.5-122b-a10b": { id: "qwen/qwen3.5-122b-a10b", name: "Qwen3.5 122B-A10B", @@ -7874,25 +7723,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 32000, } satisfies Model<"openai-completions">, - "claude-fable-5": { - id: "claude-fable-5", - name: "Claude Fable 5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, "claude-haiku-4-5": { id: "claude-haiku-4-5", name: "Claude Haiku 4.5", @@ -8059,14 +7889,14 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], cost: { input: 0.14, output: 0.28, - cacheRead: 0.03, + cacheRead: 0.028, cacheWrite: 0, }, contextWindow: 1000000, @@ -8091,6 +7921,25 @@ export const MODELS = { contextWindow: 200000, maxTokens: 128000, } satisfies Model<"openai-completions">, + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 1.74, + output: 3.84, + cacheRead: 0.145, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, "gemini-3-flash": { id: "gemini-3-flash", name: "Gemini 3 Flash", @@ -8494,7 +8343,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -8512,7 +8361,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -8566,7 +8415,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -8724,31 +8573,13 @@ export const MODELS = { contextWindow: 202752, maxTokens: 32768, } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, "kimi-k2.6": { id: "kimi-k2.6", name: "Kimi K2.6", api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], @@ -8761,6 +8592,24 @@ export const MODELS = { contextWindow: 262144, maxTokens: 65536, } satisfies Model<"openai-completions">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, "mimo-v2.5": { id: "mimo-v2.5", name: "MiMo V2.5", @@ -8797,23 +8646,6 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 128000, } satisfies Model<"openai-completions">, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, "minimax-m2.7": { id: "minimax-m2.7", name: "MiniMax M2.7", @@ -8834,16 +8666,16 @@ export const MODELS = { } satisfies Model<"openai-completions">, "minimax-m3": { id: "minimax-m3", - name: "MiniMax M3", + name: "MiniMax M3 (3x usage)", api: "anthropic-messages", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go", reasoning: true, input: ["text", "image"], cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, + input: 0.1, + output: 0.4, + cacheRead: 0.02, cacheWrite: 0, }, contextWindow: 512000, @@ -8997,8 +8829,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.7999999999999999, - output: 3.1999999999999997, + input: 0.8, + output: 3.2, cacheRead: 0, cacheWrite: 0, }, @@ -9031,7 +8863,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.7999999999999999, + input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1, @@ -9067,7 +8899,7 @@ export const MODELS = { cost: { input: 1, output: 5, - cacheRead: 0.09999999999999999, + cacheRead: 0.1, cacheWrite: 1.25, }, contextWindow: 200000, @@ -9411,8 +9243,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, + input: 0.1, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -9462,8 +9294,8 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.20020000000000002, - output: 0.8000999999999999, + input: 0.2002, + output: 0.8001, cacheRead: 0, cacheWrite: 0, }, @@ -9479,7 +9311,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.77, cacheRead: 0.135, cacheWrite: 0, @@ -9497,7 +9329,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.21, - output: 0.7899999999999999, + output: 0.79, cacheRead: 0.13, cacheWrite: 0, }, @@ -9531,7 +9363,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.5, - output: 2.1500000000000004, + output: 2.15, cacheRead: 0.35, cacheWrite: 0, }, @@ -9600,13 +9432,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, input: ["text"], cost: { - input: 0.0983, - output: 0.1966, - cacheRead: 0.019700000000000002, + input: 0.098, + output: 0.196, + cacheRead: 0.02, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 131072, + maxTokens: 4096, } satisfies Model<"openai-completions">, "deepseek/deepseek-v4-pro": { id: "deepseek/deepseek-v4-pro", @@ -9656,7 +9488,7 @@ export const MODELS = { input: 0.3, output: 2.5, cacheRead: 0.03, - cacheWrite: 0.08333333333333334, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65535, @@ -9670,10 +9502,10 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, + input: 0.1, + output: 0.4, cacheRead: 0.01, - cacheWrite: 0.08333333333333334, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65535, @@ -9687,10 +9519,10 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, + input: 0.1, + output: 0.4, cacheRead: 0.01, - cacheWrite: 0.08333333333333334, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65535, @@ -9757,8 +9589,8 @@ export const MODELS = { cost: { input: 0.5, output: 3, - cacheRead: 0.049999999999999996, - cacheWrite: 0.08333333333333334, + cacheRead: 0.05, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65536, @@ -9774,8 +9606,8 @@ export const MODELS = { cost: { input: 0.25, output: 1.5, - cacheRead: 0.024999999999999998, - cacheWrite: 0.08333333333333334, + cacheRead: 0.025, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65536, @@ -9791,8 +9623,8 @@ export const MODELS = { cost: { input: 0.25, output: 1.5, - cacheRead: 0.024999999999999998, - cacheWrite: 0.08333333333333334, + cacheRead: 0.025, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65536, @@ -9808,7 +9640,7 @@ export const MODELS = { cost: { input: 2, output: 12, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0.375, }, contextWindow: 1048576, @@ -9825,7 +9657,7 @@ export const MODELS = { cost: { input: 2, output: 12, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0.375, }, contextWindow: 1048756, @@ -9843,7 +9675,7 @@ export const MODELS = { input: 1.5, output: 9, cacheRead: 0.15, - cacheWrite: 0.08333333333333334, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65536, @@ -9857,7 +9689,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.049999999999999996, + input: 0.05, output: 0.15, cacheRead: 0, cacheWrite: 0, @@ -9926,12 +9758,12 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.12, - output: 0.36, + output: 0.35, cacheRead: 0.09, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 8192, + maxTokens: 262144, } satisfies Model<"openai-completions">, "google/gemma-4-31b-it:free": { id: "google/gemma-4-31b-it:free", @@ -9959,9 +9791,9 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.049999999999999996, - output: 0.09999999999999999, - cacheRead: 0.049999999999999996, + input: 0.05, + output: 0.1, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 131072, @@ -9979,7 +9811,7 @@ export const MODELS = { cost: { input: 0.25, output: 0.75, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 128000, @@ -10062,8 +9894,8 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, + input: 0.4, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -10096,7 +9928,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.32, cacheRead: 0, cacheWrite: 0, @@ -10147,7 +9979,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0, @@ -10164,7 +9996,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2.2, cacheRead: 0, cacheWrite: 0, @@ -10216,8 +10048,8 @@ export const MODELS = { input: ["text"], cost: { input: 0.15, - output: 0.8999999999999999, - cacheRead: 0.049999999999999996, + output: 0.9, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 204800, @@ -10232,9 +10064,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.27, - output: 1.08, - cacheRead: 0.054, + input: 0.25, + output: 1, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 204800, @@ -10267,7 +10099,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.3, - output: 0.8999999999999999, + output: 0.9, cacheRead: 0.03, cacheWrite: 0, }, @@ -10283,7 +10115,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2, cacheRead: 0.04, cacheWrite: 0, @@ -10300,8 +10132,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.19999999999999998, - output: 0.19999999999999998, + input: 0.2, + output: 0.2, cacheRead: 0.02, cacheWrite: 0, }, @@ -10317,8 +10149,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, + input: 0.1, + output: 0.1, cacheRead: 0.01, cacheWrite: 0, }, @@ -10353,7 +10185,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 128000, @@ -10370,7 +10202,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 131072, @@ -10387,7 +10219,7 @@ export const MODELS = { cost: { input: 0.5, output: 1.5, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, @@ -10402,7 +10234,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2, cacheRead: 0.04, cacheWrite: 0, @@ -10436,7 +10268,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2, cacheRead: 0.04, cacheWrite: 0, @@ -10470,7 +10302,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.6, cacheRead: 0.02, cacheWrite: 0, @@ -10505,7 +10337,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.075, - output: 0.19999999999999998, + output: 0.2, cacheRead: 0, cacheWrite: 0, }, @@ -10523,7 +10355,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 65536, @@ -10538,7 +10370,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0.01, cacheWrite: 0, @@ -10555,7 +10387,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.5700000000000001, + input: 0.57, output: 2.3, cacheRead: 0, cacheWrite: 0, @@ -10624,31 +10456,30 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.6799999999999999, + input: 0.68, output: 3.41, - cacheRead: 0.33999999999999997, + cacheRead: 0.34, cacheWrite: 0, }, contextWindow: 262144, maxTokens: 262142, } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.6:free": { - id: "moonshotai/kimi-k2.6:free", - name: "MoonshotAI: Kimi K2.6 (free)", + "moonshotai/kimi-k2.7-code": { + id: "moonshotai/kimi-k2.7-code", + name: "MoonshotAI: Kimi K2.7 Code", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true}, reasoning: true, input: ["text", "image"], cost: { - input: 0, - output: 0, - cacheRead: 0, + input: 0.75, + output: 3.5, + cacheRead: 0.16, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 4096, + maxTokens: 262144, } satisfies Model<"openai-completions">, "nex-agi/nex-n2-pro:free": { id: "nex-agi/nex-n2-pro:free", @@ -10676,8 +10507,8 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, + input: 0.4, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -10693,8 +10524,8 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.049999999999999996, - output: 0.19999999999999998, + input: 0.05, + output: 0.2, cacheRead: 0, cacheWrite: 0, }, @@ -10745,7 +10576,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.09, - output: 0.44999999999999996, + output: 0.45, cacheRead: 0, cacheWrite: 0, }, @@ -10820,23 +10651,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 128000, } satisfies Model<"openai-completions">, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "NVIDIA: Nemotron Nano 9B V2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.04, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "nvidia/nemotron-nano-9b-v2:free": { id: "nvidia/nemotron-nano-9b-v2:free", name: "NVIDIA: Nemotron Nano 9B V2 (free)", @@ -10982,9 +10796,9 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.09999999999999999, + input: 0.4, + output: 1.6, + cacheRead: 0.1, cacheWrite: 0, }, contextWindow: 1047576, @@ -10999,9 +10813,9 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.024999999999999998, + input: 0.1, + output: 0.4, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 1047576, @@ -11154,7 +10968,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, @@ -11169,8 +10983,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, + input: 0.05, + output: 0.4, cacheRead: 0.01, cacheWrite: 0, }, @@ -11273,7 +11087,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, @@ -11433,7 +11247,7 @@ export const MODELS = { thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite: 0, @@ -11878,7 +11692,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.1, cacheRead: 0, cacheWrite: 0, @@ -11896,7 +11710,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.36, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -11914,7 +11728,7 @@ export const MODELS = { cost: { input: 0.26, output: 0.78, - cacheRead: 0.052000000000000005, + cacheRead: 0.052, cacheWrite: 0.325, }, contextWindow: 1000000, @@ -11963,7 +11777,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.24, cacheRead: 0, cacheWrite: 0, @@ -11980,8 +11794,8 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.45499999999999996, - output: 1.8199999999999998, + input: 0.455, + output: 1.82, cacheRead: 0, cacheWrite: 0, }, @@ -11998,7 +11812,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.09, - output: 0.09999999999999999, + output: 0.1, cacheRead: 0, cacheWrite: 0, }, @@ -12014,9 +11828,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0.09999999999999999, + input: 0.1, + output: 0.1, + cacheRead: 0.1, cacheWrite: 0, }, contextWindow: 262144, @@ -12066,7 +11880,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.08, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0.08, cacheWrite: 0, }, @@ -12099,9 +11913,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, - cacheRead: 0.049999999999999996, + input: 0.05, + output: 0.4, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 131072, @@ -12117,7 +11931,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.22, - output: 1.7999999999999998, + output: 1.8, cacheRead: 0, cacheWrite: 0, }, @@ -12168,7 +11982,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.11, - output: 0.7999999999999999, + output: 0.8, cacheRead: 0.07, cacheWrite: 0, }, @@ -12303,7 +12117,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.88, cacheRead: 0.11, cacheWrite: 0, @@ -12371,8 +12185,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.10400000000000001, - output: 0.41600000000000004, + input: 0.104, + output: 0.416, cacheRead: 0, cacheWrite: 0, }, @@ -12458,7 +12272,7 @@ export const MODELS = { cost: { input: 0.14, output: 1, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, @@ -12490,7 +12304,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.15, cacheRead: 0, cacheWrite: 0, @@ -12542,7 +12356,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.3, - output: 1.7999999999999998, + output: 1.8, cacheRead: 0, cacheWrite: 0.375, }, @@ -12558,13 +12372,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.28900000000000003, - output: 2.4, + input: 0.2885, + output: 3.17, cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 131072, + maxTokens: 262140, } satisfies Model<"openai-completions">, "qwen/qwen3.6-35b-a3b": { id: "qwen/qwen3.6-35b-a3b", @@ -12575,13 +12389,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.14, + input: 0.15, output: 1, - cacheRead: 0, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262140, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.6-flash": { id: "qwen/qwen3.6-flash", @@ -12660,10 +12474,10 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.08, - cacheWrite: 0.5, + input: 0.32, + output: 1.28, + cacheRead: 0.064, + cacheWrite: 0.4, }, contextWindow: 1000000, maxTokens: 65536, @@ -12677,8 +12491,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, + input: 0.1, + output: 0.1, cacheRead: 0, cacheWrite: 0, }, @@ -12745,7 +12559,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.15, cacheRead: 0.04, cacheWrite: 0, @@ -12764,7 +12578,7 @@ export const MODELS = { cost: { input: 0.063, output: 0.21, - cacheRead: 0.020999999999999998, + cacheRead: 0.021, cacheWrite: 0, }, contextWindow: 262144, @@ -12779,7 +12593,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.16999999999999998, + input: 0.17, output: 0.43, cacheRead: 0, cacheWrite: 0, @@ -12796,8 +12610,8 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, + input: 0.4, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -12832,7 +12646,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -12849,7 +12663,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -12866,7 +12680,7 @@ export const MODELS = { cost: { input: 1, output: 2, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 256000, @@ -12881,7 +12695,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0.01, cacheWrite: 0, @@ -12923,23 +12737,6 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, - "z-ai/glm-4-32b": { - id: "z-ai/glm-4-32b", - name: "Z.ai: GLM 4 32B ", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "z-ai/glm-4.5": { id: "z-ai/glm-4.5", name: "Z.ai: GLM 4.5", @@ -12974,23 +12771,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131070, } satisfies Model<"openai-completions">, - "z-ai/glm-4.5-air:free": { - id: "z-ai/glm-4.5-air:free", - name: "Z.ai: GLM 4.5 Air (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: 96000, - } satisfies Model<"openai-completions">, "z-ai/glm-4.5v": { id: "z-ai/glm-4.5v", name: "Z.ai: GLM 4.5V", @@ -13001,7 +12781,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.6, - output: 1.7999999999999998, + output: 1.8, cacheRead: 0.11, cacheWrite: 0, }, @@ -13035,12 +12815,12 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.3, - output: 0.8999999999999999, - cacheRead: 0.049999999999999996, + output: 0.9, + cacheRead: 0.055, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 24000, + maxTokens: 32768, } satisfies Model<"openai-completions">, "z-ai/glm-4.7": { id: "z-ai/glm-4.7", @@ -13051,7 +12831,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 1.75, cacheRead: 0.08, cacheWrite: 0, @@ -13069,7 +12849,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.06, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0.01, cacheWrite: 0, }, @@ -13107,7 +12887,7 @@ export const MODELS = { cacheRead: 0.24, cacheWrite: 0, }, - contextWindow: 202752, + contextWindow: 262144, maxTokens: 131072, } satisfies Model<"openai-completions">, "z-ai/glm-5.1": { @@ -13127,23 +12907,6 @@ export const MODELS = { contextWindow: 202752, maxTokens: 4096, } satisfies Model<"openai-completions">, - "z-ai/glm-5v-turbo": { - id: "z-ai/glm-5v-turbo", - name: "Z.ai: GLM 5V Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", name: "Anthropic: Claude Fable Latest", @@ -13172,7 +12935,7 @@ export const MODELS = { cost: { input: 1, output: 5, - cacheRead: 0.09999999999999999, + cacheRead: 0.1, cacheWrite: 1.25, }, contextWindow: 200000, @@ -13224,7 +12987,7 @@ export const MODELS = { input: 1.5, output: 9, cacheRead: 0.15, - cacheWrite: 0.08333333333333334, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65536, @@ -13240,7 +13003,7 @@ export const MODELS = { cost: { input: 2, output: 12, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0.375, }, contextWindow: 1048576, @@ -13255,9 +13018,9 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.6799999999999999, + input: 0.68, output: 3.41, - cacheRead: 0.33999999999999997, + cacheRead: 0.34, cacheWrite: 0, }, contextWindow: 262144, @@ -13299,25 +13062,6 @@ export const MODELS = { } satisfies Model<"openai-completions">, }, "together": { - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "MiniMaxAI/MiniMax-M2.7": { id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax-M2.7", @@ -13337,28 +13081,28 @@ export const MODELS = { contextWindow: 202752, maxTokens: 131072, } satisfies Model<"openai-completions">, - "Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput", - name: "Qwen3 235B A22B Instruct 2507 FP8", + "MiniMaxAI/MiniMax-M3": { + id: "MiniMaxAI/MiniMax-M3", + name: "MiniMax-M3", api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], + input: ["text", "image"], cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, + input: 0.3, + output: 1.2, + cacheRead: 0.06, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 262144, + contextWindow: 524288, + maxTokens: 250000, } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", - name: "Qwen3 Coder 480B A35B Instruct", + "Qwen/Qwen2.5-7B-Instruct-Turbo": { + id: "Qwen/Qwen2.5-7B-Instruct-Turbo", + name: "Qwen 2.5 7B Instruct Turbo", api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", @@ -13366,27 +13110,26 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 2, - output: 2, + input: 0.3, + output: 0.3, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 262144, + contextWindow: 32768, + maxTokens: 32768, } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-Next-FP8": { - id: "Qwen/Qwen3-Coder-Next-FP8", - name: "Qwen3 Coder Next FP8", + "Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { + id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput", + name: "Qwen3 235B A22B Instruct 2507 FP8", api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, input: ["text"], cost: { - input: 0.5, - output: 1.2, + input: 0.2, + output: 0.6, cacheRead: 0, cacheWrite: 0, }, @@ -13412,6 +13155,25 @@ export const MODELS = { contextWindow: 262144, maxTokens: 130000, } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-9B": { + id: "Qwen/Qwen3.5-9B", + name: "Qwen3.5 9B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "Qwen/Qwen3.6-Plus": { id: "Qwen/Qwen3.6-Plus", name: "Qwen3.6 Plus", @@ -13437,9 +13199,8 @@ export const MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, input: ["text"], cost: { input: 2.5, @@ -13450,44 +13211,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 500000, } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "DeepSeek-V3", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 1.25, - output: 1.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V3-1": { - id: "deepseek-ai/DeepSeek-V3-1", - name: "DeepSeek V3.1", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.6, - output: 1.7, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "deepseek-ai/DeepSeek-V4-Pro": { id: "deepseek-ai/DeepSeek-V4-Pro", name: "DeepSeek V4 Pro", @@ -13499,8 +13222,8 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null}, input: ["text"], cost: { - input: 2.1, - output: 4.4, + input: 1.74, + output: 3.48, cacheRead: 0.2, cacheWrite: 0, }, @@ -13536,8 +13259,8 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], cost: { - input: 0.2, - output: 0.5, + input: 0.39, + output: 0.97, cacheRead: 0, cacheWrite: 0, }, @@ -13562,25 +13285,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131072, } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.5, - output: 2.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, "moonshotai/Kimi-K2.6": { id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6", @@ -13638,6 +13342,44 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131072, } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"openai"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null}, + input: ["text"], + cost: { + input: 0.05, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5": { + id: "zai-org/GLM-5", + name: "GLM-5", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "zai-org/GLM-5.1": { id: "zai-org/GLM-5.1", name: "GLM-5.1", @@ -13753,7 +13495,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 4, cacheRead: 0, cacheWrite: 0, @@ -13823,7 +13565,7 @@ export const MODELS = { cost: { input: 1, output: 5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -13923,7 +13665,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 4, cacheRead: 0, cacheWrite: 0, @@ -13940,8 +13682,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, + input: 0.1, + output: 0.4, cacheRead: 0.001, cacheWrite: 0.125, }, @@ -13957,7 +13699,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2.4, cacheRead: 0.04, cacheWrite: 0.5, @@ -13975,7 +13717,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.6, - output: 3.5999999999999996, + output: 3.6, cacheRead: 0, cacheWrite: 0, }, @@ -13993,7 +13735,7 @@ export const MODELS = { cost: { input: 0.5, output: 3, - cacheRead: 0.09999999999999999, + cacheRead: 0.1, cacheWrite: 0.625, }, contextWindow: 1000000, @@ -14025,8 +13767,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, + input: 0.4, + output: 1.6, cacheRead: 0.08, cacheWrite: 0.5, }, @@ -14059,7 +13801,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.7999999999999999, + input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1, @@ -14067,25 +13809,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8192, } satisfies Model<"anthropic-messages">, - "anthropic/claude-fable-5": { - id: "anthropic/claude-fable-5", - name: "Claude Fable 5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, "anthropic/claude-haiku-4.5": { id: "anthropic/claude-haiku-4.5", name: "Claude Haiku 4.5", @@ -14097,7 +13820,7 @@ export const MODELS = { cost: { input: 1, output: 5, - cacheRead: 0.09999999999999999, + cacheRead: 0.1, cacheWrite: 1.25, }, contextWindow: 200000, @@ -14290,7 +14013,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.25, - output: 0.8999999999999999, + output: 0.9, cacheRead: 0, cacheWrite: 0, }, @@ -14308,7 +14031,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 256000, @@ -14493,8 +14216,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, + input: 0.1, + output: 0.4, cacheRead: 0.01, cacheWrite: 0, }, @@ -14529,7 +14252,7 @@ export const MODELS = { cost: { input: 0.5, output: 3, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 1000000, @@ -14546,7 +14269,7 @@ export const MODELS = { cost: { input: 2, output: 12, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -14597,7 +14320,7 @@ export const MODELS = { cost: { input: 2, output: 12, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -14647,7 +14370,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.14, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -14665,7 +14388,7 @@ export const MODELS = { cost: { input: 0.25, output: 0.75, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 128000, @@ -14817,7 +14540,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.24, - output: 0.9700000000000001, + output: 0.97, cacheRead: 0, cacheWrite: 0, }, @@ -14833,7 +14556,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.16999999999999998, + input: 0.17, output: 0.66, cacheRead: 0, cacheWrite: 0, @@ -14987,7 +14710,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.3, - output: 0.8999999999999999, + output: 0.9, cacheRead: 0, cacheWrite: 0, }, @@ -15003,7 +14726,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2, cacheRead: 0, cacheWrite: 0, @@ -15020,7 +14743,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0, @@ -15037,7 +14760,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0, @@ -15054,8 +14777,8 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, + input: 0.1, + output: 0.1, cacheRead: 0, cacheWrite: 0, }, @@ -15088,7 +14811,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2, cacheRead: 0, cacheWrite: 0, @@ -15139,7 +14862,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0, @@ -15190,7 +14913,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.5700000000000001, + input: 0.57, output: 2.3, cacheRead: 0, cacheWrite: 0, @@ -15215,40 +14938,6 @@ export const MODELS = { contextWindow: 262114, maxTokens: 262114, } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-thinking-turbo": { - id: "moonshotai/kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-turbo": { - id: "moonshotai/kimi-k2-turbo", - name: "Kimi K2 Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, "moonshotai/kimi-k2.5": { id: "moonshotai/kimi-k2.5", name: "Kimi K2.5", @@ -15260,7 +14949,7 @@ export const MODELS = { cost: { input: 0.6, output: 3, - cacheRead: 0.09999999999999999, + cacheRead: 0.1, cacheWrite: 0, }, contextWindow: 262114, @@ -15283,6 +14972,23 @@ export const MODELS = { contextWindow: 262000, maxTokens: 262000, } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2.7-code": { + id: "moonshotai/kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, "nvidia/nemotron-3-super-120b-a12b": { id: "nvidia/nemotron-3-super-120b-a12b", name: "NVIDIA Nemotron 3 Super 120B A12B", @@ -15326,7 +15032,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.6, cacheRead: 0, cacheWrite: 0, @@ -15344,7 +15050,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.06, - output: 0.22999999999999998, + output: 0.23, cacheRead: 0, cacheWrite: 0, }, @@ -15394,9 +15100,9 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.09999999999999999, + input: 0.4, + output: 1.6, + cacheRead: 0.1, cacheWrite: 0, }, contextWindow: 1047576, @@ -15411,9 +15117,9 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.024999999999999998, + input: 0.1, + output: 0.4, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 1047576, @@ -15515,7 +15221,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, @@ -15530,8 +15236,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, + input: 0.05, + output: 0.4, cacheRead: 0.005, cacheWrite: 0, }, @@ -15600,7 +15306,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, @@ -15794,7 +15500,7 @@ export const MODELS = { thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite: 0, @@ -15882,8 +15588,8 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.049999999999999996, - output: 0.19999999999999998, + input: 0.05, + output: 0.2, cacheRead: 0, cacheWrite: 0, }, @@ -16069,7 +15775,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.15, cacheRead: 0.04, cacheWrite: 0, @@ -16086,9 +15792,9 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.5, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 1000000, @@ -16103,9 +15809,9 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.5, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 1000000, @@ -16122,7 +15828,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -16139,7 +15845,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -16156,7 +15862,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -16173,7 +15879,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -16190,7 +15896,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -16207,7 +15913,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -16224,7 +15930,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -16241,7 +15947,7 @@ export const MODELS = { cost: { input: 1, output: 2, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 256000, @@ -16256,7 +15962,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0.01, cacheWrite: 0, @@ -16275,7 +15981,7 @@ export const MODELS = { cost: { input: 1, output: 3, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -16341,7 +16047,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.1, cacheRead: 0.03, cacheWrite: 0, @@ -16359,7 +16065,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.6, - output: 1.7999999999999998, + output: 1.8, cacheRead: 0.11, cacheWrite: 0, }, @@ -16393,8 +16099,8 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.3, - output: 0.8999999999999999, - cacheRead: 0.049999999999999996, + output: 0.9, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 128000, @@ -16444,7 +16150,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.07, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -16461,7 +16167,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.06, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0.01, cacheWrite: 0, }, @@ -16478,8 +16184,8 @@ export const MODELS = { input: ["text"], cost: { input: 1, - output: 3.1999999999999997, - cacheRead: 0.19999999999999998, + output: 3.2, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 202800, @@ -16749,6 +16455,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-ams": { "mimo-v2-omni": { @@ -16823,6 +16547,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-cn": { "mimo-v2-omni": { @@ -16897,6 +16639,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-sgp": { "mimo-v2-omni": { @@ -16971,6 +16731,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "zai": { "glm-4.5-air": { @@ -17045,6 +16823,24 @@ export const MODELS = { contextWindow: 200000, maxTokens: 131072, } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "glm-5v-turbo": { id: "glm-5v-turbo", name: "GLM-5V-Turbo", @@ -17137,6 +16933,24 @@ export const MODELS = { contextWindow: 200000, maxTokens: 131072, } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "glm-5v-turbo": { id: "glm-5v-turbo", name: "GLM-5V-Turbo", diff --git a/packages/ai/src/utils/overflow.ts b/packages/ai/src/utils/overflow.ts index e3a9b37ab..623b873ab 100644 --- a/packages/ai/src/utils/overflow.ts +++ b/packages/ai/src/utils/overflow.ts @@ -12,6 +12,7 @@ import type { AssistantMessage } from "../types.ts"; * - 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" + * - OpenAI-compatible: "Input length (265330) exceeds model's maximum context length (262144)." * - 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" @@ -36,7 +37,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) + /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/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 @@ -85,7 +86,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" or "exceeds the model's maximum context length of X tokens" + * - OpenAI (Completions & Responses): "exceeds the context window", "exceeds the model's maximum context length of X tokens", or "exceeds model's maximum context length (X)" * - 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" diff --git a/packages/ai/test/anthropic-adaptive-thinking-models.test.ts b/packages/ai/test/anthropic-adaptive-thinking-models.test.ts index da042e2f4..18023e11e 100644 --- a/packages/ai/test/anthropic-adaptive-thinking-models.test.ts +++ b/packages/ai/test/anthropic-adaptive-thinking-models.test.ts @@ -5,9 +5,8 @@ import type { Api, Model } from "../src/types.ts"; const EXPECTED_CURRENT_ADAPTIVE_THINKING_MODELS = [ "anthropic/claude-fable-5", "anthropic/claude-opus-4-8", - "opencode/claude-fable-5", + "cloudflare-ai-gateway/claude-fable-5", "opencode/claude-opus-4-8", - "vercel-ai-gateway/anthropic/claude-fable-5", "vercel-ai-gateway/anthropic/claude-opus-4.8", ]; diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index 8828ba864..6e2c1a5bc 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { MODELS } from "../src/models.generated.ts"; import { getModel } from "../src/models.ts"; import { streamAnthropic } from "../src/providers/anthropic.ts"; import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; @@ -13,6 +14,11 @@ class PayloadCaptured extends Error { } } +interface OpenAICompletionsCachePayload { + prompt_cache_key?: string; + prompt_cache_retention?: string; +} + function stopAfterPayload(capture: (payload: TPayload) => void): (payload: unknown) => never { return (payload: unknown): never => { capture(payload as TPayload); @@ -455,5 +461,39 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { expect(capturedPayload.prompt_cache_key).toBeUndefined(); expect(capturedPayload.prompt_cache_retention).toBeUndefined(); }); + + it.each([ + MODELS.opencode["deepseek-v4-flash"], + MODELS.opencode["deepseek-v4-pro"], + MODELS.opencode["kimi-k2.5"], + MODELS.opencode["kimi-k2.6"], + MODELS.opencode["minimax-m2.7"], + MODELS["opencode-go"]["kimi-k2.6"], + ] as const)("should omit long cache retention for $provider/$id", async (metadata) => { + const model = metadata as Model<"openai-completions">; + let capturedPayload: OpenAICompletionsCachePayload | undefined; + + try { + const s = streamOpenAICompletions(model, context, { + apiKey: "fake-key", + cacheRetention: "long", + sessionId: "session-opencode-long-cache-unsupported", + onPayload: stopAfterPayload((payload) => { + capturedPayload = payload; + }), + }); + + for await (const event of s) { + if (event.type === "error") break; + } + } catch { + // Expected to fail + } + + expect(model.compat?.supportsLongCacheRetention).toBe(false); + expect(capturedPayload).toBeDefined(); + expect(capturedPayload?.prompt_cache_key).toBeUndefined(); + expect(capturedPayload?.prompt_cache_retention).toBeUndefined(); + }); }); }); diff --git a/packages/ai/test/overflow.test.ts b/packages/ai/test/overflow.test.ts index 346d20069..621089115 100644 --- a/packages/ai/test/overflow.test.ts +++ b/packages/ai/test/overflow.test.ts @@ -49,6 +49,13 @@ describe("isContextOverflow", () => { expect(isContextOverflow(message, 131072)).toBe(true); }); + it("detects OpenAI-compatible parenthesized maximum context length errors", () => { + const message = createErrorMessage( + "Error: 400 Input length (265330) exceeds model's maximum context length (262144).", + ); + expect(isContextOverflow(message, 262144)).toBe(true); + }); + it("detects OpenRouter Poolside maximum allowed input length errors", () => { const message = createErrorMessage( "Provider returned error: Input length 131393 exceeds the maximum allowed input length of 131040 tokens.", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e770ebdbf..71de722b3 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,17 +2,49 @@ ## [Unreleased] -### Added +### Fixed -- Added an experimental first-time setup flow behind `PI_EXPERIMENTAL=1` that asks for a dark/light theme choice (preselecting the detected appearance) and opt-in analytics data sharing on first launch with the default agent directory; opting in stores a `trackingId` in `settings.json`. +- Fixed `pi update` for pnpm global installs whose configured `global-bin-dir` no longer matches the active pnpm home ([#5689](https://github.com/earendil-works/pi/issues/5689)). +- Fixed npm package specs that use ranges or tags (for example `@^1.2.7`) so installed package resources still load instead of being treated as mismatched exact pins ([#5695](https://github.com/earendil-works/pi/issues/5695)). +- Fixed inherited OpenCode/OpenCode Go completion model metadata to omit long-retention cache fields for routes that reject `prompt_cache_retention` ([#5702](https://github.com/earendil-works/pi/issues/5702)). +- Fixed custom provider config so plain uppercase API key and header values remain literals instead of being treated as legacy environment references; use explicit `$ENV_VAR` syntax for environment variables ([#5661](https://github.com/earendil-works/pi/issues/5661)). + +## [0.79.3] - 2026-06-13 ### Fixed +- Fixed inherited OpenAI GPT-5.4/GPT-5.5 and OpenAI Codex GPT-5.4/GPT-5.4 mini/GPT-5.5 context window metadata to use the observed 272k-token Codex backend limit, avoiding a billing hazard from prompts above Codex's accepted limit (reported by [@trethore](https://github.com/trethore)). + +## [0.79.2] - 2026-06-12 + +### New Features + +- **Clearer Bedrock validation guidance** - Amazon Bedrock data retention validation errors now link to AWS data retention documentation. See [Amazon Bedrock](docs/providers.md#amazon-bedrock). + +### Added + +- Added an experimental first-time setup flow behind `PI_EXPERIMENTAL=1` that asks for a dark/light theme choice (preselecting the detected appearance) and opt-in analytics data sharing on first launch with the default agent directory; opting in stores a `trackingId` in `settings.json` ([#5587](https://github.com/earendil-works/pi/pull/5587) by [@vegarsti](https://github.com/vegarsti)). +- Added AWS data retention documentation links to inherited Amazon Bedrock unsupported data retention mode validation errors ([#5561](https://github.com/earendil-works/pi/pull/5561) by [@unexge](https://github.com/unexge)). + +### Fixed + +- Fixed project trust detection to ignore global `~/.pi/agent` state when running from `$HOME`, and made `pi update` use only saved or explicit project trust without prompting ([#5619](https://github.com/earendil-works/pi/issues/5619)). +- Fixed experimental first-time setup to skip forked sessions instead of rerunning the setup prompts ([#5627](https://github.com/earendil-works/pi/pull/5627) by [@vegarsti](https://github.com/vegarsti)). +- Fixed inherited OpenAI-compatible context overflow detection for parenthesized `maximum context length (N)` errors ([#5677](https://github.com/earendil-works/pi/issues/5677)). +- Fixed inherited OpenAI GPT-5.4/GPT-5.5 and OpenAI Codex GPT-5.4/GPT-5.4 mini/GPT-5.5 context window metadata to match current OpenAI limits ([#5644](https://github.com/earendil-works/pi/issues/5644)). +- Fixed inherited Anthropic refusal stops to preserve provider `stop_details` explanations in error messages ([#5666](https://github.com/earendil-works/pi/pull/5666) by [@rwachtler](https://github.com/rwachtler)). +- Increased the inherited OpenAI Codex Responses SSE response-header timeout to 20 seconds to reduce false-positive stalls while retaining the bounded wait introduced for zero-event hangs ([#4945](https://github.com/earendil-works/pi/issues/4945)). +- Fixed inherited Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). +- Fixed inherited late tool progress callbacks after tool settlement to be ignored instead of emitting stale `tool_execution_update` events ([#5573](https://github.com/earendil-works/pi/issues/5573)). - Fixed inherited user-message transcript rendering so standalone `+` messages no longer render as `-` ([#5657](https://github.com/earendil-works/pi/issues/5657)). +- Fixed inherited slash-separated fuzzy queries so provider/model completions remain matchable after insertion. +- Fixed inherited WezTerm inline Kitty image rendering so reserved row clears do not erase all but the top strip of tool image previews ([#5618](https://github.com/earendil-works/pi/issues/5618)). +- Fixed inherited editor wrapping for CJK text to break at character boundaries instead of leaving large trailing gaps ([#5585](https://github.com/earendil-works/pi/pull/5585) by [@haoqixu](https://github.com/haoqixu)). +- Fixed inherited loose Markdown list rendering to preserve blank-line separation between list items ([#5562](https://github.com/earendil-works/pi/pull/5562) by [@Perlence](https://github.com/Perlence)). - Fixed `--model` resolution for authenticated custom model IDs whose slash prefix matches an unauthenticated built-in provider ([#5643](https://github.com/earendil-works/pi/issues/5643)). - Fixed `/fork` to keep session parent chains connected when the forked path contains labels ([#5669](https://github.com/earendil-works/pi/issues/5669)). - Fixed `/share` and `/export` HTML exports to use the active fallback theme when the configured custom theme no longer exists ([#5596](https://github.com/earendil-works/pi/issues/5596)). -- Fixed custom fallback model IDs with `:` suffixes to preserve the requested thinking level when the provider template model does not advertise reasoning ([#5552](https://github.com/earendil-works/pi/issues/5552)). +- Fixed custom fallback model IDs with `:` suffixes to preserve the requested thinking level when the provider template model does not advertise reasoning ([#5560](https://github.com/earendil-works/pi/pull/5560) by [@haoqixu](https://github.com/haoqixu)). ## [0.79.1] - 2026-06-09 diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 6e5c70595..41a7bbc28 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -291,15 +291,15 @@ See [docs/settings.md](docs/settings.md) for all options. ### Project Trust -On interactive startup, pi asks before trusting a project folder that contains project-local extensions or settings and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. +On interactive startup, pi asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. Before the trust decision, pi loads only context files, user/global extensions, and CLI `-e` extensions so they can handle the `project_trust` event. Project-local extensions, project package-managed extensions, and project settings are loaded only after the project is trusted. This split also applies when switching to a session from a different cwd whose trust has not been resolved in the current process. -Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore trust-gated project inputs, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. +Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore those project resources, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. If no extension or saved decision applies, `defaultProjectTrust` controls the fallback behavior. Set it to `"ask"`, `"always"`, or `"never"` in `~/.pi/agent/settings.json`, or change it with `/settings`. -`pi config` and package commands use the same project trust flow. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. +`pi config` and package commands use the same project trust flow, except `pi update` never prompts. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. Use `/trust` in interactive mode to save a project trust decision for future sessions, including trust for the immediate parent folder. It writes `~/.pi/agent/trust.json` only; the current session is not reloaded, so restart pi for changes to take effect. @@ -527,7 +527,7 @@ pi list # List installed packages pi config # Enable/disable package resources ``` -`pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. +`pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust. ### Modes diff --git a/packages/coding-agent/docs/models.md b/packages/coding-agent/docs/models.md index 39b56aa17..830eaacda 100644 --- a/packages/coding-agent/docs/models.md +++ b/packages/coding-agent/docs/models.md @@ -161,13 +161,11 @@ The `apiKey` and `headers` fields support command execution, environment interpo "apiKey": "$$literal-dollar-prefix" "apiKey": "$!literal-bang-prefix" ``` -- **Literal value:** Used directly +- **Literal value:** Used directly. Plain uppercase strings such as `MY_API_KEY` are literals; use `$MY_API_KEY` for environment variables. ```json "apiKey": "sk-..." ``` -Legacy uppercase env-var-like values such as `MY_API_KEY` are migrated to `$MY_API_KEY` on startup. - For `models.json`, shell commands are resolved at request time. pi intentionally does not apply built-in TTL, stale reuse, or recovery logic for arbitrary commands. Different commands need different caching and failure strategies, and pi cannot infer the right one. If your command is slow, expensive, rate-limited, or should keep using a previous value on transient failures, wrap it in your own script or command that implements the caching or TTL behavior you want. diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index 185405cd1..f139aa300 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -124,13 +124,13 @@ The `key` field supports command execution, environment interpolation, and liter { "type": "api_key", "key": "$$literal-dollar-prefix" } { "type": "api_key", "key": "$!literal-bang-prefix" } ``` -- **Literal value:** Used directly +- **Literal value:** Used directly. Plain uppercase strings such as `MY_API_KEY` are literals; use `$MY_API_KEY` for environment variables. ```json { "type": "api_key", "key": "sk-ant-..." } { "type": "api_key", "key": "public" } ``` -Legacy uppercase env-var-like values such as `MY_API_KEY` are migrated to `$MY_API_KEY` on startup. OAuth credentials are also stored here after `/login` and managed automatically. +OAuth credentials are also stored here after `/login` and managed automatically. ## Cloud Providers diff --git a/packages/coding-agent/docs/security.md b/packages/coding-agent/docs/security.md index 0c6d387a4..3a268a8af 100644 --- a/packages/coding-agent/docs/security.md +++ b/packages/coding-agent/docs/security.md @@ -6,14 +6,18 @@ Pi is a local coding agent. It runs with the permissions of the user account tha Project trust controls whether pi loads project-local settings, resources, packages, and extensions. It is not a sandbox and it does not restrict what the model can ask tools to do after you start working in a directory. -Pi considers a project to have trust inputs when it finds any of these from the current working directory: +Pi considers a project to have resources that require trust when it finds any of these from the current working directory: -- `.pi/` in the current directory -- `.agents/skills` in the current directory or an ancestor directory +- `.pi/settings.json` +- `.pi/extensions`, `.pi/skills`, `.pi/prompts`, or `.pi/themes` +- `.pi/SYSTEM.md` or `.pi/APPEND_SYSTEM.md` +- project `.agents/skills` in the current directory or an ancestor directory -When an interactive session starts in a project with configs in `.pi` or `.agents/skills` and no saved decision for the current directory or a parent directory, pi follows `defaultProjectTrust` from global settings. The default value is `"ask"`, which asks whether to trust the project when UI is available. Saved decisions are stored by canonical directory in `~/.pi/agent/trust.json`, and the closest saved decision on the current or parent path applies before the global default. +A bare `.pi` directory does not count as a project resource that requires trust. -Trusting a project allows pi to load trust-gated project inputs, including: +When an interactive session starts in a project with resources that require trust and no saved decision for the current directory or a parent directory, pi follows `defaultProjectTrust` from global settings. The default value is `"ask"`, which asks whether to trust the project when UI is available. Saved decisions are stored by canonical directory in `~/.pi/agent/trust.json`, and the closest saved decision on the current or parent path applies before the global default. + +Trusting a project allows pi to load project resources that require trust, including: - `.pi/settings.json` - `.pi` resources such as extensions, skills, prompt templates, themes, and system prompt files diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 2cf843d11..fccfab063 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -11,13 +11,13 @@ Edit directly or use `/settings` for common options. ## Project Trust -On interactive startup, pi asks before trusting a project folder that contains trust-gated project inputs and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. +On interactive startup, pi asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. -Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore trust-gated project inputs, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. +Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore those project resources, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. If no extension or saved decision applies, `defaultProjectTrust` controls the fallback behavior. Set it to `"ask"`, `"always"`, or `"never"` in `~/.pi/agent/settings.json`, or change it with `/settings`. -`pi config` and package commands use the same project trust flow. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. +`pi config` and package commands use the same project trust flow, except `pi update` never prompts. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. Use `/trust` in interactive mode to save a project trust decision for future sessions, including trust for the immediate parent folder. It writes `~/.pi/agent/trust.json` only; the current session is not reloaded, so restart pi for changes to take effect. diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index bb8a4c253..4f8f5954f 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -112,15 +112,15 @@ Append to the default prompt without replacing it with `APPEND_SYSTEM.md` in eit ### Project Trust -On interactive startup, pi asks before trusting a project folder that contains project-local extensions or settings and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. +On interactive startup, pi asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. Before the trust decision, pi loads only context files, user/global extensions, and CLI `-e` extensions so they can handle the `project_trust` event. Project-local extensions, project package-managed extensions, and project settings are loaded only after the project is trusted. This split also applies when switching to a session from a different cwd whose trust has not been resolved in the current process. -Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore trust-gated project inputs, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. +Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore those project resources, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. If no extension or saved decision applies, `defaultProjectTrust` controls the fallback behavior. Set it to `"ask"`, `"always"`, or `"never"` in `~/.pi/agent/settings.json`, or change it with `/settings`. -`pi config` and package commands use the same project trust flow. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. +`pi config` and package commands use the same project trust flow, except `pi update` never prompts. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. Use `/trust` in interactive mode to save a project trust decision for future sessions, including trust for the immediate parent folder. It writes `~/.pi/agent/trust.json` only; the current session is not reloaded, so restart pi for changes to take effect. @@ -153,7 +153,7 @@ pi list # List installed packages pi config # Enable/disable package resources ``` -These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). `pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. +These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). `pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust. See [Pi Packages](packages.md) for package sources and security notes. diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index dc287898d..906cc6512 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.1", + "version": "0.79.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.1", + "version": "0.79.3", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 4b1dd16b4..b2082217c 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.1", + "version": "0.79.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index fba74f25e..3e80ae345 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.1", + "version": "0.79.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index b71b386c5..cbd6cfec9 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.1", + "version": "0.79.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.1", + "version": "0.79.3", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 514480001..a339c43a5 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.1", + "version": "0.79.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 360c0def6..04da2c15a 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.1", + "version": "1.9.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.1", + "version": "1.9.3", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index f285b6ce0..60573f2a0 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.1", + "version": "1.9.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index ee75d24fd..df12962dc 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.1", + "version": "0.79.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.1", + "version": "0.79.3", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index fab9f1454..b47c9e68c 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.1", + "version": "0.79.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 9132d4890..74eff303f 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.1", + "version": "0.79.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.1", + "version": "0.79.3", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.1", - "@earendil-works/pi-ai": "^0.79.1", - "@earendil-works/pi-tui": "^0.79.1", + "@earendil-works/pi-agent-core": "^0.79.3", + "@earendil-works/pi-ai": "^0.79.3", + "@earendil-works/pi-tui": "^0.79.3", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -23,6 +23,7 @@ "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", + "semver": "7.8.0", "typebox": "1.1.38", "undici": "8.3.0", "yaml": "2.9.0" @@ -473,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.1.tgz", + "version": "0.79.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.3.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.1", + "@earendil-works/pi-ai": "^0.79.3", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -487,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.1.tgz", + "version": "0.79.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.3.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -510,8 +511,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.1.tgz", + "version": "0.79.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.3.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", @@ -1636,6 +1637,18 @@ } ] }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index b9381465a..ff4522ce3 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.1", + "version": "0.79.3", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.1", - "@earendil-works/pi-ai": "^0.79.1", - "@earendil-works/pi-tui": "^0.79.1", + "@earendil-works/pi-agent-core": "^0.79.3", + "@earendil-works/pi-ai": "^0.79.3", + "@earendil-works/pi-tui": "^0.79.3", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -50,6 +50,7 @@ "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", + "semver": "7.8.0", "typebox": "1.1.38", "undici": "8.3.0", "yaml": "2.9.0" @@ -70,6 +71,7 @@ "@types/ms": "2.1.0", "@types/node": "24.12.4", "@types/proper-lockfile": "4.1.4", + "@types/semver": "7.7.1", "shx": "0.4.0", "typescript": "5.9.3", "vitest": "3.2.4" diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 65fb220e4..75b2efaf1 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -109,19 +109,27 @@ function getSelfUpdateCommandForMethod( switch (method) { case "bun-binary": return undefined; - case "pnpm": + case "pnpm": { + const match = readCommandOutput("pnpm", ["root", "-g"]) + ? undefined + : /^(.*[\\/]global[\\/][^\\/]+)[\\/]\.pnpm[\\/]/.exec(getPackageDir()); + const binDirArgs = match + ? [`--config.global-bin-dir=${process.env.PNPM_HOME || dirname(dirname(match[1]))}`] + : []; return makeSelfUpdateCommand( makeSelfUpdateCommandStep("pnpm", [ "install", "-g", "--ignore-scripts", "--config.minimumReleaseAge=0", + ...binDirArgs, updatePackageName, ]), updatePackageName === installedPackageName ? undefined - : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", installedPackageName]), + : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", ...binDirArgs, installedPackageName]), ); + } case "yarn": return makeSelfUpdateCommand( makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]), @@ -205,7 +213,9 @@ function getGlobalPackageRoots(method: InstallMethod, _packageName: string, npmC } case "pnpm": { const root = readCommandOutput("pnpm", ["root", "-g"]); - return root ? [root, dirname(root)] : []; + if (root) return [root, dirname(root)]; + const match = /^(.*[\\/]global[\\/][^\\/]+)[\\/]\.pnpm[\\/]/.exec(getPackageDir()); + return match ? [match[1]] : []; } case "yarn": { const dir = readCommandOutput("yarn", ["global", "dir"]); diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 00c815761..ff1729666 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -25,7 +25,6 @@ import { type Static, Type } from "typebox"; import { Compile } from "typebox/compile"; import type { TLocalizedValidationError } from "typebox/error"; import { getAgentDir } from "../config.ts"; -import { warnDeprecation } from "../utils/deprecation.ts"; import { stripJsonComments } from "../utils/json.ts"; import { normalizePath } from "../utils/paths.ts"; import type { AuthStatus, AuthStorage } from "./auth-storage.ts"; @@ -35,7 +34,6 @@ import { getConfigValueEnvVarNames, isCommandConfigValue, isConfigValueConfigured, - isLegacyEnvVarNameConfigValue, resolveConfigValueOrThrow, resolveConfigValueUncached, resolveHeadersOrThrow, @@ -237,77 +235,6 @@ interface ProviderRequestConfig { authHeader?: boolean; } -function migrateLegacyRegisterProviderConfigValue(providerName: string, field: string, value: string): string { - if (!isLegacyEnvVarNameConfigValue(value)) return value; - warnDeprecation( - `registerProvider("${providerName}") ${field} value "${value}" is treated as a legacy environment variable reference. This will no longer be detected as an environment variable reference in a future release. Pass "$${value}" instead.`, - ); - return `$${value}`; -} - -function migrateLegacyRegisterProviderHeaders( - providerName: string, - field: string, - headers: Record | undefined, -): Record | undefined { - if (!headers) return undefined; - let migratedHeaders: Record | undefined; - for (const [key, value] of Object.entries(headers)) { - const migratedValue = migrateLegacyRegisterProviderConfigValue(providerName, `${field} header "${key}"`, value); - if (migratedValue === value) continue; - migratedHeaders ??= { ...headers }; - migratedHeaders[key] = migratedValue; - } - return migratedHeaders ?? headers; -} - -function migrateLegacyRegisterProviderConfigValues( - providerName: string, - config: ProviderConfigInput, -): ProviderConfigInput { - let migratedConfig: ProviderConfigInput | undefined; - - const setMigratedConfigValue = ( - key: TKey, - value: ProviderConfigInput[TKey], - ) => { - migratedConfig ??= { ...config }; - migratedConfig[key] = value; - }; - - if (config.apiKey) { - const apiKey = migrateLegacyRegisterProviderConfigValue(providerName, "apiKey", config.apiKey); - if (apiKey !== config.apiKey) { - setMigratedConfigValue("apiKey", apiKey); - } - } - - const headers = migrateLegacyRegisterProviderHeaders(providerName, "headers", config.headers); - if (headers !== config.headers) { - setMigratedConfigValue("headers", headers); - } - - if (config.models) { - let models: ProviderConfigInput["models"] | undefined; - for (let index = 0; index < config.models.length; index++) { - const model = config.models[index]; - const modelHeaders = migrateLegacyRegisterProviderHeaders( - providerName, - `model "${model.id}" headers`, - model.headers, - ); - if (modelHeaders === model.headers) continue; - models ??= [...config.models]; - models[index] = { ...model, headers: modelHeaders }; - } - if (models) { - setMigratedConfigValue("models", models); - } - } - - return migratedConfig ?? config; -} - export type ResolvedRequestAuth = | { ok: true; @@ -869,10 +796,9 @@ export class ModelRegistry { * If provider has oauth: registers OAuth provider for /login support. */ registerProvider(providerName: string, config: ProviderConfigInput): void { - const migratedConfig = migrateLegacyRegisterProviderConfigValues(providerName, config); - this.validateProviderConfig(providerName, migratedConfig); - this.applyProviderConfig(providerName, migratedConfig); - this.upsertRegisteredProvider(providerName, migratedConfig); + this.validateProviderConfig(providerName, config); + this.applyProviderConfig(providerName, config); + this.upsertRegisteredProvider(providerName, config); } /** diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 5120c9ade..ebdf974ba 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -27,6 +27,7 @@ import type { Readable } from "node:stream"; import { globSync } from "glob"; import ignore from "ignore"; import { minimatch } from "minimatch"; +import { maxSatisfying, rcompare, satisfies, valid, validRange } from "semver"; import { CONFIG_DIR_NAME } from "../config.ts"; import { spawnProcess, spawnProcessSync } from "../utils/child-process.ts"; import { type GitSource, parseGitUrl } from "../utils/git.ts"; @@ -44,6 +45,14 @@ function isOfflineModeEnabled(): boolean { return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes"; } +function isExactNpmVersion(version: string | undefined): boolean { + return valid(version ?? "") !== null; +} + +function getNpmVersionRange(version: string | undefined): string | undefined { + return version ? (validRange(version) ?? undefined) : undefined; +} + export interface PathMetadata { source: string; scope: SourceScope; @@ -119,6 +128,8 @@ type NpmSource = { type: "npm"; spec: string; name: string; + version?: string; + range?: string; pinned: boolean; }; @@ -1113,8 +1124,8 @@ export class DefaultPackageManager implements PackageManager { } try { - const latestVersion = await this.getLatestNpmVersion(source.name); - return latestVersion !== installedVersion; + const targetVersion = await this.getLatestNpmVersion(source.version ? source.spec : source.name, source.range); + return targetVersion !== installedVersion; } catch { // Preserve existing update behavior when version lookup fails. return true; @@ -1128,7 +1139,7 @@ export class DefaultPackageManager implements PackageManager { const sourceLabel = sources.length === 1 ? sources[0].source : `${scope} npm packages`; const message = sources.length === 1 ? `Updating ${sources[0].source}...` : `Updating ${scope} npm packages...`; - const specs = sources.map((entry) => `${entry.parsed.name}@latest`); + const specs = sources.map((entry) => (entry.parsed.version ? entry.parsed.spec : `${entry.parsed.name}@latest`)); await this.withProgress("update", sourceLabel, message, async () => { await this.installNpmBatch(specs, scope); @@ -1241,8 +1252,7 @@ export class DefaultPackageManager implements PackageManager { if (parsed.type === "npm") { let installedPath = this.getNpmInstallPath(parsed, scope); const needsInstall = - !existsSync(installedPath) || - (parsed.pinned && !(await this.installedNpmMatchesPinnedVersion(parsed, installedPath))); + !existsSync(installedPath) || !(await this.installedNpmMatchesConfiguredVersion(parsed, installedPath)); if (needsInstall) { const installed = await installMissing(); if (!installed) continue; @@ -1394,7 +1404,9 @@ export class DefaultPackageManager implements PackageManager { type: "npm", spec, name, - pinned: Boolean(version), + version, + range: getNpmVersionRange(version), + pinned: isExactNpmVersion(version), }; } @@ -1411,18 +1423,12 @@ export class DefaultPackageManager implements PackageManager { return { type: "local", path: source }; } - private async installedNpmMatchesPinnedVersion(source: NpmSource, installedPath: string): Promise { + private async installedNpmMatchesConfiguredVersion(source: NpmSource, installedPath: string): Promise { const installedVersion = this.getInstalledNpmVersion(installedPath); if (!installedVersion) { return false; } - - const { version: pinnedVersion } = this.parseNpmSpec(source.spec); - if (!pinnedVersion) { - return true; - } - - return installedVersion === pinnedVersion; + return source.range ? satisfies(installedVersion, source.range) : true; } private async npmHasAvailableUpdate(source: NpmSource, installedPath: string): Promise { @@ -1436,8 +1442,8 @@ export class DefaultPackageManager implements PackageManager { } try { - const latestVersion = await this.getLatestNpmVersion(source.name); - return latestVersion !== installedVersion; + const targetVersion = await this.getLatestNpmVersion(source.version ? source.spec : source.name, source.range); + return targetVersion !== installedVersion; } catch { return false; } @@ -1455,16 +1461,25 @@ export class DefaultPackageManager implements PackageManager { } } - private async getLatestNpmVersion(packageName: string): Promise { + private async getLatestNpmVersion(packageSpec: string, range?: string): Promise { const npmCommand = this.getNpmCommand(); const stdout = await this.runCommandCapture( npmCommand.command, - [...npmCommand.args, "view", packageName, "version", "--json"], + [...npmCommand.args, "view", packageSpec, "version", "--json"], { cwd: this.cwd, timeoutMs: NETWORK_TIMEOUT_MS }, ); const raw = stdout.trim(); if (!raw) throw new Error("Empty response from npm view"); - return JSON.parse(raw); + const parsed = JSON.parse(raw) as unknown; + if (typeof parsed === "string") { + return parsed; + } + if (Array.isArray(parsed)) { + const versions = parsed.filter((value): value is string => typeof value === "string" && value.length > 0); + const latest = range ? maxSatisfying(versions, range) : [...versions].sort(rcompare)[0]; + if (latest) return latest; + } + throw new Error("Unexpected response from npm view"); } private async gitHasAvailableUpdate(installedPath: string): Promise { diff --git a/packages/coding-agent/src/core/project-trust.ts b/packages/coding-agent/src/core/project-trust.ts index c8b572509..c0892439c 100644 --- a/packages/coding-agent/src/core/project-trust.ts +++ b/packages/coding-agent/src/core/project-trust.ts @@ -3,7 +3,7 @@ import type { LoadExtensionsResult, ProjectTrustContext } from "./extensions/typ import type { DefaultProjectTrust } from "./settings-manager.ts"; import { getProjectTrustOptions, - hasProjectTrustInputs, + hasTrustRequiringProjectResources, type ProjectTrustOption, type ProjectTrustStore, } from "./trust-manager.ts"; @@ -46,7 +46,7 @@ export async function resolveProjectTrusted(options: ResolveProjectTrustedOption if (options.trustOverride !== undefined) { return options.trustOverride; } - if (!hasProjectTrustInputs(options.cwd)) { + if (!hasTrustRequiringProjectResources(options.cwd)) { return true; } diff --git a/packages/coding-agent/src/core/resolve-config-value.ts b/packages/coding-agent/src/core/resolve-config-value.ts index 119e379d6..6c2263f65 100644 --- a/packages/coding-agent/src/core/resolve-config-value.ts +++ b/packages/coding-agent/src/core/resolve-config-value.ts @@ -10,7 +10,6 @@ import { getShellConfig } from "../utils/shell.ts"; const commandResultCache = new Map(); const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; const ENV_VAR_NAME_PREFIX_RE = /^[A-Za-z_][A-Za-z0-9_]*/; -const LEGACY_ENV_VAR_NAME_RE = /^[A-Z_][A-Z0-9_]*$/; type TemplatePart = { type: "literal"; value: string } | { type: "env"; name: string }; @@ -136,10 +135,6 @@ export function isConfigValueConfigured(config: string): boolean { return getMissingConfigValueEnvVarNames(config).length === 0; } -export function isLegacyEnvVarNameConfigValue(config: string): boolean { - return LEGACY_ENV_VAR_NAME_RE.test(config); -} - /** * Resolve a config value (API key, header value, etc.) to an actual value. * - If starts with "!", executes the rest as a shell command and uses stdout (cached) diff --git a/packages/coding-agent/src/core/trust-manager.ts b/packages/coding-agent/src/core/trust-manager.ts index 69f616ae7..9c494b47a 100644 --- a/packages/coding-agent/src/core/trust-manager.ts +++ b/packages/coding-agent/src/core/trust-manager.ts @@ -1,4 +1,5 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; import { dirname, join } from "node:path"; import lockfile from "proper-lockfile"; import { CONFIG_DIR_NAME } from "../config.ts"; @@ -25,6 +26,16 @@ export interface ProjectTrustOption { type TrustFile = Record; +const TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [ + "settings.json", + "extensions", + "skills", + "prompts", + "themes", + "SYSTEM.md", + "APPEND_SYSTEM.md", +] as const; + function normalizeCwd(cwd: string): string { return canonicalizePath(resolvePath(cwd)); } @@ -45,18 +56,14 @@ function findNearestTrustEntry(data: TrustFile, cwd: string): ProjectTrustStoreE } } -export function getProjectTrustPath(cwd: string): string { - return normalizeCwd(cwd); -} - export function getProjectTrustParentPath(cwd: string): string | undefined { - const trustPath = getProjectTrustPath(cwd); + const trustPath = normalizeCwd(cwd); const parentDir = dirname(trustPath); return parentDir === trustPath ? undefined : parentDir; } export function getProjectTrustOptions(cwd: string, options?: { includeSessionOnly?: boolean }): ProjectTrustOption[] { - const trustPath = getProjectTrustPath(cwd); + const trustPath = normalizeCwd(cwd); const trustOptions: ProjectTrustOption[] = [ { label: "Trust", trusted: true, updates: [{ path: trustPath, decision: true }], savedPath: trustPath }, ]; @@ -167,18 +174,26 @@ function withTrustFileLock(path: string, fn: () => T): T { } } -export function hasProjectConfigDir(cwd: string): boolean { - return existsSync(join(canonicalizePath(resolvePath(cwd)), CONFIG_DIR_NAME)); -} - -export function hasProjectTrustInputs(cwd: string): boolean { +/** + * Returns true when cwd has project-local resources that must be gated by + * project trust: trust-requiring entries under cwd/.pi, or .agents/skills in + * cwd or one of its ancestors. Returns false when no such project resources + * exist. The user/global ~/.agents/skills directory is always treated as a + * trusted user resource and is ignored here, even when cwd is $HOME. + */ +export function hasTrustRequiringProjectResources(cwd: string): boolean { + const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir())); + const userAgentsSkillsDir = join(homeDir, ".agents", "skills"); let currentDir = canonicalizePath(resolvePath(cwd)); - if (hasProjectConfigDir(currentDir)) { + + const configDir = join(currentDir, CONFIG_DIR_NAME); + if (TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry)))) { return true; } while (true) { - if (existsSync(join(currentDir, ".agents", "skills"))) { + const agentsSkillsDir = join(currentDir, ".agents", "skills"); + if (agentsSkillsDir !== userAgentsSkillsDir && existsSync(agentsSkillsDir)) { return true; } diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 958c7ebb1..3176a1675 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -289,7 +289,7 @@ export { withFileMutationQueue, } from "./core/tools/index.ts"; export { - hasProjectTrustInputs, + hasTrustRequiringProjectResources, type ProjectTrustDecision, ProjectTrustStore, type ProjectTrustStoreEntry, diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 0bc24685d..63d6ec1ef 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -41,7 +41,7 @@ import { import { assertValidSessionId, SessionManager } from "./core/session-manager.ts"; import { SettingsManager } from "./core/settings-manager.ts"; import { printTimings, resetTimings, time } from "./core/timings.ts"; -import { hasProjectTrustInputs, ProjectTrustStore } from "./core/trust-manager.ts"; +import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.ts"; import { runMigrations, showDeprecationWarnings } from "./migrations.ts"; import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.ts"; import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.ts"; @@ -572,7 +572,9 @@ export async function main(args: string[], options?: MainOptions) { const trustStore = new ProjectTrustStore(agentDir); const sessionCwd = sessionManager.getCwd(); const autoTrustOnReloadCwd = - parsed.projectTrustOverride === undefined && !hasProjectTrustInputs(sessionCwd) ? sessionCwd : undefined; + parsed.projectTrustOverride === undefined && !hasTrustRequiringProjectResources(sessionCwd) + ? sessionCwd + : undefined; const trustPromptMode: AppMode = parsed.help || parsed.listModels !== undefined ? "print" : appMode; const projectTrustByCwd = new Map(); @@ -591,12 +593,14 @@ export async function main(args: string[], options?: MainOptions) { const isInitialRuntime = sessionStartEvent === undefined; const projectTrustDiagnostics: AgentSessionRuntimeDiagnostic[] = []; const cachedProjectTrust = projectTrustByCwd.get(cwd); - const hasTrustInputs = hasProjectTrustInputs(cwd); + const hasTrustRequiringResources = hasTrustRequiringProjectResources(cwd); const shouldResolveProjectTrust = - parsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustInputs; + parsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustRequiringResources; const projectTrusted = shouldResolveProjectTrust ? false - : (cachedProjectTrust ?? parsed.projectTrustOverride ?? (!hasTrustInputs || trustStore.get(cwd) === true)); + : (cachedProjectTrust ?? + parsed.projectTrustOverride ?? + (!hasTrustRequiringResources || trustStore.get(cwd) === true)); const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted }); const services = await createAgentSessionServices({ cwd, diff --git a/packages/coding-agent/src/migrations.ts b/packages/coding-agent/src/migrations.ts index 5cce43b82..39aeea043 100644 --- a/packages/coding-agent/src/migrations.ts +++ b/packages/coding-agent/src/migrations.ts @@ -3,12 +3,10 @@ */ import chalk from "chalk"; -import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs"; +import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs"; import { dirname, join } from "path"; import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.ts"; import { migrateKeybindingsConfig } from "./core/keybindings.ts"; -import { isLegacyEnvVarNameConfigValue } from "./core/resolve-config-value.ts"; -import { stripJsonComments } from "./utils/json.ts"; const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration"; @@ -74,140 +72,6 @@ export function migrateAuthToAuthJson(): string[] { return providers; } -interface ConfigValueMigration { - location: string; - from: string; - to: string; -} - -function migrateLegacyEnvVarString(value: string): string | undefined { - return isLegacyEnvVarNameConfigValue(value) ? `$${value}` : undefined; -} - -function migrateStringProperty( - record: Record, - key: string, - location: string, - migrations: ConfigValueMigration[], -): boolean { - const value = record[key]; - if (typeof value !== "string") return false; - const migrated = migrateLegacyEnvVarString(value); - if (migrated === undefined) return false; - record[key] = migrated; - migrations.push({ location, from: value, to: migrated }); - return true; -} - -function migrateHeadersConfig(headers: unknown, location: string, migrations: ConfigValueMigration[]): boolean { - if (typeof headers !== "object" || headers === null || Array.isArray(headers)) return false; - const headerRecord = headers as Record; - let migrated = false; - for (const [key, value] of Object.entries(headerRecord)) { - if (typeof value !== "string") continue; - const migratedValue = migrateLegacyEnvVarString(value); - if (migratedValue === undefined) continue; - headerRecord[key] = migratedValue; - migrations.push({ location: `${location}[${JSON.stringify(key)}]`, from: value, to: migratedValue }); - migrated = true; - } - return migrated; -} - -function migrateAuthJsonConfigValues(agentDir: string): ConfigValueMigration[] { - const authPath = join(agentDir, "auth.json"); - if (!existsSync(authPath)) return []; - - try { - const parsed = JSON.parse(readFileSync(authPath, "utf-8")) as unknown; - if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return []; - const authData = parsed as Record; - - const migrations: ConfigValueMigration[] = []; - for (const [provider, credential] of Object.entries(authData)) { - if (typeof credential !== "object" || credential === null || Array.isArray(credential)) continue; - const credentialRecord = credential as Record; - if (credentialRecord.type !== "api_key") continue; - migrateStringProperty(credentialRecord, "key", `auth.json[${JSON.stringify(provider)}].key`, migrations); - } - - if (migrations.length === 0) return []; - writeFileSync(authPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf-8"); - chmodSync(authPath, 0o600); - return migrations; - } catch { - return []; - } -} - -function migrateModelsJsonConfigValues(agentDir: string): ConfigValueMigration[] { - const modelsPath = join(agentDir, "models.json"); - if (!existsSync(modelsPath)) return []; - - try { - const parsed = JSON.parse(stripJsonComments(readFileSync(modelsPath, "utf-8"))) as unknown; - if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return []; - const modelsData = parsed as Record; - const providers = modelsData.providers; - if (typeof providers !== "object" || providers === null || Array.isArray(providers)) return []; - - const migrations: ConfigValueMigration[] = []; - for (const [provider, providerConfig] of Object.entries(providers)) { - if (typeof providerConfig !== "object" || providerConfig === null || Array.isArray(providerConfig)) continue; - const providerRecord = providerConfig as Record; - const providerLocation = `models.json.providers[${JSON.stringify(provider)}]`; - migrateStringProperty(providerRecord, "apiKey", `${providerLocation}.apiKey`, migrations); - migrateHeadersConfig(providerRecord.headers, `${providerLocation}.headers`, migrations); - - if (Array.isArray(providerRecord.models)) { - for (let index = 0; index < providerRecord.models.length; index++) { - const modelConfig = providerRecord.models[index]; - if (typeof modelConfig !== "object" || modelConfig === null || Array.isArray(modelConfig)) continue; - const modelRecord = modelConfig as Record; - const modelKey = typeof modelRecord.id === "string" ? JSON.stringify(modelRecord.id) : String(index); - migrateHeadersConfig(modelRecord.headers, `${providerLocation}.models[${modelKey}].headers`, migrations); - } - } - - const modelOverrides = providerRecord.modelOverrides; - if (typeof modelOverrides === "object" && modelOverrides !== null && !Array.isArray(modelOverrides)) { - for (const [modelId, modelOverride] of Object.entries(modelOverrides)) { - if (typeof modelOverride !== "object" || modelOverride === null || Array.isArray(modelOverride)) - continue; - const modelOverrideRecord = modelOverride as Record; - migrateHeadersConfig( - modelOverrideRecord.headers, - `${providerLocation}.modelOverrides[${JSON.stringify(modelId)}].headers`, - migrations, - ); - } - } - } - - if (migrations.length === 0) return []; - writeFileSync(modelsPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf-8"); - return migrations; - } catch { - return []; - } -} - -function migrateExplicitEnvVarConfigValues(): void { - const agentDir = getAgentDir(); - const migrations = [...migrateAuthJsonConfigValues(agentDir), ...migrateModelsJsonConfigValues(agentDir)]; - if (migrations.length === 0) return; - - const details = migrations.map((migration) => ` - ${migration.location}: ${migration.from} -> ${migration.to}`); - console.log( - chalk.yellow( - [ - "Warning: Migrated API key/header environment references to explicit $ENV_VAR syntax. Plain strings will be treated as literals.", - ...details, - ].join("\n"), - ), - ); -} - /** * Migrate sessions from ~/.pi/agent/*.jsonl to proper session directories. * @@ -443,7 +307,6 @@ export function runMigrations(cwd: string): { deprecationWarnings: string[]; } { const migratedAuthProviders = migrateAuthToAuthJson(); - migrateExplicitEnvVarConfigValues(); migrateSessionsFromAgentRoot(); migrateToolsToBin(); migrateKeybindingsConfigFile(); diff --git a/packages/coding-agent/src/modes/interactive/components/trust-selector.ts b/packages/coding-agent/src/modes/interactive/components/trust-selector.ts index b7b1fe00c..92c232889 100644 --- a/packages/coding-agent/src/modes/interactive/components/trust-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/trust-selector.ts @@ -1,7 +1,6 @@ import { Container, getKeybindings, Spacer, Text } from "@earendil-works/pi-tui"; import { getProjectTrustOptions, - getProjectTrustPath, type ProjectTrustOption, type ProjectTrustStoreEntry, } from "../../../core/trust-manager.ts"; @@ -19,12 +18,12 @@ export interface TrustSelectorOptions { onCancel: () => void; } -function formatDecision(cwd: string, decision: ProjectTrustStoreEntry | null): string { +function formatDecision(trustPath: string | undefined, decision: ProjectTrustStoreEntry | null): string { if (decision === null) { return "none"; } const label = decision.decision ? "trusted" : "untrusted"; - if (decision.path !== getProjectTrustPath(cwd)) { + if (trustPath !== undefined && decision.path !== trustPath) { return `${label} (inherited from ${decision.path})`; } return `${label} (${decision.path})`; @@ -56,7 +55,14 @@ export class TrustSelectorComponent extends Container { this.addChild(new Text(theme.fg("muted", options.cwd), 1, 0)); this.addChild(new Spacer(1)); this.addChild( - new Text(theme.fg("muted", `Saved decision: ${formatDecision(options.cwd, options.savedDecision)}`), 1, 0), + new Text( + theme.fg( + "muted", + `Saved decision: ${formatDecision(this.trustOptions[0]?.savedPath, options.savedDecision)}`, + ), + 1, + 0, + ), ); this.addChild( new Text(theme.fg("muted", `Current session: ${options.projectTrusted ? "trusted" : "untrusted"}`), 1, 0), diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index d50611afa..fc25792e4 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -86,7 +86,7 @@ import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.ts"; import type { SourceInfo } from "../../core/source-info.ts"; import { isInstallTelemetryEnabled } from "../../core/telemetry.ts"; import type { TruncationResult } from "../../core/tools/truncate.ts"; -import { hasProjectConfigDir, hasProjectTrustInputs, ProjectTrustStore } from "../../core/trust-manager.ts"; +import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.ts"; import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.ts"; import { copyToClipboard } from "../../utils/clipboard.ts"; import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts"; @@ -3271,7 +3271,7 @@ export class InteractiveMode { } private renderProjectTrustWarningIfNeeded(): void { - if (this.settingsManager.isProjectTrusted() || !hasProjectTrustInputs(this.sessionManager.getCwd())) { + if (this.settingsManager.isProjectTrusted() || !hasTrustRequiringProjectResources(this.sessionManager.getCwd())) { return; } @@ -4198,7 +4198,7 @@ export class InteractiveMode { if (this.autoTrustOnReloadCwd !== cwd) { return false; } - if (!this.settingsManager.isProjectTrusted() || !hasProjectConfigDir(cwd)) { + if (!this.settingsManager.isProjectTrusted() || !hasTrustRequiringProjectResources(cwd)) { return false; } diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index 94dbff85f..0dbe750c4 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -18,7 +18,7 @@ import { DefaultPackageManager } from "./core/package-manager.ts"; import { type AppMode, resolveProjectTrusted } from "./core/project-trust.ts"; import { DefaultResourceLoader } from "./core/resource-loader.ts"; import { SettingsManager } from "./core/settings-manager.ts"; -import { hasProjectTrustInputs, ProjectTrustStore } from "./core/trust-manager.ts"; +import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.ts"; import { spawnProcess } from "./utils/child-process.ts"; import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.ts"; import { @@ -452,13 +452,21 @@ async function createCommandSettingsManager(options: { cwd: string; agentDir: string; projectTrustOverride?: boolean; + useSavedProjectTrustOnly?: boolean; extensionFactories?: ExtensionFactory[]; }): Promise { const settingsManager = SettingsManager.create(options.cwd, options.agentDir, { projectTrusted: false }); const projectTrustWarnings: string[] = []; + const trustStore = new ProjectTrustStore(options.agentDir); + if (options.useSavedProjectTrustOnly) { + const savedProjectTrusted = trustStore.get(options.cwd) === true; + settingsManager.setProjectTrusted(options.projectTrustOverride ?? savedProjectTrusted); + return { settingsManager, projectTrustWarnings }; + } + const appMode = getCommandAppMode(); const extensionsResult = - options.projectTrustOverride === undefined && hasProjectTrustInputs(options.cwd) + options.projectTrustOverride === undefined && hasTrustRequiringProjectResources(options.cwd) ? await new DefaultResourceLoader({ cwd: options.cwd, agentDir: options.agentDir, @@ -472,7 +480,7 @@ async function createCommandSettingsManager(options: { const projectTrusted = await resolveProjectTrusted({ cwd: options.cwd, - trustStore: new ProjectTrustStore(options.agentDir), + trustStore, trustOverride: options.projectTrustOverride, defaultProjectTrust: settingsManager.getDefaultProjectTrust(), extensionsResult, @@ -576,6 +584,7 @@ export async function handlePackageCommand( cwd, agentDir, projectTrustOverride: options.projectTrustOverride, + useSavedProjectTrustOnly: options.command === "update", extensionFactories: runtimeOptions.extensionFactories, }); reportProjectTrustWarnings(projectTrustWarnings); diff --git a/packages/coding-agent/src/utils/version-check.ts b/packages/coding-agent/src/utils/version-check.ts index aec25bdb5..a6a4f4691 100644 --- a/packages/coding-agent/src/utils/version-check.ts +++ b/packages/coding-agent/src/utils/version-check.ts @@ -1,3 +1,4 @@ +import { compare, valid } from "semver"; import { getPiUserAgent } from "./pi-user-agent.ts"; const LATEST_VERSION_URL = "https://pi.dev/api/latest-version"; @@ -9,40 +10,13 @@ export interface LatestPiRelease { note?: string; } -interface ParsedVersion { - major: number; - minor: number; - patch: number; - prerelease?: string; -} - -function parsePackageVersion(version: string): ParsedVersion | undefined { - const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/); - if (!match) { - return undefined; - } - return { - major: Number.parseInt(match[1], 10), - minor: Number.parseInt(match[2], 10), - patch: Number.parseInt(match[3], 10), - prerelease: match[4], - }; -} - export function comparePackageVersions(leftVersion: string, rightVersion: string): number | undefined { - const left = parsePackageVersion(leftVersion); - const right = parsePackageVersion(rightVersion); + const left = valid(leftVersion.trim()); + const right = valid(rightVersion.trim()); if (!left || !right) { return undefined; } - - if (left.major !== right.major) return left.major - right.major; - if (left.minor !== right.minor) return left.minor - right.minor; - if (left.patch !== right.patch) return left.patch - right.patch; - if (left.prerelease === right.prerelease) return 0; - if (!left.prerelease) return 1; - if (!right.prerelease) return -1; - return left.prerelease.localeCompare(right.prerelease); + return compare(left, right); } export function isNewerPackageVersion(candidateVersion: string, currentVersion: string): boolean { diff --git a/packages/coding-agent/test/config-value-migration.test.ts b/packages/coding-agent/test/config-value-migration.test.ts index d0bb81257..8a273b536 100644 --- a/packages/coding-agent/test/config-value-migration.test.ts +++ b/packages/coding-agent/test/config-value-migration.test.ts @@ -37,7 +37,7 @@ describe("config value env var syntax migration", () => { } } - it("rewrites legacy uppercase auth.json API key values to explicit env references", () => { + it("leaves uppercase auth.json API key values unchanged", () => { const agentDir = createAgentDir(); fs.writeFileSync( path.join(agentDir, "auth.json"), @@ -61,19 +61,17 @@ describe("config value env var syntax migration", () => { string, Record >; - expect(migrated.anthropic.key).toBe("$ANTHROPIC_API_KEY"); + expect(migrated.anthropic.key).toBe("ANTHROPIC_API_KEY"); expect(migrated.openai.key).toBe("$OPENAI_API_KEY"); expect(migrated.opencode.key).toBe("public"); expect(migrated.github.access).toBe("ACCESS_TOKEN"); - const logMessage = String(logSpy.mock.calls[0]?.[0] ?? ""); - expect(logMessage).toContain("explicit $ENV_VAR syntax"); - expect(logMessage).toContain('auth.json["anthropic"].key: ANTHROPIC_API_KEY -> $ANTHROPIC_API_KEY'); + expect(logSpy).not.toHaveBeenCalled(); }); it.each([ ["malformed", '{\n "providers": {\n'], ["blank", ""], - ])("does not throw on %s models.json during config migration", (_name, content) => { + ])("does not throw on %s models.json during migrations", (_name, content) => { const agentDir = createAgentDir(); const modelsPath = path.join(agentDir, "models.json"); fs.writeFileSync(modelsPath, content, "utf-8"); @@ -87,71 +85,93 @@ describe("config value env var syntax migration", () => { expect(loadError).toContain(`File: ${modelsPath}`); }); - it("rewrites legacy uppercase models.json API key and header values", () => { + it("leaves uppercase models.json API key and header values unchanged", async () => { const agentDir = createAgentDir(); - fs.writeFileSync( - path.join(agentDir, "models.json"), - `${JSON.stringify( - { - providers: { - "custom-provider": { - baseUrl: "https://example.com/v1", - apiKey: "CUSTOM_API_KEY", - api: "openai-completions", - headers: { - "x-api-key": "HEADER_API_KEY", - "x-literal": "literal", - }, - models: [ - { - id: "model-a", - headers: { "x-model-key": "MODEL_API_KEY" }, + const envKeys = ["CUSTOM_API_KEY", "HEADER_API_KEY", "MODEL_API_KEY", "OVERRIDE_API_KEY"]; + const savedEnv: Record = {}; + for (const key of envKeys) { + savedEnv[key] = process.env[key]; + process.env[key] = `env-${key}`; + } + + try { + fs.writeFileSync( + path.join(agentDir, "models.json"), + `${JSON.stringify( + { + providers: { + "custom-provider": { + baseUrl: "https://example.com/v1", + apiKey: "CUSTOM_API_KEY", + api: "openai-completions", + headers: { + "x-api-key": "HEADER_API_KEY", + "x-literal": "literal", + }, + models: [ + { + id: "model-a", + headers: { "x-model-key": "MODEL_API_KEY" }, + }, + ], + modelOverrides: { + "model-b": { headers: { "x-override-key": "OVERRIDE_API_KEY" } }, }, - ], - modelOverrides: { - "model-b": { headers: { "x-override-key": "OVERRIDE_API_KEY" } }, }, }, }, + null, + 2, + )}\n`, + "utf-8", + ); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + withAgentDir(agentDir, () => runMigrations(agentDir)); + + const migrated = JSON.parse(fs.readFileSync(path.join(agentDir, "models.json"), "utf-8")) as { + providers: Record< + string, + { + apiKey?: string; + headers?: Record; + models?: Array<{ headers?: Record }>; + modelOverrides?: Record }>; + } + >; + }; + const provider = migrated.providers["custom-provider"]!; + expect(provider.apiKey).toBe("CUSTOM_API_KEY"); + expect(provider.headers?.["x-api-key"]).toBe("HEADER_API_KEY"); + expect(provider.headers?.["x-literal"]).toBe("literal"); + expect(provider.models?.[0]?.headers?.["x-model-key"]).toBe("MODEL_API_KEY"); + expect(provider.modelOverrides?.["model-b"]?.headers?.["x-override-key"]).toBe("OVERRIDE_API_KEY"); + expect(logSpy).not.toHaveBeenCalled(); + + const registry = ModelRegistry.create( + AuthStorage.create(path.join(agentDir, "auth.json")), + path.join(agentDir, "models.json"), + ); + const model = registry.find("custom-provider", "model-a"); + expect(model).toBeDefined(); + expect(await registry.getApiKeyForProvider("custom-provider")).toBe("CUSTOM_API_KEY"); + expect(await registry.getApiKeyAndHeaders(model!)).toMatchObject({ + ok: true, + apiKey: "CUSTOM_API_KEY", + headers: { + "x-api-key": "HEADER_API_KEY", + "x-literal": "literal", + "x-model-key": "MODEL_API_KEY", }, - null, - 2, - )}\n`, - "utf-8", - ); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - - withAgentDir(agentDir, () => runMigrations(agentDir)); - - const migrated = JSON.parse(fs.readFileSync(path.join(agentDir, "models.json"), "utf-8")) as { - providers: Record< - string, - { - apiKey?: string; - headers?: Record; - models?: Array<{ headers?: Record }>; - modelOverrides?: Record }>; + }); + } finally { + for (const key of envKeys) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; } - >; - }; - const provider = migrated.providers["custom-provider"]!; - expect(provider.apiKey).toBe("$CUSTOM_API_KEY"); - expect(provider.headers?.["x-api-key"]).toBe("$HEADER_API_KEY"); - expect(provider.headers?.["x-literal"]).toBe("literal"); - expect(provider.models?.[0]?.headers?.["x-model-key"]).toBe("$MODEL_API_KEY"); - expect(provider.modelOverrides?.["model-b"]?.headers?.["x-override-key"]).toBe("$OVERRIDE_API_KEY"); - const logMessage = String(logSpy.mock.calls[0]?.[0] ?? ""); - expect(logMessage).toContain( - 'models.json.providers["custom-provider"].apiKey: CUSTOM_API_KEY -> $CUSTOM_API_KEY', - ); - expect(logMessage).toContain( - 'models.json.providers["custom-provider"].headers["x-api-key"]: HEADER_API_KEY -> $HEADER_API_KEY', - ); - expect(logMessage).toContain( - 'models.json.providers["custom-provider"].models["model-a"].headers["x-model-key"]: MODEL_API_KEY -> $MODEL_API_KEY', - ); - expect(logMessage).toContain( - 'models.json.providers["custom-provider"].modelOverrides["model-b"].headers["x-override-key"]: OVERRIDE_API_KEY -> $OVERRIDE_API_KEY', - ); + } + } }); }); diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 95b70fbd1..3cba5264f 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -7,7 +7,6 @@ import { getOAuthProvider } from "@earendil-works/pi-ai/oauth"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; import { clearApiKeyCache, ModelRegistry, type ProviderConfigInput } from "../src/core/model-registry.ts"; -import { clearDeprecationWarningsForTests } from "../src/utils/deprecation.ts"; describe("ModelRegistry", () => { let tempDir: string; @@ -19,7 +18,6 @@ describe("ModelRegistry", () => { mkdirSync(tempDir, { recursive: true }); modelsJsonPath = join(tempDir, "models.json"); authStorage = AuthStorage.create(join(tempDir, "auth.json")); - clearDeprecationWarningsForTests(); }); afterEach(() => { @@ -27,7 +25,6 @@ describe("ModelRegistry", () => { rmSync(tempDir, { recursive: true }); } clearApiKeyCache(); - clearDeprecationWarningsForTests(); vi.restoreAllMocks(); }); @@ -896,26 +893,55 @@ describe("ModelRegistry", () => { expect(registry.getProviderDisplayName("oauth-provider")).toBe("OAuth Provider"); }); - test("registerProvider warns and temporarily treats uppercase apiKey as an env reference", async () => { - const originalEnv = process.env.CUSTOM_NAME; - process.env.CUSTOM_NAME = "legacy-env-key"; + test("registerProvider treats uppercase apiKey and headers as literals", async () => { + const envKeys = ["CUSTOM_NAME", "BEARER", "MODEL_TOKEN"]; + const savedEnv: Record = {}; + for (const key of envKeys) { + savedEnv[key] = process.env[key]; + process.env[key] = `env-${key}`; + } const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); try { const registry = ModelRegistry.create(authStorage, modelsJsonPath); - registry.registerProvider("legacy-provider", { + registry.registerProvider("literal-provider", { ...providerConfig("https://provider.test/v1", [{ id: "demo-model" }], "openai-completions"), apiKey: "CUSTOM_NAME", + headers: { Authorization: "BEARER" }, + models: [ + { + id: "demo-model", + name: "demo-model", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 100000, + maxTokens: 8000, + headers: { "x-model-token": "MODEL_TOKEN" }, + }, + ], }); - expect(await registry.getApiKeyForProvider("legacy-provider")).toBe("legacy-env-key"); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Pass "$CUSTOM_NAME" instead')); + expect(await registry.getApiKeyForProvider("literal-provider")).toBe("CUSTOM_NAME"); + const model = registry.find("literal-provider", "demo-model"); + expect(model).toBeDefined(); + expect(await registry.getApiKeyAndHeaders(model!)).toMatchObject({ + ok: true, + apiKey: "CUSTOM_NAME", + headers: { + Authorization: "BEARER", + "x-model-token": "MODEL_TOKEN", + }, + }); + expect(warnSpy).not.toHaveBeenCalled(); } finally { - if (originalEnv === undefined) { - delete process.env.CUSTOM_NAME; - } else { - process.env.CUSTOM_NAME = originalEnv; + for (const key of envKeys) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; + } } } }); diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index 25a55b359..1cff83f6f 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -1,4 +1,4 @@ -import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -202,6 +202,69 @@ describe("package commands", () => { } }); + it("does not prompt or ask extensions for project trust during update", async () => { + mkdirSync(join(projectDir, ".pi"), { recursive: true }); + writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ defaultProjectTrust: "always" })); + const fakeNpmPath = join(tempDir, "fake-project-npm.cjs"); + const recordPath = join(tempDir, "project-update.json"); + writeFileSync( + fakeNpmPath, + `const fs=require("node:fs");fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(process.argv.slice(2)));`, + ); + writeFileSync( + join(projectDir, ".pi", "settings.json"), + JSON.stringify({ packages: ["npm:fake-package"], npmCommand: [originalExecPath, fakeNpmPath] }), + ); + let projectTrustCalled = false; + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + try { + await expect( + main(["update", "--extensions"], { + extensionFactories: [ + (pi) => { + pi.on("project_trust", () => { + projectTrustCalled = true; + return { trusted: "yes" }; + }); + }, + ], + }), + ).resolves.toBeUndefined(); + + expect(projectTrustCalled).toBe(false); + expect(existsSync(recordPath)).toBe(false); + expect(process.exitCode).toBeUndefined(); + } finally { + logSpy.mockRestore(); + } + }); + + it("uses saved project trust during update", async () => { + mkdirSync(join(projectDir, ".pi"), { recursive: true }); + const fakeNpmPath = join(tempDir, "fake-trusted-project-npm.cjs"); + const recordPath = join(tempDir, "trusted-project-update.json"); + writeFileSync( + fakeNpmPath, + `const fs=require("node:fs");fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(process.argv.slice(2)));`, + ); + writeFileSync( + join(projectDir, ".pi", "settings.json"), + JSON.stringify({ packages: ["npm:fake-package"], npmCommand: [originalExecPath, fakeNpmPath] }), + ); + new ProjectTrustStore(agentDir).set(projectDir, true); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + try { + await expect(main(["update", "--extensions"])).resolves.toBeUndefined(); + + expect(existsSync(recordPath)).toBe(true); + expect(process.exitCode).toBeUndefined(); + } finally { + logSpy.mockRestore(); + } + }); + it("lets trust.json override default project trust", async () => { mkdirSync(join(projectDir, ".pi"), { recursive: true }); writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ defaultProjectTrust: "always" })); @@ -223,6 +286,7 @@ describe("package commands", () => { it("blocks local package changes when project is untrusted", async () => { mkdirSync(join(projectDir, ".pi"), { recursive: true }); + writeFileSync(join(projectDir, ".pi", "settings.json"), "{}"); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); try { diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index cd1f4532d..9b1b0d7f3 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -1128,8 +1128,17 @@ Content`, }); it("should parse package source types from docs examples", () => { - expect((packageManager as any).parseSource("npm:@scope/pkg@1.2.3").type).toBe("npm"); - expect((packageManager as any).parseSource("npm:pkg").type).toBe("npm"); + const parseNpm = (source: string) => { + const parsed = (packageManager as any).parseSource(source); + if (parsed.type !== "npm") { + throw new Error(`Expected npm source: ${source}`); + } + return parsed; + }; + + expect(parseNpm("npm:@scope/pkg@1.2.3").pinned).toBe(true); + expect(parseNpm("npm:@scope/pkg@^1.2.3").pinned).toBe(false); + expect(parseNpm("npm:pkg").pinned).toBe(false); expect((packageManager as any).parseSource("git:github.com/user/repo@v1").type).toBe("git"); expect((packageManager as any).parseSource("https://github.com/user/repo@v1").type).toBe("git"); @@ -2052,25 +2061,27 @@ export default function(api) { api.registerTool({ name: "test", description: "te }); describe("offline mode and network timeouts", () => { - it("should update project npm packages using @latest when newer version is available", async () => { + it("should update npm range packages using the configured spec", async () => { const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example"); mkdirSync(installedPath, { recursive: true }); writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "1.0.0" })); - settingsManager.setProjectPackages(["npm:example"]); + settingsManager.setProjectPackages(["npm:example@^1.0.0"]); - const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.2.3"'); + const runCommandCaptureSpy = vi + .spyOn(packageManager as any, "runCommandCapture") + .mockResolvedValue('["1.0.0","1.2.0"]'); const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined); await packageManager.update("npm:example"); expect(runCommandCaptureSpy).toHaveBeenCalledWith( "npm", - ["view", "example", "version", "--json"], + ["view", "example@^1.0.0", "version", "--json"], expect.objectContaining({ cwd: tempDir, timeoutMs: expect.any(Number) }), ); expect(runCommandSpy).toHaveBeenCalledWith( "npm", - ["install", "example@latest", "--prefix", join(tempDir, ".pi", "npm"), "--legacy-peer-deps"], + ["install", "example@^1.0.0", "--prefix", join(tempDir, ".pi", "npm"), "--legacy-peer-deps"], undefined, ); }); @@ -2078,17 +2089,19 @@ export default function(api) { api.registerTool({ name: "test", description: "te it("should skip project npm update when installed version matches latest", async () => { const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example"); mkdirSync(installedPath, { recursive: true }); - writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "1.2.3" })); - settingsManager.setProjectPackages(["npm:example"]); + writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "1.3.1" })); + settingsManager.setProjectPackages(["npm:example@^1.0.0"]); - const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.2.3"'); + const runCommandCaptureSpy = vi + .spyOn(packageManager as any, "runCommandCapture") + .mockResolvedValue('["1.0.0","1.3.1","1.0.2"]'); const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined); await packageManager.update("npm:example"); expect(runCommandCaptureSpy).toHaveBeenCalledWith( "npm", - ["view", "example", "version", "--json"], + ["view", "example@^1.0.0", "version", "--json"], expect.objectContaining({ cwd: tempDir, timeoutMs: expect.any(Number) }), ); expect(runCommandSpy).not.toHaveBeenCalled(); @@ -2298,11 +2311,12 @@ export default function(api) { api.registerTool({ name: "test", description: "te }); it("should not run npm view during resolve for installed unpinned packages", async () => { + process.env.PI_OFFLINE = "1"; const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example"); mkdirSync(join(installedPath, "extensions"), { recursive: true }); writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "1.0.0" })); writeFileSync(join(installedPath, "extensions", "index.ts"), "export default function() {};"); - settingsManager.setProjectPackages(["npm:example"]); + settingsManager.setProjectPackages(["npm:example@^1.0.0"]); const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture"); diff --git a/packages/coding-agent/test/resource-loader.test.ts b/packages/coding-agent/test/resource-loader.test.ts index 7258b1219..72cee79a7 100644 --- a/packages/coding-agent/test/resource-loader.test.ts +++ b/packages/coding-agent/test/resource-loader.test.ts @@ -376,7 +376,7 @@ Content`, expect(loader.getSystemPrompt()).toBe("You are a helpful assistant."); }); - it("should skip trust-gated project resources when project is not trusted", async () => { + it("should skip project resources that require trust when project is not trusted", async () => { const piDir = join(cwd, ".pi"); const extensionsDir = join(piDir, "extensions"); const skillDir = join(piDir, "skills", "project-skill"); diff --git a/packages/coding-agent/test/suite/regressions/5661-uppercase-header-values.test.ts b/packages/coding-agent/test/suite/regressions/5661-uppercase-header-values.test.ts new file mode 100644 index 000000000..e625a3c6c --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5661-uppercase-header-values.test.ts @@ -0,0 +1,91 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { ENV_AGENT_DIR } from "../../../src/config.ts"; +import { AuthStorage } from "../../../src/core/auth-storage.ts"; +import { ModelRegistry } from "../../../src/core/model-registry.ts"; +import { runMigrations } from "../../../src/migrations.ts"; +import { createHarness } from "../harness.ts"; + +describe("regression #5661: uppercase models.json header values", () => { + const cleanups: Array<() => void> = []; + + afterEach(() => { + while (cleanups.length > 0) { + cleanups.pop()?.(); + } + }); + + function withAgentDir(agentDir: string, fn: () => void): void { + const previousAgentDir = process.env[ENV_AGENT_DIR]; + process.env[ENV_AGENT_DIR] = agentDir; + try { + fn(); + } finally { + if (previousAgentDir === undefined) { + delete process.env[ENV_AGENT_DIR]; + } else { + process.env[ENV_AGENT_DIR] = previousAgentDir; + } + } + } + + it("keeps uppercase header strings as literals during startup migrations", async () => { + const harness = await createHarness({ withConfiguredAuth: false }); + cleanups.push(harness.cleanup); + + const envKeys = ["CUSTOM_API_KEY", "BEARER"]; + const savedEnv: Record = {}; + for (const key of envKeys) { + savedEnv[key] = process.env[key]; + process.env[key] = `env-${key}`; + } + cleanups.push(() => { + for (const key of envKeys) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; + } + } + }); + + const modelsPath = join(harness.tempDir, "models.json"); + writeFileSync( + modelsPath, + `${JSON.stringify( + { + providers: { + "my-provider": { + baseUrl: "https://example.com/v1", + apiKey: "CUSTOM_API_KEY", + api: "openai-completions", + headers: { Authorization: "BEARER" }, + models: [{ id: "my-model" }], + }, + }, + }, + null, + 2, + )}\n`, + "utf-8", + ); + + withAgentDir(harness.tempDir, () => runMigrations(harness.tempDir)); + + const migrated = JSON.parse(readFileSync(modelsPath, "utf-8")) as { + providers: Record }>; + }; + expect(migrated.providers["my-provider"]?.apiKey).toBe("CUSTOM_API_KEY"); + expect(migrated.providers["my-provider"]?.headers?.Authorization).toBe("BEARER"); + + const registry = ModelRegistry.create(AuthStorage.create(join(harness.tempDir, "auth.json")), modelsPath); + const model = registry.find("my-provider", "my-model"); + expect(model).toBeDefined(); + expect(await registry.getApiKeyAndHeaders(model!)).toMatchObject({ + ok: true, + apiKey: "CUSTOM_API_KEY", + headers: { Authorization: "BEARER" }, + }); + }); +}); diff --git a/packages/coding-agent/test/trust-manager.test.ts b/packages/coding-agent/test/trust-manager.test.ts index 2716da363..01500bac2 100644 --- a/packages/coding-agent/test/trust-manager.test.ts +++ b/packages/coding-agent/test/trust-manager.test.ts @@ -1,13 +1,8 @@ -import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { - getProjectTrustPath, - hasProjectConfigDir, - hasProjectTrustInputs, - ProjectTrustStore, -} from "../src/core/trust-manager.ts"; +import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../src/core/trust-manager.ts"; describe("ProjectTrustStore", () => { let tempDir: string; @@ -26,86 +21,47 @@ describe("ProjectTrustStore", () => { rmSync(tempDir, { recursive: true, force: true }); }); - it("stores decisions per cwd", () => { - const store = new ProjectTrustStore(agentDir); - - expect(store.get(cwd)).toBeNull(); - expect(store.getEntry(cwd)).toBeNull(); - store.set(cwd, true); - expect(store.get(cwd)).toBe(true); - expect(store.getEntry(cwd)).toEqual({ path: getProjectTrustPath(cwd), decision: true }); - store.set(cwd, false); - expect(store.get(cwd)).toBe(false); - expect(store.getEntry(cwd)).toEqual({ path: getProjectTrustPath(cwd), decision: false }); - store.set(cwd, null); - expect(store.get(cwd)).toBeNull(); - expect(store.getEntry(cwd)).toBeNull(); - }); - - it("inherits the closest saved decision from parent directories", () => { - const store = new ProjectTrustStore(agentDir); - const parentDir = join(tempDir, "trusted-parent"); - const childDir = join(parentDir, "project"); - const grandchildDir = join(childDir, "nested"); - mkdirSync(grandchildDir, { recursive: true }); - - store.set(parentDir, true); - expect(store.get(childDir)).toBe(true); - expect(store.getEntry(childDir)).toEqual({ path: getProjectTrustPath(parentDir), decision: true }); - expect(store.get(grandchildDir)).toBe(true); - expect(store.getEntry(grandchildDir)).toEqual({ path: getProjectTrustPath(parentDir), decision: true }); - - store.set(childDir, false); - expect(store.get(grandchildDir)).toBe(false); - expect(store.getEntry(grandchildDir)).toEqual({ path: getProjectTrustPath(childDir), decision: false }); - }); - - it("can clear a child override to inherit parent trust", () => { + it("stores decisions and inherits from parent directories", () => { const store = new ProjectTrustStore(agentDir); const parentDir = join(tempDir, "trusted-parent"); const childDir = join(parentDir, "project"); mkdirSync(childDir, { recursive: true }); + expect(store.get(childDir)).toBeNull(); store.set(parentDir, true); - store.set(childDir, false); - expect(store.getEntry(childDir)).toEqual({ path: getProjectTrustPath(childDir), decision: false }); - - store.setMany([ - { path: parentDir, decision: true }, - { path: childDir, decision: null }, - ]); expect(store.get(childDir)).toBe(true); - expect(store.getEntry(childDir)).toEqual({ path: getProjectTrustPath(parentDir), decision: true }); + store.set(childDir, false); + expect(store.get(childDir)).toBe(false); + store.set(childDir, null); + expect(store.get(childDir)).toBe(true); }); - it("fails loudly without overwriting malformed trust stores", () => { - const trustPath = join(agentDir, "trust.json"); - writeFileSync(trustPath, "{not json", "utf-8"); - const store = new ProjectTrustStore(agentDir); + it("detects trust-requiring project resources", () => { + const originalHome = process.env.HOME; + process.env.HOME = tempDir; + try { + mkdirSync(join(tempDir, ".pi", "agent"), { recursive: true }); + mkdirSync(join(tempDir, ".agents", "skills"), { recursive: true }); + expect(hasTrustRequiringProjectResources(tempDir)).toBe(false); + expect(hasTrustRequiringProjectResources(cwd)).toBe(false); - expect(() => store.get(cwd)).toThrow(/Failed to read trust store/); - expect(() => store.set(cwd, true)).toThrow(/Failed to read trust store/); - expect(readFileSync(trustPath, "utf-8")).toBe("{not json"); - }); + writeFileSync(join(tempDir, ".pi", "settings.json"), "{}"); + expect(hasTrustRequiringProjectResources(tempDir)).toBe(true); + rmSync(join(tempDir, ".pi", "settings.json"), { force: true }); - it("detects project trust inputs", () => { - expect(hasProjectConfigDir(cwd)).toBe(false); - expect(hasProjectTrustInputs(cwd)).toBe(false); + mkdirSync(join(cwd, ".pi"), { recursive: true }); + writeFileSync(join(cwd, ".pi", "settings.json"), "{}"); + expect(hasTrustRequiringProjectResources(cwd)).toBe(true); - mkdirSync(join(cwd, ".pi"), { recursive: true }); - expect(hasProjectConfigDir(cwd)).toBe(true); - expect(hasProjectTrustInputs(cwd)).toBe(true); - rmSync(join(cwd, ".pi"), { recursive: true, force: true }); - - writeFileSync(join(cwd, "AGENTS.md"), "Project instructions"); - expect(hasProjectTrustInputs(cwd)).toBe(false); - rmSync(join(cwd, "AGENTS.md"), { force: true }); - - writeFileSync(join(cwd, "CLAUDE.md"), "Legacy project instructions"); - expect(hasProjectTrustInputs(cwd)).toBe(false); - rmSync(join(cwd, "CLAUDE.md"), { force: true }); - - mkdirSync(join(cwd, ".agents", "skills"), { recursive: true }); - expect(hasProjectTrustInputs(cwd)).toBe(true); + rmSync(join(cwd, ".pi"), { recursive: true, force: true }); + mkdirSync(join(cwd, ".agents", "skills"), { recursive: true }); + expect(hasTrustRequiringProjectResources(cwd)).toBe(true); + } finally { + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + } }); }); diff --git a/packages/coding-agent/test/version-check.test.ts b/packages/coding-agent/test/version-check.test.ts index 70197fa59..b294c6273 100644 --- a/packages/coding-agent/test/version-check.test.ts +++ b/packages/coding-agent/test/version-check.test.ts @@ -29,6 +29,7 @@ describe("version checks", () => { expect(comparePackageVersions("0.70.6", "0.70.5")).toBeGreaterThan(0); expect(comparePackageVersions("0.70.5", "0.70.5")).toBe(0); expect(comparePackageVersions("0.70.4", "0.70.5")).toBeLessThan(0); + expect(comparePackageVersions("5.0.0-beta.20", "5.0.0-beta.9")).toBeGreaterThan(0); expect(isNewerPackageVersion("0.70.5", "0.70.5")).toBe(false); expect(isNewerPackageVersion("0.70.6", "0.70.5")).toBe(true); }); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index b5e277a93..69b0529a5 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,11 +2,17 @@ ## [Unreleased] +## [0.79.3] - 2026-06-13 + +## [0.79.2] - 2026-06-12 + ### Fixed - Fixed Markdown source list marker preservation to include unordered markers, so standalone `+` user messages no longer render as `-` ([#5657](https://github.com/earendil-works/pi/issues/5657)). - Fixed slash-separated fuzzy queries so provider/model completions remain matchable after insertion. - Fixed WezTerm inline Kitty image rendering so reserved row clears do not erase all but the top strip of tool image previews ([#5618](https://github.com/earendil-works/pi/issues/5618)). +- Fixed editor wrapping for CJK text to break at character boundaries instead of leaving large trailing gaps ([#5585](https://github.com/earendil-works/pi/pull/5585) by [@haoqixu](https://github.com/haoqixu)). +- Fixed loose Markdown list rendering to preserve blank-line separation between list items ([#5562](https://github.com/earendil-works/pi/pull/5562) by [@Perlence](https://github.com/Perlence)). ## [0.79.1] - 2026-06-09 diff --git a/packages/tui/package.json b/packages/tui/package.json index 94ba86ad6..a808a640c 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.1", + "version": "0.79.3", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js",