13
AGENTS.md
13
AGENTS.md
@@ -153,17 +153,18 @@ Create provider file exporting:
|
|||||||
|
|
||||||
### 5. Tests (`packages/ai/test/`)
|
### 5. Tests (`packages/ai/test/`)
|
||||||
|
|
||||||
Add provider to: `stream.test.ts`, `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `image-limits.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`.
|
- Always add the provider to `stream.test.ts` with at least one representative model, even if it reuses an existing API implementation such as `openai-completions`.
|
||||||
|
- Add the provider to the broader provider matrix where applicable: `tokens.test.ts`, `abort.test.ts`, `empty.test.ts`, `context-overflow.test.ts`, `image-limits.test.ts`, `unicode-surrogate.test.ts`, `tool-call-without-result.test.ts`, `image-tool-result.test.ts`, `total-tokens.test.ts`, `cross-provider-handoff.test.ts`.
|
||||||
For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (for example GPT and Claude), add at least one pair per family.
|
- For `cross-provider-handoff.test.ts`, add at least one provider/model pair. If the provider exposes multiple model families (for example GPT and Claude), add at least one pair per family.
|
||||||
|
- For non-standard auth, create utility (e.g., `bedrock-utils.ts`) with credential detection.
|
||||||
For non-standard auth, create utility (e.g., `bedrock-utils.ts`) with credential detection.
|
|
||||||
|
|
||||||
### 6. Coding Agent (`packages/coding-agent/`)
|
### 6. Coding Agent (`packages/coding-agent/`)
|
||||||
|
|
||||||
- `src/core/model-resolver.ts`: Add default model ID to `DEFAULT_MODELS`
|
- `src/core/model-resolver.ts`: Add default model ID to `defaultModelPerProvider`
|
||||||
|
- `src/modes/interactive/interactive-mode.ts`: Add API-key login display name to `API_KEY_LOGIN_PROVIDERS` so `/login` shows the provider for built-in API-key auth.
|
||||||
- `src/cli/args.ts`: Add env var documentation
|
- `src/cli/args.ts`: Add env var documentation
|
||||||
- `README.md`: Add provider setup instructions
|
- `README.md`: Add provider setup instructions
|
||||||
|
- `docs/providers.md`: Add setup instructions, env var, and `auth.json` key
|
||||||
|
|
||||||
### 7. Documentation
|
### 7. Documentation
|
||||||
|
|
||||||
|
|||||||
88
package-lock.json
generated
88
package-lock.json
generated
@@ -1575,31 +1575,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard": {
|
"node_modules/@mariozechner/clipboard": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.3.tgz",
|
||||||
"integrity": "sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA==",
|
"integrity": "sha512-e7jASirzfm+ROiOGFh843+cFZTy3DfzP+jldCvh8RnEk0C3QihDTn7dd7Yh7KAJydwIJ18FJSZ2swHvCJhk18g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@mariozechner/clipboard-darwin-arm64": "0.3.2",
|
"@mariozechner/clipboard-darwin-arm64": "0.3.3",
|
||||||
"@mariozechner/clipboard-darwin-universal": "0.3.2",
|
"@mariozechner/clipboard-darwin-universal": "0.3.3",
|
||||||
"@mariozechner/clipboard-darwin-x64": "0.3.2",
|
"@mariozechner/clipboard-darwin-x64": "0.3.3",
|
||||||
"@mariozechner/clipboard-linux-arm64-gnu": "0.3.2",
|
"@mariozechner/clipboard-linux-arm64-gnu": "0.3.3",
|
||||||
"@mariozechner/clipboard-linux-arm64-musl": "0.3.2",
|
"@mariozechner/clipboard-linux-arm64-musl": "0.3.3",
|
||||||
"@mariozechner/clipboard-linux-riscv64-gnu": "0.3.2",
|
"@mariozechner/clipboard-linux-riscv64-gnu": "0.3.3",
|
||||||
"@mariozechner/clipboard-linux-x64-gnu": "0.3.2",
|
"@mariozechner/clipboard-linux-x64-gnu": "0.3.3",
|
||||||
"@mariozechner/clipboard-linux-x64-musl": "0.3.2",
|
"@mariozechner/clipboard-linux-x64-musl": "0.3.3",
|
||||||
"@mariozechner/clipboard-win32-arm64-msvc": "0.3.2",
|
"@mariozechner/clipboard-win32-arm64-msvc": "0.3.3",
|
||||||
"@mariozechner/clipboard-win32-x64-msvc": "0.3.2"
|
"@mariozechner/clipboard-win32-x64-msvc": "0.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard-darwin-arm64": {
|
"node_modules/@mariozechner/clipboard-darwin-arm64": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.3.tgz",
|
||||||
"integrity": "sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==",
|
"integrity": "sha512-+zhuZGXqVrdkbIRdnwiZNbTJ7V3elq/A+C5d5laJoyhJgWs41eO5NUMkBkj6f23F2L4PRXEhdn5/ktlPx+bG3Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1613,9 +1613,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard-darwin-universal": {
|
"node_modules/@mariozechner/clipboard-darwin-universal": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.3.tgz",
|
||||||
"integrity": "sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==",
|
"integrity": "sha512-x9aRfTyndVqpEQ44LNNCK/EXZd9y8rWkLQgNhmWpby9PXrjPhNxfjUc2Db4mt4nJjU/4zzO8F5v/XyzlUGSdhQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1626,9 +1626,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard-darwin-x64": {
|
"node_modules/@mariozechner/clipboard-darwin-x64": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.3.tgz",
|
||||||
"integrity": "sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==",
|
"integrity": "sha512-6ut/NawB0KiYPCwrirgNp6Br62LntL978q7G6d/Rs2pmPvQb53bP96eUMYl+Y3a7Qk13bGZ4w9rVPFxRE9m9ag==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1642,9 +1642,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard-linux-arm64-gnu": {
|
"node_modules/@mariozechner/clipboard-linux-arm64-gnu": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.3.tgz",
|
||||||
"integrity": "sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==",
|
"integrity": "sha512-gf3dH4kBddU1AOyHVB53mjLUFfJAKlTmxTMw51jdeg7eE7IjfEBXVvM4bifMtBxbWkT0eA0FUZ1C0KQ6Z5l6pw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1658,9 +1658,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard-linux-arm64-musl": {
|
"node_modules/@mariozechner/clipboard-linux-arm64-musl": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.3.tgz",
|
||||||
"integrity": "sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==",
|
"integrity": "sha512-o1paj2+zmAQ/LaPS85XJCxhNowNQpxYM2cGY6pWvB5Kqmz6hZjl6CzDg5tbf1hZkn/Em6jpOaE2UtMxKdELBDA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1674,9 +1674,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard-linux-riscv64-gnu": {
|
"node_modules/@mariozechner/clipboard-linux-riscv64-gnu": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.3.tgz",
|
||||||
"integrity": "sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==",
|
"integrity": "sha512-dkEhE4ekePJwMbBq9HP1//CFMNmDzA/iV9AXqBfvL5CWmmDIRXqh4A3YZt3tWO/HdMerX+xNCEiR7WiOsIG+UA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -1690,9 +1690,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard-linux-x64-gnu": {
|
"node_modules/@mariozechner/clipboard-linux-x64-gnu": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.3.tgz",
|
||||||
"integrity": "sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==",
|
"integrity": "sha512-lT2yANtTLlEtFBIH3uGoRa/CQas/eBoLNi3qr9axQFoRgF4RGPSJ66yHOSnMECBneTIb1Iqv3UxokTfX27CdoQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1706,9 +1706,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard-linux-x64-musl": {
|
"node_modules/@mariozechner/clipboard-linux-x64-musl": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.3.tgz",
|
||||||
"integrity": "sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==",
|
"integrity": "sha512-saq/MCB0QHK/7ZZLjAZ0QkbY944dyjOsur8gneGCfMitt+GOiE1CU4OUipHC4b6x8UDY9bRLsR4aBaxu22OFPA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1722,9 +1722,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard-win32-arm64-msvc": {
|
"node_modules/@mariozechner/clipboard-win32-arm64-msvc": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.3.tgz",
|
||||||
"integrity": "sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==",
|
"integrity": "sha512-cGuvSj0/2X2w983yEcKw+i+r1EBej6ZZIN+fXG3eY2G/HaIQpbXpLvMxKyZ9LKtbZx+Z6q/gELEoSBMLML6BaQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1738,9 +1738,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mariozechner/clipboard-win32-x64-msvc": {
|
"node_modules/@mariozechner/clipboard-win32-x64-msvc": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.3.tgz",
|
||||||
"integrity": "sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==",
|
"integrity": "sha512-5hvaEq/bgYovTIGx43O/S7loIHYV3ue90WcV1dz0wdMXroVKZKeU/yfwM0PALQA1OcrEHiGXGySFReXr72lGtA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8659,7 +8659,7 @@
|
|||||||
"node": ">=20.6.0"
|
"node": ">=20.6.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@mariozechner/clipboard": "^0.3.2"
|
"@mariozechner/clipboard": "^0.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/coding-agent/examples/extensions/custom-provider-anthropic": {
|
"packages/coding-agent/examples/extensions/custom-provider-anthropic": {
|
||||||
|
|||||||
@@ -2,8 +2,13 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added DeepSeek as a built-in OpenAI-compatible provider with V4 Flash and V4 Pro models and `DEEPSEEK_API_KEY` authentication.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed DeepSeek V4 session replay 400 errors by adding `thinkingFormat: "deepseek"` (sends `thinking: { type }` + `reasoning_effort`), a `reasoningEffortMap`, and `requiresReasoningContentOnAssistantMessages` compat that injects empty `reasoning_content` on all replayed assistant messages when reasoning is enabled ([#3636](https://github.com/badlogic/pi-mono/issues/3636))
|
||||||
- Fixed GPT-5.5 generated context window metadata to use the observed 272k limit.
|
- Fixed GPT-5.5 generated context window metadata to use the observed 272k limit.
|
||||||
|
|
||||||
## [0.70.0] - 2026-04-23
|
## [0.70.0] - 2026-04-23
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an
|
|||||||
- **OpenAI**
|
- **OpenAI**
|
||||||
- **Azure OpenAI (Responses)**
|
- **Azure OpenAI (Responses)**
|
||||||
- **OpenAI Codex** (ChatGPT Plus/Pro subscription, requires OAuth, see below)
|
- **OpenAI Codex** (ChatGPT Plus/Pro subscription, requires OAuth, see below)
|
||||||
|
- **DeepSeek**
|
||||||
- **Anthropic**
|
- **Anthropic**
|
||||||
- **Google**
|
- **Google**
|
||||||
- **Vertex AI** (Gemini via Vertex AI)
|
- **Vertex AI** (Gemini via Vertex AI)
|
||||||
@@ -857,7 +858,8 @@ interface OpenAICompletionsCompat {
|
|||||||
requiresToolResultName?: boolean; // Whether tool results require the `name` field (default: false)
|
requiresToolResultName?: boolean; // Whether tool results require the `name` field (default: false)
|
||||||
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
|
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
|
||||||
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
|
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
|
||||||
thinkingFormat?: 'openai' | 'zai' | 'qwen' | 'qwen-chat-template'; // Format for reasoning param: 'openai' uses reasoning_effort, 'zai' uses thinking: { type: "enabled" }, 'qwen' uses enable_thinking: boolean, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking (default: openai)
|
requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
|
||||||
|
thinkingFormat?: 'openai' | 'deepseek' | 'zai' | 'qwen' | 'qwen-chat-template'; // Format for reasoning param: 'openai' uses reasoning_effort, 'deepseek' uses thinking: { type } plus reasoning_effort, 'zai' uses enable_thinking, 'qwen' uses enable_thinking, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking (default: openai)
|
||||||
cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content
|
cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content
|
||||||
openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {})
|
openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {})
|
||||||
vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {})
|
vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {})
|
||||||
@@ -1020,6 +1022,7 @@ In Node.js environments, you can set environment variables to avoid passing API
|
|||||||
| OpenAI | `OPENAI_API_KEY` |
|
| OpenAI | `OPENAI_API_KEY` |
|
||||||
| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` or `AZURE_OPENAI_RESOURCE_NAME` (optional `AZURE_OPENAI_API_VERSION`, `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` like `model=deployment,model2=deployment2`) |
|
| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` or `AZURE_OPENAI_RESOURCE_NAME` (optional `AZURE_OPENAI_API_VERSION`, `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` like `model=deployment,model2=deployment2`) |
|
||||||
| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` |
|
| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` |
|
||||||
|
| DeepSeek | `DEEPSEEK_API_KEY` |
|
||||||
| Google | `GEMINI_API_KEY` |
|
| Google | `GEMINI_API_KEY` |
|
||||||
| Vertex AI | `GOOGLE_CLOUD_API_KEY` or `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |
|
| Vertex AI | `GOOGLE_CLOUD_API_KEY` or `GOOGLE_CLOUD_PROJECT` (or `GCLOUD_PROJECT`) + `GOOGLE_CLOUD_LOCATION` + ADC |
|
||||||
| Mistral | `MISTRAL_API_KEY` |
|
| Mistral | `MISTRAL_API_KEY` |
|
||||||
|
|||||||
@@ -1012,6 +1012,57 @@ async function generateModels() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deepseekCompat: OpenAICompletionsCompat = {
|
||||||
|
requiresReasoningContentOnAssistantMessages: true,
|
||||||
|
thinkingFormat: "deepseek",
|
||||||
|
reasoningEffortMap: {
|
||||||
|
minimal: "high",
|
||||||
|
low: "high",
|
||||||
|
medium: "high",
|
||||||
|
high: "high",
|
||||||
|
xhigh: "max",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const deepseekV4Models: Model<"openai-completions">[] = [
|
||||||
|
{
|
||||||
|
id: "deepseek-v4-flash",
|
||||||
|
name: "DeepSeek V4 Flash",
|
||||||
|
api: "openai-completions",
|
||||||
|
baseUrl: "https://api.deepseek.com",
|
||||||
|
provider: "deepseek",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.14,
|
||||||
|
output: 0.28,
|
||||||
|
cacheRead: 0.028,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxTokens: 384000,
|
||||||
|
compat: deepseekCompat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deepseek-v4-pro",
|
||||||
|
name: "DeepSeek V4 Pro",
|
||||||
|
api: "openai-completions",
|
||||||
|
baseUrl: "https://api.deepseek.com",
|
||||||
|
provider: "deepseek",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 1.74,
|
||||||
|
output: 3.48,
|
||||||
|
cacheRead: 0.145,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxTokens: 384000,
|
||||||
|
compat: deepseekCompat,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
allModels.push(...deepseekV4Models);
|
||||||
|
|
||||||
const minimaxDirectSupportedIds = new Set(["MiniMax-M2.7", "MiniMax-M2.7-highspeed"]);
|
const minimaxDirectSupportedIds = new Set(["MiniMax-M2.7", "MiniMax-M2.7-highspeed"]);
|
||||||
|
|
||||||
for (const candidate of allModels) {
|
for (const candidate of allModels) {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ function getApiKeyEnvVars(provider: string): readonly string[] | undefined {
|
|||||||
const envMap: Record<string, string> = {
|
const envMap: Record<string, string> = {
|
||||||
openai: "OPENAI_API_KEY",
|
openai: "OPENAI_API_KEY",
|
||||||
"azure-openai-responses": "AZURE_OPENAI_API_KEY",
|
"azure-openai-responses": "AZURE_OPENAI_API_KEY",
|
||||||
|
deepseek: "DEEPSEEK_API_KEY",
|
||||||
google: "GEMINI_API_KEY",
|
google: "GEMINI_API_KEY",
|
||||||
"google-vertex": "GOOGLE_CLOUD_API_KEY",
|
"google-vertex": "GOOGLE_CLOUD_API_KEY",
|
||||||
groq: "GROQ_API_KEY",
|
groq: "GROQ_API_KEY",
|
||||||
|
|||||||
@@ -328,6 +328,40 @@ export const MODELS = {
|
|||||||
contextWindow: 1000000,
|
contextWindow: 1000000,
|
||||||
maxTokens: 64000,
|
maxTokens: 64000,
|
||||||
} satisfies Model<"bedrock-converse-stream">,
|
} satisfies Model<"bedrock-converse-stream">,
|
||||||
|
"au.anthropic.claude-opus-4-6-v1": {
|
||||||
|
id: "au.anthropic.claude-opus-4-6-v1",
|
||||||
|
name: "AU Anthropic Claude Opus 4.6",
|
||||||
|
api: "bedrock-converse-stream",
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 16.5,
|
||||||
|
output: 82.5,
|
||||||
|
cacheRead: 0.5,
|
||||||
|
cacheWrite: 6.25,
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxTokens: 128000,
|
||||||
|
} satisfies Model<"bedrock-converse-stream">,
|
||||||
|
"au.anthropic.claude-sonnet-4-6": {
|
||||||
|
id: "au.anthropic.claude-sonnet-4-6",
|
||||||
|
name: "AU Anthropic Claude Sonnet 4.6",
|
||||||
|
api: "bedrock-converse-stream",
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 3.3,
|
||||||
|
output: 16.5,
|
||||||
|
cacheRead: 0.33,
|
||||||
|
cacheWrite: 4.125,
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxTokens: 128000,
|
||||||
|
} satisfies Model<"bedrock-converse-stream">,
|
||||||
"deepseek.r1-v1:0": {
|
"deepseek.r1-v1:0": {
|
||||||
id: "deepseek.r1-v1:0",
|
id: "deepseek.r1-v1:0",
|
||||||
name: "DeepSeek-R1",
|
name: "DeepSeek-R1",
|
||||||
@@ -2715,6 +2749,44 @@ export const MODELS = {
|
|||||||
maxTokens: 40000,
|
maxTokens: 40000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
},
|
},
|
||||||
|
"deepseek": {
|
||||||
|
"deepseek-v4-flash": {
|
||||||
|
id: "deepseek-v4-flash",
|
||||||
|
name: "DeepSeek V4 Flash",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "deepseek",
|
||||||
|
baseUrl: "https://api.deepseek.com",
|
||||||
|
compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek","reasoningEffortMap":{"minimal":"high","low":"high","medium":"high","high":"high","xhigh":"max"}},
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.14,
|
||||||
|
output: 0.28,
|
||||||
|
cacheRead: 0.028,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxTokens: 384000,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
|
"deepseek-v4-pro": {
|
||||||
|
id: "deepseek-v4-pro",
|
||||||
|
name: "DeepSeek V4 Pro",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "deepseek",
|
||||||
|
baseUrl: "https://api.deepseek.com",
|
||||||
|
compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek","reasoningEffortMap":{"minimal":"high","low":"high","medium":"high","high":"high","xhigh":"max"}},
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 1.74,
|
||||||
|
output: 3.48,
|
||||||
|
cacheRead: 0.145,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxTokens: 384000,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
|
},
|
||||||
"fireworks": {
|
"fireworks": {
|
||||||
"accounts/fireworks/models/deepseek-v3p1": {
|
"accounts/fireworks/models/deepseek-v3p1": {
|
||||||
id: "accounts/fireworks/models/deepseek-v3p1",
|
id: "accounts/fireworks/models/deepseek-v3p1",
|
||||||
@@ -8162,6 +8234,40 @@ export const MODELS = {
|
|||||||
contextWindow: 163840,
|
contextWindow: 163840,
|
||||||
maxTokens: 65536,
|
maxTokens: 65536,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"deepseek/deepseek-v4-flash": {
|
||||||
|
id: "deepseek/deepseek-v4-flash",
|
||||||
|
name: "DeepSeek: DeepSeek V4 Flash",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.14,
|
||||||
|
output: 0.28,
|
||||||
|
cacheRead: 0.028,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1048576,
|
||||||
|
maxTokens: 384000,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
|
"deepseek/deepseek-v4-pro": {
|
||||||
|
id: "deepseek/deepseek-v4-pro",
|
||||||
|
name: "DeepSeek: DeepSeek V4 Pro",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 1.74,
|
||||||
|
output: 3.48,
|
||||||
|
cacheRead: 0.145,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1048576,
|
||||||
|
maxTokens: 384000,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"essentialai/rnj-1-instruct": {
|
"essentialai/rnj-1-instruct": {
|
||||||
id: "essentialai/rnj-1-instruct",
|
id: "essentialai/rnj-1-instruct",
|
||||||
name: "EssentialAI: Rnj 1 Instruct",
|
name: "EssentialAI: Rnj 1 Instruct",
|
||||||
@@ -12380,6 +12486,40 @@ export const MODELS = {
|
|||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 64000,
|
maxTokens: 64000,
|
||||||
} satisfies Model<"anthropic-messages">,
|
} satisfies Model<"anthropic-messages">,
|
||||||
|
"deepseek/deepseek-v4-flash": {
|
||||||
|
id: "deepseek/deepseek-v4-flash",
|
||||||
|
name: "DeepSeek V4 Flash",
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "vercel-ai-gateway",
|
||||||
|
baseUrl: "https://ai-gateway.vercel.sh",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.14,
|
||||||
|
output: 0.28,
|
||||||
|
cacheRead: 0.014,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxTokens: 384000,
|
||||||
|
} satisfies Model<"anthropic-messages">,
|
||||||
|
"deepseek/deepseek-v4-pro": {
|
||||||
|
id: "deepseek/deepseek-v4-pro",
|
||||||
|
name: "DeepSeek V4 Pro",
|
||||||
|
api: "anthropic-messages",
|
||||||
|
provider: "vercel-ai-gateway",
|
||||||
|
baseUrl: "https://ai-gateway.vercel.sh",
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 1.74,
|
||||||
|
output: 3.48,
|
||||||
|
cacheRead: 0.145,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
maxTokens: 384000,
|
||||||
|
} satisfies Model<"anthropic-messages">,
|
||||||
"google/gemini-2.0-flash": {
|
"google/gemini-2.0-flash": {
|
||||||
id: "google/gemini-2.0-flash",
|
id: "google/gemini-2.0-flash",
|
||||||
name: "Gemini 2.0 Flash",
|
name: "Gemini 2.0 Flash",
|
||||||
|
|||||||
@@ -526,6 +526,11 @@ function buildParams(
|
|||||||
enable_thinking: !!options?.reasoningEffort,
|
enable_thinking: !!options?.reasoningEffort,
|
||||||
preserve_thinking: true,
|
preserve_thinking: true,
|
||||||
};
|
};
|
||||||
|
} else if (compat.thinkingFormat === "deepseek" && model.reasoning) {
|
||||||
|
(params as any).thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" };
|
||||||
|
if (options?.reasoningEffort) {
|
||||||
|
(params as any).reasoning_effort = mapReasoningEffort(options.reasoningEffort, compat.reasoningEffortMap);
|
||||||
|
}
|
||||||
} else if (compat.thinkingFormat === "openrouter" && model.reasoning) {
|
} else if (compat.thinkingFormat === "openrouter" && model.reasoning) {
|
||||||
// OpenRouter normalizes reasoning across providers via a nested reasoning object.
|
// OpenRouter normalizes reasoning across providers via a nested reasoning object.
|
||||||
const openRouterParams = params as typeof params & { reasoning?: { effort?: string } };
|
const openRouterParams = params as typeof params & { reasoning?: { effort?: string } };
|
||||||
@@ -831,6 +836,13 @@ export function convertMessages(
|
|||||||
(assistantMsg as any).reasoning_details = reasoningDetails;
|
(assistantMsg as any).reasoning_details = reasoningDetails;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
compat.requiresReasoningContentOnAssistantMessages &&
|
||||||
|
model.reasoning &&
|
||||||
|
(assistantMsg as { reasoning_content?: string }).reasoning_content === undefined
|
||||||
|
) {
|
||||||
|
(assistantMsg as { reasoning_content?: string }).reasoning_content = "";
|
||||||
|
}
|
||||||
// Skip assistant messages that have no content and no tool calls.
|
// Skip assistant messages that have no content and no tool calls.
|
||||||
// Some providers require "either content or tool_calls, but not none".
|
// Some providers require "either content or tool_calls, but not none".
|
||||||
// Other providers also don't accept empty assistant messages.
|
// Other providers also don't accept empty assistant messages.
|
||||||
@@ -1021,10 +1033,18 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
|||||||
|
|
||||||
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
|
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
|
||||||
const isGroq = provider === "groq" || baseUrl.includes("groq.com");
|
const isGroq = provider === "groq" || baseUrl.includes("groq.com");
|
||||||
|
const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
|
||||||
const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined;
|
const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined;
|
||||||
|
|
||||||
const reasoningEffortMap =
|
const reasoningEffortMap = isDeepSeek
|
||||||
isGroq && model.id === "qwen/qwen3-32b"
|
? {
|
||||||
|
minimal: "high",
|
||||||
|
low: "high",
|
||||||
|
medium: "high",
|
||||||
|
high: "high",
|
||||||
|
xhigh: "max",
|
||||||
|
}
|
||||||
|
: isGroq && model.id === "qwen/qwen3-32b"
|
||||||
? {
|
? {
|
||||||
minimal: "default",
|
minimal: "default",
|
||||||
low: "default",
|
low: "default",
|
||||||
@@ -1043,7 +1063,10 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
|||||||
requiresToolResultName: false,
|
requiresToolResultName: false,
|
||||||
requiresAssistantAfterToolResult: false,
|
requiresAssistantAfterToolResult: false,
|
||||||
requiresThinkingAsText: false,
|
requiresThinkingAsText: false,
|
||||||
thinkingFormat: isZai
|
requiresReasoningContentOnAssistantMessages: isDeepSeek,
|
||||||
|
thinkingFormat: isDeepSeek
|
||||||
|
? "deepseek"
|
||||||
|
: isZai
|
||||||
? "zai"
|
? "zai"
|
||||||
: provider === "openrouter" || baseUrl.includes("openrouter.ai")
|
: provider === "openrouter" || baseUrl.includes("openrouter.ai")
|
||||||
? "openrouter"
|
? "openrouter"
|
||||||
@@ -1077,6 +1100,9 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion
|
|||||||
requiresAssistantAfterToolResult:
|
requiresAssistantAfterToolResult:
|
||||||
model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
|
model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
|
||||||
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
|
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
|
||||||
|
requiresReasoningContentOnAssistantMessages:
|
||||||
|
model.compat.requiresReasoningContentOnAssistantMessages ??
|
||||||
|
detected.requiresReasoningContentOnAssistantMessages,
|
||||||
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
|
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
|
||||||
openRouterRouting: model.compat.openRouterRouting ?? {},
|
openRouterRouting: model.compat.openRouterRouting ?? {},
|
||||||
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
|
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type KnownProvider =
|
|||||||
| "openai"
|
| "openai"
|
||||||
| "azure-openai-responses"
|
| "azure-openai-responses"
|
||||||
| "openai-codex"
|
| "openai-codex"
|
||||||
|
| "deepseek"
|
||||||
| "github-copilot"
|
| "github-copilot"
|
||||||
| "xai"
|
| "xai"
|
||||||
| "groq"
|
| "groq"
|
||||||
@@ -282,8 +283,10 @@ export interface OpenAICompletionsCompat {
|
|||||||
requiresAssistantAfterToolResult?: boolean;
|
requiresAssistantAfterToolResult?: boolean;
|
||||||
/** Whether thinking blocks must be converted to text blocks with <thinking> delimiters. Default: auto-detected from URL. */
|
/** Whether thinking blocks must be converted to text blocks with <thinking> delimiters. Default: auto-detected from URL. */
|
||||||
requiresThinkingAsText?: boolean;
|
requiresThinkingAsText?: boolean;
|
||||||
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
|
/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */
|
||||||
thinkingFormat?: "openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template";
|
requiresReasoningContentOnAssistantMessages?: boolean;
|
||||||
|
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
|
||||||
|
thinkingFormat?: "openai" | "openrouter" | "deepseek" | "zai" | "qwen" | "qwen-chat-template";
|
||||||
/** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */
|
/** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */
|
||||||
openRouterRouting?: OpenRouterRouting;
|
openRouterRouting?: OpenRouterRouting;
|
||||||
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
|
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const compat = {
|
|||||||
requiresToolResultName: false,
|
requiresToolResultName: false,
|
||||||
requiresAssistantAfterToolResult: false,
|
requiresAssistantAfterToolResult: false,
|
||||||
requiresThinkingAsText: true,
|
requiresThinkingAsText: true,
|
||||||
|
requiresReasoningContentOnAssistantMessages: false,
|
||||||
thinkingFormat: "openai",
|
thinkingFormat: "openai",
|
||||||
openRouterRouting: {},
|
openRouterRouting: {},
|
||||||
vercelGatewayRouting: {},
|
vercelGatewayRouting: {},
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const compat: Required<OpenAICompletionsCompat> = {
|
|||||||
requiresToolResultName: false,
|
requiresToolResultName: false,
|
||||||
requiresAssistantAfterToolResult: false,
|
requiresAssistantAfterToolResult: false,
|
||||||
requiresThinkingAsText: false,
|
requiresThinkingAsText: false,
|
||||||
|
requiresReasoningContentOnAssistantMessages: false,
|
||||||
thinkingFormat: "openai",
|
thinkingFormat: "openai",
|
||||||
openRouterRouting: {},
|
openRouterRouting: {},
|
||||||
vercelGatewayRouting: {},
|
vercelGatewayRouting: {},
|
||||||
|
|||||||
@@ -447,6 +447,33 @@ describe("Generate E2E Tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
|
||||||
|
"DeepSeek Provider (deepseek-v4-flash via OpenAI Completions)",
|
||||||
|
() => {
|
||||||
|
const llm = getModel("deepseek", "deepseek-v4-flash");
|
||||||
|
|
||||||
|
it("should complete basic text generation", { retry: 3 }, async () => {
|
||||||
|
await basicTextGeneration(llm);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tool calling", { retry: 3 }, async () => {
|
||||||
|
await handleToolCall(llm);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle streaming", { retry: 3 }, async () => {
|
||||||
|
await handleStreaming(llm);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle thinking mode", { retry: 3 }, async () => {
|
||||||
|
await handleThinking(llm, { reasoningEffort: "high" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => {
|
||||||
|
await multiTurn(llm, { reasoningEffort: "high" });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
describe.skipIf(!process.env.OPENAI_API_KEY)("OpenAI Responses Provider (gpt-5.4)", () => {
|
describe.skipIf(!process.env.OPENAI_API_KEY)("OpenAI Responses Provider (gpt-5.4)", () => {
|
||||||
const llm = getModel("openai", "gpt-5.4");
|
const llm = getModel("openai", "gpt-5.4");
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `/copy` to avoid unbounded OSC 52 writes and clipboard races that could break terminal rendering or panic the native clipboard addon ([#3639](https://github.com/badlogic/pi-mono/issues/3639))
|
||||||
- Fixed extension flag docs to show `pi.getFlag()` using registered flag names without the CLI `--` prefix ([#3614](https://github.com/badlogic/pi-mono/issues/3614))
|
- Fixed extension flag docs to show `pi.getFlag()` using registered flag names without the CLI `--` prefix ([#3614](https://github.com/badlogic/pi-mono/issues/3614))
|
||||||
|
|
||||||
## [0.70.0] - 2026-04-23
|
## [0.70.0] - 2026-04-23
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ For each built-in provider, pi maintains a list of tool-capable models, updated
|
|||||||
- Anthropic
|
- Anthropic
|
||||||
- OpenAI
|
- OpenAI
|
||||||
- Azure OpenAI
|
- Azure OpenAI
|
||||||
|
- DeepSeek
|
||||||
- Google Gemini
|
- Google Gemini
|
||||||
- Google Vertex
|
- Google Vertex
|
||||||
- Amazon Bedrock
|
- Amazon Bedrock
|
||||||
|
|||||||
@@ -627,11 +627,12 @@ interface ProviderModelConfig {
|
|||||||
requiresToolResultName?: boolean;
|
requiresToolResultName?: boolean;
|
||||||
requiresAssistantAfterToolResult?: boolean;
|
requiresAssistantAfterToolResult?: boolean;
|
||||||
requiresThinkingAsText?: boolean;
|
requiresThinkingAsText?: boolean;
|
||||||
thinkingFormat?: "openai" | "zai" | "qwen" | "qwen-chat-template";
|
requiresReasoningContentOnAssistantMessages?: boolean;
|
||||||
|
thinkingFormat?: "openai" | "deepseek" | "zai" | "qwen" | "qwen-chat-template";
|
||||||
cacheControlFormat?: "anthropic";
|
cacheControlFormat?: "anthropic";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
`deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
||||||
`cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content.
|
`cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content.
|
||||||
|
|||||||
@@ -338,7 +338,8 @@ For providers with partial OpenAI compatibility, use the `compat` field.
|
|||||||
| `requiresToolResultName` | Include `name` on tool result messages |
|
| `requiresToolResultName` | Include `name` on tool result messages |
|
||||||
| `requiresAssistantAfterToolResult` | Insert an assistant message before a user message after tool results |
|
| `requiresAssistantAfterToolResult` | Insert an assistant message before a user message after tool results |
|
||||||
| `requiresThinkingAsText` | Convert thinking blocks to plain text |
|
| `requiresThinkingAsText` | Convert thinking blocks to plain text |
|
||||||
| `thinkingFormat` | Use `reasoning_effort`, `zai`, `qwen`, or `qwen-chat-template` thinking parameters |
|
| `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled |
|
||||||
|
| `thinkingFormat` | Use `reasoning_effort`, `deepseek`, `zai`, `qwen`, or `qwen-chat-template` thinking parameters |
|
||||||
| `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user/assistant text content. Currently only `anthropic` is supported. |
|
| `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user/assistant text content. Currently only `anthropic` is supported. |
|
||||||
| `supportsStrictMode` | Include the `strict` field in tool definitions |
|
| `supportsStrictMode` | Include the `strict` field in tool definitions |
|
||||||
| `supportsLongCacheRetention` | Whether the provider accepts long cache retention when cache retention is `long`: `prompt_cache_retention: "24h"` for OpenAI prompt caching, or `cache_control.ttl: "1h"` when `cacheControlFormat` is `anthropic`. Default: `true`. |
|
| `supportsLongCacheRetention` | Whether the provider accepts long cache retention when cache retention is `long`: `prompt_cache_retention: "24h"` for OpenAI prompt caching, or `cache_control.ttl: "1h"` when `cacheControlFormat` is `anthropic`. Default: `true`. |
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ pi
|
|||||||
| Anthropic | `ANTHROPIC_API_KEY` | `anthropic` |
|
| Anthropic | `ANTHROPIC_API_KEY` | `anthropic` |
|
||||||
| Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |
|
| Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |
|
||||||
| OpenAI | `OPENAI_API_KEY` | `openai` |
|
| OpenAI | `OPENAI_API_KEY` | `openai` |
|
||||||
|
| DeepSeek | `DEEPSEEK_API_KEY` | `deepseek` |
|
||||||
| Google Gemini | `GEMINI_API_KEY` | `google` |
|
| Google Gemini | `GEMINI_API_KEY` | `google` |
|
||||||
| Mistral | `MISTRAL_API_KEY` | `mistral` |
|
| Mistral | `MISTRAL_API_KEY` | `mistral` |
|
||||||
| Groq | `GROQ_API_KEY` | `groq` |
|
| Groq | `GROQ_API_KEY` | `groq` |
|
||||||
@@ -82,6 +83,7 @@ Store credentials in `~/.pi/agent/auth.json`:
|
|||||||
{
|
{
|
||||||
"anthropic": { "type": "api_key", "key": "sk-ant-..." },
|
"anthropic": { "type": "api_key", "key": "sk-ant-..." },
|
||||||
"openai": { "type": "api_key", "key": "sk-..." },
|
"openai": { "type": "api_key", "key": "sk-..." },
|
||||||
|
"deepseek": { "type": "api_key", "key": "sk-..." },
|
||||||
"google": { "type": "api_key", "key": "..." },
|
"google": { "type": "api_key", "key": "..." },
|
||||||
"opencode": { "type": "api_key", "key": "..." },
|
"opencode": { "type": "api_key", "key": "..." },
|
||||||
"opencode-go": { "type": "api_key", "key": "..." }
|
"opencode-go": { "type": "api_key", "key": "..." }
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@mariozechner/clipboard": "^0.3.2"
|
"@mariozechner/clipboard": "^0.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ ${chalk.bold("Environment Variables:")}
|
|||||||
AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)
|
AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)
|
||||||
AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)
|
AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)
|
||||||
AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)
|
AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)
|
||||||
|
DEEPSEEK_API_KEY - DeepSeek API key
|
||||||
GEMINI_API_KEY - Google Gemini API key
|
GEMINI_API_KEY - Google Gemini API key
|
||||||
GROQ_API_KEY - Groq API key
|
GROQ_API_KEY - Groq API key
|
||||||
CEREBRAS_API_KEY - Cerebras API key
|
CEREBRAS_API_KEY - Cerebras API key
|
||||||
|
|||||||
@@ -98,10 +98,12 @@ const OpenAICompletionsCompatSchema = Type.Object({
|
|||||||
requiresToolResultName: Type.Optional(Type.Boolean()),
|
requiresToolResultName: Type.Optional(Type.Boolean()),
|
||||||
requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),
|
requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),
|
||||||
requiresThinkingAsText: Type.Optional(Type.Boolean()),
|
requiresThinkingAsText: Type.Optional(Type.Boolean()),
|
||||||
|
requiresReasoningContentOnAssistantMessages: Type.Optional(Type.Boolean()),
|
||||||
thinkingFormat: Type.Optional(
|
thinkingFormat: Type.Optional(
|
||||||
Type.Union([
|
Type.Union([
|
||||||
Type.Literal("openai"),
|
Type.Literal("openai"),
|
||||||
Type.Literal("openrouter"),
|
Type.Literal("openrouter"),
|
||||||
|
Type.Literal("deepseek"),
|
||||||
Type.Literal("zai"),
|
Type.Literal("zai"),
|
||||||
Type.Literal("qwen"),
|
Type.Literal("qwen"),
|
||||||
Type.Literal("qwen-chat-template"),
|
Type.Literal("qwen-chat-template"),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
|||||||
openai: "gpt-5.4",
|
openai: "gpt-5.4",
|
||||||
"azure-openai-responses": "gpt-5.4",
|
"azure-openai-responses": "gpt-5.4",
|
||||||
"openai-codex": "gpt-5.5",
|
"openai-codex": "gpt-5.5",
|
||||||
|
deepseek: "deepseek-v4-pro",
|
||||||
google: "gemini-3.1-pro-preview",
|
google: "gemini-3.1-pro-preview",
|
||||||
"google-gemini-cli": "gemini-3.1-pro-preview",
|
"google-gemini-cli": "gemini-3.1-pro-preview",
|
||||||
"google-antigravity": "gemini-3.1-pro-high",
|
"google-antigravity": "gemini-3.1-pro-high",
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ const API_KEY_LOGIN_PROVIDERS: Record<string, string> = {
|
|||||||
[BEDROCK_PROVIDER_ID]: "Amazon Bedrock",
|
[BEDROCK_PROVIDER_ID]: "Amazon Bedrock",
|
||||||
"azure-openai-responses": "Azure OpenAI Responses",
|
"azure-openai-responses": "Azure OpenAI Responses",
|
||||||
cerebras: "Cerebras",
|
cerebras: "Cerebras",
|
||||||
|
deepseek: "DeepSeek",
|
||||||
fireworks: "Fireworks",
|
fireworks: "Fireworks",
|
||||||
google: "Google Gemini",
|
google: "Google Gemini",
|
||||||
"google-vertex": "Google Vertex AI",
|
"google-vertex": "Google Vertex AI",
|
||||||
|
|||||||
@@ -17,40 +17,64 @@ function copyToX11Clipboard(options: NativeClipboardExecOptions): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyToClipboard(text: string): Promise<void> {
|
const MAX_OSC52_ENCODED_LENGTH = 100_000;
|
||||||
// Always emit OSC 52 - works over SSH/mosh, harmless locally
|
|
||||||
const encoded = Buffer.from(text).toString("base64");
|
|
||||||
process.stdout.write(`\x1b]52;c;${encoded}\x07`);
|
|
||||||
|
|
||||||
|
function isRemoteSession(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||||
|
return Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitOsc52(text: string): boolean {
|
||||||
|
const encoded = Buffer.from(text).toString("base64");
|
||||||
|
if (encoded.length > MAX_OSC52_ENCODED_LENGTH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
process.stdout.write(`\x1b]52;c;${encoded}\x07`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyToClipboard(text: string): Promise<void> {
|
||||||
|
let copied = false;
|
||||||
|
|
||||||
|
// Prefer direct clipboard writes. Emitting OSC 52 first can make terminals
|
||||||
|
// write the same native clipboard concurrently with the addon, and very large
|
||||||
|
// OSC 52 payloads can desynchronize terminal rendering.
|
||||||
try {
|
try {
|
||||||
if (clipboard) {
|
if (clipboard) {
|
||||||
await clipboard.setText(text);
|
await clipboard.setText(text);
|
||||||
return;
|
copied = true;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fall through to platform-specific clipboard tools.
|
// Fall through to platform-specific clipboard tools.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also try native tools (best effort for local sessions)
|
const remote = isRemoteSession();
|
||||||
|
if (copied && !remote) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const p = platform();
|
const p = platform();
|
||||||
const options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: ["pipe", "ignore", "ignore"] };
|
const options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: ["pipe", "ignore", "ignore"] };
|
||||||
|
|
||||||
|
if (!copied) {
|
||||||
try {
|
try {
|
||||||
if (p === "darwin") {
|
if (p === "darwin") {
|
||||||
execSync("pbcopy", options);
|
execSync("pbcopy", options);
|
||||||
|
copied = true;
|
||||||
} else if (p === "win32") {
|
} else if (p === "win32") {
|
||||||
execSync("clip", options);
|
execSync("clip", options);
|
||||||
|
copied = true;
|
||||||
} else {
|
} else {
|
||||||
// Linux. Try Termux, Wayland, or X11 clipboard tools.
|
// Linux. Try Termux, Wayland, or X11 clipboard tools.
|
||||||
if (process.env.TERMUX_VERSION) {
|
if (process.env.TERMUX_VERSION) {
|
||||||
try {
|
try {
|
||||||
execSync("termux-clipboard-set", options);
|
execSync("termux-clipboard-set", options);
|
||||||
return;
|
copied = true;
|
||||||
} catch {
|
} catch {
|
||||||
// Fall back to Wayland or X11 tools.
|
// Fall back to Wayland or X11 tools.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!copied) {
|
||||||
const hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);
|
const hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);
|
||||||
const hasX11Display = Boolean(process.env.DISPLAY);
|
const hasX11Display = Boolean(process.env.DISPLAY);
|
||||||
const isWayland = isWaylandSession();
|
const isWayland = isWaylandSession();
|
||||||
@@ -66,16 +90,30 @@ export async function copyToClipboard(text: string): Promise<void> {
|
|||||||
proc.stdin.write(text);
|
proc.stdin.write(text);
|
||||||
proc.stdin.end();
|
proc.stdin.end();
|
||||||
proc.unref();
|
proc.unref();
|
||||||
|
copied = true;
|
||||||
} catch {
|
} catch {
|
||||||
if (hasX11Display) {
|
if (hasX11Display) {
|
||||||
copyToX11Clipboard(options);
|
copyToX11Clipboard(options);
|
||||||
|
copied = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (hasX11Display) {
|
} else if (hasX11Display) {
|
||||||
copyToX11Clipboard(options);
|
copyToX11Clipboard(options);
|
||||||
|
copied = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore - OSC 52 already emitted as fallback
|
// Fall through to OSC 52 fallback.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remote || !copied) {
|
||||||
|
const osc52Copied = emitOsc52(text);
|
||||||
|
copied = copied || osc52Copied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!copied) {
|
||||||
|
throw new Error("Failed to copy to clipboard");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
145
packages/coding-agent/test/clipboard.test.ts
Normal file
145
packages/coding-agent/test/clipboard.test.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { execSync, spawn } from "child_process";
|
||||||
|
import { platform } from "os";
|
||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { copyToClipboard } from "../src/utils/clipboard.js";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => {
|
||||||
|
return {
|
||||||
|
clipboard: {
|
||||||
|
setText: vi.fn<(text: string) => Promise<void>>(),
|
||||||
|
},
|
||||||
|
execSync: vi.fn(),
|
||||||
|
spawn: vi.fn(),
|
||||||
|
platform: vi.fn<() => NodeJS.Platform>(),
|
||||||
|
isWaylandSession: vi.fn<() => boolean>(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../src/utils/clipboard-native.js", () => {
|
||||||
|
return {
|
||||||
|
clipboard: mocks.clipboard,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("child_process", () => {
|
||||||
|
return {
|
||||||
|
execSync: mocks.execSync,
|
||||||
|
spawn: mocks.spawn,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("os", () => {
|
||||||
|
return {
|
||||||
|
platform: mocks.platform,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../src/utils/clipboard-image.js", () => {
|
||||||
|
return {
|
||||||
|
isWaylandSession: mocks.isWaylandSession,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockedExecSync = vi.mocked(execSync);
|
||||||
|
const mockedSpawn = vi.mocked(spawn);
|
||||||
|
const mockedPlatform = vi.mocked(platform);
|
||||||
|
|
||||||
|
let originalWrite: typeof process.stdout.write;
|
||||||
|
let stdoutWrites: string[];
|
||||||
|
let nativeResolved = false;
|
||||||
|
|
||||||
|
function osc52Writes(): string[] {
|
||||||
|
return stdoutWrites.filter((write) => write.startsWith("\x1b]52;c;"));
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
stdoutWrites = [];
|
||||||
|
nativeResolved = false;
|
||||||
|
mocks.clipboard.setText.mockReset();
|
||||||
|
mocks.execSync.mockReset();
|
||||||
|
mocks.spawn.mockReset();
|
||||||
|
mocks.platform.mockReset();
|
||||||
|
mocks.isWaylandSession.mockReset();
|
||||||
|
mockedPlatform.mockReturnValue("darwin");
|
||||||
|
mocks.isWaylandSession.mockReturnValue(false);
|
||||||
|
mocks.clipboard.setText.mockImplementation(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||||
|
nativeResolved = true;
|
||||||
|
});
|
||||||
|
originalWrite = process.stdout.write.bind(process.stdout);
|
||||||
|
process.stdout.write = ((...args: Parameters<typeof process.stdout.write>) => {
|
||||||
|
const [chunk] = args;
|
||||||
|
if (typeof chunk === "string" && chunk.startsWith("\x1b]52;c;")) {
|
||||||
|
stdoutWrites.push(chunk);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return originalWrite(...args);
|
||||||
|
}) as typeof process.stdout.write;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.stdout.write = originalWrite;
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("copyToClipboard", () => {
|
||||||
|
test("local native success skips OSC 52 and shell fallbacks", async () => {
|
||||||
|
await copyToClipboard("hello");
|
||||||
|
|
||||||
|
expect(mocks.clipboard.setText).toHaveBeenCalledWith("hello");
|
||||||
|
expect(osc52Writes()).toHaveLength(0);
|
||||||
|
expect(mockedExecSync).not.toHaveBeenCalled();
|
||||||
|
expect(mockedSpawn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("remote native success emits OSC 52 after native write", async () => {
|
||||||
|
vi.stubEnv("SSH_CONNECTION", "client server");
|
||||||
|
mocks.clipboard.setText.mockImplementation(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||||
|
expect(osc52Writes()).toHaveLength(0);
|
||||||
|
nativeResolved = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
await copyToClipboard("hello");
|
||||||
|
|
||||||
|
expect(nativeResolved).toBe(true);
|
||||||
|
expect(osc52Writes()).toHaveLength(1);
|
||||||
|
expect(mockedExecSync).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("local shell fallback success skips OSC 52", async () => {
|
||||||
|
mocks.clipboard.setText.mockRejectedValue(new Error("native failed"));
|
||||||
|
mockedExecSync.mockReturnValue(Buffer.alloc(0));
|
||||||
|
|
||||||
|
await copyToClipboard("hello");
|
||||||
|
|
||||||
|
expect(mockedExecSync).toHaveBeenCalledWith("pbcopy", {
|
||||||
|
input: "hello",
|
||||||
|
stdio: ["pipe", "ignore", "ignore"],
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
expect(osc52Writes()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses OSC 52 fallback when native and shell tools fail", async () => {
|
||||||
|
mocks.clipboard.setText.mockRejectedValue(new Error("native failed"));
|
||||||
|
mockedExecSync.mockImplementation(() => {
|
||||||
|
throw new Error("pbcopy failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
await copyToClipboard("hello");
|
||||||
|
|
||||||
|
expect(osc52Writes()).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not emit oversized OSC 52 payloads", async () => {
|
||||||
|
mocks.clipboard.setText.mockRejectedValue(new Error("native failed"));
|
||||||
|
mockedExecSync.mockImplementation(() => {
|
||||||
|
throw new Error("pbcopy failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(copyToClipboard("x".repeat(80_000))).rejects.toThrow("Failed to copy to clipboard");
|
||||||
|
expect(osc52Writes()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user