diff --git a/gitnexus-web/src/components/SettingsPanel.tsx b/gitnexus-web/src/components/SettingsPanel.tsx
index 9b32ffd92..0d4ab2e11 100644
--- a/gitnexus-web/src/components/SettingsPanel.tsx
+++ b/gitnexus-web/src/components/SettingsPanel.tsx
@@ -371,6 +371,7 @@ export const SettingsPanel = ({
'minimax',
'glm',
'deepseek',
+ 'orcarouter',
];
return (
@@ -484,7 +485,9 @@ export const SettingsPanel = ({
? '🔮'
: provider === 'deepseek'
? '🐋'
- : '☁️'}
+ : provider === 'orcarouter'
+ ? '🐳'
+ : '☁️'}
{getProviderDisplayName(provider)}
@@ -1007,6 +1010,50 @@ export const SettingsPanel = ({
)}
+ {/* OrcaRouter Settings */}
+ {settings.activeProvider === 'orcarouter' && (
+
+ setSettings((prev) => ({
+ ...prev,
+ orcarouter: { ...prev.orcarouter!, apiKey: value },
+ })),
+ onToggleVisibility: () => toggleApiKeyVisibility('orcarouter'),
+ }}
+ model={{
+ value: settings.orcarouter?.model ?? 'orcarouter/auto',
+ placeholder: t('settings:providers.orcarouter.modelPlaceholder'),
+ onChange: (value) =>
+ setSettings((prev) => ({
+ ...prev,
+ orcarouter: { ...prev.orcarouter!, model: value },
+ })),
+ helperText: 'orcarouter/auto (default) — or any model id exposed by the gateway',
+ }}
+ >
+
+ OpenAI-compatible gateway. Get your API key from{' '}
+
+ orcarouter.ai
+
+ .
+
+
+ )}
+
{/* GLM Settings */}
{settings.activeProvider === 'glm' && (
diff --git a/gitnexus-web/src/config/ui-constants.ts b/gitnexus-web/src/config/ui-constants.ts
index ef361881f..3a02b5a44 100644
--- a/gitnexus-web/src/config/ui-constants.ts
+++ b/gitnexus-web/src/config/ui-constants.ts
@@ -7,6 +7,8 @@ export const DEFAULT_BACKEND_URL =
'http://localhost:4747';
export const DEFAULT_OLLAMA_BASE_URL = 'http://localhost:11434';
export const DEFAULT_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
+export const DEFAULT_ORCAROUTER_BASE_URL = 'https://api.orcarouter.ai/v1';
+export const DEFAULT_ORCAROUTER_MODEL = 'orcarouter/auto';
/**
* sessionStorage key for the deploy access token sent as
diff --git a/gitnexus-web/src/core/llm/agent.ts b/gitnexus-web/src/core/llm/agent.ts
index 555cf0d10..8f92adcfd 100644
--- a/gitnexus-web/src/core/llm/agent.ts
+++ b/gitnexus-web/src/core/llm/agent.ts
@@ -31,6 +31,7 @@ import type {
MiniMaxConfig,
GLMConfig,
DeepSeekConfig,
+ OrcaRouterConfig,
AgentStreamChunk,
AgentHistoryMessage,
MiniMaxThinkingMode,
@@ -41,7 +42,11 @@ import {
buildDynamicSystemPrompt,
CHAT_ONLY_PROMPT_NOTE,
} from './context-builder';
-import { DEFAULT_OLLAMA_BASE_URL, DEFAULT_OPENROUTER_BASE_URL } from '../../config/ui-constants';
+import {
+ DEFAULT_OLLAMA_BASE_URL,
+ DEFAULT_OPENROUTER_BASE_URL,
+ DEFAULT_ORCAROUTER_BASE_URL,
+} from '../../config/ui-constants';
import {
DeepSeekChatOpenAI,
normalizeMessageContent,
@@ -271,6 +276,36 @@ export const createChatModel = (config: ProviderConfig): BaseChatModel => {
});
}
+ case 'orcarouter': {
+ const orcaRouterConfig = config as OrcaRouterConfig;
+
+ // Debug logging
+ if (import.meta.env.DEV) {
+ console.log('🐋 OrcaRouter config:', {
+ hasApiKey: !!orcaRouterConfig.apiKey,
+ model: orcaRouterConfig.model,
+ baseUrl: orcaRouterConfig.baseUrl,
+ });
+ }
+
+ if (!orcaRouterConfig.apiKey || orcaRouterConfig.apiKey.trim() === '') {
+ throw new Error('OrcaRouter API key is required but was not provided');
+ }
+
+ return new ChatOpenAI({
+ openAIApiKey: orcaRouterConfig.apiKey,
+ apiKey: orcaRouterConfig.apiKey, // Fallback for some versions
+ modelName: orcaRouterConfig.model,
+ temperature: orcaRouterConfig.temperature ?? 0.1,
+ maxTokens: orcaRouterConfig.maxTokens,
+ configuration: {
+ apiKey: orcaRouterConfig.apiKey, // Ensure client receives it
+ baseURL: orcaRouterConfig.baseUrl ?? DEFAULT_ORCAROUTER_BASE_URL,
+ },
+ streaming: true,
+ });
+ }
+
case 'minimax': {
const minimaxConfig = config as MiniMaxConfig;
diff --git a/gitnexus-web/src/core/llm/settings-service.ts b/gitnexus-web/src/core/llm/settings-service.ts
index fb2591172..4099485e5 100644
--- a/gitnexus-web/src/core/llm/settings-service.ts
+++ b/gitnexus-web/src/core/llm/settings-service.ts
@@ -18,10 +18,15 @@ import {
MiniMaxConfig,
GLMConfig,
DeepSeekConfig,
+ OrcaRouterConfig,
ProviderConfig,
MINIMAX_MODEL_IDS,
} from './types';
-import { DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OLLAMA_BASE_URL } from '../../config/ui-constants';
+import {
+ DEFAULT_OPENROUTER_BASE_URL,
+ DEFAULT_OLLAMA_BASE_URL,
+ DEFAULT_ORCAROUTER_BASE_URL,
+} from '../../config/ui-constants';
import { resilientFetch } from 'gitnexus-shared';
const STORAGE_KEY = 'gitnexus-llm-settings';
@@ -81,6 +86,10 @@ const mergeWithDefaults = (parsed?: Partial | null): LLMSettings =>
...DEFAULT_LLM_SETTINGS.deepseek,
...parsed?.deepseek,
},
+ orcarouter: {
+ ...DEFAULT_LLM_SETTINGS.orcarouter,
+ ...parsed?.orcarouter,
+ },
});
const readSettings = (storage: Storage): Partial | null => {
@@ -168,7 +177,9 @@ export const updateProviderSettings = (
? Partial>
: T extends 'deepseek'
? Partial>
- : never
+ : T extends 'orcarouter'
+ ? Partial>
+ : never
>,
): LLMSettings => {
const current = loadSettings();
@@ -274,6 +285,17 @@ export const updateProviderSettings = (
saveSettings(updated);
return updated;
}
+ case 'orcarouter': {
+ const updated: LLMSettings = {
+ ...current,
+ orcarouter: {
+ ...(current.orcarouter ?? {}),
+ ...(updates as Partial>),
+ },
+ };
+ saveSettings(updated);
+ return updated;
+ }
default: {
// Should be unreachable due to T extends LLMProvider, but keep a safe fallback
const updated: LLMSettings = { ...current };
@@ -355,6 +377,17 @@ const providerBuilders: Record = {
if (!settings.deepseek?.apiKey) return null;
return { provider: 'deepseek', ...settings.deepseek } as DeepSeekConfig;
},
+ orcarouter: (settings) => {
+ if (!settings.orcarouter?.apiKey || settings.orcarouter.apiKey.trim() === '') return null;
+ return {
+ provider: 'orcarouter',
+ apiKey: settings.orcarouter.apiKey,
+ model: settings.orcarouter.model || 'orcarouter/auto',
+ baseUrl: settings.orcarouter.baseUrl || DEFAULT_ORCAROUTER_BASE_URL,
+ temperature: settings.orcarouter.temperature,
+ maxTokens: settings.orcarouter.maxTokens,
+ } as OrcaRouterConfig;
+ },
};
export const getActiveProviderConfig = (): ProviderConfig | null => {
@@ -427,6 +460,8 @@ export const getProviderDisplayName = (provider: LLMProvider): string => {
return 'GLM (Z.AI)';
case 'deepseek':
return 'DeepSeek';
+ case 'orcarouter':
+ return 'OrcaRouter';
default:
return provider;
}
@@ -459,6 +494,8 @@ export const getAvailableModels = (provider: LLMProvider): string[] => {
return ['GLM-5', 'GLM-5-Turbo', 'GLM-4.7', 'GLM-4.5'];
case 'deepseek':
return ['deepseek-v4-flash', 'deepseek-v4-pro', 'deepseek-chat', 'deepseek-reasoner'];
+ case 'orcarouter':
+ return ['orcarouter/auto'];
default:
return [];
}
diff --git a/gitnexus-web/src/core/llm/types.ts b/gitnexus-web/src/core/llm/types.ts
index c5198bd16..35f0d7024 100644
--- a/gitnexus-web/src/core/llm/types.ts
+++ b/gitnexus-web/src/core/llm/types.ts
@@ -2,13 +2,18 @@
* LLM Provider Types
*
* Type definitions for multi-provider LLM support.
- * Supports OpenAI, Azure OpenAI, Gemini, Anthropic, Ollama, OpenRouter, MiniMax, GLM, and DeepSeek.
+ * Supports OpenAI, Azure OpenAI, Gemini, Anthropic, Ollama, OpenRouter, MiniMax, GLM, DeepSeek, and OrcaRouter.
*/
/**
* Supported LLM providers
*/
-import { DEFAULT_OLLAMA_BASE_URL, DEFAULT_OPENROUTER_BASE_URL } from '../../config/ui-constants';
+import {
+ DEFAULT_OLLAMA_BASE_URL,
+ DEFAULT_OPENROUTER_BASE_URL,
+ DEFAULT_ORCAROUTER_BASE_URL,
+ DEFAULT_ORCAROUTER_MODEL,
+} from '../../config/ui-constants';
export type LLMProvider =
| 'openai'
| 'azure-openai'
@@ -18,7 +23,8 @@ export type LLMProvider =
| 'openrouter'
| 'minimax'
| 'glm'
- | 'deepseek';
+ | 'deepseek'
+ | 'orcarouter';
export const MINIMAX_ANTHROPIC_BASE_URLS = {
global_en: 'https://api.minimax.io/anthropic',
@@ -183,6 +189,16 @@ export interface DeepSeekConfig extends BaseProviderConfig {
model: string; // e.g., 'deepseek-v4-flash', 'deepseek-v4-pro'
}
+/**
+ * OrcaRouter configuration — OpenAI-compatible gateway
+ */
+export interface OrcaRouterConfig extends BaseProviderConfig {
+ provider: 'orcarouter';
+ apiKey: string;
+ model: string; // e.g., 'orcarouter/auto'
+ baseUrl?: string; // defaults to https://api.orcarouter.ai/v1
+}
+
/**
* Union type for all provider configurations
*/
@@ -195,7 +211,8 @@ export type ProviderConfig =
| OpenRouterConfig
| MiniMaxConfig
| GLMConfig
- | DeepSeekConfig;
+ | DeepSeekConfig
+ | OrcaRouterConfig;
/**
* Stored settings (what goes to localStorage)
@@ -215,6 +232,7 @@ export interface LLMSettings {
minimax?: Partial>;
glm?: Partial>;
deepseek?: Partial>;
+ orcarouter?: Partial>;
// Intelligent Clustering Settings
intelligentClustering: boolean;
@@ -283,6 +301,12 @@ export const DEFAULT_LLM_SETTINGS: LLMSettings = {
model: 'deepseek-v4-flash',
temperature: 0.1,
},
+ orcarouter: {
+ apiKey: '',
+ model: DEFAULT_ORCAROUTER_MODEL,
+ baseUrl: DEFAULT_ORCAROUTER_BASE_URL,
+ temperature: 0.1,
+ },
};
/**
diff --git a/gitnexus-web/src/locales/en/settings.json b/gitnexus-web/src/locales/en/settings.json
index 91c6b2a68..04c964b30 100644
--- a/gitnexus-web/src/locales/en/settings.json
+++ b/gitnexus-web/src/locales/en/settings.json
@@ -93,6 +93,10 @@
},
"glm": {
"apiKeyPlaceholder": "Enter your Z.AI API key"
+ },
+ "orcarouter": {
+ "apiKeyPlaceholder": "Enter your OrcaRouter API key",
+ "helperLinkLabel": "OrcaRouter"
}
},
"loadingModels": "Loading models...",
diff --git a/gitnexus-web/src/locales/zh-CN/settings.json b/gitnexus-web/src/locales/zh-CN/settings.json
index 4bc4fb05b..00cca59e7 100644
--- a/gitnexus-web/src/locales/zh-CN/settings.json
+++ b/gitnexus-web/src/locales/zh-CN/settings.json
@@ -93,6 +93,10 @@
},
"glm": {
"apiKeyPlaceholder": "输入 Z.AI API Key"
+ },
+ "orcarouter": {
+ "apiKeyPlaceholder": "输入 OrcaRouter API Key",
+ "helperLinkLabel": "OrcaRouter"
}
},
"loadingModels": "正在加载模型...",
diff --git a/gitnexus-web/test/unit/settings-service.test.ts b/gitnexus-web/test/unit/settings-service.test.ts
index b0762604f..932607a90 100644
--- a/gitnexus-web/test/unit/settings-service.test.ts
+++ b/gitnexus-web/test/unit/settings-service.test.ts
@@ -151,6 +151,19 @@ describe('getActiveProviderConfig', () => {
expect(config!.provider).toBe('deepseek');
});
+ it('returns config for orcarouter when API key is set', () => {
+ const settings = loadSettings();
+ settings.activeProvider = 'orcarouter';
+ settings.orcarouter = { ...settings.orcarouter, apiKey: 'sk-orca-123' };
+ saveSettings(settings);
+
+ const config = getActiveProviderConfig();
+ expect(config).not.toBeNull();
+ expect(config!.provider).toBe('orcarouter');
+ expect(config!.baseUrl).toBe('https://api.orcarouter.ai/v1');
+ expect(config!.model).toBe('orcarouter/auto');
+ });
+
it('returns the regional endpoint and thinking mode for MiniMax', () => {
const settings = loadSettings();
settings.activeProvider = 'minimax';
@@ -179,6 +192,15 @@ describe('getActiveProviderConfig', () => {
expect(getActiveProviderConfig()).toBeNull();
});
+
+ it('returns null for orcarouter with empty API key', () => {
+ const settings = loadSettings();
+ settings.activeProvider = 'orcarouter';
+ settings.orcarouter = { ...settings.orcarouter, apiKey: ' ' };
+ saveSettings(settings);
+
+ expect(getActiveProviderConfig()).toBeNull();
+ });
});
describe('isProviderConfigured', () => {
@@ -207,6 +229,7 @@ describe('getProviderDisplayName', () => {
expect(getProviderDisplayName('ollama')).toBe('Ollama (Local)');
expect(getProviderDisplayName('openrouter')).toBe('OpenRouter');
expect(getProviderDisplayName('deepseek')).toBe('DeepSeek');
+ expect(getProviderDisplayName('orcarouter')).toBe('OrcaRouter');
});
});
@@ -217,6 +240,7 @@ describe('getAvailableModels', () => {
expect(getAvailableModels('anthropic')).toContain('claude-sonnet-4-20250514');
expect(getAvailableModels('deepseek')).toContain('deepseek-v4-flash');
expect(getAvailableModels('minimax')).toEqual([...MINIMAX_MODEL_IDS]);
+ expect(getAvailableModels('orcarouter')).toContain('orcarouter/auto');
});
it('describes MiniMax model input and thinking capabilities', () => {