Feat/bankr provider (#888)

* feat(provider): add Bankr LLM Gateway support

Add Bankr as an OpenAI-compatible provider preset with dedicated env vars:
- BNKR_API_KEY, BANKR_BASE_URL, BANKR_MODEL
- Uses X-API-Key header instead of Authorization Bearer
- Base URL: https://llm.bankr.bot/v1
- Default model: claude-opus-4.6

Changes:
- Add 'bankr' to VALID_PROVIDERS and provider flag handling
- Add buildBankrProfileEnv() with env key registration
- Add Bankr detection in startup screen and provider discovery
- Map Bankr env vars to OpenAI-compatible vars in shim
- Add Bankr preset to ProviderManager (alphabetical order)
- Update PRESET_ORDER test to include Bankr

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fixup(provider): address Bankr PR review feedback

1. Map BNKR_API_KEY → OPENAI_API_KEY in providerFlag.ts so
   --provider bankr works with BNKR_API_KEY in non-interactive startup.

2. Remove unconditional BANKR_MODEL read from model.ts; it maps to
   OPENAI_MODEL via providerFlag.ts and openaiShim.ts, preventing
   cross-provider leakage.

3. Use X-API-Key for Bankr model discovery in openaiModelDiscovery.ts
   and providerDiscovery.ts, matching chat request auth.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
This commit is contained in:
Kevin Codex
2026-04-24 23:03:45 +08:00
committed by GitHub
parent 5a21d05741
commit 64b1014b9a
10 changed files with 130 additions and 6 deletions

View File

@@ -110,6 +110,7 @@ const PRESET_ORDER = [
'Anthropic', 'Anthropic',
'Atomic Chat', 'Atomic Chat',
'Azure OpenAI', 'Azure OpenAI',
'Bankr',
'Codex OAuth', 'Codex OAuth',
'DeepSeek', 'DeepSeek',
'Google Gemini', 'Google Gemini',

View File

@@ -1270,6 +1270,11 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
label: 'Azure OpenAI', label: 'Azure OpenAI',
description: 'Azure OpenAI endpoint (model=deployment name)', description: 'Azure OpenAI endpoint (model=deployment name)',
}, },
{
value: 'bankr',
label: 'Bankr',
description: 'Bankr LLM Gateway (OpenAI-compatible)',
},
...(canUseCodexOAuth ...(canUseCodexOAuth
? [ ? [
{ {

View File

@@ -144,6 +144,8 @@ export function detectProvider(): { name: string; model: string; baseUrl: string
else if (/deepseek/i.test(rawModel)) name = 'DeepSeek' else if (/deepseek/i.test(rawModel)) name = 'DeepSeek'
else if (/mistral/i.test(rawModel)) name = 'Mistral' else if (/mistral/i.test(rawModel)) name = 'Mistral'
else if (/llama/i.test(rawModel)) name = 'Meta Llama' 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) else if (isLocal) name = getLocalOpenAICompatibleProviderLabel(baseUrl)
// Resolve model alias to actual model name + reasoning effort // Resolve model alias to actual model name + reasoning effort

View File

@@ -1594,10 +1594,18 @@ class OpenAIShimMessages {
(hostname.includes('cognitiveservices') || hostname.includes('openai') || hostname.includes('services.ai')) (hostname.includes('cognitiveservices') || hostname.includes('openai') || hostname.includes('services.ai'))
} catch { /* malformed URL — not Azure */ } } catch { /* malformed URL — not Azure */ }
let isBankr = false
try {
isBankr = request.baseUrl.toLowerCase().includes('bankr')
} catch { /* malformed URL — not Bankr */ }
if (apiKey) { if (apiKey) {
if (isAzure) { if (isAzure) {
// Azure uses api-key header instead of Bearer token // Azure uses api-key header instead of Bearer token
headers['api-key'] = apiKey headers['api-key'] = apiKey
} else if (isBankr) {
// Bankr uses X-API-Key header instead of Bearer token
headers['X-API-Key'] = apiKey
} else { } else {
headers.Authorization = `Bearer ${apiKey}` headers.Authorization = `Bearer ${apiKey}`
} }
@@ -2152,6 +2160,17 @@ export function createOpenAIShimClient(options: {
process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? '' 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({ const beta = new OpenAIShimBeta({
...(options.defaultHeaders ?? {}), ...(options.defaultHeaders ?? {}),
}, options.reasoningEffort, options.providerOverride) }, options.reasoningEffort, options.providerOverride)

View File

@@ -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<string, string> { function getOpenAIAuthHeaders(baseUrl: string): Record<string, string> {
const apiKey = process.env.OPENAI_API_KEY?.trim() const apiKey = process.env.OPENAI_API_KEY?.trim()
if (!apiKey) { if (!apiKey) {
return {} return {}
} }
if (isBankrBaseUrl(baseUrl)) {
return { 'X-API-Key': apiKey }
}
const headers: Record<string, string> = { const headers: Record<string, string> = {
Authorization: `Bearer ${apiKey}`, Authorization: `Bearer ${apiKey}`,
} }

View File

@@ -197,6 +197,10 @@ export function getLocalOpenAICompatibleProviderLabel(baseUrl?: string): string
if (host.includes('minimax') || haystack.includes('minimax')) { if (host.includes('minimax') || haystack.includes('minimax')) {
return 'MiniMax' return 'MiniMax'
} }
// Check for Bankr LLM gateway
if (host.includes('bankr') || haystack.includes('bankr')) {
return 'Bankr'
}
// Moonshot AI (Kimi) direct API // Moonshot AI (Kimi) direct API
if (host.includes('moonshot') || haystack.includes('moonshot') || haystack.includes('kimi')) { if (host.includes('moonshot') || haystack.includes('moonshot') || haystack.includes('kimi')) {
return 'Moonshot (Kimi)' return 'Moonshot (Kimi)'
@@ -226,14 +230,16 @@ export async function listOpenAICompatibleModels(options?: {
}): Promise<string[] | null> { }): Promise<string[] | null> {
const { signal, clear } = withTimeoutSignal(5000) const { signal, clear } = withTimeoutSignal(5000)
try { try {
const baseUrl = getOpenAICompatibleModelsBaseUrl(options?.baseUrl)
const isBankr = baseUrl.toLowerCase().includes('bankr')
const response = await fetch( const response = await fetch(
`${getOpenAICompatibleModelsBaseUrl(options?.baseUrl)}/models`, `${baseUrl}/models`,
{ {
method: 'GET', method: 'GET',
headers: options?.apiKey headers: options?.apiKey
? { ? isBankr
Authorization: `Bearer ${options.apiKey}`, ? { 'X-API-Key': options.apiKey }
} : { Authorization: `Bearer ${options.apiKey}` }
: undefined, : undefined,
signal, signal,
}, },

View File

@@ -14,6 +14,7 @@
export const VALID_PROVIDERS = [ export const VALID_PROVIDERS = [
'anthropic', 'anthropic',
'bankr',
'openai', 'openai',
'gemini', 'gemini',
'mistral', 'mistral',
@@ -148,6 +149,16 @@ export function applyProviderFlag(
process.env.OPENAI_MODEL ??= 'MiniMax-M2.5' process.env.OPENAI_MODEL ??= 'MiniMax-M2.5'
if (model) process.env.OPENAI_MODEL = model if (model) process.env.OPENAI_MODEL = model
break 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 {} return {}

View File

@@ -70,6 +70,9 @@ const PROFILE_ENV_KEYS = [
'MISTRAL_BASE_URL', 'MISTRAL_BASE_URL',
'MISTRAL_API_KEY', 'MISTRAL_API_KEY',
'MISTRAL_MODEL', 'MISTRAL_MODEL',
'BANKR_BASE_URL',
'BNKR_API_KEY',
'BANKR_MODEL',
] as const ] as const
const SECRET_ENV_KEYS = [ const SECRET_ENV_KEYS = [
@@ -80,6 +83,7 @@ const SECRET_ENV_KEYS = [
'NVIDIA_API_KEY', 'NVIDIA_API_KEY',
'MINIMAX_API_KEY', 'MINIMAX_API_KEY',
'MISTRAL_API_KEY', 'MISTRAL_API_KEY',
'BNKR_API_KEY',
] as const ] as const
export type ProviderProfile = 'openai' | 'ollama' | 'codex' | 'gemini' | 'atomic-chat' | 'nvidia-nim' | 'minimax' | 'mistral' 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_BASE_URL?: string
MISTRAL_API_KEY?: string MISTRAL_API_KEY?: string
MISTRAL_MODEL?: string MISTRAL_MODEL?: string
BANKR_BASE_URL?: string
BNKR_API_KEY?: string
BANKR_MODEL?: string
} }
export type ProfileFile = { export type ProfileFile = {
@@ -121,7 +128,8 @@ type SecretValueSource = Partial<
| 'GOOGLE_API_KEY' | 'GOOGLE_API_KEY'
| 'NVIDIA_API_KEY' | 'NVIDIA_API_KEY'
| 'MINIMAX_API_KEY' | 'MINIMAX_API_KEY'
| 'MISTRAL_API_KEY', | 'MISTRAL_API_KEY'
| 'BNKR_API_KEY',
string | undefined string | undefined
> >
> >
@@ -395,6 +403,42 @@ export function buildMistralProfileEnv(options: {
return env 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( export function buildCodexOAuthProfileEnv(
tokens: { tokens: {
accessToken: string accessToken: string

View File

@@ -35,6 +35,7 @@ export type ProviderPreset =
| 'custom' | 'custom'
| 'nvidia-nim' | 'nvidia-nim'
| 'minimax' | 'minimax'
| 'bankr'
| 'atomic-chat' | 'atomic-chat'
export type ProviderProfileInput = { export type ProviderProfileInput = {
@@ -297,6 +298,15 @@ export function getProviderPresetDefaults(
apiKey: '', apiKey: '',
requiresApiKey: false, 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': case 'ollama':
default: default:
return { return {
@@ -481,7 +491,11 @@ function isProcessEnvAlignedWithProfile(
sameOptionalEnvValue(processEnv.OPENAI_BASE_URL, profile.baseUrl) && sameOptionalEnvValue(processEnv.OPENAI_BASE_URL, profile.baseUrl) &&
sameOptionalEnvValue(processEnv.OPENAI_MODEL, getPrimaryModel(profile.model)) && sameOptionalEnvValue(processEnv.OPENAI_MODEL, getPrimaryModel(profile.model)) &&
(!includeApiKey || (!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.MINIMAX_API_KEY
delete processEnv.NVIDIA_API_KEY delete processEnv.NVIDIA_API_KEY
delete processEnv.NVIDIA_NIM delete processEnv.NVIDIA_NIM
delete processEnv.BANKR_BASE_URL
delete processEnv.BNKR_API_KEY
delete processEnv.BANKR_MODEL
} }
export function applyProviderProfileToProcessEnv(profile: ProviderProfile): void { 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')) { if (baseUrl.includes('nvidia') || baseUrl.includes('integrate.api.nvidia')) {
process.env.NVIDIA_API_KEY = profile.apiKey process.env.NVIDIA_API_KEY = profile.apiKey
} }
if (baseUrl.includes('bankr')) {
process.env.BNKR_API_KEY = profile.apiKey
}
} else { } else {
delete process.env.OPENAI_API_KEY delete process.env.OPENAI_API_KEY
} }
@@ -860,6 +880,9 @@ function buildOpenAICompatibleStartupEnv(
} }
if (activeProfile.apiKey) { if (activeProfile.apiKey) {
env.OPENAI_API_KEY = activeProfile.apiKey env.OPENAI_API_KEY = activeProfile.apiKey
if (activeProfile.baseUrl?.toLowerCase().includes('bankr')) {
env.BNKR_API_KEY = activeProfile.apiKey
}
} else { } else {
delete env.OPENAI_API_KEY delete env.OPENAI_API_KEY
} }

View File

@@ -4,6 +4,7 @@ const SECRET_ENV_KEYS = [
'GEMINI_API_KEY', 'GEMINI_API_KEY',
'GOOGLE_API_KEY', 'GOOGLE_API_KEY',
'MISTRAL_API_KEY', 'MISTRAL_API_KEY',
'BNKR_API_KEY',
] as const ] as const
export type SecretValueSource = Partial< export type SecretValueSource = Partial<