From e14fa19d83b2aae715046dc4f8e57788f79fcbd1 Mon Sep 17 00:00:00 2001 From: martinzudergaming-a11y Date: Mon, 31 Aug 2026 13:44:14 +0000 Subject: [PATCH] feat: add OrcaRouter as a first-class provider Add OrcaRouter (OpenAI-compatible AI gateway) as a named provider, mirroring the NanoGPT/OpenRouter wiring: new provider identifier, RouterProvider-based handler, public model catalog fetcher, settings schema, UI component, validation, and locale strings. Closes #1463 --- .../__tests__/provider-identifiers.test.ts | 2 + .../src/__tests__/provider-model-id.test.ts | 1 + packages/types/src/global-settings.ts | 1 + packages/types/src/provider-identifiers.ts | 1 + packages/types/src/provider-settings.ts | 3 + packages/types/src/provider-settings/index.ts | 2 + .../src/provider-settings/orca-router.ts | 17 +++ packages/types/src/providers/index.ts | 4 + packages/types/src/providers/orca-router.ts | 18 +++ src/api/__tests__/index.spec.ts | 2 + src/api/index.ts | 3 + src/api/providers/fetchers/modelCache.ts | 8 ++ src/api/providers/fetchers/orca-router.ts | 83 +++++++++++ src/api/providers/index.ts | 1 + src/api/providers/orca-router.ts | 135 ++++++++++++++++++ .../webview/__tests__/ClineProvider.spec.ts | 5 + .../__tests__/webviewMessageHandler.spec.ts | 13 ++ src/core/webview/webviewMessageHandler.ts | 5 + src/shared/ProfileValidator.ts | 2 + src/shared/api.ts | 1 + .../src/components/settings/ApiOptions.tsx | 12 ++ .../src/components/settings/ModelPicker.tsx | 1 + .../src/components/settings/constants.ts | 1 + .../settings/providers/OrcaRouter.tsx | 94 ++++++++++++ .../providers/__tests__/NanoGPT.spec.tsx | 1 + .../components/settings/providers/index.ts | 1 + .../settings/utils/providerModelConfig.ts | 2 + .../components/ui/hooks/useSelectedModel.ts | 10 ++ webview-ui/src/i18n/locales/ca/settings.json | 6 + webview-ui/src/i18n/locales/de/settings.json | 6 + webview-ui/src/i18n/locales/en/settings.json | 6 + webview-ui/src/i18n/locales/es/settings.json | 6 + webview-ui/src/i18n/locales/fr/settings.json | 6 + webview-ui/src/i18n/locales/hi/settings.json | 6 + webview-ui/src/i18n/locales/id/settings.json | 6 + webview-ui/src/i18n/locales/it/settings.json | 6 + webview-ui/src/i18n/locales/ja/settings.json | 6 + webview-ui/src/i18n/locales/ko/settings.json | 6 + webview-ui/src/i18n/locales/nl/settings.json | 6 + webview-ui/src/i18n/locales/pl/settings.json | 6 + .../src/i18n/locales/pt-BR/settings.json | 6 + webview-ui/src/i18n/locales/ru/settings.json | 6 + webview-ui/src/i18n/locales/tr/settings.json | 6 + webview-ui/src/i18n/locales/vi/settings.json | 6 + .../src/i18n/locales/zh-CN/settings.json | 6 + .../src/i18n/locales/zh-TW/settings.json | 6 + .../src/utils/__tests__/validate.spec.ts | 1 + webview-ui/src/utils/validate.ts | 5 + 48 files changed, 543 insertions(+) create mode 100644 packages/types/src/provider-settings/orca-router.ts create mode 100644 packages/types/src/providers/orca-router.ts create mode 100644 src/api/providers/fetchers/orca-router.ts create mode 100644 src/api/providers/orca-router.ts create mode 100644 webview-ui/src/components/settings/providers/OrcaRouter.tsx diff --git a/packages/types/src/__tests__/provider-identifiers.test.ts b/packages/types/src/__tests__/provider-identifiers.test.ts index 29e55b31cf..a924e0a068 100644 --- a/packages/types/src/__tests__/provider-identifiers.test.ts +++ b/packages/types/src/__tests__/provider-identifiers.test.ts @@ -23,6 +23,7 @@ import { const expectedProviderIdentifiers = [ "openrouter", + "orcarouter", "vercel-ai-gateway", "zoo-gateway", "litellm", @@ -97,6 +98,7 @@ describe("provider identifiers", () => { it("derives provider category collections from canonical identifiers", () => { expect(dynamicProviders).toEqual([ providerIdentifiers.openrouter, + providerIdentifiers.orcaRouter, providerIdentifiers.vercelAiGateway, providerIdentifiers.zooGateway, providerIdentifiers.litellm, diff --git a/packages/types/src/__tests__/provider-model-id.test.ts b/packages/types/src/__tests__/provider-model-id.test.ts index 7c404c1cd4..d3abb40b84 100644 --- a/packages/types/src/__tests__/provider-model-id.test.ts +++ b/packages/types/src/__tests__/provider-model-id.test.ts @@ -3,6 +3,7 @@ import { getModelId, modelIdKeys, providerIdentifiers, type ProviderSettings } f const expectedModelIdKeys = [ "apiModelId", "openRouterModelId", + "orcaRouterModelId", "openAiModelId", "ollamaModelId", "lmStudioModelId", diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 95f246dbe7..55e5a25cf7 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -339,6 +339,7 @@ export const SECRET_STATE_KEYS = [ "opencodeGoApiKey", "kenariApiKey", "nanoGptApiKey", + "orcaRouterApiKey", "basetenApiKey", ] as const diff --git a/packages/types/src/provider-identifiers.ts b/packages/types/src/provider-identifiers.ts index fdf507bb6b..cbbc5b870e 100644 --- a/packages/types/src/provider-identifiers.ts +++ b/packages/types/src/provider-identifiers.ts @@ -5,6 +5,7 @@ */ export const providerIdentifiers = { openrouter: "openrouter", + orcaRouter: "orcarouter", vercelAiGateway: "vercel-ai-gateway", zooGateway: "zoo-gateway", litellm: "litellm", diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 4a432970c2..f6f5acded9 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -61,6 +61,7 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 export const dynamicProviders = [ providerIdentifiers.openrouter, + providerIdentifiers.orcaRouter, providerIdentifiers.vercelAiGateway, providerIdentifiers.zooGateway, providerIdentifiers.litellm, @@ -275,6 +276,7 @@ export type ModelIdKey = Extract object const expectedHandlers = { [providerIdentifiers.anthropic]: AnthropicHandler, [providerIdentifiers.openrouter]: OpenRouterHandler, + [providerIdentifiers.orcaRouter]: OrcaRouterHandler, [providerIdentifiers.bedrock]: AwsBedrockHandler, [providerIdentifiers.openai]: OpenAiHandler, [providerIdentifiers.ollama]: NativeOllamaHandler, diff --git a/src/api/index.ts b/src/api/index.ts index 8e7f20d66f..93652374b9 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -16,6 +16,7 @@ import { AnthropicHandler, AwsBedrockHandler, OpenRouterHandler, + OrcaRouterHandler, PoeHandler, VertexHandler, AnthropicVertexHandler, @@ -169,6 +170,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new AnthropicHandler(options) case providerIdentifiers.openrouter: return new OpenRouterHandler(options) + case providerIdentifiers.orcaRouter: + return new OrcaRouterHandler(options) case providerIdentifiers.bedrock: return new AwsBedrockHandler(options) case providerIdentifiers.vertex: diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 50dbe12f6e..a1ba54655e 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -18,6 +18,7 @@ import type { RouterName } from "../../../shared/api" import { fileExistsAtPath } from "../../../utils/fs" import { getOpenRouterModels } from "./openrouter" +import { getOrcaRouterModels } from "./orca-router" import { getVercelAiGatewayModels } from "./vercel-ai-gateway" import { getOpencodeGoModels } from "./opencode-go" import { getKenariModels } from "./kenari" @@ -230,6 +231,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise { provider: providerIdentifiers.openrouter, options: { provider: providerIdentifiers.openrouter }, }, + { + provider: providerIdentifiers.orcaRouter, + options: { provider: providerIdentifiers.orcaRouter }, + }, { provider: providerIdentifiers.vercelAiGateway, options: { provider: providerIdentifiers.vercelAiGateway }, diff --git a/src/api/providers/fetchers/orca-router.ts b/src/api/providers/fetchers/orca-router.ts new file mode 100644 index 0000000000..7c58865c2f --- /dev/null +++ b/src/api/providers/fetchers/orca-router.ts @@ -0,0 +1,83 @@ +import axios from "axios" +import { z } from "zod" + +import { ORCA_ROUTER_BASE_URL, orcaRouterDefaultModelInfo, type ModelInfo, type ModelRecord } from "@roo-code/types" + +const orcaRouterModelSchema = z.object({ + id: z.string().min(1), + name: z.string().optional(), + description: z.string().optional(), + context_length: z.number().positive().nullish(), + max_completion_tokens: z.number().positive().nullish(), + architecture: z + .object({ + input_modalities: z.array(z.string()).optional(), + }) + .optional(), + pricing: z + .object({ + prompt: z.string().nullish(), + completion: z.string().nullish(), + }) + .optional(), +}) + +const orcaRouterModelsResponseSchema = z.object({ + data: z.array(z.unknown()), +}) + +function parseApiPrice(value: string | null | undefined): number | undefined { + if (value === null || value === undefined) { + return undefined + } + + const price = Number(value) + return Number.isFinite(price) ? price : undefined +} + +export const parseOrcaRouterModel = (model: z.infer): ModelInfo => ({ + contextWindow: model.context_length ?? orcaRouterDefaultModelInfo.contextWindow, + maxTokens: model.max_completion_tokens ?? orcaRouterDefaultModelInfo.maxTokens, + supportsPromptCache: false, + supportsImages: model.architecture?.input_modalities?.includes("image") ?? false, + ...(model.name !== undefined ? { displayName: model.name } : {}), + ...(model.description !== undefined ? { description: model.description } : {}), + ...(model.pricing?.prompt !== undefined ? { inputPrice: parseApiPrice(model.pricing.prompt) } : {}), + ...(model.pricing?.completion !== undefined ? { outputPrice: parseApiPrice(model.pricing.completion) } : {}), +}) + +/** + * Fetches OrcaRouter's public model catalog. The endpoint is unauthenticated, + * so the API key is optional and only forwarded when present. + */ +export async function getOrcaRouterModels(apiKey?: string): Promise { + try { + const response = await axios.get(`${ORCA_ROUTER_BASE_URL}/models`, { + headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined, + timeout: 10_000, + }) + const responseResult = orcaRouterModelsResponseSchema.safeParse(response.data) + if (!responseResult.success) { + console.warn("OrcaRouter models response did not match the expected top-level schema") + return {} + } + + const models: ModelRecord = {} + for (const rawModel of responseResult.data.data) { + const modelResult = orcaRouterModelSchema.safeParse(rawModel) + if (!modelResult.success) { + continue + } + + models[modelResult.data.id] = parseOrcaRouterModel(modelResult.data) + } + + return models + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error( + `Error fetching OrcaRouter models: ${apiKey ? message.replaceAll(apiKey, "[REDACTED]") : message}`, + ) + return {} + } +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 8ba1eae382..b08be6ddfc 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -16,6 +16,7 @@ export { OpenAiHandler } from "./openai" export { OpenAICompatibleHandler } from "./openai-compatible" export type { OpenAICompatibleConfig } from "./openai-compatible" export { OpenRouterHandler } from "./openrouter" +export { OrcaRouterHandler } from "./orca-router" export { PoeHandler } from "./poe" export { QwenCodeHandler } from "./qwen-code" export { RequestyHandler } from "./requesty" diff --git a/src/api/providers/orca-router.ts b/src/api/providers/orca-router.ts new file mode 100644 index 0000000000..e6a550c598 --- /dev/null +++ b/src/api/providers/orca-router.ts @@ -0,0 +1,135 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { + ORCA_ROUTER_BASE_URL, + orcaRouterDefaultModelId, + orcaRouterDefaultModelInfo, + providerIdentifiers, +} from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" + +import type { ApiStream, ApiStreamUsageChunk } from "../transform/stream" +import { convertToOpenAiMessages } from "../transform/openai-format" +import type { ApiHandlerCreateMessageMetadata, CompletePromptOptions, SingleCompletionHandler } from "../index" +import { RouterProvider } from "./router-provider" +import { handleProviderError } from "./utils/error-handler" +import { extractReasoningFromDelta } from "./utils/extract-reasoning" + +type OrcaRouterUsage = OpenAI.CompletionUsage & { + reasoning_tokens?: number + cost?: number +} + +function mapOrcaRouterUsage(usage: OrcaRouterUsage): ApiStreamUsageChunk { + return { + type: "usage", + inputTokens: usage.prompt_tokens ?? 0, + outputTokens: usage.completion_tokens ?? 0, + cacheReadTokens: usage.prompt_tokens_details?.cached_tokens, + reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? usage.reasoning_tokens, + totalCost: usage.cost, + } +} + +export class OrcaRouterHandler extends RouterProvider implements SingleCompletionHandler { + constructor(options: ApiHandlerOptions) { + super({ + options, + name: providerIdentifiers.orcaRouter, + baseURL: ORCA_ROUTER_BASE_URL, + apiKey: options.orcaRouterApiKey, + modelId: options.orcaRouterModelId, + defaultModelId: orcaRouterDefaultModelId, + defaultModelInfo: orcaRouterDefaultModelInfo, + }) + } + + private createSafeError(operation: string, error: unknown): Error { + return handleProviderError(error, "OrcaRouter", { + messagePrefix: operation, + messageTransformer: (message) => + this.options.orcaRouterApiKey + ? `OrcaRouter ${operation} error: ${message.replaceAll(this.options.orcaRouterApiKey, "[REDACTED]")}` + : `OrcaRouter ${operation} error: ${message}`, + }) + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { id: modelId, info } = await this.fetchModel() + const body: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { + model: modelId, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + max_tokens: info.maxTokens ?? undefined, + tools: this.convertToolsForOpenAI(metadata?.tools), + tool_choice: metadata?.tool_choice, + parallel_tool_calls: metadata?.parallelToolCalls ?? true, + } + + if (this.options.modelTemperature !== undefined && this.supportsTemperature(modelId)) { + body.temperature = this.options.modelTemperature + } + + try { + const completion = await this.client.chat.completions.create(body, { signal: metadata?.abortSignal }) + for await (const chunk of completion) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { type: "text", text: delta.content } + } + + const reasoning = extractReasoningFromDelta(delta) + if (reasoning) { + yield { type: "reasoning", text: reasoning } + } + + for (const toolCall of delta?.tool_calls ?? []) { + yield { + type: "tool_call_partial", + index: toolCall.index, + id: toolCall.id, + name: toolCall.function?.name, + arguments: toolCall.function?.arguments, + } + } + + if (chunk.usage) { + yield mapOrcaRouterUsage(chunk.usage as OrcaRouterUsage) + } + } + } catch (error) { + throw this.createSafeError("streaming", error) + } + } + + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { + const { id: modelId, info } = await this.fetchModel() + const body: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { + model: modelId, + messages: [{ role: "user", content: prompt }], + stream: false, + max_tokens: info.maxTokens ?? undefined, + } + + if (this.options.modelTemperature !== undefined && this.supportsTemperature(modelId)) { + body.temperature = this.options.modelTemperature + } + + try { + const response = await this.client.chat.completions.create(body, { + signal: options?.abortSignal, + timeout: options?.timeoutMs, + }) + return response.choices[0]?.message.content ?? "" + } catch (error) { + throw this.createSafeError("completion", error) + } + } +} diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 731124cccc..8d8ced3cba 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -3583,6 +3583,7 @@ describe("ClineProvider - Router Models", () => { // Verify getModels was called for each provider with correct options expect(getModels).toHaveBeenCalledWith({ provider: providerIdentifiers.openrouter }) + expect(getModels).toHaveBeenCalledWith({ provider: providerIdentifiers.orcaRouter }) expect(getModels).toHaveBeenCalledWith({ provider: providerIdentifiers.requesty, apiKey: "requesty-key" }) expect(getModels).toHaveBeenCalledWith({ provider: providerIdentifiers.unbound }) expect(getModels).toHaveBeenCalledWith({ provider: providerIdentifiers.vercelAiGateway }) @@ -3603,6 +3604,7 @@ describe("ClineProvider - Router Models", () => { type: "routerModels", routerModels: { openrouter: mockModels, + orcarouter: mockModels, requesty: mockModels, unbound: mockModels, "vercel-ai-gateway": mockModels, @@ -3643,6 +3645,7 @@ describe("ClineProvider - Router Models", () => { // Mock some providers to succeed and others to fail vi.mocked(getModels) .mockResolvedValueOnce(mockModels) // openrouter success + .mockResolvedValueOnce(mockModels) // orcarouter success .mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail .mockResolvedValueOnce(mockModels) // unbound success .mockResolvedValueOnce(mockModels) // vercel-ai-gateway success @@ -3659,6 +3662,7 @@ describe("ClineProvider - Router Models", () => { type: "routerModels", routerModels: { openrouter: mockModels, + orcarouter: mockModels, requesty: {}, unbound: mockModels, "vercel-ai-gateway": mockModels, @@ -3760,6 +3764,7 @@ describe("ClineProvider - Router Models", () => { type: "routerModels", routerModels: { openrouter: mockModels, + orcarouter: mockModels, requesty: mockModels, unbound: mockModels, "vercel-ai-gateway": mockModels, diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 4c2a301965..b14e84f21f 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -538,6 +538,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { // Verify getModels was called for each provider expect(mockGetModels).toHaveBeenCalledWith({ provider: providerIdentifiers.openrouter }) + expect(mockGetModels).toHaveBeenCalledWith({ provider: providerIdentifiers.orcaRouter }) expect(mockGetModels).toHaveBeenCalledWith({ provider: providerIdentifiers.requesty, apiKey: "requesty-key" }) expect(mockGetModels).toHaveBeenCalledWith( expect.objectContaining({ @@ -564,6 +565,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { type: "routerModels", routerModels: { openrouter: mockModels, + orcarouter: mockModels, requesty: mockModels, unbound: mockModels, "vercel-ai-gateway": mockModels, @@ -812,6 +814,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { type: "routerModels", routerModels: { openrouter: mockModels, + orcarouter: mockModels, requesty: mockModels, unbound: mockModels, "vercel-ai-gateway": mockModels, @@ -844,6 +847,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { // Mock some providers to succeed and others to fail mockGetModels .mockResolvedValueOnce(mockModels) // openrouter + .mockResolvedValueOnce(mockModels) // orcarouter .mockRejectedValueOnce(new Error("Requesty API error")) // requesty .mockResolvedValueOnce(mockModels) // unbound .mockResolvedValueOnce(mockModels) // vercel-ai-gateway @@ -877,6 +881,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { type: "routerModels", routerModels: { openrouter: mockModels, + orcarouter: mockModels, requesty: {}, unbound: mockModels, "vercel-ai-gateway": mockModels, @@ -900,6 +905,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { // Mock providers to fail with different error types mockGetModels .mockRejectedValueOnce(new Error("Structured error message")) // openrouter + .mockRejectedValueOnce(new Error("OrcaRouter error")) // orcarouter .mockRejectedValueOnce(new Error("Requesty API error")) // requesty .mockRejectedValueOnce(new Error("Unbound error")) // unbound .mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway @@ -918,6 +924,13 @@ describe("webviewMessageHandler - requestRouterModels", () => { values: { provider: providerIdentifiers.openrouter }, }) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "OrcaRouter error", + values: { provider: providerIdentifiers.orcaRouter }, + }) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 0dad65a480..1ff77155b9 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1120,6 +1120,7 @@ export const webviewMessageHandler = async ( ? ({} as Record) : { [providerIdentifiers.openrouter]: {}, + [providerIdentifiers.orcaRouter]: {}, [providerIdentifiers.vercelAiGateway]: {}, [providerIdentifiers.zooGateway]: {}, [providerIdentifiers.litellm]: {}, @@ -1155,6 +1156,10 @@ export const webviewMessageHandler = async ( key: providerIdentifiers.openrouter, options: { provider: providerIdentifiers.openrouter }, }, + { + key: providerIdentifiers.orcaRouter, + options: { provider: providerIdentifiers.orcaRouter }, + }, { key: providerIdentifiers.requesty, options: { diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index 923582bd06..fe162237f1 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -74,6 +74,8 @@ export class ProfileValidator { return profile.vsCodeLmModelSelector?.id case providerIdentifiers.openrouter: return profile.openRouterModelId + case providerIdentifiers.orcaRouter: + return profile.orcaRouterModelId case providerIdentifiers.ollama: return profile.ollamaModelId case providerIdentifiers.requesty: diff --git a/src/shared/api.ts b/src/shared/api.ts index 1787e15e88..a7df95d52c 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -177,6 +177,7 @@ type CommonFetchParams = { // until a corresponding entry is added here. const dynamicProviderExtras = { openrouter: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type + orcarouter: {} as { apiKey?: string }, // eslint-disable-line @typescript-eslint/no-empty-object-type "vercel-ai-gateway": {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type "zoo-gateway": {} as { apiKey?: string; baseUrl?: string }, litellm: {} as { apiKey?: string; baseUrl: string }, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 3e1495baff..a355fc888c 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -67,6 +67,7 @@ import { OpenAICompatible, OpenAICodex, OpenRouter, + OrcaRouter, Poe, QwenCode, Requesty, @@ -693,6 +694,17 @@ const ApiOptions = ({ /> )} + {selectedProvider === providerIdentifiers.orcaRouter && ( + + )} + {selectedProvider === providerIdentifiers.zooGateway && ( = [ { value: providerIdentifiers.openrouter, label: "OpenRouter", proxy: false }, + { value: providerIdentifiers.orcaRouter, label: "OrcaRouter", proxy: false }, { value: providerIdentifiers.anthropic, label: "Anthropic", proxy: false }, { value: providerIdentifiers.gemini, label: "Google Gemini", proxy: false }, { value: providerIdentifiers.deepseek, label: "DeepSeek", proxy: false }, diff --git a/webview-ui/src/components/settings/providers/OrcaRouter.tsx b/webview-ui/src/components/settings/providers/OrcaRouter.tsx new file mode 100644 index 0000000000..2e6d7e1830 --- /dev/null +++ b/webview-ui/src/components/settings/providers/OrcaRouter.tsx @@ -0,0 +1,94 @@ +import { useCallback, useEffect } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import { + type OrganizationAllowList, + type ProviderSettings, + type RouterModels, + orcaRouterDefaultModelId, + providerIdentifiers, + RouterModelsMessageType, +} from "@roo-code/types" + +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { vscode } from "@src/utils/vscode" + +import { ModelPicker } from "../ModelPicker" +import { inputEventTransform } from "../transforms" + +type OrcaRouterProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void + routerModels?: RouterModels + organizationAllowList: OrganizationAllowList + modelValidationError?: string + simplifySettings?: boolean +} + +export const OrcaRouter = ({ + apiConfiguration, + setApiConfigurationField, + routerModels, + organizationAllowList, + modelValidationError, + simplifySettings, +}: OrcaRouterProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + useEffect(() => { + vscode.postMessage({ + type: RouterModelsMessageType.requestRouterModels, + values: { + provider: providerIdentifiers.orcaRouter, + orcaRouterApiKey: apiConfiguration.orcaRouterApiKey, + }, + }) + }, [apiConfiguration.orcaRouterApiKey]) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration.orcaRouterApiKey && ( + + {t("settings:providers.orcaRouter.getApiKey")} + + )} + + + + ) +} diff --git a/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx index bb810caa75..bbda51e03a 100644 --- a/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx @@ -102,6 +102,7 @@ describe("NanoGPT", () => { moonshot: {}, "opencode-go": {}, kenari: {}, + orcarouter: {}, nanogpt: { "openai/test": { contextWindow: 1, maxTokens: 1, supportsPromptCache: false } }, "kimi-code": {}, ollama: {}, diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 12ea4cd786..ab0cd26d3a 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -11,6 +11,7 @@ export { OpenAI } from "./OpenAI" export { OpenAICodex } from "./OpenAICodex" export { OpenAICompatible } from "./OpenAICompatible" export { OpenRouter } from "./OpenRouter" +export { OrcaRouter } from "./OrcaRouter" export { Poe } from "./Poe" export { QwenCode } from "./QwenCode" export { Requesty } from "./Requesty" diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index 7230e4cbe4..1ede09e51d 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -9,6 +9,7 @@ import { geminiDefaultModelId, mistralDefaultModelId, openRouterDefaultModelId, + orcaRouterDefaultModelId, openAiNativeDefaultModelId, openAiCodexDefaultModelId, qwenCodeDefaultModelId, @@ -116,6 +117,7 @@ export type ProviderModelConfig = { // Kept in this file to keep ApiOptions.tsx from growing a second registry. const PROVIDER_MODEL_CONFIG: Partial> = { [providerIdentifiers.openrouter]: { field: "openRouterModelId", default: openRouterDefaultModelId }, + [providerIdentifiers.orcaRouter]: { field: "orcaRouterModelId", default: orcaRouterDefaultModelId }, [providerIdentifiers.requesty]: { field: "requestyModelId", default: requestyDefaultModelId }, [providerIdentifiers.unbound]: { field: "unboundModelId", default: unboundDefaultModelId }, [providerIdentifiers.litellm]: { field: "litellmModelId", default: litellmDefaultModelId }, diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index b7ad9e87e5..1cc7842c19 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -32,6 +32,7 @@ import { opencodeGoDefaultModelInfo, kenariDefaultModelInfo, nanoGptDefaultModelInfo, + orcaRouterDefaultModelInfo, BEDROCK_1M_CONTEXT_MODEL_IDS, VERTEX_1M_CONTEXT_MODEL_IDS, isDynamicProvider, @@ -444,6 +445,15 @@ function getSelectedModel({ const info = routerModels[providerIdentifiers.nanogpt]?.[id] ?? nanoGptDefaultModelInfo return { id, info } } + case providerIdentifiers.orcaRouter: { + const id = getValidatedModelId( + apiConfiguration.orcaRouterModelId, + routerModels[providerIdentifiers.orcaRouter], + defaultModelId, + ) + const info = routerModels[providerIdentifiers.orcaRouter]?.[id] ?? orcaRouterDefaultModelInfo + return { id, info } + } case providerIdentifiers.zooGateway: { const id = getValidatedModelId( apiConfiguration.zooGatewayModelId, diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 52805e74e8..f3d2cc4d21 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Les preferències d'encaminament avançades poden utilitzar encaminament de pagament per ús i poden quedar fora de la cobertura de la subscripció, segons les regles de facturació actuals de NanoGPT.", "routingDocs": "Consulta els detalls d'encaminament i facturació de NanoGPT" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "Les claus API s'emmagatzemen de forma segura a l'Emmagatzematge Secret de VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index b895717422..b847567074 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Erweiterte Routing-Präferenzen können Pay-as-you-go-Routing verwenden und je nach den aktuellen Abrechnungsregeln von NanoGPT die Abdeckung deines Abos umgehen.", "routingDocs": "Routing- und Abrechnungsdetails von NanoGPT anzeigen" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "API-Schlüssel werden sicher im VSCode Secret Storage gespeichert", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index eaa37b7034..137c34d0a8 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -501,6 +501,12 @@ "billingWarning": "Advanced routing preferences may use pay-as-you-go routing and may bypass subscription coverage, depending on NanoGPT's current billing rules.", "routingDocs": "View NanoGPT routing and billing details" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "API keys are stored securely in VSCode's Secret Storage", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index abb8a60609..d6ca1c7c17 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Las preferencias de enrutamiento avanzadas pueden usar enrutamiento de pago por uso y pueden omitir la cobertura de la suscripción, según las reglas de facturación actuales de NanoGPT.", "routingDocs": "Ver detalles de enrutamiento y facturación de NanoGPT" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "Las claves API se almacenan de forma segura en el Almacenamiento Secreto de VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 272f21a6ee..3c67ae6ffb 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Les préférences de routage avancées peuvent utiliser une facturation à l'usage et peuvent contourner la couverture de l'abonnement, selon les règles de facturation actuelles de NanoGPT.", "routingDocs": "Voir les détails de routage et de facturation de NanoGPT" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "Les clés API sont stockées en toute sécurité dans le stockage sécurisé de VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 0a4152b17a..4c9ca81073 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -421,6 +421,12 @@ "billingWarning": "NanoGPT के मौजूदा बिलिंग नियमों के आधार पर उन्नत रूटिंग प्राथमिकताएँ भुगतान-प्रति-उपयोग रूटिंग का उपयोग कर सकती हैं और सदस्यता कवरेज को छोड़ सकती हैं।", "routingDocs": "NanoGPT रूटिंग और बिलिंग विवरण देखें" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "API कुंजियाँ VSCode के सुरक्षित स्टोरेज में सुरक्षित रूप से संग्रहीत हैं", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index b8abe9ab25..dfa3de7f24 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Preferensi routing lanjutan dapat menggunakan routing bayar sesuai pemakaian dan dapat melewati cakupan langganan, tergantung aturan penagihan NanoGPT saat ini.", "routingDocs": "Lihat detail routing dan penagihan NanoGPT" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "API key disimpan dengan aman di Secret Storage VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index e49ede9cec..754e225b58 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Le preferenze di routing avanzate possono usare il routing a consumo e possono escludere la copertura dell'abbonamento, in base alle regole di fatturazione attuali di NanoGPT.", "routingDocs": "Visualizza i dettagli di routing e fatturazione di NanoGPT" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "Le chiavi API sono memorizzate in modo sicuro nell'Archivio Segreto di VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index d58c86c95d..1cfc5da7ba 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -421,6 +421,12 @@ "billingWarning": "高度なルーティング設定では従量課金ルーティングが使用され、NanoGPTの現在の請求ルールによってはサブスクリプションの対象外になる場合があります。", "routingDocs": "NanoGPTのルーティングと請求の詳細を表示" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "APIキーはVSCodeのシークレットストレージに安全に保存されます", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 68ce8b2523..9b47c0806e 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -421,6 +421,12 @@ "billingWarning": "고급 라우팅 기본 설정은 사용량 기반 라우팅을 사용할 수 있으며 NanoGPT의 현재 결제 규칙에 따라 구독 범위를 벗어날 수 있습니다.", "routingDocs": "NanoGPT 라우팅 및 결제 세부 정보 보기" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "API 키는 VSCode의 보안 저장소에 안전하게 저장됩니다", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 8d90d7747e..d0c2ac9ccc 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Geavanceerde routeringsvoorkeuren kunnen betalen-per-gebruik-routering gebruiken en kunnen, afhankelijk van de huidige factureringsregels van NanoGPT, buiten je abonnement vallen.", "routingDocs": "Routerings- en factureringsdetails van NanoGPT bekijken" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "API-sleutels worden veilig opgeslagen in de geheime opslag van VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index ffc1cdf1a4..41f111691f 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Zaawansowane preferencje routingu mogą używać rozliczeń za użycie i mogą omijać zakres subskrypcji, zależnie od aktualnych zasad rozliczeń NanoGPT.", "routingDocs": "Zobacz szczegóły routingu i rozliczeń NanoGPT" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "Klucze API są bezpiecznie przechowywane w Tajnym Magazynie VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index cf92b76ac7..0f64ac12dd 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Preferências de roteamento avançadas podem usar cobrança por uso e podem ignorar a cobertura da assinatura, dependendo das regras de cobrança atuais do NanoGPT.", "routingDocs": "Ver detalhes de roteamento e cobrança do NanoGPT" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "As chaves de API são armazenadas com segurança no Armazenamento Secreto do VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 23ff32faa9..3fc87bb0a7 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Расширенные предпочтения маршрутизации могут использовать оплату по факту использования и могут обходить покрытие подписки в зависимости от текущих правил оплаты NanoGPT.", "routingDocs": "Подробнее о маршрутизации и оплате NanoGPT" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "API-ключи хранятся безопасно в Secret Storage VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index f674e116d2..e00bc1eb3a 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Gelişmiş yönlendirme tercihleri kullanıma göre ödeme yönlendirmesini kullanabilir ve NanoGPT'nin güncel faturalandırma kurallarına bağlı olarak abonelik kapsamını atlayabilir.", "routingDocs": "NanoGPT yönlendirme ve faturalandırma ayrıntılarını görüntüle" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "API anahtarları VSCode'un Gizli Depolamasında güvenli bir şekilde saklanır", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 4b908ca658..ee2e27ff84 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -421,6 +421,12 @@ "billingWarning": "Tùy chọn định tuyến nâng cao có thể sử dụng định tuyến trả theo mức dùng và có thể không thuộc phạm vi gói đăng ký, tùy theo quy tắc thanh toán hiện tại của NanoGPT.", "routingDocs": "Xem chi tiết định tuyến và thanh toán NanoGPT" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "Khóa API được lưu trữ an toàn trong Bộ lưu trữ bí mật của VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index d79edca302..e1021a0ffa 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -421,6 +421,12 @@ "billingWarning": "高级路由偏好可能使用按用量付费路由,并且根据 NanoGPT 当前的计费规则,可能不在订阅范围内。", "routingDocs": "查看 NanoGPT 路由和计费详情" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "API 密钥安全存储在 VSCode 的密钥存储中", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 250cc2111b..bf89688255 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -448,6 +448,12 @@ "billingWarning": "進階路由偏好可能使用隨用隨付路由,並且根據 NanoGPT 目前的計費規則,可能不在訂閱涵蓋範圍內。", "routingDocs": "查看 NanoGPT 路由與計費詳細資訊" }, + "orcaRouter": { + "provider": "OrcaRouter", + "apiKey": "OrcaRouter API Key", + "getApiKey": "Get OrcaRouter API Key", + "model": "Model" + }, "apiKeyStorageNotice": "API 金鑰會安全地儲存在 VS Code 的 Secret Storage 中", "openAiCodexRateLimits": { "title": "Codex 用量限制{{planLabel}}", diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 6535e3ba61..cae3608251 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -58,6 +58,7 @@ describe("Model Validation Functions", () => { deepseek: {}, "opencode-go": {}, kenari: {}, + orcarouter: {}, nanogpt: {}, "zoo-gateway": {}, "kimi-code": {}, diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 4174e8f821..d902172f59 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -148,6 +148,11 @@ function validateModelsAndKeysProvided( return i18next.t("settings:validation.apiKey") } break + case providerIdentifiers.orcaRouter: + if (!apiConfiguration.orcaRouterApiKey) { + return i18next.t("settings:validation.apiKey") + } + break case providerIdentifiers.zooGateway: if (!apiConfiguration.zooSessionToken && !zooCodeIsAuthenticated) { return i18next.t("settings:validation.zooGatewaySignIn")