Compare commits

...

1 Commits

Author SHA1 Message Date
Saoud Rizwan a3c3e88da4 fix: remove unnecessary vercel stream transform 2025-11-05 14:27:30 -08:00
3 changed files with 53 additions and 202 deletions
+6 -2
View File
@@ -5,14 +5,16 @@ import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/com
import { fetch } from "@/shared/net"
import { ApiHandler, CommonApiHandlerOptions } from "../index"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream } from "../transform/stream"
import { ToolCallProcessor } from "../transform/tool-call-processor"
import { createVercelAIGatewayStream } from "../transform/vercel-ai-gateway-stream"
interface VercelAIGatewayHandlerOptions extends CommonApiHandlerOptions {
vercelAiGatewayApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterProviderSorting?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
}
@@ -53,12 +55,14 @@ export class VercelAIGatewayHandler implements ApiHandler {
const modelInfo = this.getModel().info
try {
const stream = await createVercelAIGatewayStream(
const stream = await createOpenRouterStream(
client,
systemPrompt,
messages,
{ id: modelId, info: modelInfo },
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
tools,
)
let didOutputUsage: boolean = false
+47 -109
View File
@@ -34,95 +34,48 @@ export async function createOpenRouterStream(
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
}
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
// handles direct model.id match logic
switch (model.id) {
case "anthropic/claude-haiku-4.5":
case "anthropic/claude-4.5-haiku":
case "anthropic/claude-sonnet-4.5":
case "anthropic/claude-4.5-sonnet": // OpenRouter accidentally included this in model list for a brief moment, and users may be using this model id. And to support prompt caching, we need to add it here.
case "anthropic/claude-sonnet-4":
case "anthropic/claude-opus-4.1":
case "anthropic/claude-opus-4":
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
case "anthropic/claude-3.5-sonnet-20240620":
case "anthropic/claude-3.5-sonnet-20240620:beta":
case "anthropic/claude-3-5-haiku":
case "anthropic/claude-3-5-haiku:beta":
case "anthropic/claude-3-5-haiku-20241022":
case "anthropic/claude-3-5-haiku-20241022:beta":
case "anthropic/claude-3-haiku":
case "anthropic/claude-3-haiku:beta":
case "anthropic/claude-3-opus":
case "anthropic/claude-3-opus:beta":
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
// Add cache_control to the last two user messages
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
lastTwoUserMessages.forEach((msg) => {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
const isAnthropicModel = model.id.startsWith("anthropic/")
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// prompt caching: https://openrouter.ai/docs/prompt-caching
if (isAnthropicModel) {
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
cache_control: { type: "ephemeral" },
},
],
}
// Add cache_control to the last two user messages
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
lastTwoUserMessages.forEach((msg) => {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
})
break
default:
break
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
})
}
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
// The anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
switch (model.id) {
case "anthropic/claude-haiku-4.5":
case "anthropic/claude-4.5-haiku":
case "anthropic/claude-sonnet-4.5":
case "anthropic/claude-4.5-sonnet":
case "anthropic/claude-sonnet-4":
case "anthropic/claude-opus-4.1":
case "anthropic/claude-opus-4":
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
case "anthropic/claude-3.5-sonnet-20240620":
case "anthropic/claude-3.5-sonnet-20240620:beta":
case "anthropic/claude-3-5-haiku":
case "anthropic/claude-3-5-haiku:beta":
case "anthropic/claude-3-5-haiku-20241022":
case "anthropic/claude-3-5-haiku-20241022:beta":
maxTokens = 8_192
break
if (isAnthropicModel) {
maxTokens = 8_192
}
let temperature: number | undefined = 0
@@ -140,32 +93,18 @@ export async function createOpenRouterStream(
}
let reasoning: { max_tokens: number } | undefined
switch (model.id) {
case "anthropic/claude-haiku-4.5":
case "anthropic/claude-4.5-haiku":
case "anthropic/claude-sonnet-4.5":
case "anthropic/claude-4.5-sonnet":
case "anthropic/claude-sonnet-4":
case "anthropic/claude-opus-4.1":
case "anthropic/claude-opus-4":
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
const budget_tokens = thinkingBudgetTokens || 0
const reasoningOn = budget_tokens !== 0
if (reasoningOn) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: budget_tokens }
}
break
default:
if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: thinkingBudgetTokens }
break
}
if (isAnthropicModel) {
const budget_tokens = thinkingBudgetTokens || 0
const reasoningOn = budget_tokens !== 0
if (reasoningOn) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: budget_tokens }
}
} else {
if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: thinkingBudgetTokens }
}
}
const providerPreferences = OPENROUTER_PROVIDER_PREFERENCES[model.id]
@@ -187,7 +126,6 @@ export async function createOpenRouterStream(
...(reasoning ? { reasoning } : {}),
...(openRouterProviderSorting && !providerPreferences ? { provider: { sort: openRouterProviderSorting } } : {}),
...(providerPreferences ? { provider: providerPreferences } : {}),
...(isClaudeSonnet1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
...getOpenAIToolParams(tools),
})
@@ -1,91 +0,0 @@
import { Anthropic } from "@anthropic-ai/sdk"
import {
CLAUDE_SONNET_1M_SUFFIX,
ModelInfo,
openRouterClaudeSonnet41mModelId,
openRouterClaudeSonnet451mModelId,
} from "@shared/api"
import OpenAI from "openai"
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { getOpenAIToolParams } from "./tool-call-processor"
export async function createVercelAIGatewayStream(
client: OpenAI,
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
model: { id: string; info: ModelInfo },
thinkingBudgetTokens?: number,
tools?: OpenAITool[],
) {
// Convert Anthropic messages to OpenAI format
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const isClaudeSonnet1m = model.id === openRouterClaudeSonnet41mModelId || model.id === openRouterClaudeSonnet451mModelId
if (isClaudeSonnet1m) {
// remove the custom :1m suffix, to create the model id openrouter API expects
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
}
const isAnthropicModel = model.id.startsWith("anthropic/")
if (isAnthropicModel) {
openAiMessages[0] = {
role: "system",
content: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
}
// Add cache_control to the last two user messages for conversation context caching
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
lastTwoUserMessages.forEach((msg) => {
if (typeof msg.content === "string" && msg.content.length > 0) {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// Find the last text part in the message content
const lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (lastTextPart && lastTextPart.text && lastTextPart.text.length > 0) {
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
})
}
// Configure reasoning parameters similar to OpenRouter
let temperature: number | undefined = 0
let reasoning: { max_tokens: number } | undefined
if (isAnthropicModel) {
const budget_tokens = thinkingBudgetTokens || 0
const reasoningOn = budget_tokens !== 0
if (reasoningOn) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: budget_tokens }
}
} else if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
temperature = undefined // extended thinking does not support non-1 temperature
reasoning = { max_tokens: thinkingBudgetTokens }
}
// @ts-ignore-next-line
const stream = await client.chat.completions.create({
model: model.id,
max_tokens: model.info.maxTokens,
temperature: temperature,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
include_reasoning: true,
...(reasoning ? { reasoning } : {}),
...getOpenAIToolParams(tools),
})
return stream
}