feat: add OrcaRouter as a named model provider (#3502)

## 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.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: XiaoHuo888-hue <jinhao.song@myflashcloud.com>
Co-authored-by: XiaoHuo888-hue <jinhao.song@myflashcloud.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
XiaoHuo888-hue
2026-08-22 10:30:08 +08:00
committed by GitHub
parent 5e8d7ae9ae
commit 41c23adf15
25 changed files with 242 additions and 3 deletions

View File

@@ -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.

View File

@@ -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: <https://openrouter.ai/docs/api-reference/models>. |
| `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: <https://openrouter.ai/docs/features/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**. |

View File

@@ -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 |

View File

@@ -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<typeof vi.fn>).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 });

View File

@@ -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<void> {
const { authStorage, modelRegistry, log } = options;
const apiKey = await authStorage.getApiKey("orcarouter");
const headers: Record<string, string> = {};
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<string, string>;
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<vo
}
}
if (settings.orcarouterModelSync !== false) {
try {
await syncOrcaRouterModels(options);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
options.log("orcarouter", `Failed to sync models: ${message}`);
}
}
if (settings.opencodeGoModelSync !== false) {
const opencodeGoApiKey = await options.authStorage.getApiKey("opencode-go") ?? await options.authStorage.getApiKey("opencode");
await refreshOpencodeGoModels({ modelRegistry: options.modelRegistry, log: options.log, apiKey: opencodeGoApiKey });

View File

@@ -437,6 +437,12 @@ describe("settings key parity", () => {
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);

View File

@@ -215,6 +215,7 @@ export const DEFAULT_GLOBAL_SETTINGS = {
openrouterAppAttribution: undefined,
openrouterModelFilters: undefined,
openrouterProviderPreferences: undefined,
orcarouterModelSync: true,
opencodeGoModelSync: true,
updateCheckEnabled: true,
fnBinaryCheckEnabled: true,

View File

@@ -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. */

View File

@@ -9,7 +9,7 @@ import "./CustomProviderForm.css";
export const BUILT_IN_PROVIDER_IDS = new Set<string>([
"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",

View File

@@ -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<string, {
openai: { pattern: /^sk-/, hint: "Starts with sk-", example: "sk-..." },
"openai-codex": { pattern: /^sk-/, hint: "Starts with sk-", example: "sk-..." },
openrouter: { pattern: /^sk-or-/, hint: "Starts with sk-or-", example: "sk-or-v1-..." },
orcarouter: { pattern: /^sk-orca-/, hint: "Starts with sk-orca-", example: "sk-orca-..." },
google: { pattern: /^AIza/, hint: "Starts with AIza", example: "AIza..." },
gemini: { pattern: /^AIza/, hint: "Starts with AIza", example: "AIza..." },
minimax: { pattern: /^.{8,}$/, hint: "At least 8 characters", example: "..." },
@@ -218,6 +229,7 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
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",

View File

@@ -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 (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-testid="orcarouter-icon"
aria-label={label}
>
<path
d="M12 3c4.5 5.5 7 10 7 13a7 7 0 0 1-14 0c0-3 2.5-7.5 7-13Z"
fill={color}
/>
<path
d="M5 18.5c3.5.5 7.5 1 11 .5"
stroke={color}
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
);
}
// 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.

View File

@@ -230,6 +230,7 @@ export const GLOBAL_SECTION_KEYS: Record<string, ReadonlySet<string>> = {
"openrouterModelFilters",
"openrouterModelSync",
"openrouterProviderPreferences",
"orcarouterModelSync",
"executionGlobalProvider",
"executionGlobalModelId",
"planningGlobalProvider",
@@ -266,6 +267,7 @@ export const GLOBAL_SECTION_KEYS: Record<string, ReadonlySet<string>> = {
"openrouterModelFilters",
"openrouterModelSync",
"openrouterProviderPreferences",
"orcarouterModelSync",
"executionGlobalProvider",
"executionGlobalModelId",
"planningGlobalProvider",

View File

@@ -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",

View File

@@ -199,6 +199,16 @@ export function GlobalModelsSection({ form, setForm, availableModels, modelsLoad
value={form.openrouterModelSync !== false}
onChange={(v) => setForm((f) => ({ ...f, openrouterModelSync: v === true }))}
/>
<SettingsToggleRow
descriptor={{
key: "orcarouterModelSync",
label: t("settings.globalModels.syncOrcaRouterModelListAtStartup", " Sync OrcaRouter model list at startup "),
help: t("settings.globalModels.whenEnabledStartupFetchesTheLatestOrcaRouterModels", " When enabled, startup fetches the latest available models from the OrcaRouter API so model pickers include the OrcaRouter catalog. Default: enabled. "),
scope: "global",
}}
value={form.orcarouterModelSync !== false}
onChange={(v) => setForm((f) => ({ ...f, orcarouterModelSync: v === true }))}
/>
{/*
FNXC:SettingsStyling 2026-07-15-17:35:
Left on hand-rolled markup deliberately: its help text embeds a <code> 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.

View File

@@ -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;

View File

@@ -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";
}

View File

@@ -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" },

View File

@@ -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" },

View File

@@ -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"
}
},

View File

@@ -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": ""
}
},

View File

@@ -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": ""
}
},

View File

@@ -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": ""
}
},

View File

@@ -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"
}
},

View File

@@ -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": ""
}
},

View File

@@ -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": ""
}
},