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:
7
.changeset/add-orcarouter-provider.md
Normal file
7
.changeset/add-orcarouter-provider.md
Normal 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.
|
||||
@@ -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**. |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -215,6 +215,7 @@ export const DEFAULT_GLOBAL_SETTINGS = {
|
||||
openrouterAppAttribution: undefined,
|
||||
openrouterModelFilters: undefined,
|
||||
openrouterProviderPreferences: undefined,
|
||||
orcarouterModelSync: true,
|
||||
opencodeGoModelSync: true,
|
||||
updateCheckEnabled: true,
|
||||
fnBinaryCheckEnabled: true,
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user