diff --git a/src/components/ProviderManager.test.tsx b/src/components/ProviderManager.test.tsx index 44f75adc..383d8d39 100644 --- a/src/components/ProviderManager.test.tsx +++ b/src/components/ProviderManager.test.tsx @@ -110,6 +110,7 @@ const PRESET_ORDER = [ 'Anthropic', 'Atomic Chat', 'Azure OpenAI', + 'Bankr', 'Codex OAuth', 'DeepSeek', 'Google Gemini', diff --git a/src/components/ProviderManager.tsx b/src/components/ProviderManager.tsx index ee586dc4..570f86a7 100644 --- a/src/components/ProviderManager.tsx +++ b/src/components/ProviderManager.tsx @@ -1270,6 +1270,11 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode { label: 'Azure OpenAI', description: 'Azure OpenAI endpoint (model=deployment name)', }, + { + value: 'bankr', + label: 'Bankr', + description: 'Bankr LLM Gateway (OpenAI-compatible)', + }, ...(canUseCodexOAuth ? [ { diff --git a/src/components/StartupScreen.ts b/src/components/StartupScreen.ts index 6b38b26e..7ee937ed 100644 --- a/src/components/StartupScreen.ts +++ b/src/components/StartupScreen.ts @@ -144,6 +144,8 @@ export function detectProvider(): { name: string; model: string; baseUrl: string else if (/deepseek/i.test(rawModel)) name = 'DeepSeek' else if (/mistral/i.test(rawModel)) name = 'Mistral' else if (/llama/i.test(rawModel)) name = 'Meta Llama' + else if (/bankr/i.test(baseUrl)) name = 'Bankr' + else if (/bankr/i.test(rawModel)) name = 'Bankr' else if (isLocal) name = getLocalOpenAICompatibleProviderLabel(baseUrl) // Resolve model alias to actual model name + reasoning effort diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 8f8500ba..40b35c8e 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -1594,10 +1594,18 @@ class OpenAIShimMessages { (hostname.includes('cognitiveservices') || hostname.includes('openai') || hostname.includes('services.ai')) } catch { /* malformed URL — not Azure */ } + let isBankr = false + try { + isBankr = request.baseUrl.toLowerCase().includes('bankr') + } catch { /* malformed URL — not Bankr */ } + if (apiKey) { if (isAzure) { // Azure uses api-key header instead of Bearer token headers['api-key'] = apiKey + } else if (isBankr) { + // Bankr uses X-API-Key header instead of Bearer token + headers['X-API-Key'] = apiKey } else { headers.Authorization = `Bearer ${apiKey}` } @@ -2152,6 +2160,17 @@ export function createOpenAIShimClient(options: { process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? '' } + // Map Bankr env vars to OpenAI-compatible ones when present + if (process.env.BNKR_API_KEY && !process.env.OPENAI_API_KEY) { + process.env.OPENAI_API_KEY = process.env.BNKR_API_KEY + } + if (process.env.BANKR_BASE_URL && !process.env.OPENAI_BASE_URL) { + process.env.OPENAI_BASE_URL = process.env.BANKR_BASE_URL + } + if (process.env.BANKR_MODEL && !process.env.OPENAI_MODEL) { + process.env.OPENAI_MODEL = process.env.BANKR_MODEL + } + const beta = new OpenAIShimBeta({ ...(options.defaultHeaders ?? {}), }, options.reasoningEffort, options.providerOverride) diff --git a/src/utils/model/openaiModelDiscovery.ts b/src/utils/model/openaiModelDiscovery.ts index 5c33da97..b22ca943 100644 --- a/src/utils/model/openaiModelDiscovery.ts +++ b/src/utils/model/openaiModelDiscovery.ts @@ -39,12 +39,24 @@ function isAzureOpenAIBaseUrl(baseUrl: string): boolean { } } +function isBankrBaseUrl(baseUrl: string): boolean { + try { + return new URL(baseUrl).hostname.toLowerCase().includes('bankr') + } catch { + return false + } +} + function getOpenAIAuthHeaders(baseUrl: string): Record { const apiKey = process.env.OPENAI_API_KEY?.trim() if (!apiKey) { return {} } + if (isBankrBaseUrl(baseUrl)) { + return { 'X-API-Key': apiKey } + } + const headers: Record = { Authorization: `Bearer ${apiKey}`, } diff --git a/src/utils/providerDiscovery.ts b/src/utils/providerDiscovery.ts index bd0e90c1..5b6cdccc 100644 --- a/src/utils/providerDiscovery.ts +++ b/src/utils/providerDiscovery.ts @@ -197,6 +197,10 @@ export function getLocalOpenAICompatibleProviderLabel(baseUrl?: string): string if (host.includes('minimax') || haystack.includes('minimax')) { return 'MiniMax' } + // Check for Bankr LLM gateway + if (host.includes('bankr') || haystack.includes('bankr')) { + return 'Bankr' + } // Moonshot AI (Kimi) direct API if (host.includes('moonshot') || haystack.includes('moonshot') || haystack.includes('kimi')) { return 'Moonshot (Kimi)' @@ -226,14 +230,16 @@ export async function listOpenAICompatibleModels(options?: { }): Promise { const { signal, clear } = withTimeoutSignal(5000) try { + const baseUrl = getOpenAICompatibleModelsBaseUrl(options?.baseUrl) + const isBankr = baseUrl.toLowerCase().includes('bankr') const response = await fetch( - `${getOpenAICompatibleModelsBaseUrl(options?.baseUrl)}/models`, + `${baseUrl}/models`, { method: 'GET', headers: options?.apiKey - ? { - Authorization: `Bearer ${options.apiKey}`, - } + ? isBankr + ? { 'X-API-Key': options.apiKey } + : { Authorization: `Bearer ${options.apiKey}` } : undefined, signal, }, diff --git a/src/utils/providerFlag.ts b/src/utils/providerFlag.ts index 9479393c..8801c6b6 100644 --- a/src/utils/providerFlag.ts +++ b/src/utils/providerFlag.ts @@ -14,6 +14,7 @@ export const VALID_PROVIDERS = [ 'anthropic', + 'bankr', 'openai', 'gemini', 'mistral', @@ -148,6 +149,16 @@ export function applyProviderFlag( process.env.OPENAI_MODEL ??= 'MiniMax-M2.5' if (model) process.env.OPENAI_MODEL = model break + + case 'bankr': + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL ??= 'https://llm.bankr.bot/v1' + process.env.OPENAI_MODEL ??= 'claude-opus-4.6' + if (model) process.env.OPENAI_MODEL = model + if (process.env.BNKR_API_KEY && !process.env.OPENAI_API_KEY) { + process.env.OPENAI_API_KEY = process.env.BNKR_API_KEY + } + break } return {} diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index d48afe43..d48b144b 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -70,6 +70,9 @@ const PROFILE_ENV_KEYS = [ 'MISTRAL_BASE_URL', 'MISTRAL_API_KEY', 'MISTRAL_MODEL', + 'BANKR_BASE_URL', + 'BNKR_API_KEY', + 'BANKR_MODEL', ] as const const SECRET_ENV_KEYS = [ @@ -80,6 +83,7 @@ const SECRET_ENV_KEYS = [ 'NVIDIA_API_KEY', 'MINIMAX_API_KEY', 'MISTRAL_API_KEY', + 'BNKR_API_KEY', ] as const export type ProviderProfile = 'openai' | 'ollama' | 'codex' | 'gemini' | 'atomic-chat' | 'nvidia-nim' | 'minimax' | 'mistral' @@ -105,6 +109,9 @@ export type ProfileEnv = { MISTRAL_BASE_URL?: string MISTRAL_API_KEY?: string MISTRAL_MODEL?: string + BANKR_BASE_URL?: string + BNKR_API_KEY?: string + BANKR_MODEL?: string } export type ProfileFile = { @@ -121,7 +128,8 @@ type SecretValueSource = Partial< | 'GOOGLE_API_KEY' | 'NVIDIA_API_KEY' | 'MINIMAX_API_KEY' - | 'MISTRAL_API_KEY', + | 'MISTRAL_API_KEY' + | 'BNKR_API_KEY', string | undefined > > @@ -395,6 +403,42 @@ export function buildMistralProfileEnv(options: { return env } +export function buildBankrProfileEnv(options: { + model?: string | null + baseUrl?: string | null + apiKey?: string | null + processEnv?: NodeJS.ProcessEnv +}): ProfileEnv | null { + const processEnv = options.processEnv ?? process.env + const key = sanitizeApiKey(options.apiKey ?? processEnv.BNKR_API_KEY) + if (!key) { + return null + } + + const env: ProfileEnv = { + BNKR_API_KEY: key, + BANKR_MODEL: + sanitizeProviderConfigValue(options.model, { BNKR_API_KEY: key }) || + sanitizeProviderConfigValue( + processEnv.BANKR_MODEL, + { BNKR_API_KEY: key }, + ) || + 'claude-opus-4.6', + } + + const baseUrl = + sanitizeProviderConfigValue(options.baseUrl, { BNKR_API_KEY: key }) || + sanitizeProviderConfigValue( + processEnv.BANKR_BASE_URL, + { BNKR_API_KEY: key }, + ) + if (baseUrl) { + env.BANKR_BASE_URL = baseUrl + } + + return env +} + export function buildCodexOAuthProfileEnv( tokens: { accessToken: string diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index 9cb0fda8..cd3b6508 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -35,6 +35,7 @@ export type ProviderPreset = | 'custom' | 'nvidia-nim' | 'minimax' + | 'bankr' | 'atomic-chat' export type ProviderProfileInput = { @@ -297,6 +298,15 @@ export function getProviderPresetDefaults( apiKey: '', requiresApiKey: false, } + case 'bankr': + return { + provider: 'openai', + name: 'Bankr', + baseUrl: 'https://llm.bankr.bot/v1', + model: process.env.BANKR_MODEL ?? 'claude-opus-4.6', + apiKey: process.env.BNKR_API_KEY ?? '', + requiresApiKey: true, + } case 'ollama': default: return { @@ -481,7 +491,11 @@ function isProcessEnvAlignedWithProfile( sameOptionalEnvValue(processEnv.OPENAI_BASE_URL, profile.baseUrl) && sameOptionalEnvValue(processEnv.OPENAI_MODEL, getPrimaryModel(profile.model)) && (!includeApiKey || - sameOptionalEnvValue(processEnv.OPENAI_API_KEY, profile.apiKey)) + sameOptionalEnvValue(processEnv.OPENAI_API_KEY, profile.apiKey)) && + (profile.baseUrl?.toLowerCase().includes('bankr') + ? !includeApiKey || + sameOptionalEnvValue(processEnv.BNKR_API_KEY, profile.apiKey) + : true) ) } @@ -534,6 +548,9 @@ export function clearProviderProfileEnvFromProcessEnv( delete processEnv.MINIMAX_API_KEY delete processEnv.NVIDIA_API_KEY delete processEnv.NVIDIA_NIM + delete processEnv.BANKR_BASE_URL + delete processEnv.BNKR_API_KEY + delete processEnv.BANKR_MODEL } export function applyProviderProfileToProcessEnv(profile: ProviderProfile): void { @@ -606,6 +623,9 @@ export function applyProviderProfileToProcessEnv(profile: ProviderProfile): void if (baseUrl.includes('nvidia') || baseUrl.includes('integrate.api.nvidia')) { process.env.NVIDIA_API_KEY = profile.apiKey } + if (baseUrl.includes('bankr')) { + process.env.BNKR_API_KEY = profile.apiKey + } } else { delete process.env.OPENAI_API_KEY } @@ -860,6 +880,9 @@ function buildOpenAICompatibleStartupEnv( } if (activeProfile.apiKey) { env.OPENAI_API_KEY = activeProfile.apiKey + if (activeProfile.baseUrl?.toLowerCase().includes('bankr')) { + env.BNKR_API_KEY = activeProfile.apiKey + } } else { delete env.OPENAI_API_KEY } diff --git a/src/utils/providerSecrets.ts b/src/utils/providerSecrets.ts index 8f90d163..18006a2e 100644 --- a/src/utils/providerSecrets.ts +++ b/src/utils/providerSecrets.ts @@ -4,6 +4,7 @@ const SECRET_ENV_KEYS = [ 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'MISTRAL_API_KEY', + 'BNKR_API_KEY', ] as const export type SecretValueSource = Partial<