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

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