fix(coding-agent): harden clipboard copy

closes #3639
This commit is contained in:
Mario Zechner
2026-04-24 12:52:32 +02:00
parent 9fdb12f985
commit 1e33492525
24 changed files with 561 additions and 108 deletions

View File

@@ -68,6 +68,7 @@ function getApiKeyEnvVars(provider: string): readonly string[] | undefined {
const envMap: Record<string, string> = {
openai: "OPENAI_API_KEY",
"azure-openai-responses": "AZURE_OPENAI_API_KEY",
deepseek: "DEEPSEEK_API_KEY",
google: "GEMINI_API_KEY",
"google-vertex": "GOOGLE_CLOUD_API_KEY",
groq: "GROQ_API_KEY",

View File

@@ -328,6 +328,40 @@ export const MODELS = {
contextWindow: 1000000,
maxTokens: 64000,
} 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": {
id: "deepseek.r1-v1:0",
name: "DeepSeek-R1",
@@ -2715,6 +2749,44 @@ export const MODELS = {
maxTokens: 40000,
} 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": {
"accounts/fireworks/models/deepseek-v3p1": {
id: "accounts/fireworks/models/deepseek-v3p1",
@@ -8162,6 +8234,40 @@ export const MODELS = {
contextWindow: 163840,
maxTokens: 65536,
} 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": {
id: "essentialai/rnj-1-instruct",
name: "EssentialAI: Rnj 1 Instruct",
@@ -12380,6 +12486,40 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 64000,
} 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": {
id: "google/gemini-2.0-flash",
name: "Gemini 2.0 Flash",

View File

@@ -526,6 +526,11 @@ function buildParams(
enable_thinking: !!options?.reasoningEffort,
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) {
// OpenRouter normalizes reasoning across providers via a nested reasoning object.
const openRouterParams = params as typeof params & { reasoning?: { effort?: string } };
@@ -831,6 +836,13 @@ export function convertMessages(
(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.
// Some providers require "either content or tool_calls, but not none".
// 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 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 reasoningEffortMap =
isGroq && model.id === "qwen/qwen3-32b"
const reasoningEffortMap = isDeepSeek
? {
minimal: "high",
low: "high",
medium: "high",
high: "high",
xhigh: "max",
}
: isGroq && model.id === "qwen/qwen3-32b"
? {
minimal: "default",
low: "default",
@@ -1043,11 +1063,14 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
requiresToolResultName: false,
requiresAssistantAfterToolResult: false,
requiresThinkingAsText: false,
thinkingFormat: isZai
? "zai"
: provider === "openrouter" || baseUrl.includes("openrouter.ai")
? "openrouter"
: "openai",
requiresReasoningContentOnAssistantMessages: isDeepSeek,
thinkingFormat: isDeepSeek
? "deepseek"
: isZai
? "zai"
: provider === "openrouter" || baseUrl.includes("openrouter.ai")
? "openrouter"
: "openai",
openRouterRouting: {},
vercelGatewayRouting: {},
zaiToolStream: false,
@@ -1077,6 +1100,9 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion
requiresAssistantAfterToolResult:
model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
requiresReasoningContentOnAssistantMessages:
model.compat.requiresReasoningContentOnAssistantMessages ??
detected.requiresReasoningContentOnAssistantMessages,
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
openRouterRouting: model.compat.openRouterRouting ?? {},
vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting,

View File

@@ -26,6 +26,7 @@ export type KnownProvider =
| "openai"
| "azure-openai-responses"
| "openai-codex"
| "deepseek"
| "github-copilot"
| "xai"
| "groq"
@@ -282,8 +283,10 @@ export interface OpenAICompletionsCompat {
requiresAssistantAfterToolResult?: boolean;
/** Whether thinking blocks must be converted to text blocks with <thinking> delimiters. Default: auto-detected from URL. */
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". */
thinkingFormat?: "openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template";
/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */
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. */
openRouterRouting?: OpenRouterRouting;
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */