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