mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: Add Vercel AI Gateway provider integration (#7396)
Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com> Co-authored-by: cte <cestreich@gmail.com>
This commit is contained in:
parent
7c91e4f259
commit
934bfd0a54
46 changed files with 1492 additions and 11 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@roo-code/types",
|
||||
"version": "1.60.0",
|
||||
"version": "1.61.0",
|
||||
"description": "TypeScript type definitions for Roo Code.",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ export const SECRET_STATE_KEYS = [
|
|||
"fireworksApiKey",
|
||||
"featherlessApiKey",
|
||||
"ioIntelligenceApiKey",
|
||||
"vercelAiGatewayApiKey",
|
||||
] as const satisfies readonly (keyof ProviderSettings)[]
|
||||
export type SecretState = Pick<ProviderSettings, (typeof SECRET_STATE_KEYS)[number]>
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export const providerNames = [
|
|||
"featherless",
|
||||
"io-intelligence",
|
||||
"roo",
|
||||
"vercel-ai-gateway",
|
||||
] as const
|
||||
|
||||
export const providerNamesSchema = z.enum(providerNames)
|
||||
|
|
@ -321,6 +322,11 @@ const rooSchema = apiModelIdProviderModelSchema.extend({
|
|||
// No additional fields needed - uses cloud authentication
|
||||
})
|
||||
|
||||
const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({
|
||||
vercelAiGatewayApiKey: z.string().optional(),
|
||||
vercelAiGatewayModelId: z.string().optional(),
|
||||
})
|
||||
|
||||
const defaultSchema = z.object({
|
||||
apiProvider: z.undefined(),
|
||||
})
|
||||
|
|
@ -360,6 +366,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
|
|||
ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
|
||||
qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
|
||||
rooSchema.merge(z.object({ apiProvider: z.literal("roo") })),
|
||||
vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
|
||||
defaultSchema,
|
||||
])
|
||||
|
||||
|
|
@ -399,6 +406,7 @@ export const providerSettingsSchema = z.object({
|
|||
...ioIntelligenceSchema.shape,
|
||||
...qwenCodeSchema.shape,
|
||||
...rooSchema.shape,
|
||||
...vercelAiGatewaySchema.shape,
|
||||
...codebaseIndexProviderSchema.shape,
|
||||
})
|
||||
|
||||
|
|
@ -425,6 +433,7 @@ export const MODEL_ID_KEYS: Partial<keyof ProviderSettings>[] = [
|
|||
"litellmModelId",
|
||||
"huggingFaceModelId",
|
||||
"ioIntelligenceModelId",
|
||||
"vercelAiGatewayModelId",
|
||||
]
|
||||
|
||||
export const getModelId = (settings: ProviderSettings): string | undefined => {
|
||||
|
|
@ -541,6 +550,7 @@ export const MODELS_BY_PROVIDER: Record<
|
|||
openrouter: { id: "openrouter", label: "OpenRouter", models: [] },
|
||||
requesty: { id: "requesty", label: "Requesty", models: [] },
|
||||
unbound: { id: "unbound", label: "Unbound", models: [] },
|
||||
"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
|
||||
}
|
||||
|
||||
export const dynamicProviders = [
|
||||
|
|
@ -550,6 +560,7 @@ export const dynamicProviders = [
|
|||
"openrouter",
|
||||
"requesty",
|
||||
"unbound",
|
||||
"vercel-ai-gateway",
|
||||
] as const satisfies readonly ProviderName[]
|
||||
|
||||
export type DynamicProvider = (typeof dynamicProviders)[number]
|
||||
|
|
|
|||
|
|
@ -27,4 +27,5 @@ export * from "./unbound.js"
|
|||
export * from "./vertex.js"
|
||||
export * from "./vscode-llm.js"
|
||||
export * from "./xai.js"
|
||||
export * from "./vercel-ai-gateway.js"
|
||||
export * from "./zai.js"
|
||||
|
|
|
|||
102
packages/types/src/providers/vercel-ai-gateway.ts
Normal file
102
packages/types/src/providers/vercel-ai-gateway.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import type { ModelInfo } from "../model.js"
|
||||
|
||||
// https://ai-gateway.vercel.sh/v1/
|
||||
export const vercelAiGatewayDefaultModelId = "anthropic/claude-sonnet-4"
|
||||
|
||||
export const VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS = new Set([
|
||||
"anthropic/claude-3-haiku",
|
||||
"anthropic/claude-3-opus",
|
||||
"anthropic/claude-3.5-haiku",
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"anthropic/claude-3.7-sonnet",
|
||||
"anthropic/claude-opus-4",
|
||||
"anthropic/claude-opus-4.1",
|
||||
"anthropic/claude-sonnet-4",
|
||||
"openai/gpt-4.1",
|
||||
"openai/gpt-4.1-mini",
|
||||
"openai/gpt-4.1-nano",
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-mini",
|
||||
"openai/gpt-5",
|
||||
"openai/gpt-5-mini",
|
||||
"openai/gpt-5-nano",
|
||||
"openai/o1",
|
||||
"openai/o3",
|
||||
"openai/o3-mini",
|
||||
"openai/o4-mini",
|
||||
])
|
||||
|
||||
export const VERCEL_AI_GATEWAY_VISION_ONLY_MODELS = new Set([
|
||||
"alibaba/qwen-3-14b",
|
||||
"alibaba/qwen-3-235b",
|
||||
"alibaba/qwen-3-30b",
|
||||
"alibaba/qwen-3-32b",
|
||||
"alibaba/qwen3-coder",
|
||||
"amazon/nova-pro",
|
||||
"anthropic/claude-3.5-haiku",
|
||||
"google/gemini-1.5-flash-8b",
|
||||
"google/gemini-2.0-flash-thinking",
|
||||
"google/gemma-3-27b",
|
||||
"mistral/devstral-small",
|
||||
"xai/grok-vision-beta",
|
||||
])
|
||||
|
||||
export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([
|
||||
"amazon/nova-lite",
|
||||
"anthropic/claude-3-haiku",
|
||||
"anthropic/claude-3-opus",
|
||||
"anthropic/claude-3-sonnet",
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"anthropic/claude-3.7-sonnet",
|
||||
"anthropic/claude-opus-4",
|
||||
"anthropic/claude-opus-4.1",
|
||||
"anthropic/claude-sonnet-4",
|
||||
"google/gemini-1.5-flash",
|
||||
"google/gemini-1.5-pro",
|
||||
"google/gemini-2.0-flash",
|
||||
"google/gemini-2.0-flash-lite",
|
||||
"google/gemini-2.0-pro",
|
||||
"google/gemini-2.5-flash",
|
||||
"google/gemini-2.5-flash-lite",
|
||||
"google/gemini-2.5-pro",
|
||||
"google/gemini-exp",
|
||||
"meta/llama-3.2-11b",
|
||||
"meta/llama-3.2-90b",
|
||||
"meta/llama-3.3",
|
||||
"meta/llama-4-maverick",
|
||||
"meta/llama-4-scout",
|
||||
"mistral/pixtral-12b",
|
||||
"mistral/pixtral-large",
|
||||
"moonshotai/kimi-k2",
|
||||
"openai/gpt-4-turbo",
|
||||
"openai/gpt-4.1",
|
||||
"openai/gpt-4.1-mini",
|
||||
"openai/gpt-4.1-nano",
|
||||
"openai/gpt-4.5-preview",
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-mini",
|
||||
"openai/gpt-oss-120b",
|
||||
"openai/gpt-oss-20b",
|
||||
"openai/o3",
|
||||
"openai/o3-pro",
|
||||
"openai/o4-mini",
|
||||
"vercel/v0-1.0-md",
|
||||
"xai/grok-2-vision",
|
||||
"zai/glm-4.5v",
|
||||
])
|
||||
|
||||
export const vercelAiGatewayDefaultModelInfo: ModelInfo = {
|
||||
maxTokens: 64000,
|
||||
contextWindow: 200000,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3,
|
||||
outputPrice: 15,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
description:
|
||||
"Claude Sonnet 4 significantly improves on Sonnet 3.7's industry-leading capabilities, excelling in coding with a state-of-the-art 72.7% on SWE-bench. The model balances performance and efficiency for internal and external use cases, with enhanced steerability for greater control over implementations. While not matching Opus 4 in most domains, it delivers an optimal mix of capability and practicality.",
|
||||
}
|
||||
|
||||
export const VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE = 0.7
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
|
|
@ -584,8 +584,8 @@ importers:
|
|||
specifier: ^1.14.0
|
||||
version: 1.14.0(typescript@5.8.3)
|
||||
'@roo-code/cloud':
|
||||
specifier: ^0.21.0
|
||||
version: 0.21.0
|
||||
specifier: ^0.22.0
|
||||
version: 0.22.0
|
||||
'@roo-code/ipc':
|
||||
specifier: workspace:^
|
||||
version: link:../packages/ipc
|
||||
|
|
@ -3262,11 +3262,11 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@roo-code/cloud@0.21.0':
|
||||
resolution: {integrity: sha512-yNVybIjaS7Hy8GwDtGJc76N1WpCXGaCSlAEsW7VGjnojpxaIzV2GcJP1j1hg5q8HqLQnU4ixV0qXxOkxwhkEiA==}
|
||||
'@roo-code/cloud@0.22.0':
|
||||
resolution: {integrity: sha512-s1d4wcDYeDzcwr+YypMWDlNKL4f2osOZ3NoIlD36LCfFeMs+hnluZPS1oXX3WHtmPDC76vSzPMfwW2Ef41hEoA==}
|
||||
|
||||
'@roo-code/types@1.60.0':
|
||||
resolution: {integrity: sha512-tQO6njPr/ZDNBoSHQg1/dpxfVEYeUzpKcernUxgJzmttn1zJbS0sc3CfUyPYOfYKB331z6O3KFUpaiqYFje1wA==}
|
||||
'@roo-code/types@1.61.0':
|
||||
resolution: {integrity: sha512-YJdFc6aYfaZ8EN08KbWaKLehRr1dcN3G3CzDjpppb08iehSEUZMycax/ryP5/G4vl34HTdtzyHNMboDen5ElUg==}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1':
|
||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||
|
|
@ -12563,9 +12563,9 @@ snapshots:
|
|||
'@rollup/rollup-win32-x64-msvc@4.40.2':
|
||||
optional: true
|
||||
|
||||
'@roo-code/cloud@0.21.0':
|
||||
'@roo-code/cloud@0.22.0':
|
||||
dependencies:
|
||||
'@roo-code/types': 1.60.0
|
||||
'@roo-code/types': 1.61.0
|
||||
ioredis: 5.6.1
|
||||
p-wait-for: 5.0.2
|
||||
socket.io-client: 4.8.1
|
||||
|
|
@ -12575,7 +12575,7 @@ snapshots:
|
|||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@roo-code/types@1.60.0':
|
||||
'@roo-code/types@1.61.0':
|
||||
dependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
FireworksHandler,
|
||||
RooHandler,
|
||||
FeatherlessHandler,
|
||||
VercelAiGatewayHandler,
|
||||
} from "./providers"
|
||||
import { NativeOllamaHandler } from "./providers/native-ollama"
|
||||
|
||||
|
|
@ -151,6 +152,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
|
|||
return new RooHandler(options)
|
||||
case "featherless":
|
||||
return new FeatherlessHandler(options)
|
||||
case "vercel-ai-gateway":
|
||||
return new VercelAiGatewayHandler(options)
|
||||
default:
|
||||
apiProvider satisfies "gemini-cli" | undefined
|
||||
return new AnthropicHandler(options)
|
||||
|
|
|
|||
383
src/api/providers/__tests__/vercel-ai-gateway.spec.ts
Normal file
383
src/api/providers/__tests__/vercel-ai-gateway.spec.ts
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
// npx vitest run src/api/providers/__tests__/vercel-ai-gateway.spec.ts
|
||||
|
||||
// Mock vscode first to avoid import errors
|
||||
vitest.mock("vscode", () => ({}))
|
||||
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
|
||||
import { VercelAiGatewayHandler } from "../vercel-ai-gateway"
|
||||
import { ApiHandlerOptions } from "../../../shared/api"
|
||||
import { vercelAiGatewayDefaultModelId, VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE } from "@roo-code/types"
|
||||
|
||||
// Mock dependencies
|
||||
vitest.mock("openai")
|
||||
vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) }))
|
||||
vitest.mock("../fetchers/modelCache", () => ({
|
||||
getModels: vitest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
"anthropic/claude-sonnet-4": {
|
||||
maxTokens: 64000,
|
||||
contextWindow: 200000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3,
|
||||
outputPrice: 15,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
description: "Claude Sonnet 4",
|
||||
supportsComputerUse: true,
|
||||
},
|
||||
"anthropic/claude-3.5-haiku": {
|
||||
maxTokens: 32000,
|
||||
contextWindow: 200000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1,
|
||||
outputPrice: 5,
|
||||
cacheWritesPrice: 1.25,
|
||||
cacheReadsPrice: 0.1,
|
||||
description: "Claude 3.5 Haiku",
|
||||
supportsComputerUse: false,
|
||||
},
|
||||
"openai/gpt-4o": {
|
||||
maxTokens: 16000,
|
||||
contextWindow: 128000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2.5,
|
||||
outputPrice: 10,
|
||||
cacheWritesPrice: 3.125,
|
||||
cacheReadsPrice: 0.25,
|
||||
description: "GPT-4o",
|
||||
supportsComputerUse: true,
|
||||
},
|
||||
})
|
||||
}),
|
||||
}))
|
||||
|
||||
vitest.mock("../../transform/caching/vercel-ai-gateway", () => ({
|
||||
addCacheBreakpoints: vitest.fn(),
|
||||
}))
|
||||
|
||||
const mockCreate = vitest.fn()
|
||||
const mockConstructor = vitest.fn()
|
||||
|
||||
;(OpenAI as any).mockImplementation(() => ({
|
||||
chat: {
|
||||
completions: {
|
||||
create: mockCreate,
|
||||
},
|
||||
},
|
||||
}))
|
||||
;(OpenAI as any).mockImplementation = mockConstructor.mockReturnValue({
|
||||
chat: {
|
||||
completions: {
|
||||
create: mockCreate,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
describe("VercelAiGatewayHandler", () => {
|
||||
const mockOptions: ApiHandlerOptions = {
|
||||
vercelAiGatewayApiKey: "test-key",
|
||||
vercelAiGatewayModelId: "anthropic/claude-sonnet-4",
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vitest.clearAllMocks()
|
||||
mockCreate.mockClear()
|
||||
mockConstructor.mockClear()
|
||||
})
|
||||
|
||||
it("initializes with correct options", () => {
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
expect(handler).toBeInstanceOf(VercelAiGatewayHandler)
|
||||
|
||||
expect(OpenAI).toHaveBeenCalledWith({
|
||||
baseURL: "https://ai-gateway.vercel.sh/v1",
|
||||
apiKey: mockOptions.vercelAiGatewayApiKey,
|
||||
defaultHeaders: expect.objectContaining({
|
||||
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
|
||||
"X-Title": "Roo Code",
|
||||
"User-Agent": expect.stringContaining("RooCode/"),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetchModel", () => {
|
||||
it("returns correct model info when options are provided", async () => {
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
const result = await handler.fetchModel()
|
||||
|
||||
expect(result.id).toBe(mockOptions.vercelAiGatewayModelId)
|
||||
expect(result.info.maxTokens).toBe(64000)
|
||||
expect(result.info.contextWindow).toBe(200000)
|
||||
expect(result.info.supportsImages).toBe(true)
|
||||
expect(result.info.supportsPromptCache).toBe(true)
|
||||
expect(result.info.supportsComputerUse).toBe(true)
|
||||
})
|
||||
|
||||
it("returns default model info when options are not provided", async () => {
|
||||
const handler = new VercelAiGatewayHandler({})
|
||||
const result = await handler.fetchModel()
|
||||
expect(result.id).toBe(vercelAiGatewayDefaultModelId)
|
||||
expect(result.info.supportsPromptCache).toBe(true)
|
||||
})
|
||||
|
||||
it("uses vercel ai gateway default model when no model specified", async () => {
|
||||
const handler = new VercelAiGatewayHandler({ vercelAiGatewayApiKey: "test-key" })
|
||||
const result = await handler.fetchModel()
|
||||
expect(result.id).toBe("anthropic/claude-sonnet-4")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createMessage", () => {
|
||||
beforeEach(() => {
|
||||
mockCreate.mockImplementation(async () => ({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
choices: [
|
||||
{
|
||||
delta: { content: "Test response" },
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
usage: null,
|
||||
}
|
||||
yield {
|
||||
choices: [
|
||||
{
|
||||
delta: {},
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 5,
|
||||
total_tokens: 15,
|
||||
cache_creation_input_tokens: 2,
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: 3,
|
||||
},
|
||||
cost: 0.005,
|
||||
},
|
||||
}
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it("streams text content correctly", async () => {
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
expect(chunks).toHaveLength(2)
|
||||
expect(chunks[0]).toEqual({
|
||||
type: "text",
|
||||
text: "Test response",
|
||||
})
|
||||
expect(chunks[1]).toEqual({
|
||||
type: "usage",
|
||||
inputTokens: 10,
|
||||
outputTokens: 5,
|
||||
cacheWriteTokens: 2,
|
||||
cacheReadTokens: 3,
|
||||
totalCost: 0.005,
|
||||
})
|
||||
})
|
||||
|
||||
it("uses correct temperature from options", async () => {
|
||||
const customTemp = 0.5
|
||||
const handler = new VercelAiGatewayHandler({
|
||||
...mockOptions,
|
||||
modelTemperature: customTemp,
|
||||
})
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
await handler.createMessage(systemPrompt, messages).next()
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
temperature: customTemp,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("uses default temperature when none provided", async () => {
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
await handler.createMessage(systemPrompt, messages).next()
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
temperature: VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("adds cache breakpoints for supported models", async () => {
|
||||
const { addCacheBreakpoints } = await import("../../transform/caching/vercel-ai-gateway")
|
||||
const handler = new VercelAiGatewayHandler({
|
||||
...mockOptions,
|
||||
vercelAiGatewayModelId: "anthropic/claude-3.5-haiku",
|
||||
})
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
await handler.createMessage(systemPrompt, messages).next()
|
||||
|
||||
expect(addCacheBreakpoints).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("sets correct max_completion_tokens", async () => {
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
await handler.createMessage(systemPrompt, messages).next()
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
max_completion_tokens: 64000, // max tokens for sonnet 4
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("handles usage info correctly with all Vercel AI Gateway specific fields", async () => {
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const chunks = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
const usageChunk = chunks.find((chunk) => chunk.type === "usage")
|
||||
expect(usageChunk).toEqual({
|
||||
type: "usage",
|
||||
inputTokens: 10,
|
||||
outputTokens: 5,
|
||||
cacheWriteTokens: 2,
|
||||
cacheReadTokens: 3,
|
||||
totalCost: 0.005,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("completePrompt", () => {
|
||||
beforeEach(() => {
|
||||
mockCreate.mockImplementation(async () => ({
|
||||
choices: [
|
||||
{
|
||||
message: { role: "assistant", content: "Test completion response" },
|
||||
finish_reason: "stop",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 8,
|
||||
completion_tokens: 4,
|
||||
total_tokens: 12,
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it("completes prompt correctly", async () => {
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
const prompt = "Complete this: Hello"
|
||||
|
||||
const result = await handler.completePrompt(prompt)
|
||||
|
||||
expect(result).toBe("Test completion response")
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: "anthropic/claude-sonnet-4",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
stream: false,
|
||||
temperature: VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE,
|
||||
max_completion_tokens: 64000,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("uses custom temperature for completion", async () => {
|
||||
const customTemp = 0.8
|
||||
const handler = new VercelAiGatewayHandler({
|
||||
...mockOptions,
|
||||
modelTemperature: customTemp,
|
||||
})
|
||||
|
||||
await handler.completePrompt("Test prompt")
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
temperature: customTemp,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("handles completion errors correctly", async () => {
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
const errorMessage = "API error"
|
||||
|
||||
mockCreate.mockImplementation(() => {
|
||||
throw new Error(errorMessage)
|
||||
})
|
||||
|
||||
await expect(handler.completePrompt("Test")).rejects.toThrow(
|
||||
`Vercel AI Gateway completion error: ${errorMessage}`,
|
||||
)
|
||||
})
|
||||
|
||||
it("returns empty string when no content in response", async () => {
|
||||
const handler = new VercelAiGatewayHandler(mockOptions)
|
||||
|
||||
mockCreate.mockImplementation(async () => ({
|
||||
choices: [
|
||||
{
|
||||
message: { role: "assistant", content: null },
|
||||
finish_reason: "stop",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const result = await handler.completePrompt("Test")
|
||||
expect(result).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("temperature support", () => {
|
||||
it("applies temperature for supported models", async () => {
|
||||
const handler = new VercelAiGatewayHandler({
|
||||
...mockOptions,
|
||||
vercelAiGatewayModelId: "anthropic/claude-sonnet-4",
|
||||
modelTemperature: 0.9,
|
||||
})
|
||||
|
||||
await handler.completePrompt("Test")
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
temperature: 0.9,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
317
src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts
Normal file
317
src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
// npx vitest run src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts
|
||||
|
||||
import axios from "axios"
|
||||
import { VERCEL_AI_GATEWAY_VISION_ONLY_MODELS, VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS } from "@roo-code/types"
|
||||
|
||||
import { getVercelAiGatewayModels, parseVercelAiGatewayModel } from "../vercel-ai-gateway"
|
||||
|
||||
vitest.mock("axios")
|
||||
const mockedAxios = axios as any
|
||||
|
||||
describe("Vercel AI Gateway Fetchers", () => {
|
||||
beforeEach(() => {
|
||||
vitest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("getVercelAiGatewayModels", () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4",
|
||||
object: "model",
|
||||
created: 1640995200,
|
||||
owned_by: "anthropic",
|
||||
name: "Claude Sonnet 4",
|
||||
description:
|
||||
"Claude Sonnet 4 significantly improves on Sonnet 3.7's industry-leading capabilities",
|
||||
context_window: 200000,
|
||||
max_tokens: 64000,
|
||||
type: "language",
|
||||
pricing: {
|
||||
input: "3.00",
|
||||
output: "15.00",
|
||||
input_cache_write: "3.75",
|
||||
input_cache_read: "0.30",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "anthropic/claude-3.5-haiku",
|
||||
object: "model",
|
||||
created: 1640995200,
|
||||
owned_by: "anthropic",
|
||||
name: "Claude 3.5 Haiku",
|
||||
description: "Claude 3.5 Haiku is fast and lightweight",
|
||||
context_window: 200000,
|
||||
max_tokens: 32000,
|
||||
type: "language",
|
||||
pricing: {
|
||||
input: "1.00",
|
||||
output: "5.00",
|
||||
input_cache_write: "1.25",
|
||||
input_cache_read: "0.10",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "dall-e-3",
|
||||
object: "model",
|
||||
created: 1640995200,
|
||||
owned_by: "openai",
|
||||
name: "DALL-E 3",
|
||||
description: "DALL-E 3 image generation model",
|
||||
context_window: 4000,
|
||||
max_tokens: 1000,
|
||||
type: "image",
|
||||
pricing: {
|
||||
input: "40.00",
|
||||
output: "0.00",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
it("fetches and parses models correctly", async () => {
|
||||
mockedAxios.get.mockResolvedValueOnce(mockResponse)
|
||||
|
||||
const models = await getVercelAiGatewayModels()
|
||||
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith("https://ai-gateway.vercel.sh/v1/models")
|
||||
expect(Object.keys(models)).toHaveLength(2) // Only language models
|
||||
expect(models["anthropic/claude-sonnet-4"]).toBeDefined()
|
||||
expect(models["anthropic/claude-3.5-haiku"]).toBeDefined()
|
||||
})
|
||||
|
||||
it("handles API errors gracefully", async () => {
|
||||
const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
mockedAxios.get.mockRejectedValueOnce(new Error("Network error"))
|
||||
|
||||
const models = await getVercelAiGatewayModels()
|
||||
|
||||
expect(models).toEqual({})
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Error fetching Vercel AI Gateway models"),
|
||||
)
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("handles invalid response schema gracefully", async () => {
|
||||
const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
mockedAxios.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
invalid: "response",
|
||||
data: "not an array",
|
||||
},
|
||||
})
|
||||
|
||||
const models = await getVercelAiGatewayModels()
|
||||
|
||||
expect(models).toEqual({})
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Vercel AI Gateway models response is invalid",
|
||||
expect.any(Object),
|
||||
)
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("continues processing with partially valid schema", async () => {
|
||||
const consoleErrorSpy = vitest.spyOn(console, "error").mockImplementation(() => {})
|
||||
const invalidResponse = {
|
||||
data: {
|
||||
invalid_root: "response",
|
||||
data: [
|
||||
{
|
||||
id: "anthropic/claude-sonnet-4",
|
||||
object: "model",
|
||||
created: 1640995200,
|
||||
owned_by: "anthropic",
|
||||
name: "Claude Sonnet 4",
|
||||
description: "Claude Sonnet 4",
|
||||
context_window: 200000,
|
||||
max_tokens: 64000,
|
||||
type: "language",
|
||||
pricing: {
|
||||
input: "3.00",
|
||||
output: "15.00",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
mockedAxios.get.mockResolvedValueOnce(invalidResponse)
|
||||
|
||||
const models = await getVercelAiGatewayModels()
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalled()
|
||||
expect(models["anthropic/claude-sonnet-4"]).toBeDefined()
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseVercelAiGatewayModel", () => {
|
||||
const baseModel = {
|
||||
id: "test/model",
|
||||
object: "model",
|
||||
created: 1640995200,
|
||||
owned_by: "test",
|
||||
name: "Test Model",
|
||||
description: "A test model",
|
||||
context_window: 100000,
|
||||
max_tokens: 8000,
|
||||
type: "language",
|
||||
pricing: {
|
||||
input: "2.50",
|
||||
output: "10.00",
|
||||
},
|
||||
}
|
||||
|
||||
it("parses basic model info correctly", () => {
|
||||
const result = parseVercelAiGatewayModel({
|
||||
id: "test/model",
|
||||
model: baseModel,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
maxTokens: 8000,
|
||||
contextWindow: 100000,
|
||||
supportsImages: false,
|
||||
supportsComputerUse: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2500000,
|
||||
outputPrice: 10000000,
|
||||
cacheWritesPrice: undefined,
|
||||
cacheReadsPrice: undefined,
|
||||
description: "A test model",
|
||||
})
|
||||
})
|
||||
|
||||
it("parses cache pricing when available", () => {
|
||||
const modelWithCache = {
|
||||
...baseModel,
|
||||
pricing: {
|
||||
input: "3.00",
|
||||
output: "15.00",
|
||||
input_cache_write: "3.75",
|
||||
input_cache_read: "0.30",
|
||||
},
|
||||
}
|
||||
|
||||
const result = parseVercelAiGatewayModel({
|
||||
id: "anthropic/claude-sonnet-4",
|
||||
model: modelWithCache,
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({
|
||||
supportsPromptCache: true,
|
||||
cacheWritesPrice: 3750000,
|
||||
cacheReadsPrice: 300000,
|
||||
})
|
||||
})
|
||||
|
||||
it("detects vision-only models", () => {
|
||||
// claude 3.5 haiku in VERCEL_AI_GATEWAY_VISION_ONLY_MODELS
|
||||
const visionModel = {
|
||||
...baseModel,
|
||||
id: "anthropic/claude-3.5-haiku",
|
||||
}
|
||||
|
||||
const result = parseVercelAiGatewayModel({
|
||||
id: "anthropic/claude-3.5-haiku",
|
||||
model: visionModel,
|
||||
})
|
||||
|
||||
expect(result.supportsImages).toBe(VERCEL_AI_GATEWAY_VISION_ONLY_MODELS.has("anthropic/claude-3.5-haiku"))
|
||||
expect(result.supportsComputerUse).toBe(false)
|
||||
})
|
||||
|
||||
it("detects vision and tools models", () => {
|
||||
// 4 sonnet in VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS
|
||||
const visionToolsModel = {
|
||||
...baseModel,
|
||||
id: "anthropic/claude-sonnet-4",
|
||||
}
|
||||
|
||||
const result = parseVercelAiGatewayModel({
|
||||
id: "anthropic/claude-sonnet-4",
|
||||
model: visionToolsModel,
|
||||
})
|
||||
|
||||
expect(result.supportsImages).toBe(
|
||||
VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has("anthropic/claude-sonnet-4"),
|
||||
)
|
||||
expect(result.supportsComputerUse).toBe(
|
||||
VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has("anthropic/claude-sonnet-4"),
|
||||
)
|
||||
})
|
||||
|
||||
it("handles missing cache pricing", () => {
|
||||
const modelNoCachePricing = {
|
||||
...baseModel,
|
||||
pricing: {
|
||||
input: "2.50",
|
||||
output: "10.00",
|
||||
// No cache pricing
|
||||
},
|
||||
}
|
||||
|
||||
const result = parseVercelAiGatewayModel({
|
||||
id: "test/model",
|
||||
model: modelNoCachePricing,
|
||||
})
|
||||
|
||||
expect(result.supportsPromptCache).toBe(false)
|
||||
expect(result.cacheWritesPrice).toBeUndefined()
|
||||
expect(result.cacheReadsPrice).toBeUndefined()
|
||||
})
|
||||
|
||||
it("handles partial cache pricing", () => {
|
||||
const modelPartialCachePricing = {
|
||||
...baseModel,
|
||||
pricing: {
|
||||
input: "2.50",
|
||||
output: "10.00",
|
||||
input_cache_write: "3.00",
|
||||
// Missing input_cache_read
|
||||
},
|
||||
}
|
||||
|
||||
const result = parseVercelAiGatewayModel({
|
||||
id: "test/model",
|
||||
model: modelPartialCachePricing,
|
||||
})
|
||||
|
||||
expect(result.supportsPromptCache).toBe(false)
|
||||
expect(result.cacheWritesPrice).toBe(3000000)
|
||||
expect(result.cacheReadsPrice).toBeUndefined()
|
||||
})
|
||||
|
||||
it("validates all vision model categories", () => {
|
||||
// Test a few models from each category
|
||||
const visionOnlyModels = ["anthropic/claude-3.5-haiku", "google/gemini-1.5-flash-8b"]
|
||||
const visionAndToolsModels = ["anthropic/claude-sonnet-4", "openai/gpt-4o"]
|
||||
|
||||
visionOnlyModels.forEach((modelId) => {
|
||||
if (VERCEL_AI_GATEWAY_VISION_ONLY_MODELS.has(modelId)) {
|
||||
const result = parseVercelAiGatewayModel({
|
||||
id: modelId,
|
||||
model: { ...baseModel, id: modelId },
|
||||
})
|
||||
expect(result.supportsImages).toBe(true)
|
||||
expect(result.supportsComputerUse).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
visionAndToolsModels.forEach((modelId) => {
|
||||
if (VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has(modelId)) {
|
||||
const result = parseVercelAiGatewayModel({
|
||||
id: modelId,
|
||||
model: { ...baseModel, id: modelId },
|
||||
})
|
||||
expect(result.supportsImages).toBe(true)
|
||||
expect(result.supportsComputerUse).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -10,6 +10,7 @@ import { RouterName, ModelRecord } from "../../../shared/api"
|
|||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
|
||||
import { getOpenRouterModels } from "./openrouter"
|
||||
import { getVercelAiGatewayModels } from "./vercel-ai-gateway"
|
||||
import { getRequestyModels } from "./requesty"
|
||||
import { getGlamaModels } from "./glama"
|
||||
import { getUnboundModels } from "./unbound"
|
||||
|
|
@ -81,6 +82,9 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
|
|||
case "io-intelligence":
|
||||
models = await getIOIntelligenceModels(options.apiKey)
|
||||
break
|
||||
case "vercel-ai-gateway":
|
||||
models = await getVercelAiGatewayModels()
|
||||
break
|
||||
default: {
|
||||
// Ensures router is exhaustively checked if RouterName is a strict union
|
||||
const exhaustiveCheck: never = provider
|
||||
|
|
|
|||
120
src/api/providers/fetchers/vercel-ai-gateway.ts
Normal file
120
src/api/providers/fetchers/vercel-ai-gateway.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import axios from "axios"
|
||||
import { z } from "zod"
|
||||
|
||||
import type { ModelInfo } from "@roo-code/types"
|
||||
import { VERCEL_AI_GATEWAY_VISION_ONLY_MODELS, VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS } from "@roo-code/types"
|
||||
|
||||
import type { ApiHandlerOptions } from "../../../shared/api"
|
||||
import { parseApiPrice } from "../../../shared/cost"
|
||||
|
||||
/**
|
||||
* VercelAiGatewayPricing
|
||||
*/
|
||||
|
||||
const vercelAiGatewayPricingSchema = z.object({
|
||||
input: z.string(),
|
||||
output: z.string(),
|
||||
input_cache_write: z.string().optional(),
|
||||
input_cache_read: z.string().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* VercelAiGatewayModel
|
||||
*/
|
||||
|
||||
const vercelAiGatewayModelSchema = z.object({
|
||||
id: z.string(),
|
||||
object: z.string(),
|
||||
created: z.number(),
|
||||
owned_by: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
context_window: z.number(),
|
||||
max_tokens: z.number(),
|
||||
type: z.string(),
|
||||
pricing: vercelAiGatewayPricingSchema,
|
||||
})
|
||||
|
||||
export type VercelAiGatewayModel = z.infer<typeof vercelAiGatewayModelSchema>
|
||||
|
||||
/**
|
||||
* VercelAiGatewayModelsResponse
|
||||
*/
|
||||
|
||||
const vercelAiGatewayModelsResponseSchema = z.object({
|
||||
object: z.string(),
|
||||
data: z.array(vercelAiGatewayModelSchema),
|
||||
})
|
||||
|
||||
type VercelAiGatewayModelsResponse = z.infer<typeof vercelAiGatewayModelsResponseSchema>
|
||||
|
||||
/**
|
||||
* getVercelAiGatewayModels
|
||||
*/
|
||||
|
||||
export async function getVercelAiGatewayModels(options?: ApiHandlerOptions): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
const baseURL = "https://ai-gateway.vercel.sh/v1"
|
||||
|
||||
try {
|
||||
const response = await axios.get<VercelAiGatewayModelsResponse>(`${baseURL}/models`)
|
||||
const result = vercelAiGatewayModelsResponseSchema.safeParse(response.data)
|
||||
const data = result.success ? result.data.data : response.data.data
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Vercel AI Gateway models response is invalid", result.error.format())
|
||||
}
|
||||
|
||||
for (const model of data) {
|
||||
const { id } = model
|
||||
|
||||
// Only include language models
|
||||
if (model.type !== "language") {
|
||||
continue
|
||||
}
|
||||
|
||||
models[id] = parseVercelAiGatewayModel({
|
||||
id,
|
||||
model,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error fetching Vercel AI Gateway models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
/**
|
||||
* parseVercelAiGatewayModel
|
||||
*/
|
||||
|
||||
export const parseVercelAiGatewayModel = ({ id, model }: { id: string; model: VercelAiGatewayModel }): ModelInfo => {
|
||||
const cacheWritesPrice = model.pricing?.input_cache_write
|
||||
? parseApiPrice(model.pricing?.input_cache_write)
|
||||
: undefined
|
||||
|
||||
const cacheReadsPrice = model.pricing?.input_cache_read ? parseApiPrice(model.pricing?.input_cache_read) : undefined
|
||||
|
||||
const supportsPromptCache = typeof cacheWritesPrice !== "undefined" && typeof cacheReadsPrice !== "undefined"
|
||||
const supportsImages =
|
||||
VERCEL_AI_GATEWAY_VISION_ONLY_MODELS.has(id) || VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has(id)
|
||||
const supportsComputerUse = VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS.has(id)
|
||||
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: model.max_tokens,
|
||||
contextWindow: model.context_window,
|
||||
supportsImages,
|
||||
supportsComputerUse,
|
||||
supportsPromptCache,
|
||||
inputPrice: parseApiPrice(model.pricing?.input),
|
||||
outputPrice: parseApiPrice(model.pricing?.output),
|
||||
cacheWritesPrice,
|
||||
cacheReadsPrice,
|
||||
description: model.description,
|
||||
}
|
||||
|
||||
return modelInfo
|
||||
}
|
||||
|
|
@ -32,3 +32,4 @@ export { ZAiHandler } from "./zai"
|
|||
export { FireworksHandler } from "./fireworks"
|
||||
export { RooHandler } from "./roo"
|
||||
export { FeatherlessHandler } from "./featherless"
|
||||
export { VercelAiGatewayHandler } from "./vercel-ai-gateway"
|
||||
|
|
|
|||
115
src/api/providers/vercel-ai-gateway.ts
Normal file
115
src/api/providers/vercel-ai-gateway.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
|
||||
import {
|
||||
vercelAiGatewayDefaultModelId,
|
||||
vercelAiGatewayDefaultModelInfo,
|
||||
VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE,
|
||||
VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { ApiHandlerOptions } from "../../shared/api"
|
||||
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { addCacheBreakpoints } from "../transform/caching/vercel-ai-gateway"
|
||||
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { RouterProvider } from "./router-provider"
|
||||
|
||||
// Extend OpenAI's CompletionUsage to include Vercel AI Gateway specific fields
|
||||
interface VercelAiGatewayUsage extends OpenAI.CompletionUsage {
|
||||
cache_creation_input_tokens?: number
|
||||
cost?: number
|
||||
}
|
||||
|
||||
export class VercelAiGatewayHandler extends RouterProvider implements SingleCompletionHandler {
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super({
|
||||
options,
|
||||
name: "vercel-ai-gateway",
|
||||
baseURL: "https://ai-gateway.vercel.sh/v1",
|
||||
apiKey: options.vercelAiGatewayApiKey,
|
||||
modelId: options.vercelAiGatewayModelId,
|
||||
defaultModelId: vercelAiGatewayDefaultModelId,
|
||||
defaultModelInfo: vercelAiGatewayDefaultModelInfo,
|
||||
})
|
||||
}
|
||||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const { id: modelId, info } = await this.fetchModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS.has(modelId) && info.supportsPromptCache) {
|
||||
addCacheBreakpoints(systemPrompt, openAiMessages)
|
||||
}
|
||||
|
||||
const body: OpenAI.Chat.ChatCompletionCreateParams = {
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature: this.supportsTemperature(modelId)
|
||||
? (this.options.modelTemperature ?? VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE)
|
||||
: undefined,
|
||||
max_completion_tokens: info.maxTokens,
|
||||
stream: true,
|
||||
}
|
||||
|
||||
const completion = await this.client.chat.completions.create(body)
|
||||
|
||||
for await (const chunk of completion) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
const usage = chunk.usage as VercelAiGatewayUsage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.prompt_tokens || 0,
|
||||
outputTokens: usage.completion_tokens || 0,
|
||||
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
|
||||
cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined,
|
||||
totalCost: usage.cost ?? 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
const { id: modelId, info } = await this.fetchModel()
|
||||
|
||||
try {
|
||||
const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = {
|
||||
model: modelId,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
stream: false,
|
||||
}
|
||||
|
||||
if (this.supportsTemperature(modelId)) {
|
||||
requestOptions.temperature = this.options.modelTemperature ?? VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE
|
||||
}
|
||||
|
||||
requestOptions.max_completion_tokens = info.maxTokens
|
||||
|
||||
const response = await this.client.chat.completions.create(requestOptions)
|
||||
return response.choices[0]?.message.content || ""
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Vercel AI Gateway completion error: ${error.message}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
233
src/api/transform/caching/__tests__/vercel-ai-gateway.spec.ts
Normal file
233
src/api/transform/caching/__tests__/vercel-ai-gateway.spec.ts
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
// npx vitest run src/api/transform/caching/__tests__/vercel-ai-gateway.spec.ts
|
||||
|
||||
import OpenAI from "openai"
|
||||
import { addCacheBreakpoints } from "../vercel-ai-gateway"
|
||||
|
||||
describe("Vercel AI Gateway Caching", () => {
|
||||
describe("addCacheBreakpoints", () => {
|
||||
it("adds cache control to system message", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: "Hello" },
|
||||
]
|
||||
|
||||
addCacheBreakpoints(systemPrompt, messages)
|
||||
|
||||
expect(messages[0]).toEqual({
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
cache_control: { type: "ephemeral" },
|
||||
})
|
||||
})
|
||||
|
||||
it("adds cache control to last two user messages with string content", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "First response" },
|
||||
{ role: "user", content: "Second message" },
|
||||
{ role: "assistant", content: "Second response" },
|
||||
{ role: "user", content: "Third message" },
|
||||
{ role: "assistant", content: "Third response" },
|
||||
{ role: "user", content: "Fourth message" },
|
||||
]
|
||||
|
||||
addCacheBreakpoints(systemPrompt, messages)
|
||||
|
||||
const lastUserMessage = messages[7]
|
||||
expect(Array.isArray(lastUserMessage.content)).toBe(true)
|
||||
if (Array.isArray(lastUserMessage.content)) {
|
||||
const textPart = lastUserMessage.content.find((part) => part.type === "text")
|
||||
expect(textPart).toEqual({
|
||||
type: "text",
|
||||
text: "Fourth message",
|
||||
cache_control: { type: "ephemeral" },
|
||||
})
|
||||
}
|
||||
|
||||
const secondLastUserMessage = messages[5]
|
||||
expect(Array.isArray(secondLastUserMessage.content)).toBe(true)
|
||||
if (Array.isArray(secondLastUserMessage.content)) {
|
||||
const textPart = secondLastUserMessage.content.find((part) => part.type === "text")
|
||||
expect(textPart).toEqual({
|
||||
type: "text",
|
||||
text: "Third message",
|
||||
cache_control: { type: "ephemeral" },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it("handles messages with existing array content", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Hello with image" },
|
||||
{ type: "image_url", image_url: { url: "data:image/png;base64,..." } },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
addCacheBreakpoints(systemPrompt, messages)
|
||||
|
||||
const userMessage = messages[1]
|
||||
expect(Array.isArray(userMessage.content)).toBe(true)
|
||||
if (Array.isArray(userMessage.content)) {
|
||||
const textPart = userMessage.content.find((part) => part.type === "text")
|
||||
expect(textPart).toEqual({
|
||||
type: "text",
|
||||
text: "Hello with image",
|
||||
cache_control: { type: "ephemeral" },
|
||||
})
|
||||
|
||||
const imagePart = userMessage.content.find((part) => part.type === "image_url")
|
||||
expect(imagePart).toEqual({
|
||||
type: "image_url",
|
||||
image_url: { url: "data:image/png;base64,..." },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it("handles empty string content gracefully", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: "" },
|
||||
]
|
||||
|
||||
addCacheBreakpoints(systemPrompt, messages)
|
||||
|
||||
const userMessage = messages[1]
|
||||
expect(userMessage.content).toBe("")
|
||||
})
|
||||
|
||||
it("handles messages with no text parts", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "image_url", image_url: { url: "data:image/png;base64,..." } }],
|
||||
},
|
||||
]
|
||||
|
||||
addCacheBreakpoints(systemPrompt, messages)
|
||||
|
||||
const userMessage = messages[1]
|
||||
expect(Array.isArray(userMessage.content)).toBe(true)
|
||||
if (Array.isArray(userMessage.content)) {
|
||||
const textPart = userMessage.content.find((part) => part.type === "text")
|
||||
expect(textPart).toBeUndefined()
|
||||
|
||||
const imagePart = userMessage.content.find((part) => part.type === "image_url")
|
||||
expect(imagePart).toEqual({
|
||||
type: "image_url",
|
||||
image_url: { url: "data:image/png;base64,..." },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it("processes only user messages for conversation caching", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: "First user" },
|
||||
{ role: "assistant", content: "Assistant response" },
|
||||
{ role: "user", content: "Second user" },
|
||||
]
|
||||
|
||||
addCacheBreakpoints(systemPrompt, messages)
|
||||
|
||||
expect(messages[2]).toEqual({
|
||||
role: "assistant",
|
||||
content: "Assistant response",
|
||||
})
|
||||
|
||||
const firstUser = messages[1]
|
||||
const secondUser = messages[3]
|
||||
|
||||
expect(Array.isArray(firstUser.content)).toBe(true)
|
||||
expect(Array.isArray(secondUser.content)).toBe(true)
|
||||
})
|
||||
|
||||
it("handles case with only one user message", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: "Only message" },
|
||||
]
|
||||
|
||||
addCacheBreakpoints(systemPrompt, messages)
|
||||
|
||||
const userMessage = messages[1]
|
||||
expect(Array.isArray(userMessage.content)).toBe(true)
|
||||
if (Array.isArray(userMessage.content)) {
|
||||
const textPart = userMessage.content.find((part) => part.type === "text")
|
||||
expect(textPart).toEqual({
|
||||
type: "text",
|
||||
text: "Only message",
|
||||
cache_control: { type: "ephemeral" },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it("handles case with no user messages", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "assistant", content: "Assistant only" },
|
||||
]
|
||||
|
||||
addCacheBreakpoints(systemPrompt, messages)
|
||||
|
||||
expect(messages[0]).toEqual({
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
cache_control: { type: "ephemeral" },
|
||||
})
|
||||
|
||||
expect(messages[1]).toEqual({
|
||||
role: "assistant",
|
||||
content: "Assistant only",
|
||||
})
|
||||
})
|
||||
|
||||
it("handles messages with multiple text parts", () => {
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "First part" },
|
||||
{ type: "image_url", image_url: { url: "data:image/png;base64,..." } },
|
||||
{ type: "text", text: "Second part" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
addCacheBreakpoints(systemPrompt, messages)
|
||||
|
||||
const userMessage = messages[1]
|
||||
if (Array.isArray(userMessage.content)) {
|
||||
const textParts = userMessage.content.filter((part) => part.type === "text")
|
||||
expect(textParts).toHaveLength(2)
|
||||
|
||||
expect(textParts[0]).toEqual({
|
||||
type: "text",
|
||||
text: "First part",
|
||||
})
|
||||
|
||||
expect(textParts[1]).toEqual({
|
||||
type: "text",
|
||||
text: "Second part",
|
||||
cache_control: { type: "ephemeral" },
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
30
src/api/transform/caching/vercel-ai-gateway.ts
Normal file
30
src/api/transform/caching/vercel-ai-gateway.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import OpenAI from "openai"
|
||||
|
||||
export function addCacheBreakpoints(systemPrompt: string, messages: OpenAI.Chat.ChatCompletionMessageParam[]) {
|
||||
// Apply cache_control to system message at the message level
|
||||
messages[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 = messages.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
|
||||
let 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" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -2669,6 +2669,7 @@ describe("ClineProvider - Router Models", () => {
|
|||
expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
|
||||
expect(getModels).toHaveBeenCalledWith({ provider: "glama" })
|
||||
expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
|
||||
expect(getModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
|
||||
expect(getModels).toHaveBeenCalledWith({
|
||||
provider: "litellm",
|
||||
apiKey: "litellm-key",
|
||||
|
|
@ -2686,6 +2687,7 @@ describe("ClineProvider - Router Models", () => {
|
|||
litellm: mockModels,
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -2716,6 +2718,7 @@ describe("ClineProvider - Router Models", () => {
|
|||
.mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail
|
||||
.mockResolvedValueOnce(mockModels) // glama success
|
||||
.mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail
|
||||
.mockResolvedValueOnce(mockModels) // vercel-ai-gateway success
|
||||
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail
|
||||
|
||||
await messageHandler({ type: "requestRouterModels" })
|
||||
|
|
@ -2731,6 +2734,7 @@ describe("ClineProvider - Router Models", () => {
|
|||
ollama: {},
|
||||
lmstudio: {},
|
||||
litellm: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -2841,6 +2845,7 @@ describe("ClineProvider - Router Models", () => {
|
|||
litellm: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
|
||||
expect(mockGetModels).toHaveBeenCalledWith({ provider: "glama" })
|
||||
expect(mockGetModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
|
||||
expect(mockGetModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
|
||||
expect(mockGetModels).toHaveBeenCalledWith({
|
||||
provider: "litellm",
|
||||
apiKey: "litellm-key",
|
||||
|
|
@ -195,6 +196,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
litellm: mockModels,
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -282,6 +284,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
litellm: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -302,6 +305,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
.mockRejectedValueOnce(new Error("Requesty API error")) // requesty
|
||||
.mockResolvedValueOnce(mockModels) // glama
|
||||
.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
|
||||
.mockResolvedValueOnce(mockModels) // vercel-ai-gateway
|
||||
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
|
||||
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
|
|
@ -319,6 +323,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
litellm: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
"vercel-ai-gateway": mockModels,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -352,6 +357,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
|
|||
.mockRejectedValueOnce(new Error("Requesty API error")) // requesty
|
||||
.mockRejectedValueOnce(new Error("Glama API error")) // glama
|
||||
.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
|
||||
.mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway
|
||||
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
|
||||
|
||||
await webviewMessageHandler(mockClineProvider, {
|
||||
|
|
|
|||
|
|
@ -575,6 +575,7 @@ export const webviewMessageHandler = async (
|
|||
},
|
||||
{ key: "glama", options: { provider: "glama" } },
|
||||
{ key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
|
||||
{ key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } },
|
||||
]
|
||||
|
||||
// Add IO Intelligence if API key is provided
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@
|
|||
"@mistralai/mistralai": "^1.9.18",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"@qdrant/js-client-rest": "^1.14.0",
|
||||
"@roo-code/cloud": "^0.21.0",
|
||||
"@roo-code/cloud": "^0.22.0",
|
||||
"@roo-code/ipc": "workspace:^",
|
||||
"@roo-code/telemetry": "workspace:^",
|
||||
"@roo-code/types": "workspace:^",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const routerNames = [
|
|||
"ollama",
|
||||
"lmstudio",
|
||||
"io-intelligence",
|
||||
"vercel-ai-gateway",
|
||||
] as const
|
||||
|
||||
export type RouterName = (typeof routerNames)[number]
|
||||
|
|
@ -151,3 +152,4 @@ export type GetModelsOptions =
|
|||
| { provider: "ollama"; baseUrl?: string }
|
||||
| { provider: "lmstudio"; baseUrl?: string }
|
||||
| { provider: "io-intelligence"; apiKey: string }
|
||||
| { provider: "vercel-ai-gateway" }
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
featherlessDefaultModelId,
|
||||
ioIntelligenceDefaultModelId,
|
||||
rooDefaultModelId,
|
||||
vercelAiGatewayDefaultModelId,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
|
@ -91,6 +92,7 @@ import {
|
|||
ZAi,
|
||||
Fireworks,
|
||||
Featherless,
|
||||
VercelAiGateway,
|
||||
} from "./providers"
|
||||
|
||||
import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants"
|
||||
|
|
@ -335,6 +337,7 @@ const ApiOptions = ({
|
|||
featherless: { field: "apiModelId", default: featherlessDefaultModelId },
|
||||
"io-intelligence": { field: "ioIntelligenceModelId", default: ioIntelligenceDefaultModelId },
|
||||
roo: { field: "apiModelId", default: rooDefaultModelId },
|
||||
"vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId },
|
||||
openai: { field: "openAiModelId" },
|
||||
ollama: { field: "ollamaModelId" },
|
||||
lmstudio: { field: "lmStudioModelId" },
|
||||
|
|
@ -607,6 +610,16 @@ const ApiOptions = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "vercel-ai-gateway" && (
|
||||
<VercelAiGateway
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
routerModels={routerModels}
|
||||
organizationAllowList={organizationAllowList}
|
||||
modelValidationError={modelValidationError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "human-relay" && (
|
||||
<>
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ type ModelIdKey = keyof Pick<
|
|||
| "openAiModelId"
|
||||
| "litellmModelId"
|
||||
| "ioIntelligenceModelId"
|
||||
| "vercelAiGatewayModelId"
|
||||
>
|
||||
|
||||
interface ModelPickerProps {
|
||||
|
|
|
|||
|
|
@ -79,4 +79,5 @@ export const PROVIDERS = [
|
|||
{ value: "featherless", label: "Featherless AI" },
|
||||
{ value: "io-intelligence", label: "IO Intelligence" },
|
||||
{ value: "roo", label: "Roo Code Cloud" },
|
||||
{ value: "vercel-ai-gateway", label: "Vercel AI Gateway" },
|
||||
].sort((a, b) => a.label.localeCompare(b.label))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { useCallback } from "react"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { type ProviderSettings, vercelAiGatewayDefaultModelId } from "@roo-code/types"
|
||||
|
||||
import type { OrganizationAllowList } from "@roo/cloud"
|
||||
import type { RouterModels } from "@roo/api"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
import { ModelPicker } from "../ModelPicker"
|
||||
|
||||
type VercelAiGatewayProps = {
|
||||
apiConfiguration: ProviderSettings
|
||||
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
|
||||
routerModels?: RouterModels
|
||||
organizationAllowList: OrganizationAllowList
|
||||
modelValidationError?: string
|
||||
}
|
||||
|
||||
export const VercelAiGateway = ({
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
routerModels,
|
||||
organizationAllowList,
|
||||
modelValidationError,
|
||||
}: VercelAiGatewayProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ProviderSettings, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.vercelAiGatewayApiKey || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("vercelAiGatewayApiKey")}
|
||||
placeholder={t("settings:placeholders.apiKey")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.vercelAiGatewayApiKey")}</label>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-2">
|
||||
{t("settings:providers.apiKeyStorageNotice")}
|
||||
</div>
|
||||
{!apiConfiguration?.vercelAiGatewayApiKey && (
|
||||
<VSCodeButtonLink
|
||||
href="https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys&title=AI+Gateway+API+Key"
|
||||
appearance="primary"
|
||||
style={{ width: "100%" }}>
|
||||
{t("settings:providers.getVercelAiGatewayApiKey")}
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId={vercelAiGatewayDefaultModelId}
|
||||
models={routerModels?.["vercel-ai-gateway"] ?? {}}
|
||||
modelIdKey="vercelAiGatewayModelId"
|
||||
serviceName="Vercel AI Gateway"
|
||||
serviceUrl="https://vercel.com/ai-gateway/models"
|
||||
organizationAllowList={organizationAllowList}
|
||||
errorMessage={modelValidationError}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -28,3 +28,4 @@ export { ZAi } from "./ZAi"
|
|||
export { LiteLLM } from "./LiteLLM"
|
||||
export { Fireworks } from "./Fireworks"
|
||||
export { Featherless } from "./Featherless"
|
||||
export { VercelAiGateway } from "./VercelAiGateway"
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import {
|
|||
rooModels,
|
||||
qwenCodeDefaultModelId,
|
||||
qwenCodeModels,
|
||||
vercelAiGatewayDefaultModelId,
|
||||
BEDROCK_CLAUDE_SONNET_4_MODEL_ID,
|
||||
} from "@roo-code/types"
|
||||
|
||||
|
|
@ -329,6 +330,11 @@ function getSelectedModel({
|
|||
const info = qwenCodeModels[id as keyof typeof qwenCodeModels]
|
||||
return { id, info }
|
||||
}
|
||||
case "vercel-ai-gateway": {
|
||||
const id = apiConfiguration.vercelAiGatewayModelId ?? vercelAiGatewayDefaultModelId
|
||||
const info = routerModels["vercel-ai-gateway"]?.[id]
|
||||
return { id, info }
|
||||
}
|
||||
// case "anthropic":
|
||||
// case "human-relay":
|
||||
// case "fake-ai":
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ca/settings.json
generated
2
webview-ui/src/i18n/locales/ca/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "Assegureu-vos que la regió a l'ARN coincideix amb la regió d'AWS seleccionada anteriorment.",
|
||||
"openRouterApiKey": "Clau API d'OpenRouter",
|
||||
"getOpenRouterApiKey": "Obtenir clau API d'OpenRouter",
|
||||
"vercelAiGatewayApiKey": "Clau API de Vercel AI Gateway",
|
||||
"getVercelAiGatewayApiKey": "Obtenir clau API de Vercel AI Gateway",
|
||||
"apiKeyStorageNotice": "Les claus API s'emmagatzemen de forma segura a l'Emmagatzematge Secret de VSCode",
|
||||
"glamaApiKey": "Clau API de Glama",
|
||||
"getGlamaApiKey": "Obtenir clau API de Glama",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/de/settings.json
generated
2
webview-ui/src/i18n/locales/de/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "Stellen Sie sicher, dass die Region in der ARN mit Ihrer oben ausgewählten AWS-Region übereinstimmt.",
|
||||
"openRouterApiKey": "OpenRouter API-Schlüssel",
|
||||
"getOpenRouterApiKey": "OpenRouter API-Schlüssel erhalten",
|
||||
"vercelAiGatewayApiKey": "Vercel AI Gateway API-Schlüssel",
|
||||
"getVercelAiGatewayApiKey": "Vercel AI Gateway API-Schlüssel erhalten",
|
||||
"doubaoApiKey": "Doubao API-Schlüssel",
|
||||
"getDoubaoApiKey": "Doubao API-Schlüssel erhalten",
|
||||
"apiKeyStorageNotice": "API-Schlüssel werden sicher im VSCode Secret Storage gespeichert",
|
||||
|
|
|
|||
|
|
@ -232,6 +232,8 @@
|
|||
"awsCustomArnDesc": "Make sure the region in the ARN matches your selected AWS Region above.",
|
||||
"openRouterApiKey": "OpenRouter API Key",
|
||||
"getOpenRouterApiKey": "Get OpenRouter API Key",
|
||||
"vercelAiGatewayApiKey": "Vercel AI Gateway API Key",
|
||||
"getVercelAiGatewayApiKey": "Get Vercel AI Gateway API Key",
|
||||
"apiKeyStorageNotice": "API keys are stored securely in VSCode's Secret Storage",
|
||||
"glamaApiKey": "Glama API Key",
|
||||
"getGlamaApiKey": "Get Glama API Key",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/es/settings.json
generated
2
webview-ui/src/i18n/locales/es/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "Asegúrese de que la región en el ARN coincida con la región de AWS seleccionada anteriormente.",
|
||||
"openRouterApiKey": "Clave API de OpenRouter",
|
||||
"getOpenRouterApiKey": "Obtener clave API de OpenRouter",
|
||||
"vercelAiGatewayApiKey": "Clave API de Vercel AI Gateway",
|
||||
"getVercelAiGatewayApiKey": "Obtener clave API de Vercel AI Gateway",
|
||||
"apiKeyStorageNotice": "Las claves API se almacenan de forma segura en el Almacenamiento Secreto de VSCode",
|
||||
"glamaApiKey": "Clave API de Glama",
|
||||
"getGlamaApiKey": "Obtener clave API de Glama",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/fr/settings.json
generated
2
webview-ui/src/i18n/locales/fr/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "Assurez-vous que la région dans l'ARN correspond à la région AWS sélectionnée ci-dessus.",
|
||||
"openRouterApiKey": "Clé API OpenRouter",
|
||||
"getOpenRouterApiKey": "Obtenir la clé API OpenRouter",
|
||||
"vercelAiGatewayApiKey": "Clé API Vercel AI Gateway",
|
||||
"getVercelAiGatewayApiKey": "Obtenir la clé API Vercel AI Gateway",
|
||||
"apiKeyStorageNotice": "Les clés API sont stockées en toute sécurité dans le stockage sécurisé de VSCode",
|
||||
"glamaApiKey": "Clé API Glama",
|
||||
"getGlamaApiKey": "Obtenir la clé API Glama",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/hi/settings.json
generated
2
webview-ui/src/i18n/locales/hi/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "सुनिश्चित करें कि ARN में क्षेत्र ऊपर चयनित AWS क्षेत्र से मेल खाता है।",
|
||||
"openRouterApiKey": "OpenRouter API कुंजी",
|
||||
"getOpenRouterApiKey": "OpenRouter API कुंजी प्राप्त करें",
|
||||
"vercelAiGatewayApiKey": "Vercel AI Gateway API कुंजी",
|
||||
"getVercelAiGatewayApiKey": "Vercel AI Gateway API कुंजी प्राप्त करें",
|
||||
"apiKeyStorageNotice": "API कुंजियाँ VSCode के सुरक्षित स्टोरेज में सुरक्षित रूप से संग्रहीत हैं",
|
||||
"glamaApiKey": "Glama API कुंजी",
|
||||
"getGlamaApiKey": "Glama API कुंजी प्राप्त करें",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/id/settings.json
generated
2
webview-ui/src/i18n/locales/id/settings.json
generated
|
|
@ -237,6 +237,8 @@
|
|||
"awsCustomArnDesc": "Pastikan region di ARN cocok dengan AWS Region yang kamu pilih di atas.",
|
||||
"openRouterApiKey": "OpenRouter API Key",
|
||||
"getOpenRouterApiKey": "Dapatkan OpenRouter API Key",
|
||||
"vercelAiGatewayApiKey": "Vercel AI Gateway API Key",
|
||||
"getVercelAiGatewayApiKey": "Dapatkan Vercel AI Gateway API Key",
|
||||
"apiKeyStorageNotice": "API key disimpan dengan aman di Secret Storage VSCode",
|
||||
"glamaApiKey": "Glama API Key",
|
||||
"getGlamaApiKey": "Dapatkan Glama API Key",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/it/settings.json
generated
2
webview-ui/src/i18n/locales/it/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "Assicurati che la regione nell'ARN corrisponda alla regione AWS selezionata sopra.",
|
||||
"openRouterApiKey": "Chiave API OpenRouter",
|
||||
"getOpenRouterApiKey": "Ottieni chiave API OpenRouter",
|
||||
"vercelAiGatewayApiKey": "Chiave API Vercel AI Gateway",
|
||||
"getVercelAiGatewayApiKey": "Ottieni chiave API Vercel AI Gateway",
|
||||
"apiKeyStorageNotice": "Le chiavi API sono memorizzate in modo sicuro nell'Archivio Segreto di VSCode",
|
||||
"glamaApiKey": "Chiave API Glama",
|
||||
"getGlamaApiKey": "Ottieni chiave API Glama",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ja/settings.json
generated
2
webview-ui/src/i18n/locales/ja/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "ARN内のリージョンが上で選択したAWSリージョンと一致していることを確認してください。",
|
||||
"openRouterApiKey": "OpenRouter APIキー",
|
||||
"getOpenRouterApiKey": "OpenRouter APIキーを取得",
|
||||
"vercelAiGatewayApiKey": "Vercel AI Gateway APIキー",
|
||||
"getVercelAiGatewayApiKey": "Vercel AI Gateway APIキーを取得",
|
||||
"apiKeyStorageNotice": "APIキーはVSCodeのシークレットストレージに安全に保存されます",
|
||||
"glamaApiKey": "Glama APIキー",
|
||||
"getGlamaApiKey": "Glama APIキーを取得",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ko/settings.json
generated
2
webview-ui/src/i18n/locales/ko/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "ARN의 리전이 위에서 선택한 AWS 리전과 일치하는지 확인하세요.",
|
||||
"openRouterApiKey": "OpenRouter API 키",
|
||||
"getOpenRouterApiKey": "OpenRouter API 키 받기",
|
||||
"vercelAiGatewayApiKey": "Vercel AI Gateway API 키",
|
||||
"getVercelAiGatewayApiKey": "Vercel AI Gateway API 키 받기",
|
||||
"apiKeyStorageNotice": "API 키는 VSCode의 보안 저장소에 안전하게 저장됩니다",
|
||||
"glamaApiKey": "Glama API 키",
|
||||
"getGlamaApiKey": "Glama API 키 받기",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/nl/settings.json
generated
2
webview-ui/src/i18n/locales/nl/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "Zorg ervoor dat de regio in de ARN overeenkomt met je geselecteerde AWS-regio hierboven.",
|
||||
"openRouterApiKey": "OpenRouter API-sleutel",
|
||||
"getOpenRouterApiKey": "OpenRouter API-sleutel ophalen",
|
||||
"vercelAiGatewayApiKey": "Vercel AI Gateway API-sleutel",
|
||||
"getVercelAiGatewayApiKey": "Vercel AI Gateway API-sleutel ophalen",
|
||||
"apiKeyStorageNotice": "API-sleutels worden veilig opgeslagen in de geheime opslag van VSCode",
|
||||
"glamaApiKey": "Glama API-sleutel",
|
||||
"getGlamaApiKey": "Glama API-sleutel ophalen",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/pl/settings.json
generated
2
webview-ui/src/i18n/locales/pl/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "Upewnij się, że region w ARN odpowiada wybranemu powyżej regionowi AWS.",
|
||||
"openRouterApiKey": "Klucz API OpenRouter",
|
||||
"getOpenRouterApiKey": "Uzyskaj klucz API OpenRouter",
|
||||
"vercelAiGatewayApiKey": "Klucz API Vercel AI Gateway",
|
||||
"getVercelAiGatewayApiKey": "Uzyskaj klucz API Vercel AI Gateway",
|
||||
"apiKeyStorageNotice": "Klucze API są bezpiecznie przechowywane w Tajnym Magazynie VSCode",
|
||||
"glamaApiKey": "Klucz API Glama",
|
||||
"getGlamaApiKey": "Uzyskaj klucz API Glama",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
2
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "Certifique-se de que a região no ARN corresponde à região AWS selecionada acima.",
|
||||
"openRouterApiKey": "Chave de API OpenRouter",
|
||||
"getOpenRouterApiKey": "Obter chave de API OpenRouter",
|
||||
"vercelAiGatewayApiKey": "Chave API do Vercel AI Gateway",
|
||||
"getVercelAiGatewayApiKey": "Obter chave API do Vercel AI Gateway",
|
||||
"apiKeyStorageNotice": "As chaves de API são armazenadas com segurança no Armazenamento Secreto do VSCode",
|
||||
"glamaApiKey": "Chave de API Glama",
|
||||
"getGlamaApiKey": "Obter chave de API Glama",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ru/settings.json
generated
2
webview-ui/src/i18n/locales/ru/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "Убедитесь, что регион в ARN совпадает с выбранным выше регионом AWS.",
|
||||
"openRouterApiKey": "OpenRouter API-ключ",
|
||||
"getOpenRouterApiKey": "Получить OpenRouter API-ключ",
|
||||
"vercelAiGatewayApiKey": "Ключ API Vercel AI Gateway",
|
||||
"getVercelAiGatewayApiKey": "Получить ключ API Vercel AI Gateway",
|
||||
"apiKeyStorageNotice": "API-ключи хранятся безопасно в Secret Storage VSCode",
|
||||
"glamaApiKey": "Glama API-ключ",
|
||||
"getGlamaApiKey": "Получить Glama API-ключ",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/tr/settings.json
generated
2
webview-ui/src/i18n/locales/tr/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "ARN içindeki bölgenin yukarıda seçilen AWS Bölgesiyle eşleştiğinden emin olun.",
|
||||
"openRouterApiKey": "OpenRouter API Anahtarı",
|
||||
"getOpenRouterApiKey": "OpenRouter API Anahtarı Al",
|
||||
"vercelAiGatewayApiKey": "Vercel AI Gateway API Anahtarı",
|
||||
"getVercelAiGatewayApiKey": "Vercel AI Gateway API Anahtarı Al",
|
||||
"apiKeyStorageNotice": "API anahtarları VSCode'un Gizli Depolamasında güvenli bir şekilde saklanır",
|
||||
"glamaApiKey": "Glama API Anahtarı",
|
||||
"getGlamaApiKey": "Glama API Anahtarı Al",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/vi/settings.json
generated
2
webview-ui/src/i18n/locales/vi/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "Đảm bảo rằng vùng trong ARN khớp với vùng AWS đã chọn ở trên.",
|
||||
"openRouterApiKey": "Khóa API OpenRouter",
|
||||
"getOpenRouterApiKey": "Lấy khóa API OpenRouter",
|
||||
"vercelAiGatewayApiKey": "Khóa API Vercel AI Gateway",
|
||||
"getVercelAiGatewayApiKey": "Lấy khóa API Vercel AI Gateway",
|
||||
"apiKeyStorageNotice": "Khóa API được lưu trữ an toàn trong Bộ lưu trữ bí mật của VSCode",
|
||||
"glamaApiKey": "Khóa API Glama",
|
||||
"getGlamaApiKey": "Lấy khóa API Glama",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
2
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "请确保ARN中的区域与上方选择的AWS区域一致。",
|
||||
"openRouterApiKey": "OpenRouter API 密钥",
|
||||
"getOpenRouterApiKey": "获取 OpenRouter API 密钥",
|
||||
"vercelAiGatewayApiKey": "Vercel AI Gateway API 密钥",
|
||||
"getVercelAiGatewayApiKey": "获取 Vercel AI Gateway API 密钥",
|
||||
"apiKeyStorageNotice": "API 密钥安全存储在 VSCode 的密钥存储中",
|
||||
"glamaApiKey": "Glama API 密钥",
|
||||
"getGlamaApiKey": "获取 Glama API 密钥",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
2
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
|
|
@ -233,6 +233,8 @@
|
|||
"awsCustomArnDesc": "確保 ARN 中的區域與您上面選擇的 AWS 區域相符。",
|
||||
"openRouterApiKey": "OpenRouter API 金鑰",
|
||||
"getOpenRouterApiKey": "取得 OpenRouter API 金鑰",
|
||||
"vercelAiGatewayApiKey": "Vercel AI Gateway API 金鑰",
|
||||
"getVercelAiGatewayApiKey": "取得 Vercel AI Gateway API 金鑰",
|
||||
"apiKeyStorageNotice": "API 金鑰安全儲存於 VSCode 金鑰儲存中",
|
||||
"glamaApiKey": "Glama API 金鑰",
|
||||
"getGlamaApiKey": "取得 Glama API 金鑰",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ describe("Model Validation Functions", () => {
|
|||
ollama: {},
|
||||
lmstudio: {},
|
||||
"io-intelligence": {},
|
||||
"vercel-ai-gateway": {},
|
||||
}
|
||||
|
||||
const allowAllOrganization: OrganizationAllowList = {
|
||||
|
|
|
|||
|
|
@ -136,6 +136,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri
|
|||
return i18next.t("settings:validation.qwenCodeOauthPath")
|
||||
}
|
||||
break
|
||||
case "vercel-ai-gateway":
|
||||
if (!apiConfiguration.vercelAiGatewayApiKey) {
|
||||
return i18next.t("settings:validation.apiKey")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return undefined
|
||||
|
|
@ -204,6 +209,8 @@ function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: str
|
|||
return apiConfiguration.huggingFaceModelId
|
||||
case "io-intelligence":
|
||||
return apiConfiguration.ioIntelligenceModelId
|
||||
case "vercel-ai-gateway":
|
||||
return apiConfiguration.vercelAiGatewayModelId
|
||||
default:
|
||||
return apiConfiguration.apiModelId
|
||||
}
|
||||
|
|
@ -277,6 +284,9 @@ export function validateModelId(apiConfiguration: ProviderSettings, routerModels
|
|||
case "io-intelligence":
|
||||
modelId = apiConfiguration.ioIntelligenceModelId
|
||||
break
|
||||
case "vercel-ai-gateway":
|
||||
modelId = apiConfiguration.vercelAiGatewayModelId
|
||||
break
|
||||
}
|
||||
|
||||
if (!modelId) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue