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',
'Atomic Chat',
'Azure OpenAI',
'Bankr',
'Codex OAuth',
'DeepSeek',
'Google Gemini',

View File

@@ -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
? [
{

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 (/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

View File

@@ -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)

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

View File

@@ -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<string[] | null> {
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,
},

View File

@@ -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 {}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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<