feat(providers): add OrcaRouter as built-in cloud provider (#2187)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xilema2
2026-05-19 20:19:28 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent b0fe42b0af
commit ee34387f2e
6 changed files with 103 additions and 44 deletions
+55 -43
View File
@@ -112,6 +112,10 @@ const BUILTIN_PROVIDER_META: Record<string, { tone: string; label: string }> = {
label: 'OpenRouter',
tone: 'bg-slate-100 dark:bg-slate-500/15 ring-slate-300 text-slate-900 dark:text-slate-100',
},
orcarouter: {
label: 'OrcaRouter',
tone: 'bg-sky-50 dark:bg-sky-500/10 ring-sky-200 text-sky-900 dark:text-sky-100',
},
custom: {
label: 'Custom',
tone: 'bg-stone-100 dark:bg-neutral-800 ring-stone-300 text-stone-900 dark:text-neutral-100',
@@ -548,7 +552,9 @@ const ProviderKeyDialog = ({
? 'sk-ant-...'
: slug === 'openrouter'
? 'sk-or-...'
: 'your-api-key';
: slug === 'orcarouter'
? 'sk-orca-...'
: 'your-api-key';
const fieldLabel = isLocalRuntime ? 'Endpoint URL' : t('settings.ai.apiKeyFieldLabel');
const helper = isLocalRuntime
@@ -2063,46 +2069,50 @@ const AIPanel = ({ embedded = false }: AIPanelProps = {}) => {
)}
<div className="flex flex-wrap gap-2">
{/* Built-in cloud providers — openai/anthropic/openrouter/custom */}
{(['openai', 'anthropic', 'openrouter', 'custom'] as const).map(slug => {
const meta = BUILTIN_PROVIDER_META[slug];
const label = meta?.label ?? slug;
const existing = draft.cloudProviders.find(cp => cp.slug === slug);
const enabled = !!existing;
return (
<ProviderToggleChip
key={slug}
slug={slug}
label={label}
enabled={enabled}
busy={busyAction === `toggle-${slug}`}
onToggle={() => {
if (enabled && existing) {
// Toggle OFF: remove the provider + scrub any
// routing entries that pin to it.
const remaining = draft.cloudProviders.filter(cp => cp.id !== existing.id);
const nextRouting = Object.fromEntries(
Object.entries(draft.routing).map(([wid, ref]) => [
wid,
ref.kind === 'cloud' && ref.providerSlug === existing.slug
? ({ kind: 'openhuman' } as const)
: ref,
])
) as typeof draft.routing;
setDraft({ ...draft, cloudProviders: remaining, routing: nextRouting });
} else if (slug === 'custom') {
// Custom providers need slug + endpoint + label, not
// just an API key — defer to the full editor modal.
setEditing('new');
} else {
// Toggle ON: open the API-key popup. The chip
// only flips after the dialog saves.
setKeyDialogFor(slug);
}
}}
/>
);
})}
{/* Built-in cloud providers — openai/anthropic/openrouter/orcarouter/custom */}
{(['openai', 'anthropic', 'openrouter', 'orcarouter', 'custom'] as const).map(
slug => {
const meta = BUILTIN_PROVIDER_META[slug];
const label = meta?.label ?? slug;
const existing = draft.cloudProviders.find(cp => cp.slug === slug);
const enabled = !!existing;
return (
<ProviderToggleChip
key={slug}
slug={slug}
label={label}
enabled={enabled}
busy={busyAction === `toggle-${slug}`}
onToggle={() => {
if (enabled && existing) {
// Toggle OFF: remove the provider + scrub any
// routing entries that pin to it.
const remaining = draft.cloudProviders.filter(
cp => cp.id !== existing.id
);
const nextRouting = Object.fromEntries(
Object.entries(draft.routing).map(([wid, ref]) => [
wid,
ref.kind === 'cloud' && ref.providerSlug === existing.slug
? ({ kind: 'openhuman' } as const)
: ref,
])
) as typeof draft.routing;
setDraft({ ...draft, cloudProviders: remaining, routing: nextRouting });
} else if (slug === 'custom') {
// Custom providers need slug + endpoint + label, not
// just an API key — defer to the full editor modal.
setEditing('new');
} else {
// Toggle ON: open the API-key popup. The chip
// only flips after the dialog saves.
setKeyDialogFor(slug);
}
}}
/>
);
}
)}
{/* LM Studio + Ollama — local runtimes stored with a slug of
"lmstudio" / "ollama" so they're distinct from generic custom. */}
@@ -2508,7 +2518,7 @@ const CloudProviderEditor = ({
const { t } = useT();
const defaultSlug: string =
initial?.slug ??
(['openai', 'anthropic', 'openrouter', 'custom'] as const).find(
(['openai', 'anthropic', 'openrouter', 'orcarouter', 'custom'] as const).find(
s => !existingSlugs.includes(s)
) ??
'custom';
@@ -2554,7 +2564,7 @@ const CloudProviderEditor = ({
}}
disabled={!!initial}
className="mt-1 w-full rounded-lg border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 disabled:opacity-60 focus:border-primary-400 focus:outline-none focus:ring-1 focus:ring-primary-200">
{(['openai', 'anthropic', 'openrouter', 'custom'] as const)
{(['openai', 'anthropic', 'openrouter', 'orcarouter', 'custom'] as const)
.filter(s => s === slug || !existingSlugs.includes(s))
.map(s => (
<option key={s} value={s}>
@@ -2669,6 +2679,8 @@ function defaultEndpointFor(slug: string): string {
return 'https://api.anthropic.com/v1';
case 'openrouter':
return 'https://openrouter.ai/api/v1';
case 'orcarouter':
return 'https://api.orcarouter.ai/v1';
case 'ollama':
// Ollama exposes an OpenAI-compatible endpoint at /v1; the bare host is
// also accepted by the Rust factory (it appends /v1 internally for chat).
+7 -1
View File
@@ -24,7 +24,13 @@ export interface ModelRoute {
export type AuthStyle = 'bearer' | 'anthropic' | 'openhuman_jwt' | 'none';
/** @deprecated Use AuthStyle. Kept for back-compat with old wire format. */
export type CloudProviderType = 'openhuman' | 'openai' | 'anthropic' | 'openrouter' | 'custom';
export type CloudProviderType =
| 'openhuman'
| 'openai'
| 'anthropic'
| 'openrouter'
| 'orcarouter'
| 'custom';
/**
* Endpoint config for one cloud LLM provider (new slug-keyed shape).
@@ -174,6 +174,7 @@ fn legacy_label_for(type_str: &str) -> &'static str {
"openai" => "OpenAI",
"anthropic" => "Anthropic",
"openrouter" => "OpenRouter",
"orcarouter" => "OrcaRouter",
"custom" => "Custom",
_ => "Custom",
}
@@ -186,6 +187,7 @@ fn legacy_default_endpoint(type_str: &str) -> &'static str {
"openai" => "https://api.openai.com/v1",
"anthropic" => "https://api.anthropic.com/v1",
"openrouter" => "https://openrouter.ai/api/v1",
"orcarouter" => "https://api.orcarouter.ai/v1",
_ => "",
}
}
@@ -243,6 +245,7 @@ pub enum CloudProviderType {
Openai,
Anthropic,
Openrouter,
Orcarouter,
Custom,
}
@@ -254,6 +257,7 @@ impl CloudProviderType {
Self::Openai => "https://api.openai.com/v1",
Self::Anthropic => "https://api.anthropic.com/v1",
Self::Openrouter => "https://openrouter.ai/api/v1",
Self::Orcarouter => "https://api.orcarouter.ai/v1",
Self::Custom => "",
}
}
@@ -265,6 +269,7 @@ impl CloudProviderType {
Self::Openai => "OpenAI",
Self::Anthropic => "Anthropic",
Self::Openrouter => "OpenRouter",
Self::Orcarouter => "OrcaRouter",
Self::Custom => "Custom",
}
}
@@ -276,6 +281,7 @@ impl CloudProviderType {
Self::Openai => "openai",
Self::Anthropic => "anthropic",
Self::Openrouter => "openrouter",
Self::Orcarouter => "orcarouter",
Self::Custom => "custom",
}
}
+1
View File
@@ -15,6 +15,7 @@ const SUPPORTED_PROXY_SERVICE_KEYS: &[&str] = &[
"provider.ollama",
"provider.openai",
"provider.openrouter",
"provider.orcarouter",
"channel.dingtalk",
"channel.discord",
"channel.lark",
+1
View File
@@ -204,6 +204,7 @@ pub struct Config {
// build OpenAiCompatibleProvider with Bearer auth
// "anthropic:<model>" → type=anthropic; Bearer auth on the compat endpoint
// "openrouter:<model>" → type=openrouter; Bearer auth
// "orcarouter:<model>" → type=orcarouter; Bearer auth (e.g. "orcarouter:orcarouter/auto")
// "custom:<model>" → type=custom; Bearer auth
// "ollama:<model>" → local Ollama at config.local_ai.base_url
//
@@ -114,6 +114,39 @@ fn openrouter_slug_model() {
assert_eq!(model, "meta-llama/llama-3.1-8b");
}
#[test]
fn orcarouter_slug_model() {
let mut config = Config::default();
config.cloud_providers.push(CloudProviderCreds {
id: "p_oc".to_string(),
slug: "orcarouter".to_string(),
label: "OrcaRouter".to_string(),
endpoint: "https://api.orcarouter.ai/v1".to_string(),
auth_style: AuthStyle::Bearer,
default_model: Some("orcarouter/auto".to_string()),
..Default::default()
});
let (_, model) =
create_chat_provider_from_string("agentic", "orcarouter:orcarouter/auto", &config)
.expect("orcarouter:<model> must build");
assert_eq!(model, "orcarouter/auto");
}
#[test]
fn orcarouter_legacy_type_seeds_defaults() {
use crate::openhuman::config::schema::cloud_providers::migrate_legacy_fields;
let mut entry = CloudProviderCreds {
id: "p_oc_legacy".to_string(),
legacy_type: Some("orcarouter".to_string()),
..Default::default()
};
migrate_legacy_fields(&mut entry);
assert_eq!(entry.slug, "orcarouter");
assert_eq!(entry.label, "OrcaRouter");
assert_eq!(entry.endpoint, "https://api.orcarouter.ai/v1");
assert_eq!(entry.auth_style, AuthStyle::Bearer);
}
#[test]
fn ollama_prefix() {
let config = Config::default();