feat: add multi-key auth profile rotation for model providers
This commit is contained in:
+105
-33
@@ -1,5 +1,5 @@
|
||||
import type { Config, ModelConfig } from '../config/index.js';
|
||||
import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, GeminiClient, BedrockClient, GitHubModelsClient, SyntheticClient, ModelRouter, DEFAULT_RETRY_CONFIG } from '../models/index.js';
|
||||
import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, GeminiClient, BedrockClient, GitHubModelsClient, SyntheticClient, RotatingModelClient, ModelRouter, DEFAULT_RETRY_CONFIG } from '../models/index.js';
|
||||
import type { ModelClient, RetryConfig, ModelTier } from '../models/index.js';
|
||||
import { logger } from '../logger.js';
|
||||
import { getZaiApiKey } from '../auth/zai.js';
|
||||
@@ -33,6 +33,30 @@ function requireApiKey(cfg: ModelConfig, envVar: string): string {
|
||||
return key;
|
||||
}
|
||||
|
||||
function resolveApiKeyPool(cfg: ModelConfig, envVar?: string): string[] {
|
||||
const configured = (cfg.api_keys ?? []).map((key) => key.trim()).filter(Boolean);
|
||||
if (configured.length > 0) {
|
||||
return configured;
|
||||
}
|
||||
if (cfg.api_key?.trim()) {
|
||||
return [cfg.api_key.trim()];
|
||||
}
|
||||
if (envVar && process.env[envVar]?.trim()) {
|
||||
return [process.env[envVar]!.trim()];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function createApiKeyClient(
|
||||
keys: string[],
|
||||
build: (apiKey: string) => ModelClient,
|
||||
): ModelClient {
|
||||
if (keys.length === 1) {
|
||||
return build(keys[0]);
|
||||
}
|
||||
return new RotatingModelClient(keys.map((key) => build(key)));
|
||||
}
|
||||
|
||||
function resolveZaiCredential(cfg: ModelConfig): string {
|
||||
const raw = cfg.api_key
|
||||
?? cfg.auth_token
|
||||
@@ -75,26 +99,34 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient {
|
||||
}
|
||||
|
||||
if (authMode === 'api_key') {
|
||||
const apiKey = cfg.api_key ?? getAnthropicApiKey();
|
||||
if (!apiKey) {
|
||||
const keys = resolveApiKeyPool(cfg);
|
||||
const envKey = getAnthropicApiKey();
|
||||
const allKeys = keys.length > 0
|
||||
? keys
|
||||
: (envKey ? [envKey] : []);
|
||||
if (allKeys.length === 0) {
|
||||
throw new Error(
|
||||
'Anthropic API key not configured (auth_mode: api_key). ' +
|
||||
'Set ANTHROPIC_API_KEY, run `flynn anthropic-auth`, or provide api_key in config.',
|
||||
'Set ANTHROPIC_API_KEY, run `flynn anthropic-auth`, or provide api_key/api_keys in config.',
|
||||
);
|
||||
}
|
||||
return new AnthropicClient({
|
||||
return createApiKeyClient(allKeys, (apiKey) => new AnthropicClient({
|
||||
model: cfg.model,
|
||||
apiKey,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
// auto: prefer API key, then token
|
||||
const apiKey = cfg.api_key ?? getAnthropicApiKey();
|
||||
if (apiKey) {
|
||||
return new AnthropicClient({
|
||||
// auto: prefer API keys, then token
|
||||
const configuredKeys = resolveApiKeyPool(cfg);
|
||||
const envKey = getAnthropicApiKey();
|
||||
const allKeys = configuredKeys.length > 0
|
||||
? configuredKeys
|
||||
: (envKey ? [envKey] : []);
|
||||
if (allKeys.length > 0) {
|
||||
return createApiKeyClient(allKeys, (apiKey) => new AnthropicClient({
|
||||
model: cfg.model,
|
||||
apiKey,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
const token = cfg.auth_token ?? getAnthropicAuthToken();
|
||||
@@ -130,26 +162,34 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient {
|
||||
}
|
||||
|
||||
if (authMode === 'api_key') {
|
||||
const apiKey = cfg.api_key ?? getOpenAIApiKey();
|
||||
if (!apiKey) {
|
||||
const keys = resolveApiKeyPool(cfg);
|
||||
const envKey = getOpenAIApiKey();
|
||||
const allKeys = keys.length > 0
|
||||
? keys
|
||||
: (envKey ? [envKey] : []);
|
||||
if (allKeys.length === 0) {
|
||||
throw new Error(
|
||||
'OpenAI API key not configured (auth_mode: api_key). ' +
|
||||
'Set OPENAI_API_KEY, run `flynn openai-key`, or provide api_key in config.',
|
||||
'Set OPENAI_API_KEY, run `flynn openai-key`, or provide api_key/api_keys in config.',
|
||||
);
|
||||
}
|
||||
return new OpenAIClient({
|
||||
return createApiKeyClient(allKeys, (apiKey) => new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
// auto: prefer API key, then OAuth
|
||||
const apiKey = cfg.api_key ?? getOpenAIApiKey();
|
||||
if (apiKey) {
|
||||
return new OpenAIClient({
|
||||
// auto: prefer API keys, then OAuth
|
||||
const configuredKeys = resolveApiKeyPool(cfg);
|
||||
const envKey = getOpenAIApiKey();
|
||||
const allKeys = configuredKeys.length > 0
|
||||
? configuredKeys
|
||||
: (envKey ? [envKey] : []);
|
||||
if (allKeys.length > 0) {
|
||||
return createApiKeyClient(allKeys, (apiKey) => new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
const existing = loadStoredOpenAIAuth();
|
||||
@@ -184,11 +224,19 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient {
|
||||
apiKey: cfg.api_key,
|
||||
});
|
||||
case 'openrouter':
|
||||
return new OpenAIClient({
|
||||
{
|
||||
const keys = resolveApiKeyPool(cfg, 'OPENROUTER_API_KEY');
|
||||
if (keys.length === 0) {
|
||||
throw new Error(
|
||||
'API key required for openrouter. Set OPENROUTER_API_KEY or provide api_key/api_keys in config.',
|
||||
);
|
||||
}
|
||||
return createApiKeyClient(keys, (apiKey) => new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey: requireApiKey(cfg, 'OPENROUTER_API_KEY'),
|
||||
apiKey,
|
||||
baseURL: cfg.endpoint ?? 'https://openrouter.ai/api/v1',
|
||||
});
|
||||
}));
|
||||
}
|
||||
case 'vercel':
|
||||
return new OpenAIClient({
|
||||
model: cfg.model,
|
||||
@@ -202,23 +250,47 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient {
|
||||
baseURL: cfg.endpoint ?? 'https://api.z.ai/api/paas/v4',
|
||||
});
|
||||
case 'xai':
|
||||
return new OpenAIClient({
|
||||
{
|
||||
const keys = resolveApiKeyPool(cfg, 'XAI_API_KEY');
|
||||
if (keys.length === 0) {
|
||||
throw new Error(
|
||||
'API key required for xai. Set XAI_API_KEY or provide api_key/api_keys in config.',
|
||||
);
|
||||
}
|
||||
return createApiKeyClient(keys, (apiKey) => new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey: requireApiKey(cfg, 'XAI_API_KEY'),
|
||||
apiKey,
|
||||
baseURL: cfg.endpoint ?? 'https://api.x.ai/v1',
|
||||
});
|
||||
}));
|
||||
}
|
||||
case 'minimax':
|
||||
return new OpenAIClient({
|
||||
{
|
||||
const keys = resolveApiKeyPool(cfg, 'MINIMAX_API_KEY');
|
||||
if (keys.length === 0) {
|
||||
throw new Error(
|
||||
'API key required for minimax. Set MINIMAX_API_KEY or provide api_key/api_keys in config.',
|
||||
);
|
||||
}
|
||||
return createApiKeyClient(keys, (apiKey) => new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey: requireApiKey(cfg, 'MINIMAX_API_KEY'),
|
||||
apiKey,
|
||||
baseURL: cfg.endpoint ?? 'https://api.minimax.io/v1',
|
||||
});
|
||||
}));
|
||||
}
|
||||
case 'moonshot':
|
||||
return new OpenAIClient({
|
||||
{
|
||||
const keys = resolveApiKeyPool(cfg, 'MOONSHOT_API_KEY');
|
||||
if (keys.length === 0) {
|
||||
throw new Error(
|
||||
'API key required for moonshot. Set MOONSHOT_API_KEY or provide api_key/api_keys in config.',
|
||||
);
|
||||
}
|
||||
return createApiKeyClient(keys, (apiKey) => new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey: requireApiKey(cfg, 'MOONSHOT_API_KEY'),
|
||||
apiKey,
|
||||
baseURL: cfg.endpoint ?? 'https://api.moonshot.cn/v1',
|
||||
});
|
||||
}));
|
||||
}
|
||||
case 'bedrock':
|
||||
return new BedrockClient({
|
||||
model: cfg.model,
|
||||
|
||||
Reference in New Issue
Block a user