- Add new 'firepass' provider type alongside anthropic, openai, openrouter - FirePass uses Fireworks AI's endpoint for Kimi K2.5 Turbo model - Subscription billing model ($7/week) with 256K context window - Anthropic API compatible (uses Anthropic SDK with custom baseURL) Changes: - providers.ts: Add firepass detection and base URL handling - auth.ts: Add FirePass API key management (FIREPASS_API_KEY or FIREWORKS_API_KEY) - config.ts: Add firepassApiKey and firepass auth provider - client.ts: Add firepass client creation with custom baseURL - http.ts: Add firepass auth headers - modelStrings.ts: Return Kimi K2.5 Turbo model ID for firepass - model.ts: Add Kimi display name handling and default model logic - modelOptions.ts: Simplified model picker for firepass (Kimi K2.5 Turbo only) - status.tsx: Display FirePass in status bar - login.tsx: Add FirePass option to provider selection - FirepassLoginFlow.tsx: New component for FirePass login flow Usage: 1. Run /login and select "FirePass" 2. Enter your Fireworks API key 3. Model picker shows Kimi K2.5 Turbo
243 lines
6.8 KiB
TypeScript
243 lines
6.8 KiB
TypeScript
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/index.js'
|
|
import { isEnvTruthy } from '../envUtils.js'
|
|
|
|
export type APIProvider =
|
|
| 'firstParty'
|
|
| 'openrouter'
|
|
| 'openai'
|
|
| 'firepass'
|
|
| 'bedrock'
|
|
| 'vertex'
|
|
| 'foundry'
|
|
|
|
function getStoredProviderPreference(): APIProvider | null {
|
|
try {
|
|
// Read the global config file directly so provider selection works even
|
|
// before the guarded config loader is enabled during startup.
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const { readFileSync } = require('fs') as typeof import('fs')
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const { getGlobalClaudeFile } =
|
|
require('../env.js') as typeof import('../env.js')
|
|
const raw = readFileSync(getGlobalClaudeFile(), 'utf8')
|
|
const config = JSON.parse(raw) as {
|
|
authProvider?: 'anthropic' | 'openrouter' | 'openai' | 'firepass'
|
|
openRouterApiKey?: string
|
|
openAiApiKey?: string
|
|
openAiAccessToken?: string
|
|
firepassApiKey?: string
|
|
}
|
|
|
|
switch (config.authProvider) {
|
|
case 'openrouter':
|
|
return config.openRouterApiKey ? 'openrouter' : null
|
|
case 'openai':
|
|
return config.openAiApiKey || config.openAiAccessToken
|
|
? 'openai'
|
|
: null
|
|
case 'firepass':
|
|
return config.firepassApiKey ? 'firepass' : null
|
|
case 'anthropic':
|
|
return 'firstParty'
|
|
default:
|
|
return null
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function getExplicitProviderOverride(): APIProvider | null {
|
|
const rawProvider =
|
|
process.env.BETTER_CLAWD_API_PROVIDER ??
|
|
process.env.CLAUDE_CODE_API_PROVIDER
|
|
switch (rawProvider?.toLowerCase()) {
|
|
case 'anthropic':
|
|
case 'firstparty':
|
|
case 'first_party':
|
|
case 'first-party':
|
|
return 'firstParty'
|
|
case 'openrouter':
|
|
return 'openrouter'
|
|
case 'openai':
|
|
return 'openai'
|
|
case 'firepass':
|
|
case 'fire-pass':
|
|
case 'fire_pass':
|
|
return 'firepass'
|
|
case 'bedrock':
|
|
return 'bedrock'
|
|
case 'vertex':
|
|
return 'vertex'
|
|
case 'foundry':
|
|
return 'foundry'
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function isOpenRouterBaseUrl(baseUrl?: string | null): boolean {
|
|
if (!baseUrl) {
|
|
return false
|
|
}
|
|
try {
|
|
return new URL(baseUrl).host === 'openrouter.ai'
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function isOpenRouterConfigured(): boolean {
|
|
return (
|
|
getExplicitProviderOverride() === 'openrouter' ||
|
|
Boolean(process.env.OPENROUTER_API_KEY) ||
|
|
isOpenRouterBaseUrl(process.env.OPENROUTER_BASE_URL) ||
|
|
isOpenRouterBaseUrl(process.env.ANTHROPIC_BASE_URL)
|
|
)
|
|
}
|
|
|
|
export function isOpenAIConfigured(): boolean {
|
|
return (
|
|
getExplicitProviderOverride() === 'openai' ||
|
|
Boolean(process.env.OPENAI_API_KEY) ||
|
|
Boolean(process.env.OPENAI_BASE_URL) ||
|
|
Boolean(process.env.OPENAI_ACCESS_TOKEN) ||
|
|
Boolean(process.env.CODEX_ACCESS_TOKEN)
|
|
)
|
|
}
|
|
|
|
export function isFirepassBaseUrl(baseUrl?: string | null): boolean {
|
|
if (!baseUrl) {
|
|
return false
|
|
}
|
|
try {
|
|
const host = new URL(baseUrl).host
|
|
return host === 'api.fireworks.ai'
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function isFirepassConfigured(): boolean {
|
|
return (
|
|
getExplicitProviderOverride() === 'firepass' ||
|
|
Boolean(process.env.FIREPASS_API_KEY) ||
|
|
Boolean(process.env.FIREWORKS_API_KEY) ||
|
|
isFirepassBaseUrl(process.env.FIREPASS_BASE_URL) ||
|
|
isFirepassBaseUrl(process.env.ANTHROPIC_BASE_URL)
|
|
)
|
|
}
|
|
|
|
export function getOpenRouterBaseUrl(): string {
|
|
const configuredBaseUrl = process.env.OPENROUTER_BASE_URL
|
|
const fallbackBaseUrl = 'https://openrouter.ai/api'
|
|
if (!configuredBaseUrl) {
|
|
return fallbackBaseUrl
|
|
}
|
|
|
|
try {
|
|
const url = new URL(configuredBaseUrl)
|
|
|
|
if (url.host === 'openrouter.ai') {
|
|
const normalizedPath = url.pathname.replace(/\/+$/, '')
|
|
if (normalizedPath === '' || normalizedPath === '/') {
|
|
url.pathname = '/api'
|
|
} else if (normalizedPath === '/api/v1') {
|
|
// Anthropic SDK appends /v1/messages itself, so OpenRouter's SDK base
|
|
// must stop at /api rather than /api/v1.
|
|
url.pathname = '/api'
|
|
}
|
|
}
|
|
|
|
return url.toString().replace(/\/$/, '')
|
|
} catch {
|
|
return configuredBaseUrl
|
|
}
|
|
}
|
|
|
|
export function getOpenAIBaseUrl(): string {
|
|
return process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1'
|
|
}
|
|
|
|
/**
|
|
* Get the FirePass base URL for Anthropic-compatible API.
|
|
* FirePass uses Fireworks AI's inference endpoint with Anthropic compatibility.
|
|
* Default: https://api.fireworks.ai/inference (Anthropic SDK appends /v1/messages)
|
|
*/
|
|
export function getFirepassBaseUrl(): string {
|
|
const configuredBaseUrl = process.env.FIREPASS_BASE_URL
|
|
if (!configuredBaseUrl) {
|
|
return 'https://api.fireworks.ai/inference'
|
|
}
|
|
|
|
try {
|
|
const url = new URL(configuredBaseUrl)
|
|
|
|
// Normalize path for Anthropic SDK compatibility
|
|
// SDK appends /v1/messages, so base should be /inference not /inference/v1
|
|
if (url.host === 'api.fireworks.ai') {
|
|
const normalizedPath = url.pathname.replace(/\/+$/, '')
|
|
if (normalizedPath === '/inference/v1' || normalizedPath === '/v1') {
|
|
url.pathname = '/inference'
|
|
}
|
|
}
|
|
|
|
return url.toString().replace(/\/$/, '')
|
|
} catch {
|
|
return configuredBaseUrl
|
|
}
|
|
}
|
|
|
|
export function getAPIProvider(): APIProvider {
|
|
const explicitProvider = getExplicitProviderOverride()
|
|
if (explicitProvider) {
|
|
return explicitProvider
|
|
}
|
|
|
|
return isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)
|
|
? 'bedrock'
|
|
: isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)
|
|
? 'vertex'
|
|
: isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
|
|
? 'foundry'
|
|
: isOpenAIConfigured()
|
|
? 'openai'
|
|
: isFirepassConfigured()
|
|
? 'firepass'
|
|
: isOpenRouterConfigured()
|
|
? 'openrouter'
|
|
: getStoredProviderPreference() ?? 'firstParty'
|
|
}
|
|
|
|
export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
|
|
return getAPIProvider() as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
|
}
|
|
|
|
export function isAnthropicCompatibleProvider(
|
|
provider: APIProvider = getAPIProvider(),
|
|
): boolean {
|
|
return provider !== 'openai'
|
|
}
|
|
|
|
/**
|
|
* Check if ANTHROPIC_BASE_URL is a first-party Anthropic API URL.
|
|
* Returns true if not set (default API) or points to api.anthropic.com
|
|
* (or api-staging.anthropic.com for ant users).
|
|
*/
|
|
export function isFirstPartyAnthropicBaseUrl(): boolean {
|
|
const baseUrl = process.env.ANTHROPIC_BASE_URL
|
|
if (!baseUrl) {
|
|
return true
|
|
}
|
|
try {
|
|
const host = new URL(baseUrl).host
|
|
const allowedHosts = ['api.anthropic.com']
|
|
if (process.env.USER_TYPE === 'ant') {
|
|
allowedHosts.push('api-staging.anthropic.com')
|
|
}
|
|
return allowedHosts.includes(host)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|