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:
@@ -110,6 +110,7 @@ const PRESET_ORDER = [
|
||||
'Anthropic',
|
||||
'Atomic Chat',
|
||||
'Azure OpenAI',
|
||||
'Bankr',
|
||||
'Codex OAuth',
|
||||
'DeepSeek',
|
||||
'Google Gemini',
|
||||
|
||||
@@ -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
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<
|
||||
|
||||
Reference in New Issue
Block a user