Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions apps/desktop-tauri/src-tauri/src/commands/provider_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ fn workspace_provider(provider_id: &str) -> Option<codexbar::core::ProviderId> {
"xai" => ProviderId::Xai,
"v0" => ProviderId::V0,
"helmcode" => ProviderId::Helmcode,
"gitkraken" => ProviderId::GitKraken,
_ => return None,
})
}
Expand Down Expand Up @@ -389,7 +390,7 @@ fn litellm_workspace_change_allowed(
mod tests {
use codexbar::core::ProviderId;

use super::{litellm_workspace_change_allowed, workspace_provider};
use super::{gateway_provider, litellm_workspace_change_allowed, workspace_provider};

#[test]
fn maps_opencode_go_workspace_provider() {
Expand All @@ -399,6 +400,18 @@ mod tests {
);
}

#[test]
fn maps_gitkraken_organization_provider() {
assert_eq!(workspace_provider("gitkraken"), Some(ProviderId::GitKraken));
}

#[test]
fn gateway_provider_exposes_wayfinder_and_bifrost_only() {
assert_eq!(gateway_provider("wayfinder"), Some(ProviderId::Wayfinder));
assert_eq!(gateway_provider("bifrost"), Some(ProviderId::Bifrost));
assert_eq!(gateway_provider("codex"), None);
}

#[test]
fn litellm_endpoint_change_requires_reentering_saved_key() {
assert!(
Expand Down Expand Up @@ -450,16 +463,36 @@ pub fn get_provider_workspace_id(provider_id: String) -> Result<Option<String>,
}

fn gateway_provider(provider_id: &str) -> Option<codexbar::core::ProviderId> {
(provider_id == "wayfinder").then_some(codexbar::core::ProviderId::Wayfinder)
match provider_id {
"wayfinder" => Some(codexbar::core::ProviderId::Wayfinder),
"bifrost" => Some(codexbar::core::ProviderId::Bifrost),
_ => None,
}
}

#[tauri::command]
pub fn get_provider_gateway_url(provider_id: String) -> Result<String, String> {
let id = gateway_provider(&provider_id)
.ok_or_else(|| format!("Provider '{provider_id}' does not expose a gateway URL"))?;
Ok(Settings::load().gateway_url(id).to_string())
}

#[tauri::command]
pub fn set_provider_gateway_url(provider_id: String, gateway_url: String) -> Result<(), String> {
let id = gateway_provider(&provider_id)
.ok_or_else(|| format!("Provider '{provider_id}' does not expose a gateway URL"))?;
let gateway_url = gateway_url.trim();
codexbar::providers::wayfinder::parse_gateway_url(gateway_url)
.map_err(|error| error.to_string())?;
match id {
codexbar::core::ProviderId::Wayfinder => {
codexbar::providers::wayfinder::parse_gateway_url(gateway_url)
.map_err(|error| error.to_string())?;
}
codexbar::core::ProviderId::Bifrost => {
codexbar::providers::bifrost::validate_gateway_url(gateway_url)
.map_err(|error| error.to_string())?;
}
_ => unreachable!("gateway_provider only returns gateway providers"),
}

let mut settings = Settings::load();
settings.set_gateway_url(id, gateway_url.to_string());
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ fn main() {
commands::get_provider_region_options,
commands::set_provider_workspace_id,
commands::set_provider_gateway_url,
commands::get_provider_gateway_url,
commands::get_provider_workspace_id,
commands::get_gemini_cli_signed_in,
commands::get_vertexai_status,
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop-tauri/src/components/providers/providerIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,10 @@ export const PROVIDER_ICON_REGISTRY: Record<string, ProviderIcon> = {
gemini: { id: "gemini", brandColor: "#ab87ea", fallbackLetter: "✦", svgPath: RAW.gemini },
grok: { id: "grok", brandColor: "#111827", fallbackLetter: "G", svgPath: RAW.grok },
groq: { id: "groq", brandColor: "#f55036", fallbackLetter: "G", svgPath: RAW.groq },
bifrost: { id: "bifrost", brandColor: "#5b7cfa", fallbackLetter: "B" },
gitkraken: { id: "gitkraken", brandColor: "#179287", fallbackLetter: "G" },
huggingface: { id: "huggingface", brandColor: "#ffd21e", fallbackLetter: "H", svgPath: RAW.huggingface },
hyper: { id: "hyper", brandColor: "#7c3aed", fallbackLetter: "H" },
helmcode: { id: "helmcode", brandColor: "#4f46e5", fallbackLetter: "H" },
v0: { id: "v0", brandColor: "#111827", fallbackLetter: "V" },
typesafe: { id: "typesafe", brandColor: "#2563eb", fallbackLetter: "T" },
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src/lib/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,10 @@ export function setProviderGatewayUrl(
return invoke<void>("set_provider_gateway_url", { providerId, gatewayUrl });
}

export function getProviderGatewayUrl(providerId: string): Promise<string> {
return invoke<string>("get_provider_gateway_url", { providerId });
}

export function getProviderAzureApiVersion(providerId: string): Promise<string | null> {
return invoke<string | null>("get_provider_azure_api_version", { providerId });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useCallback, useEffect, useReducer } from "react";
import { useCallback, useEffect, useReducer, useState } from "react";
import type { SettingsSnapshot, SettingsUpdate } from "../../../types/bridge";
import { useLocale } from "../../../hooks/useLocale";
import { providerAllowsPace } from "../../../lib/providerPace";
import {
getCredentialStorageStatus,
getProviderCookieSourceOptions,
getProviderDetail,
getProviderGatewayUrl,
getProviderRegionOptions,
getTokenAccountProviders,
openProviderDashboard,
Expand Down Expand Up @@ -83,6 +84,8 @@ export function ProviderDetailPane({
onSettingsChange,
}: Props) {
const { t, language } = useLocale();
const [gatewayLoadedProviderId, setGatewayLoadedProviderId] =
useState<string | null>(null);
const [state, dispatch] = useReducer(
providerDetailPaneReducer,
{ wayfinderGatewayUrl, providerId },
Expand Down Expand Up @@ -142,11 +145,34 @@ export function ProviderDetailPane({
}
}, []);

const gatewayProviderId = providerId === "wayfinder" || providerId === "bifrost"
? providerId
: null;

useEffect(() => {
setGatewayLoadedProviderId(null);
if (!gatewayProviderId) return;
let cancelled = false;
void getProviderGatewayUrl(gatewayProviderId).then((url) => {
if (!cancelled) {
dispatch({ type: "SET_GATEWAY_DRAFT", draft: url });
setGatewayLoadedProviderId(gatewayProviderId);
}
}).catch((e) => {
if (!cancelled) {
dispatch({ type: "SAVE_GATEWAY_ERROR", error: String(e) });
setGatewayLoadedProviderId(gatewayProviderId);
}
});
return () => { cancelled = true; };
}, [gatewayProviderId]);

const saveGateway = async () => {
dispatch({ type: "SAVE_GATEWAY_START" });
try {
await setProviderGatewayUrl("wayfinder", gatewayDraft);
await load("wayfinder");
if (!gatewayProviderId) return;
await setProviderGatewayUrl(gatewayProviderId, gatewayDraft);
await load(gatewayProviderId);
} catch (e) {
dispatch({ type: "SAVE_GATEWAY_ERROR", error: String(e) });
} finally {
Expand Down Expand Up @@ -319,7 +345,8 @@ export function ProviderDetailPane({
t={t}
onChanged={reload}
/>
{detail.id === "wayfinder" && (
{(detail.id === "wayfinder" || detail.id === "bifrost") &&
gatewayLoadedProviderId === detail.id && (
<WayfinderGatewaySection
draft={gatewayDraft}
error={gatewayError}
Expand All @@ -330,6 +357,7 @@ export function ProviderDetailPane({
}
onSave={() => void saveGateway()}
t={t}
bifrost={detail.id === "bifrost"}
/>
)}
<MenuBarMetricSection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function CredentialsDispatcher({ providerId, t }: Props) {
case "zed":
case "sub2api":
case "xai":
case "gitkraken":
return <OpenAiExtras providerId={providerId} t={t} />;
case "openrouter":
return <OpenRouterManagementCreds t={t} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface Props {
onDraftChange: (draft: string) => void;
onSave: () => void;
t: (key: LocaleKey) => string;
bifrost?: boolean;
}

export function WayfinderGatewaySection({
Expand All @@ -18,12 +19,13 @@ export function WayfinderGatewaySection({
onDraftChange,
onSave,
t,
bifrost = false,
}: Props) {
return (
<section className="provider-detail__section">
<h3>{t("WayfinderGatewayTitle")}</h3>
<h3>{bifrost ? "Bifrost gateway" : t("WayfinderGatewayTitle")}</h3>
<label>
<span>{t("WayfinderGatewayLabel")}</span>
<span>{bifrost ? "Gateway URL" : t("WayfinderGatewayLabel")}</span>
<input
type="url"
value={draft}
Expand All @@ -32,7 +34,9 @@ export function WayfinderGatewaySection({
aria-describedby="wayfinder-gateway-help"
/>
</label>
<p id="wayfinder-gateway-help">{t("WayfinderGatewayHelp")}</p>
<p id="wayfinder-gateway-help">
{bifrost ? "Base URL of your Bifrost gateway." : t("WayfinderGatewayHelp")}
</p>
{error && <p role="alert">{error}</p>}
<button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const WORKSPACE_EXTRA_IDS: Record<string, true> = {
sub2api: true,
xai: true,
fireworks: true,
gitkraken: true,
};

function extraConfig(providerId: string, t: Props["t"]) {
Expand Down Expand Up @@ -176,6 +177,13 @@ function extraConfig(providerId: string, t: Props["t"]) {
placeholder: "your-account-slug",
help: "From app.fireworks.ai/accounts/<slug>. Or set FIREWORKS_ACCOUNT_SLUG. Pair with a Fireworks API key to read 30-day rated billing spend.",
};
case "gitkraken":
return {
title: "GitKraken organization",
label: "Organization ID (optional)",
placeholder: "organization-id",
help: "Adds the organization ID to shared-pool usage requests. Or set GITKRAKEN_ORG_ID.",
};
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ const POLICIES: Readonly<Record<string, UsageSourcePolicy>> = {
{ value: "oauth", label: "Hermes OAuth", description: "Uses the read-only Nous Portal token from Hermes Agent." },
],
},
hyper: {
options: [
{ value: "auto", label: "Auto", description: "Tries the Charm Hyper browser session, then the configured API key." },
{ value: "web", label: "Browser session", description: "Uses the selected hyper.charm.land browser session only." },
{ value: "oauth", label: "API", description: "Uses the configured Charm Hyper API key only." },
],
},
gitkraken: {
options: [
{ value: "auto", label: "Auto", description: "Uses the configured GitKraken access token." },
{ value: "oauth", label: "API", description: "Uses the configured GitKraken access token only." },
],
},
bifrost: {
options: [
{ value: "auto", label: "Auto", description: "Uses the configured Bifrost gateway and virtual key." },
{ value: "oauth", label: "API", description: "Uses the configured Bifrost gateway and virtual key only." },
],
},
muse: {
options: [
{ value: "auto", label: "Auto", description: "Uses the local Muse Code device login." },
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop-tauri/src/test/providerCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,7 @@ export const TEST_PROVIDER_CATALOG: Array<[string, string]> = [
["meta", "Meta"],
["muse", "Muse Code"],
["nous", "Nous Portal"],
["hyper", "Charm Hyper"],
["gitkraken", "GitKraken AI"],
["bifrost", "Bifrost"],
];
26 changes: 25 additions & 1 deletion rust/src/core/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ pub enum ProviderId {
Muse,
Replicate,
Nous,
Hyper,
GitKraken,
Bifrost,
}

impl ProviderId {
Expand Down Expand Up @@ -177,6 +180,9 @@ impl ProviderId {
ProviderId::Muse,
ProviderId::Replicate,
ProviderId::Nous,
ProviderId::Hyper,
ProviderId::GitKraken,
ProviderId::Bifrost,
]
}

Expand Down Expand Up @@ -224,6 +230,9 @@ impl ProviderId {
ProviderId::Meta => "meta",
ProviderId::Muse => "muse",
ProviderId::Nous => "nous",
ProviderId::Hyper => "hyper",
ProviderId::GitKraken => "gitkraken",
ProviderId::Bifrost => "bifrost",
ProviderId::AiAnd => "aiand",
ProviderId::Windsurf => "windsurf",
ProviderId::Manus => "manus",
Expand Down Expand Up @@ -310,6 +319,9 @@ impl ProviderId {
ProviderId::Meta => "Meta",
ProviderId::Muse => "Muse Code",
ProviderId::Nous => "Nous Portal",
ProviderId::Hyper => "Charm Hyper",
ProviderId::GitKraken => "GitKraken AI",
ProviderId::Bifrost => "Bifrost",
ProviderId::AiAnd => "ai&",
ProviderId::Windsurf => "Windsurf",
ProviderId::Manus => "Manus",
Expand Down Expand Up @@ -411,6 +423,9 @@ impl ProviderId {
ProviderId::Meta => None,
ProviderId::Muse => None,
ProviderId::Nous => None,
ProviderId::Hyper => Some("hyper.charm.land"),
ProviderId::GitKraken => None,
ProviderId::Bifrost => None,
ProviderId::AiAnd => None,
ProviderId::Windsurf => None,
ProviderId::Doubao => None,
Expand Down Expand Up @@ -490,6 +505,9 @@ impl ProviderId {
"fireworks" | "fireworks-ai" | "fw" => Some(ProviderId::Fireworks),
"muse" | "muse-code" | "muse code" => Some(ProviderId::Muse),
"nous" | "nous-portal" | "nous portal" | "hermes" => Some(ProviderId::Nous),
"hyper" | "charm-hyper" | "charm hyper" => Some(ProviderId::Hyper),
"gitkraken" | "gitkraken-ai" | "gitkraken ai" => Some(ProviderId::GitKraken),
"bifrost" | "bifrost-gateway" | "bifrost gateway" => Some(ProviderId::Bifrost),
"meta" | "metaspark" | "meta-spark" | "muse-spark" | "musespark" | "muse spark"
| "meta muse spark" => Some(ProviderId::Meta),
"aiand" | "ai&" | "ai-and" | "ai and" => Some(ProviderId::AiAnd),
Expand Down Expand Up @@ -1099,6 +1117,9 @@ pub fn brand_color(id: ProviderId) -> &'static str {
ProviderId::Muse => "#0668E1",
ProviderId::Replicate => "#000000",
ProviderId::Nous => "#D6A55C",
ProviderId::Hyper => "#7C3AED",
ProviderId::GitKraken => "#179287",
ProviderId::Bifrost => "#5B7CFA",
}
}

Expand All @@ -1113,7 +1134,7 @@ mod tests {
#[test]
fn test_provider_id_all() {
let all = ProviderId::all();
assert_eq!(all.len(), 79);
assert_eq!(all.len(), 82);
assert!(all.contains(&ProviderId::Claude));
assert!(all.contains(&ProviderId::Codex));
assert!(all.contains(&ProviderId::Pi));
Expand Down Expand Up @@ -1173,6 +1194,9 @@ mod tests {
assert!(all.contains(&ProviderId::Replicate));
assert!(all.contains(&ProviderId::Muse));
assert!(all.contains(&ProviderId::Nous));
assert!(all.contains(&ProviderId::Hyper));
assert!(all.contains(&ProviderId::GitKraken));
assert!(all.contains(&ProviderId::Bifrost));
}

#[test]
Expand Down
Loading