From 41c23adf15454bf9d101d2a25076a2304ca2d7df Mon Sep 17 00:00:00 2001 From: XiaoHuo888-hue Date: Sat, 22 Aug 2026 10:30:08 +0800 Subject: [PATCH] feat: add OrcaRouter as a named model provider (#3502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds [OrcaRouter](https://www.orcarouter.ai) as a first-class named model provider, mirroring the existing OpenRouter integration. OrcaRouter is an OpenAI-compatible gateway — `https://api.orcarouter.ai/v1` — that routes to models from many providers (Anthropic, OpenAI, DeepSeek, Google, Grok, Qwen, Zhipu, and more) through a single API key. It also runs gateway-level, zero-trust security for AI agents on the same endpoint — screening every prompt/response and governing every tool call on a default-deny basis, with no application code changes. What this PR does: - **Catalog sync**: `syncStartupModels` now fetches the OrcaRouter `/v1/models` catalog at startup (gated by a new `orcarouterModelSync` global setting, default enabled) and registers an `openai-completions` provider at `https://api.orcarouter.ai/v1`. The key resolves from the `ORCAROUTER_API_KEY` environment variable. - **Auth + onboarding**: OrcaRouter appears in the API-key auth catalog, onboarding setup copy + key hints, and quick-start provider list (key format `sk-orca-...`, dashboard link to `https://www.orcarouter.ai`). - **Icons**: New OrcaRouter brand mark (`OrcaRouterIcon`) with a dedicated color token, wired into `ProviderIcon` and `inferProviderIconKey` so `orcarouter/...` model ids render the brand mark everywhere. - **Settings**: Startup-sync toggle under **Settings → Global Models**, settings search entry, and save-split allowlists. - **Docs + i18n**: `docs/settings-reference.md` / `docs/storage.md` rows and `en` + secondary-locale catalog keys. - **Changeset**: `.changeset/add-orcarouter-provider.md` (minor). Disclosure: I'm an engineer on the OrcaRouter team. ## Test plan - `pnpm --filter @runfusion/fusion exec vitest run src/commands/__tests__/startup-model-sync.test.ts` — 18 passed - `pnpm --filter @fusion/core exec vitest run src/__tests__/settings-parity.test.ts` — 80 passed - `pnpm --filter @fusion/dashboard exec vitest run app/components/__tests__/ProviderIcon.test.tsx` — 115 passed - `pnpm --filter @fusion/dashboard exec vitest run app/components/__tests__/AuthenticationSection.test.tsx app/components/__tests__/ModelOnboardingModal.test.tsx` — 222 passed - `tsc --noEmit` in `@fusion/cli`, `@fusion/core`, `@fusion/dashboard`, `@fusion/engine` — all clean - `pnpm lint` on changed files — clean - `pnpm check:changesets` — clean - **L3 live test**: exercised the production `syncStartupModels` path against the live OrcaRouter API — catalog sync registered 209 models and a real chat completion returned **HTTP 200** (`finish_reason: "stop"`). Note: `pnpm i18n:status` reports pre-existing Jira key-parity gaps in the `app.json` catalogs on a clean checkout (unrelated to this change); the `common.json` catalogs touched here are in parity. ## Summary by CodeRabbit - **New Features** - Added OrcaRouter as a built-in OpenAI-compatible provider. - Added API-key setup, validation, authentication, onboarding guidance, provider branding, and model catalog visibility. - Added automatic startup synchronization of OrcaRouter models, enabled by default. - Added a global setting to enable or disable model synchronization. - **Documentation** - Documented the new synchronization setting and its configuration. - **Localization** - Added OrcaRouter setup and usage text across supported languages. Signed-off-by: XiaoHuo888-hue Co-authored-by: XiaoHuo888-hue Co-authored-by: Claude --- .changeset/add-orcarouter-provider.md | 7 +++ docs/settings-reference.md | 1 + docs/storage.md | 1 + .../__tests__/startup-model-sync.test.ts | 58 +++++++++++++++++- .../cli/src/commands/startup-model-sync.ts | 61 +++++++++++++++++++ .../src/__tests__/settings-parity.test.ts | 6 ++ packages/core/src/config/settings-schema.ts | 1 + .../core/src/types/settings/settings-scope.ts | 4 ++ .../app/components/CustomProviderForm.tsx | 2 +- .../app/components/ModelOnboardingModal.tsx | 15 ++++- .../dashboard/app/components/ProviderIcon.tsx | 33 ++++++++++ .../app/components/settings/save-split.ts | 2 + .../sections/GlobalModelsSection.search.ts | 10 +++ .../settings/sections/GlobalModelsSection.tsx | 10 +++ packages/dashboard/app/styles.css | 1 + .../dashboard/app/utils/providerIconKey.ts | 3 + .../src/routes/auth-provider-catalog.ts | 1 + packages/engine/src/auth/provider-auth.ts | 1 + packages/i18n/locales/en/common.json | 4 ++ packages/i18n/locales/es/common.json | 4 ++ packages/i18n/locales/fr/common.json | 4 ++ packages/i18n/locales/ko/common.json | 4 ++ packages/i18n/locales/pt-BR/common.json | 4 ++ packages/i18n/locales/zh-CN/common.json | 4 ++ packages/i18n/locales/zh-TW/common.json | 4 ++ 25 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 .changeset/add-orcarouter-provider.md diff --git a/.changeset/add-orcarouter-provider.md b/.changeset/add-orcarouter-provider.md new file mode 100644 index 0000000000..db0244106f --- /dev/null +++ b/.changeset/add-orcarouter-provider.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add OrcaRouter as a named model provider with startup catalog sync. +category: feature +dev: Syncs the OrcaRouter `/v1/models` catalog at startup (gated by `orcarouterModelSync`), registers an `openai-completions` provider at `https://api.orcarouter.ai/v1` resolving its key from `ORCAROUTER_API_KEY`, and surfaces OrcaRouter across the auth catalog, onboarding quick-start, provider icons, and settings. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 931a343230..6569b19edb 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -131,6 +131,7 @@ Fusion automatically falls back to ntfy's JSON publish format when a notificatio | `openrouterModelFilters` | `{ supported_parameters?: string[]; output_modalities?: string[] }` | `undefined` | Optional model-catalog filters appended to OpenRouter sync requests as comma-joined query params (`supported_parameters`, `output_modalities`). See OpenRouter models API: . | | `openrouterProviderPreferences` | `{ order?: string[]; ignore?: string[]; only?: string[]; allow_fallbacks?: boolean; sort?: "price" \| "throughput" \| "latency"; require_parameters?: boolean }` | `undefined` | Optional OpenRouter provider routing preferences forwarded via `compat.openRouterRouting` on chat-completion requests. See OpenRouter provider routing: . | | `opencodeGoModelSync` | `boolean` | `true` | Sync opencode-go model catalog at startup via `opencode models opencode --refresh`, and re-run that refresh after saving an `opencode`/`opencode-go` API key in Dashboard Settings, normalizing discovered `opencode/...` IDs into the `opencode-go` provider surface used by `/api/models`. | +| `orcarouterModelSync` | `boolean` | `true` | Sync the OrcaRouter model catalog into model pickers at startup from `https://api.orcarouter.ai/v1/models`. When an OrcaRouter API key is configured it is sent as a bearer token; the catalog is public so sync works without one. | | `updateCheckEnabled` | `boolean` | `true` | When enabled, Fusion performs a daily npm registry check for new `@runfusion/fusion` versions and shows update notices in CLI/dashboard. | | `updateChannel` | `"stable" \| "beta"` | `"stable"` | Release track for every update surface (CLI `fn update`, dashboard update check, desktop auto-updater). `stable` follows the npm `latest` dist-tag; `beta` follows the semver-max of `latest` and `beta`, so beta users also receive each promoted stable release. Switching beta → stable never downgrades — the install stays on its beta until the next stable overtakes it (`fn update --channel stable --force` downgrades explicitly). Dashboard location: **Settings → General → Release channel**. See `RELEASING.md` → "Release tracks". | | `autoUpdateEnabled` | `boolean` | `undefined` (effective `false`) | Enables channel-aware background installation after the six-hour watcher check (first check about one minute after boot). `true` installs supported dashboard updates; `false` disables background installation. It is shown in **Settings → General → Automatically install updates**. | diff --git a/docs/storage.md b/docs/storage.md index 9db18bb545..23c33217e7 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -378,6 +378,7 @@ API endpoints reviewed: | `favoriteProviders` | Global | `GET/PUT /api/settings/global` | Favorited providers | | `favoriteModels` | Global | `GET/PUT /api/settings/global` | Favorited models | | `openrouterModelSync` | Global | `GET/PUT /api/settings/global` | Startup model sync behavior | +| `orcarouterModelSync` | Global | `GET/PUT /api/settings/global` | OrcaRouter startup model sync behavior | | `modelOnboardingComplete` | Global | `GET/PUT /api/settings/global` | Onboarding completion flag | | `executionGlobalProvider` | Global | `GET/PUT /api/settings/global` | Global baseline AI provider for task execution | | `executionGlobalModelId` | Global | `GET/PUT /api/settings/global` | Global baseline AI model ID for task execution | diff --git a/packages/cli/src/commands/__tests__/startup-model-sync.test.ts b/packages/cli/src/commands/__tests__/startup-model-sync.test.ts index 9e2d09ac07..87b37b64f3 100644 --- a/packages/cli/src/commands/__tests__/startup-model-sync.test.ts +++ b/packages/cli/src/commands/__tests__/startup-model-sync.test.ts @@ -39,6 +39,13 @@ describe("startup-model-sync", () => { json: vi.fn().mockResolvedValue(response.body ?? { data: [] }), }); } + // OrcaRouter sync runs alongside OpenRouter by default; give it a benign + // catalog response so OpenRouter-focused tests do not consume an undefined mock. + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ data: [] }), + }); vi.stubGlobal("fetch", fetchMock); } @@ -88,7 +95,7 @@ describe("startup-model-sync", () => { const registerProvider = vi.fn(); await syncStartupModels({ - getSettings: vi.fn().mockResolvedValue({ openrouterModelSync: false, opencodeGoModelSync: false }), + getSettings: vi.fn().mockResolvedValue({ openrouterModelSync: false, opencodeGoModelSync: false, orcarouterModelSync: false }), authStorage: { getApiKey: vi.fn() }, modelRegistry: { registerProvider }, log: vi.fn(), @@ -99,6 +106,55 @@ describe("startup-model-sync", () => { expect(registerProvider).not.toHaveBeenCalled(); }); + it("syncs OrcaRouter models as a named OpenAI-compatible provider", async () => { + mockOpenRouterFetchSequence({ ok: true }); + + const registerProvider = vi.fn(); + const log = vi.fn(); + await syncStartupModels({ + getSettings: vi.fn().mockResolvedValue({ + openrouterModelSync: false, + orcarouterModelSync: true, + opencodeGoModelSync: false, + }), + authStorage: { getApiKey: vi.fn().mockResolvedValue("sk-orca-test") }, + modelRegistry: { registerProvider }, + log, + }); + + const orcaRouterCall = registerProvider.mock.calls.find(([name]) => name === "orcarouter"); + expect(orcaRouterCall).toBeDefined(); + expect(orcaRouterCall?.[1]).toMatchObject({ + baseUrl: "https://api.orcarouter.ai/v1", + apiKey: "ORCAROUTER_API_KEY", + api: "openai-completions", + models: expect.any(Array), + }); + expect(log).toHaveBeenCalledWith("orcarouter", expect.stringContaining("Synced")); + }); + + it("sends an OrcaRouter bearer token when an API key is stored", async () => { + mockOpenRouterFetchSequence({ ok: true }); + + await syncStartupModels({ + getSettings: vi.fn().mockResolvedValue({ + openrouterModelSync: false, + orcarouterModelSync: true, + opencodeGoModelSync: false, + }), + authStorage: { getApiKey: vi.fn().mockResolvedValue("sk-orca-secret") }, + modelRegistry: { registerProvider: vi.fn() }, + log: vi.fn(), + }); + + const calls = (globalThis.fetch as ReturnType).mock.calls; + expect(calls.some(([url]) => String(url).includes("api.orcarouter.ai"))).toBe(true); + const orcaCall = calls.find(([url]) => String(url).includes("api.orcarouter.ai"))!; + expect(orcaCall?.[1]).toEqual({ + headers: { Authorization: "Bearer sk-orca-secret" }, + }); + }); + it("sends default OpenRouter attribution headers", async () => { mockOpenRouterFetchSequence({ ok: true }); diff --git a/packages/cli/src/commands/startup-model-sync.ts b/packages/cli/src/commands/startup-model-sync.ts index 72e7c25f51..c974bcbba7 100644 --- a/packages/cli/src/commands/startup-model-sync.ts +++ b/packages/cli/src/commands/startup-model-sync.ts @@ -4,6 +4,8 @@ const OPENROUTER_PUBLIC_MODELS_URL = "https://openrouter.ai/api/v1/models"; const OPENROUTER_USER_MODELS_URL = "https://openrouter.ai/api/v1/models/user"; const OPENROUTER_DEFAULT_REFERER = "https://runfusion.ai"; const OPENROUTER_DEFAULT_TITLE = "Fusion"; +const ORCAROUTER_MODELS_URL = "https://api.orcarouter.ai/v1/models"; +const ORCAROUTER_DEFAULT_BASE_URL = "https://api.orcarouter.ai/v1"; const OPENCODE_MODELS_TIMEOUT_MS = 15_000; type ModelConfig = { @@ -52,6 +54,7 @@ interface OpenRouterProviderPreferences { interface SettingsLike { openrouterModelSync?: boolean; + orcarouterModelSync?: boolean; opencodeGoModelSync?: boolean; openrouterAppAttribution?: { referer?: string; title?: string }; openrouterModelFilters?: OpenRouterModelFilters; @@ -203,6 +206,55 @@ async function syncOpenRouterModels(options: StartupSyncOptions, settings: Setti log("openrouter", `Synced ${models.length} models from OpenRouter API`); } +/* +FNXC:OrcaRouterProvider 2026-08-21-14:00: +Fusion ships OrcaRouter as a named OpenAI-compatible gateway provider, mirroring +the OpenRouter catalog-sync pattern. OrcaRouter exposes a public `/v1/models` +catalog (same OpenAI shape as OpenRouter), so the same `toOpenRouterModels` +mapping is reused — the catalog returns prefixed model IDs +(`anthropic/claude-sonnet-5`, `deepseek/deepseek-v4-flash-0731`) that OrcaRouter +accepts verbatim on `POST /v1/chat/completions`. Registration uses +`api: "openai-completions"` and resolves its key from the `ORCAROUTER_API_KEY` +environment variable so operators can store a key without touching the +dashboard. Sync is gated by `orcarouterModelSync` (default true), and requests +carry the key only when present. +*/ +async function syncOrcaRouterModels(options: StartupSyncOptions): Promise { + const { authStorage, modelRegistry, log } = options; + const apiKey = await authStorage.getApiKey("orcarouter"); + const headers: Record = {}; + if (apiKey) { + headers.Authorization = `Bearer ${apiKey}`; + } + + const response = await fetch(ORCAROUTER_MODELS_URL, { headers }); + + if (!response.ok) { + log("orcarouter", `Failed to sync models: HTTP ${response.status}`); + return; + } + + const json = await response.json() as { + data?: Array<{ + id: string; + name: string; + context_length?: number; + top_provider?: { max_completion_tokens?: number }; + pricing?: Record; + architecture?: { modality?: string; input_modalities?: string[] }; + }>; + }; + + const models = toOpenRouterModels(json); + modelRegistry.registerProvider("orcarouter", { + baseUrl: ORCAROUTER_DEFAULT_BASE_URL, + apiKey: "ORCAROUTER_API_KEY", + api: "openai-completions", + models, + }); + log("orcarouter", `Synced ${models.length} models from OrcaRouter API`); +} + export function normalizeOpencodeGoModel(modelId: string): ModelConfig { const trimmed = modelId.trim(); // Strip the provider prefix (opencode/ or opencode-go/) — the Pi SDK @@ -330,6 +382,15 @@ export async function syncStartupModels(options: StartupSyncOptions): Promise { expect(isProjectSettingsKey("openrouterProviderPreferences")).toBe(false); }); + it("keeps OrcaRouter model sync global with an enabled default", () => { + expect(DEFAULT_GLOBAL_SETTINGS.orcarouterModelSync).toBe(true); + expect(isGlobalSettingsKey("orcarouterModelSync")).toBe(true); + expect(isProjectSettingsKey("orcarouterModelSync")).toBe(false); + }); + it("defaults stale high fan-out blocker escalation age threshold", () => { expect(DEFAULT_PROJECT_SETTINGS.staleHighFanoutBlockerAgeThresholdMs).toBe(2 * 60 * 60 * 1000); expect(isProjectSettingsKey("staleHighFanoutBlockerAgeThresholdMs")).toBe(true); diff --git a/packages/core/src/config/settings-schema.ts b/packages/core/src/config/settings-schema.ts index 047dd085a0..ec4fa8ea6f 100644 --- a/packages/core/src/config/settings-schema.ts +++ b/packages/core/src/config/settings-schema.ts @@ -215,6 +215,7 @@ export const DEFAULT_GLOBAL_SETTINGS = { openrouterAppAttribution: undefined, openrouterModelFilters: undefined, openrouterProviderPreferences: undefined, + orcarouterModelSync: true, opencodeGoModelSync: true, updateCheckEnabled: true, fnBinaryCheckEnabled: true, diff --git a/packages/core/src/types/settings/settings-scope.ts b/packages/core/src/types/settings/settings-scope.ts index 4935210cda..e6bba4f1ce 100644 --- a/packages/core/src/types/settings/settings-scope.ts +++ b/packages/core/src/types/settings/settings-scope.ts @@ -552,6 +552,10 @@ export interface GlobalSettings { * opencode-go provider list without waiting for a later session bootstrap. * Default: true. */ opencodeGoModelSync?: boolean; + /** When true (default), startup syncs the OrcaRouter model catalog from + * `https://api.orcarouter.ai/v1/models` into model pickers so operators can + * select OrcaRouter-hosted models by name. Default: true. */ + orcarouterModelSync?: boolean; /** When true (default), checks npm for new versions of @runfusion/fusion and * shows update notices in the CLI and dashboard. The actual cadence is * governed by `updateCheckFrequency`. Disabled = no automatic checks at all. */ diff --git a/packages/dashboard/app/components/CustomProviderForm.tsx b/packages/dashboard/app/components/CustomProviderForm.tsx index be148d6aa9..852bdc3b05 100644 --- a/packages/dashboard/app/components/CustomProviderForm.tsx +++ b/packages/dashboard/app/components/CustomProviderForm.tsx @@ -9,7 +9,7 @@ import "./CustomProviderForm.css"; export const BUILT_IN_PROVIDER_IDS = new Set([ "anthropic", "claude-cli", "pi-claude-cli", "openai", "openai-codex", "google", "gemini", "google-antigravity", "antigravity", "google-vertex", "vertex", "google-cloud-code", "cloud-code", "google-gemini-cli", "google-generative-ai", - "ollama", "github", "github-copilot", "openrouter", "minimax", "minimax-cn", "zai", "kimi", "moonshot", "kimi-coding", + "ollama", "github", "github-copilot", "openrouter", "orcarouter", "minimax", "minimax-cn", "zai", "kimi", "moonshot", "kimi-coding", "bedrock", "amazon-bedrock", "xai", "grok", "opencode", "opencode-go", "qwen", "qwen-ai", "qwen-coder", "alibaba", "tongyi", "lmstudio", "lm-studio", "huggingface", "hugging-face", "hf", "mistral", "mistral-ai", "azure", "azure-openai", "azure-openai-responses", "fireworks", "fireworks-ai", "fireworksai", "cerebras", "groq", "vercel", "vercel-ai-gateway", diff --git a/packages/dashboard/app/components/ModelOnboardingModal.tsx b/packages/dashboard/app/components/ModelOnboardingModal.tsx index f0b6333bc3..017d788336 100644 --- a/packages/dashboard/app/components/ModelOnboardingModal.tsx +++ b/packages/dashboard/app/components/ModelOnboardingModal.tsx @@ -182,6 +182,16 @@ function getProviderInfoMap(t: (key: string, defaultValue: string) => string): R usageDescription: t("setup.apiKeyUsage.openrouter", "Routes to multiple AI model providers through a single key"), }, }, + orcarouter: { + description: t("setup.providerDesc.orcarouter", "OrcaRouter — one gateway for multiple AI providers with gateway-level agent security"), + apiKeyInfo: { + fieldLabel: t("setup.apiKeyLabel.orcarouter", "OrcaRouter API Key"), + setupInstructions: t("setup.apiKeySetup.orcarouter", "Create an API key from your OrcaRouter dashboard."), + dashboardUrl: "https://www.orcarouter.ai", + inputPlaceholder: "sk-orca-...", + usageDescription: t("setup.apiKeyUsage.orcarouter", "Routes to multiple AI model providers through a single key"), + }, + }, }; } @@ -195,6 +205,7 @@ const PROVIDER_KEY_HINTS: Record = { openai: "OpenAI", "openai-codex": "OpenAI Codex", openrouter: "OpenRouter", + orcarouter: "OrcaRouter", google: "Google", gemini: "Gemini", minimax: "MiniMax", @@ -273,7 +285,7 @@ Keep Anthropic subscription OAuth and raw Anthropic API-key auth as separate fir FNXC:Onboarding 2026-07-18-03:40: The OpenAI Codex subscription card belongs in quick start (it was buried in Advanced), placed directly AFTER the Anthropic subscription and BEFORE the API-key options — subscription sign-ins are the primary first-run path, keys are the fallback. */ -const QUICK_START_PROVIDER_IDS = ["anthropic-subscription", "openai-codex", "anthropic-api-key", "anthropic", "openai", "google", "gemini", "openrouter", "ollama"] as const; +const QUICK_START_PROVIDER_IDS = ["anthropic-subscription", "openai-codex", "anthropic-api-key", "anthropic", "openai", "google", "gemini", "openrouter", "orcarouter", "ollama"] as const; const ONBOARDING_CURATED_PROVIDER_FAMILY_ORDER = [ "anthropic", @@ -283,6 +295,7 @@ const ONBOARDING_CURATED_PROVIDER_FAMILY_ORDER = [ "llama-cpp", "openai-codex", "openrouter", + "orcarouter", "gemini", "minimax", "kimi", diff --git a/packages/dashboard/app/components/ProviderIcon.tsx b/packages/dashboard/app/components/ProviderIcon.tsx index f2c6870b1b..ecedc7ddcb 100644 --- a/packages/dashboard/app/components/ProviderIcon.tsx +++ b/packages/dashboard/app/components/ProviderIcon.tsx @@ -569,6 +569,37 @@ function OpenRouterIcon({ size, color, label = "OpenRouter" }: { size: number; c ); } +/* +FNXC:OrcaRouterProvider 2026-08-21-14:00: +OrcaRouter brand mark — an orca fin motif (two smooth arcs forming a dorsal fin above +the waterline). Simplified geometric symbol that keeps its silhouette legible at +compact card sizes, matching the hand-drawn marks used for other gateway providers. +*/ +function OrcaRouterIcon({ size, color, label = "OrcaRouter" }: { size: number; color: string; label?: string }) { + return ( + + + + + ); +} + // GitHub logo (Octocat mark) from SimpleIcons. function GitHubIcon({ size, color, label = "GitHub" }: { size: number; color: string; label?: string }) { return ( @@ -854,6 +885,8 @@ const providerConfig: Record< // OpenRouter appears in onboarding auth cards and should not fall back to CPU. openrouter: { component: OpenRouterIcon, color: "var(--provider-openrouter)" }, + // OrcaRouter is a named gateway provider in the auth catalog and onboarding. + orcarouter: { component: OrcaRouterIcon, color: "var(--provider-orcarouter)" }, /* FNXC:ProviderIcon 2026-07-22-12:00: FN-8488 makes every static Authentication catalog ID resolve through a branded ProviderIcon entry. Brave Search and Tavily are API-key catalog providers, not custom provider names, so they must never reach the unknown Cpu fallback. diff --git a/packages/dashboard/app/components/settings/save-split.ts b/packages/dashboard/app/components/settings/save-split.ts index 8518d6a9f5..4779338c98 100644 --- a/packages/dashboard/app/components/settings/save-split.ts +++ b/packages/dashboard/app/components/settings/save-split.ts @@ -230,6 +230,7 @@ export const GLOBAL_SECTION_KEYS: Record> = { "openrouterModelFilters", "openrouterModelSync", "openrouterProviderPreferences", + "orcarouterModelSync", "executionGlobalProvider", "executionGlobalModelId", "planningGlobalProvider", @@ -266,6 +267,7 @@ export const GLOBAL_SECTION_KEYS: Record> = { "openrouterModelFilters", "openrouterModelSync", "openrouterProviderPreferences", + "orcarouterModelSync", "executionGlobalProvider", "executionGlobalModelId", "planningGlobalProvider", diff --git a/packages/dashboard/app/components/settings/sections/GlobalModelsSection.search.ts b/packages/dashboard/app/components/settings/sections/GlobalModelsSection.search.ts index f1458140d6..a087403c6a 100644 --- a/packages/dashboard/app/components/settings/sections/GlobalModelsSection.search.ts +++ b/packages/dashboard/app/components/settings/sections/GlobalModelsSection.search.ts @@ -33,6 +33,16 @@ export const globalModelsSearchEntries: SettingsSearchEntry[] = [ " When enabled, startup fetches the latest available models from the OpenRouter API so model pickers always include the newest catalog. Default: enabled. ", keywords: ["refresh", "boot"], }, + { + sectionId: "global-models", + key: "orcarouterModelSync", + labelKey: "settings.globalModels.syncOrcaRouterModelListAtStartup", + labelFallback: " Sync OrcaRouter model list at startup ", + helpKey: "settings.globalModels.whenEnabledStartupFetchesTheLatestOrcaRouterModels", + helpFallback: + " When enabled, startup fetches the latest available models from the OrcaRouter API so model pickers include the OrcaRouter catalog. Default: enabled. ", + keywords: ["refresh", "boot"], + }, { sectionId: "global-models", key: "openrouterAppAttribution.referer", diff --git a/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx b/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx index 87ba49defc..244b7cfffd 100644 --- a/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx @@ -199,6 +199,16 @@ export function GlobalModelsSection({ form, setForm, availableModels, modelsLoad value={form.openrouterModelSync !== false} onChange={(v) => setForm((f) => ({ ...f, openrouterModelSync: v === true }))} /> + setForm((f) => ({ ...f, orcarouterModelSync: v === true }))} + /> {/* FNXC:SettingsStyling 2026-07-15-17:35: Left on hand-rolled markup deliberately: its help text embeds a element for the `opencode models opencode --refresh` command, and the primitives take help as a pre-translated string. Migrating it would mean either dropping the code formatting or splicing the command in as a bare literal, so the row keeps its markup until the descriptor can carry rich help. diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index 1602cd4585..2e53d3f7f2 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -397,6 +397,7 @@ svg.spinning { --provider-openai: #10a37f; --provider-gemini: #4285f4; --provider-openrouter: #6f4cff; + --provider-orcarouter: #0e7c86; --provider-brave: #fb542b; --provider-tavily: #4f46e5; --provider-minimax: #e73562; diff --git a/packages/dashboard/app/utils/providerIconKey.ts b/packages/dashboard/app/utils/providerIconKey.ts index 060543c155..405a063b8a 100644 --- a/packages/dashboard/app/utils/providerIconKey.ts +++ b/packages/dashboard/app/utils/providerIconKey.ts @@ -66,6 +66,9 @@ export function inferProviderIconKey(modelOrProviderName: string): string { if (normalized.includes("opencode")) { return "opencode"; } + if (normalized.includes("orcarouter")) { + return "orcarouter"; + } if (normalized.includes("copilot") || normalized === "github copilot") { return "github-copilot"; } diff --git a/packages/dashboard/src/routes/auth-provider-catalog.ts b/packages/dashboard/src/routes/auth-provider-catalog.ts index c240911114..e9e75dd076 100644 --- a/packages/dashboard/src/routes/auth-provider-catalog.ts +++ b/packages/dashboard/src/routes/auth-provider-catalog.ts @@ -59,6 +59,7 @@ export const STATIC_API_KEY_PROVIDER_CATALOG: AuthProviderCatalogEntry[] = [ { id: "kimi-coding", name: "Kimi" }, { id: "minimax", name: "Minimax" }, { id: "openrouter", name: "OpenRouter" }, + { id: "orcarouter", name: "OrcaRouter" }, { id: "opencode-go", name: "Opencode (Go)" }, { id: "tavily", name: "Tavily" }, { id: "zai", name: "Zai" }, diff --git a/packages/engine/src/auth/provider-auth.ts b/packages/engine/src/auth/provider-auth.ts index 08c3496642..5ce2746ef6 100644 --- a/packages/engine/src/auth/provider-auth.ts +++ b/packages/engine/src/auth/provider-auth.ts @@ -82,6 +82,7 @@ export const BUILT_IN_API_KEY_PROVIDERS: ReadonlyArray<{ id: string; name: strin { id: "kimi-coding", name: "Kimi" }, { id: "minimax", name: "Minimax" }, { id: "openrouter", name: "OpenRouter" }, + { id: "orcarouter", name: "OrcaRouter" }, { id: "opencode-go", name: "Opencode (Go)" }, { id: "tavily", name: "Tavily" }, { id: "zai", name: "Zai" }, diff --git a/packages/i18n/locales/en/common.json b/packages/i18n/locales/en/common.json index 39bc16f5a5..e208865db7 100644 --- a/packages/i18n/locales/en/common.json +++ b/packages/i18n/locales/en/common.json @@ -166,6 +166,7 @@ "ollama": "Ollama Endpoint", "openai": "OpenAI API Key", "openrouter": "OpenRouter API Key", + "orcarouter": "OrcaRouter API Key", "zai": "Zhipu AI API Key" }, "apiKeyPlaceholder": { @@ -182,6 +183,7 @@ "ollama": "Enter your Ollama endpoint URL (for example http://localhost:11434).", "openai": "Create an API key from your OpenAI dashboard under API keys.", "openrouter": "Create an API key from your OpenRouter account key management page.", + "orcarouter": "Create an API key from your OrcaRouter dashboard.", "zai": "Create an API key in the Zhipu AI open platform account settings." }, "apiKeyUsage": { @@ -191,6 +193,7 @@ "ollama": "Connects to your local Ollama instance", "openai": "Used for GPT models in task execution and planning", "openrouter": "Routes to multiple AI model providers through a single key", + "orcarouter": "Routes to multiple AI model providers through a single key", "zai": "Used for GLM models in task execution" }, "providerDesc": { @@ -206,6 +209,7 @@ "openai": "GPT models — versatile for a wide range of tasks", "openaiCodex": "Codex models by OpenAI — optimized for coding tasks", "openrouter": "OpenRouter — route requests across multiple AI providers", + "orcarouter": "OrcaRouter — one gateway for multiple AI providers with gateway-level agent security", "zai": "GLM models by Zhipu AI — strong multilingual support" } }, diff --git a/packages/i18n/locales/es/common.json b/packages/i18n/locales/es/common.json index 3753662f96..b254015d7c 100644 --- a/packages/i18n/locales/es/common.json +++ b/packages/i18n/locales/es/common.json @@ -166,6 +166,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "apiKeyPlaceholder": { @@ -182,6 +183,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "apiKeyUsage": { @@ -191,6 +193,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "providerDesc": { @@ -206,6 +209,7 @@ "openai": "", "openaiCodex": "", "openrouter": "", + "orcarouter": "", "zai": "" } }, diff --git a/packages/i18n/locales/fr/common.json b/packages/i18n/locales/fr/common.json index 3753662f96..b254015d7c 100644 --- a/packages/i18n/locales/fr/common.json +++ b/packages/i18n/locales/fr/common.json @@ -166,6 +166,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "apiKeyPlaceholder": { @@ -182,6 +183,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "apiKeyUsage": { @@ -191,6 +193,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "providerDesc": { @@ -206,6 +209,7 @@ "openai": "", "openaiCodex": "", "openrouter": "", + "orcarouter": "", "zai": "" } }, diff --git a/packages/i18n/locales/ko/common.json b/packages/i18n/locales/ko/common.json index 3753662f96..b254015d7c 100644 --- a/packages/i18n/locales/ko/common.json +++ b/packages/i18n/locales/ko/common.json @@ -166,6 +166,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "apiKeyPlaceholder": { @@ -182,6 +183,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "apiKeyUsage": { @@ -191,6 +193,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "providerDesc": { @@ -206,6 +209,7 @@ "openai": "", "openaiCodex": "", "openrouter": "", + "orcarouter": "", "zai": "" } }, diff --git a/packages/i18n/locales/pt-BR/common.json b/packages/i18n/locales/pt-BR/common.json index a5887b7ad5..e9d21fb156 100644 --- a/packages/i18n/locales/pt-BR/common.json +++ b/packages/i18n/locales/pt-BR/common.json @@ -166,6 +166,7 @@ "ollama": "Endpoint do Ollama", "openai": "Chave de API da OpenAI", "openrouter": "Chave de API do OpenRouter", + "orcarouter": "", "zai": "Chave de API da Zhipu AI" }, "apiKeyPlaceholder": { @@ -182,6 +183,7 @@ "ollama": "Digite a URL do endpoint do Ollama (por exemplo, http://localhost:11434).", "openai": "Crie uma chave de API no painel da OpenAI, em API keys.", "openrouter": "Crie uma chave de API na página de gerenciamento de chaves da sua conta OpenRouter.", + "orcarouter": "", "zai": "Crie uma chave de API nas configurações de conta da plataforma aberta da Zhipu AI." }, "apiKeyUsage": { @@ -191,6 +193,7 @@ "ollama": "Conecta à sua instância local do Ollama", "openai": "Usada para modelos GPT na execução e no planejamento de tarefas", "openrouter": "Roteia para múltiplos provedores de modelos de IA através de uma única chave", + "orcarouter": "", "zai": "Usada para modelos GLM na execução de tarefas" }, "providerDesc": { @@ -206,6 +209,7 @@ "openai": "Modelos GPT — versáteis para uma ampla variedade de tarefas", "openaiCodex": "Modelos Codex da OpenAI — otimizados para tarefas de programação", "openrouter": "OpenRouter — roteia solicitações entre múltiplos provedores de IA", + "orcarouter": "", "zai": "Modelos GLM da Zhipu AI — forte suporte multilíngue" } }, diff --git a/packages/i18n/locales/zh-CN/common.json b/packages/i18n/locales/zh-CN/common.json index 3753662f96..b254015d7c 100644 --- a/packages/i18n/locales/zh-CN/common.json +++ b/packages/i18n/locales/zh-CN/common.json @@ -166,6 +166,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "apiKeyPlaceholder": { @@ -182,6 +183,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "apiKeyUsage": { @@ -191,6 +193,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "providerDesc": { @@ -206,6 +209,7 @@ "openai": "", "openaiCodex": "", "openrouter": "", + "orcarouter": "", "zai": "" } }, diff --git a/packages/i18n/locales/zh-TW/common.json b/packages/i18n/locales/zh-TW/common.json index 3753662f96..b254015d7c 100644 --- a/packages/i18n/locales/zh-TW/common.json +++ b/packages/i18n/locales/zh-TW/common.json @@ -166,6 +166,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "apiKeyPlaceholder": { @@ -182,6 +183,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "apiKeyUsage": { @@ -191,6 +193,7 @@ "ollama": "", "openai": "", "openrouter": "", + "orcarouter": "", "zai": "" }, "providerDesc": { @@ -206,6 +209,7 @@ "openai": "", "openaiCodex": "", "openrouter": "", + "orcarouter": "", "zai": "" } },