daemon: enforce auth_mode for OpenAI and Anthropic
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createClientFromConfig, anthropicToGitHubModel, createAutoFallbackClient } from './index.js';
|
||||
import { mkdtempSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AnthropicClient } from '../models/anthropic.js';
|
||||
import { OpenAIClient } from '../models/openai.js';
|
||||
import { OllamaClient } from '../models/local/ollama.js';
|
||||
@@ -8,114 +11,130 @@ import { GeminiClient } from '../models/gemini.js';
|
||||
import { BedrockClient } from '../models/bedrock.js';
|
||||
import { GitHubModelsClient } from '../models/github.js';
|
||||
|
||||
async function loadFactory(): Promise<typeof import('./index.js')> {
|
||||
return import('./index.js');
|
||||
}
|
||||
|
||||
describe('createClientFromConfig', () => {
|
||||
it('creates AnthropicClient for anthropic provider', () => {
|
||||
it('creates AnthropicClient for anthropic provider', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'anthropic',
|
||||
model: 'claude-sonnet-4-5-20250514',
|
||||
api_key: 'sk-ant-test',
|
||||
});
|
||||
expect(client).toBeInstanceOf(AnthropicClient);
|
||||
expect(client.constructor.name).toBe('AnthropicClient');
|
||||
});
|
||||
|
||||
it('creates OpenAIClient for openai provider', () => {
|
||||
it('creates OpenAIClient for openai provider', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o',
|
||||
api_key: 'sk-test',
|
||||
});
|
||||
expect(client).toBeInstanceOf(OpenAIClient);
|
||||
expect(client.constructor.name).toBe('OpenAIClient');
|
||||
});
|
||||
|
||||
it('creates OllamaClient for ollama provider', () => {
|
||||
it('creates OllamaClient for ollama provider', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'ollama',
|
||||
model: 'llama3.2:1b',
|
||||
endpoint: 'http://localhost:11434',
|
||||
});
|
||||
expect(client).toBeInstanceOf(OllamaClient);
|
||||
expect(client.constructor.name).toBe('OllamaClient');
|
||||
});
|
||||
|
||||
it('creates OllamaClient with num_gpu option', () => {
|
||||
it('creates OllamaClient with num_gpu option', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'ollama',
|
||||
model: 'llama3.2:1b',
|
||||
num_gpu: 0,
|
||||
});
|
||||
expect(client).toBeInstanceOf(OllamaClient);
|
||||
expect(client.constructor.name).toBe('OllamaClient');
|
||||
});
|
||||
|
||||
it('creates LlamaCppClient for llamacpp provider', () => {
|
||||
it('creates LlamaCppClient for llamacpp provider', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'llamacpp',
|
||||
model: 'ministral-reasoning',
|
||||
endpoint: 'http://localhost:8080',
|
||||
});
|
||||
expect(client).toBeInstanceOf(LlamaCppClient);
|
||||
expect(client.constructor.name).toBe('LlamaCppClient');
|
||||
});
|
||||
|
||||
it('defaults llamacpp endpoint to localhost:8080', () => {
|
||||
it('defaults llamacpp endpoint to localhost:8080', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'llamacpp',
|
||||
model: 'test-model',
|
||||
});
|
||||
expect(client).toBeInstanceOf(LlamaCppClient);
|
||||
expect(client.constructor.name).toBe('LlamaCppClient');
|
||||
});
|
||||
|
||||
it('creates GeminiClient for gemini provider', () => {
|
||||
it('creates GeminiClient for gemini provider', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'gemini',
|
||||
model: 'gemini-2.5-pro',
|
||||
api_key: 'test-key',
|
||||
});
|
||||
expect(client).toBeInstanceOf(GeminiClient);
|
||||
expect(client.constructor.name).toBe('GeminiClient');
|
||||
});
|
||||
|
||||
it('throws for unknown provider', () => {
|
||||
it('throws for unknown provider', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
expect(() => createClientFromConfig({
|
||||
provider: 'unknown' as 'anthropic',
|
||||
model: 'test',
|
||||
})).toThrow('Unknown model provider: unknown');
|
||||
});
|
||||
|
||||
it('creates OpenAIClient with OpenRouter baseURL for openrouter provider', () => {
|
||||
it('creates OpenAIClient with OpenRouter baseURL for openrouter provider', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'openrouter',
|
||||
model: 'meta-llama/llama-3.1-70b',
|
||||
api_key: 'test-key',
|
||||
});
|
||||
expect(client).toBeInstanceOf(OpenAIClient);
|
||||
expect(client.constructor.name).toBe('OpenAIClient');
|
||||
});
|
||||
|
||||
it('creates OpenAIClient with Zhipu AI baseURL for zhipuai provider', () => {
|
||||
it('creates OpenAIClient with Zhipu AI baseURL for zhipuai provider', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'zhipuai',
|
||||
model: 'glm-4.5',
|
||||
api_key: 'test-key',
|
||||
});
|
||||
expect(client).toBeInstanceOf(OpenAIClient);
|
||||
expect(client.constructor.name).toBe('OpenAIClient');
|
||||
});
|
||||
|
||||
it('creates OpenAIClient for zhipuai when using auth_token', () => {
|
||||
it('creates OpenAIClient for zhipuai when using auth_token', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'zhipuai',
|
||||
model: 'glm-4.5',
|
||||
auth_token: 'oauth-access-token',
|
||||
});
|
||||
expect(client).toBeInstanceOf(OpenAIClient);
|
||||
expect(client.constructor.name).toBe('OpenAIClient');
|
||||
});
|
||||
|
||||
it('creates OpenAIClient for zhipuai when use_oauth is enabled and ZAI_API_KEY is set', () => {
|
||||
it('creates OpenAIClient for zhipuai when use_oauth is enabled and ZAI_API_KEY is set', async () => {
|
||||
const prev = process.env.ZAI_API_KEY;
|
||||
process.env.ZAI_API_KEY = 'zai-api-key';
|
||||
|
||||
try {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'zhipuai',
|
||||
model: 'glm-4.7',
|
||||
use_oauth: true,
|
||||
});
|
||||
expect(client).toBeInstanceOf(OpenAIClient);
|
||||
expect(client.constructor.name).toBe('OpenAIClient');
|
||||
} finally {
|
||||
if (prev === undefined) {
|
||||
delete process.env.ZAI_API_KEY;
|
||||
@@ -125,16 +144,17 @@ describe('createClientFromConfig', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('creates OpenAIClient for zhipuai using ZHIPUAI_AUTH_TOKEN env var', () => {
|
||||
it('creates OpenAIClient for zhipuai using ZHIPUAI_AUTH_TOKEN env var', async () => {
|
||||
const prev = process.env.ZHIPUAI_AUTH_TOKEN;
|
||||
process.env.ZHIPUAI_AUTH_TOKEN = 'oauth-access-token';
|
||||
|
||||
try {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'zhipuai',
|
||||
model: 'glm-4.5',
|
||||
});
|
||||
expect(client).toBeInstanceOf(OpenAIClient);
|
||||
expect(client.constructor.name).toBe('OpenAIClient');
|
||||
} finally {
|
||||
if (prev === undefined) {
|
||||
delete process.env.ZHIPUAI_AUTH_TOKEN;
|
||||
@@ -144,92 +164,180 @@ describe('createClientFromConfig', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('creates BedrockClient for bedrock provider', () => {
|
||||
it('creates BedrockClient for bedrock provider', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'bedrock',
|
||||
model: 'anthropic.claude-3-sonnet',
|
||||
});
|
||||
expect(client).toBeInstanceOf(BedrockClient);
|
||||
expect(client.constructor.name).toBe('BedrockClient');
|
||||
});
|
||||
|
||||
it('creates GitHubModelsClient for github provider', () => {
|
||||
it('creates GitHubModelsClient for github provider', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'github',
|
||||
model: 'claude-sonnet-4.5',
|
||||
});
|
||||
expect(client).toBeInstanceOf(GitHubModelsClient);
|
||||
expect(client.constructor.name).toBe('GitHubModelsClient');
|
||||
});
|
||||
|
||||
it('auto-maps Anthropic model names to GitHub equivalents for github provider', () => {
|
||||
it('auto-maps Anthropic model names to GitHub equivalents for github provider', async () => {
|
||||
// User might copy-paste the Anthropic model name into a github fallback block
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'github',
|
||||
model: 'claude-sonnet-4-5-20250929',
|
||||
});
|
||||
expect(client).toBeInstanceOf(GitHubModelsClient);
|
||||
expect(client.constructor.name).toBe('GitHubModelsClient');
|
||||
});
|
||||
|
||||
it('auth_mode overrides use_oauth for OpenAI', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o',
|
||||
auth_mode: 'api_key',
|
||||
use_oauth: true,
|
||||
api_key: 'sk-test',
|
||||
});
|
||||
expect(client.constructor.name).toBe('OpenAIClient');
|
||||
expect((client as any).useOAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('auth_mode oauth selects Anthropic auth_token without requiring api_key', async () => {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'anthropic',
|
||||
model: 'claude-sonnet-4-5-20250514',
|
||||
auth_mode: 'oauth',
|
||||
auth_token: 'tok-test',
|
||||
});
|
||||
expect(client.constructor.name).toBe('AnthropicClient');
|
||||
});
|
||||
|
||||
it('auth_mode api_key throws for OpenAI when no key is available', async () => {
|
||||
const prev = process.env.OPENAI_API_KEY;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
try {
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
expect(() => createClientFromConfig({
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o',
|
||||
auth_mode: 'api_key',
|
||||
})).toThrow(/OpenAI API key not configured/i);
|
||||
} finally {
|
||||
if (prev === undefined) {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
} else {
|
||||
process.env.OPENAI_API_KEY = prev;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('auth_mode oauth uses stored OpenAI OAuth tokens', async () => {
|
||||
const originalHome = process.env.HOME;
|
||||
const homeDir = mkdtempSync(join(tmpdir(), 'flynn-clientfactory-openai-'));
|
||||
process.env.HOME = homeDir;
|
||||
|
||||
try {
|
||||
vi.resetModules();
|
||||
const { storeOpenAIAuth } = await import('../auth/openai.js');
|
||||
storeOpenAIAuth({
|
||||
access_token: 'at',
|
||||
refresh_token: 'rt',
|
||||
expires_at: Date.now() + 60_000,
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const { createClientFromConfig } = await loadFactory();
|
||||
const client = createClientFromConfig({
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o',
|
||||
auth_mode: 'oauth',
|
||||
});
|
||||
expect(client.constructor.name).toBe('OpenAIClient');
|
||||
expect((client as any).useOAuth).toBe(true);
|
||||
} finally {
|
||||
process.env.HOME = originalHome;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('anthropicToGitHubModel', () => {
|
||||
it('maps claude-sonnet-4-20250514 to claude-sonnet-4', () => {
|
||||
it('maps claude-sonnet-4-20250514 to claude-sonnet-4', async () => {
|
||||
const { anthropicToGitHubModel } = await loadFactory();
|
||||
expect(anthropicToGitHubModel('claude-sonnet-4-20250514')).toBe('claude-sonnet-4');
|
||||
});
|
||||
|
||||
it('maps claude-sonnet-4-5-20250929 to claude-sonnet-4.5', () => {
|
||||
it('maps claude-sonnet-4-5-20250929 to claude-sonnet-4.5', async () => {
|
||||
const { anthropicToGitHubModel } = await loadFactory();
|
||||
expect(anthropicToGitHubModel('claude-sonnet-4-5-20250929')).toBe('claude-sonnet-4.5');
|
||||
});
|
||||
|
||||
it('maps claude-opus-4-20250514 to claude-opus-4', () => {
|
||||
it('maps claude-opus-4-20250514 to claude-opus-4', async () => {
|
||||
const { anthropicToGitHubModel } = await loadFactory();
|
||||
expect(anthropicToGitHubModel('claude-opus-4-20250514')).toBe('claude-opus-4');
|
||||
});
|
||||
|
||||
it('maps claude-opus-4-5-20250918 to claude-opus-4.5', () => {
|
||||
it('maps claude-opus-4-5-20250918 to claude-opus-4.5', async () => {
|
||||
const { anthropicToGitHubModel } = await loadFactory();
|
||||
expect(anthropicToGitHubModel('claude-opus-4-5-20250918')).toBe('claude-opus-4.5');
|
||||
});
|
||||
|
||||
it('maps claude-opus-4-6-20250715 to claude-opus-4.6', () => {
|
||||
it('maps claude-opus-4-6-20250715 to claude-opus-4.6', async () => {
|
||||
const { anthropicToGitHubModel } = await loadFactory();
|
||||
expect(anthropicToGitHubModel('claude-opus-4-6-20250715')).toBe('claude-opus-4.6');
|
||||
});
|
||||
|
||||
it('maps claude-3-5-haiku-20241022 to claude-haiku-4.5', () => {
|
||||
it('maps claude-3-5-haiku-20241022 to claude-haiku-4.5', async () => {
|
||||
const { anthropicToGitHubModel } = await loadFactory();
|
||||
expect(anthropicToGitHubModel('claude-3-5-haiku-20241022')).toBe('claude-haiku-4.5');
|
||||
});
|
||||
|
||||
it('maps claude-haiku-4-5-20251001 to claude-haiku-4.5', () => {
|
||||
it('maps claude-haiku-4-5-20251001 to claude-haiku-4.5', async () => {
|
||||
const { anthropicToGitHubModel } = await loadFactory();
|
||||
expect(anthropicToGitHubModel('claude-haiku-4-5-20251001')).toBe('claude-haiku-4.5');
|
||||
});
|
||||
|
||||
it('strips date suffix and converts trailing version number with dot for unknown models', () => {
|
||||
it('strips date suffix and converts trailing version number with dot for unknown models', async () => {
|
||||
// "claude-sonnet-5-7-20260101" → strip date → "claude-sonnet-5-7" → dot → "claude-sonnet-5.7"
|
||||
const { anthropicToGitHubModel } = await loadFactory();
|
||||
expect(anthropicToGitHubModel('claude-sonnet-5-7-20260101')).toBe('claude-sonnet-5.7');
|
||||
});
|
||||
|
||||
it('strips date suffix for models without sub-version', () => {
|
||||
it('strips date suffix for models without sub-version', async () => {
|
||||
// "claude-sonnet-5-20260101" → strip date → "claude-sonnet-5" (no trailing -N to dot-convert)
|
||||
const { anthropicToGitHubModel } = await loadFactory();
|
||||
expect(anthropicToGitHubModel('claude-sonnet-5-20260101')).toBe('claude-sonnet-5');
|
||||
});
|
||||
|
||||
it('returns undefined for models without date suffix', () => {
|
||||
it('returns undefined for models without date suffix', async () => {
|
||||
const { anthropicToGitHubModel } = await loadFactory();
|
||||
expect(anthropicToGitHubModel('llama3.2:1b')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAutoFallbackClient', () => {
|
||||
it('creates a GitHubModelsClient for anthropic provider', () => {
|
||||
it('creates a GitHubModelsClient for anthropic provider', async () => {
|
||||
const { createAutoFallbackClient } = await loadFactory();
|
||||
const client = createAutoFallbackClient({
|
||||
provider: 'anthropic',
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
});
|
||||
expect(client).toBeInstanceOf(GitHubModelsClient);
|
||||
expect(client).toBeDefined();
|
||||
expect((client as any).constructor.name).toBe('GitHubModelsClient');
|
||||
});
|
||||
|
||||
it('returns undefined for non-anthropic providers', () => {
|
||||
it('returns undefined for non-anthropic providers', async () => {
|
||||
const { createAutoFallbackClient } = await loadFactory();
|
||||
expect(createAutoFallbackClient({ provider: 'openai', model: 'gpt-4o' })).toBeUndefined();
|
||||
expect(createAutoFallbackClient({ provider: 'ollama', model: 'llama3.2:1b' })).toBeUndefined();
|
||||
expect(createAutoFallbackClient({ provider: 'gemini', model: 'gemini-2.5-pro' })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for unmappable anthropic models', () => {
|
||||
it('returns undefined for unmappable anthropic models', async () => {
|
||||
const { createAutoFallbackClient } = await loadFactory();
|
||||
expect(createAutoFallbackClient({ provider: 'anthropic', model: 'custom-model' })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
+119
-14
@@ -3,7 +3,20 @@ import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, GeminiClie
|
||||
import type { ModelClient, RetryConfig, ModelTier } from '../models/index.js';
|
||||
import { logger } from '../logger.js';
|
||||
import { getZaiApiKey } from '../auth/zai.js';
|
||||
import { getAnthropicApiKey } from '../auth/anthropic.js';
|
||||
import { getAnthropicApiKey, getAnthropicAuthToken } from '../auth/anthropic.js';
|
||||
import { getOpenAIApiKey, loadStoredOpenAIAuth } from '../auth/openai.js';
|
||||
|
||||
type AuthMode = 'auto' | 'api_key' | 'oauth';
|
||||
|
||||
function getEffectiveAuthMode(cfg: ModelConfig): AuthMode {
|
||||
if (cfg.auth_mode) {
|
||||
return cfg.auth_mode;
|
||||
}
|
||||
if (cfg.use_oauth) {
|
||||
return 'oauth';
|
||||
}
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an API key from config or environment variable.
|
||||
@@ -45,23 +58,115 @@ function resolveAuthCredential(cfg: ModelConfig, apiKeyEnvVar: string, authToken
|
||||
export function createClientFromConfig(cfg: ModelConfig): ModelClient {
|
||||
switch (cfg.provider) {
|
||||
case 'anthropic':
|
||||
if (!cfg.api_key && !getAnthropicApiKey()) {
|
||||
{
|
||||
const authMode = getEffectiveAuthMode(cfg);
|
||||
|
||||
if (authMode === 'oauth') {
|
||||
const token = cfg.auth_token ?? getAnthropicAuthToken();
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
'Anthropic auth token not configured (auth_mode: oauth). ' +
|
||||
'Set ANTHROPIC_AUTH_TOKEN, run `flynn anthropic-auth --token`, or provide auth_token in config.',
|
||||
);
|
||||
}
|
||||
return new AnthropicClient({
|
||||
model: cfg.model,
|
||||
authToken: token,
|
||||
});
|
||||
}
|
||||
|
||||
if (authMode === 'api_key') {
|
||||
const apiKey = cfg.api_key ?? getAnthropicApiKey();
|
||||
if (!apiKey) {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
return new AnthropicClient({
|
||||
model: cfg.model,
|
||||
apiKey,
|
||||
});
|
||||
}
|
||||
|
||||
// auto: prefer API key, then token
|
||||
const apiKey = cfg.api_key ?? getAnthropicApiKey();
|
||||
if (apiKey) {
|
||||
return new AnthropicClient({
|
||||
model: cfg.model,
|
||||
apiKey,
|
||||
});
|
||||
}
|
||||
|
||||
const token = cfg.auth_token ?? getAnthropicAuthToken();
|
||||
if (token) {
|
||||
return new AnthropicClient({
|
||||
model: cfg.model,
|
||||
authToken: token,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Anthropic API key not configured. ' +
|
||||
'Set ANTHROPIC_API_KEY, run `flynn anthropic-auth`, or provide api_key in config.',
|
||||
'Anthropic credentials not configured (auth_mode: auto). ' +
|
||||
'Set ANTHROPIC_API_KEY (or run `flynn anthropic-auth`), ' +
|
||||
'or set ANTHROPIC_AUTH_TOKEN (or run `flynn anthropic-auth --token`).',
|
||||
);
|
||||
}
|
||||
return new AnthropicClient({
|
||||
model: cfg.model,
|
||||
apiKey: cfg.api_key ?? getAnthropicApiKey() ?? undefined,
|
||||
authToken: cfg.auth_token,
|
||||
});
|
||||
case 'openai':
|
||||
return new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey: cfg.api_key,
|
||||
useOAuth: Boolean(cfg.use_oauth),
|
||||
});
|
||||
{
|
||||
const authMode = getEffectiveAuthMode(cfg);
|
||||
|
||||
if (authMode === 'oauth') {
|
||||
const existing = loadStoredOpenAIAuth();
|
||||
if (!existing) {
|
||||
throw new Error(
|
||||
'OpenAI OAuth is not configured (auth_mode: oauth). ' +
|
||||
'Run `flynn openai-auth` to authenticate.',
|
||||
);
|
||||
}
|
||||
return new OpenAIClient({
|
||||
model: cfg.model,
|
||||
useOAuth: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (authMode === 'api_key') {
|
||||
const apiKey = cfg.api_key ?? getOpenAIApiKey();
|
||||
if (!apiKey) {
|
||||
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.',
|
||||
);
|
||||
}
|
||||
return new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey,
|
||||
});
|
||||
}
|
||||
|
||||
// auto: prefer API key, then OAuth
|
||||
const apiKey = cfg.api_key ?? getOpenAIApiKey();
|
||||
if (apiKey) {
|
||||
return new OpenAIClient({
|
||||
model: cfg.model,
|
||||
apiKey,
|
||||
});
|
||||
}
|
||||
|
||||
const existing = loadStoredOpenAIAuth();
|
||||
if (existing) {
|
||||
return new OpenAIClient({
|
||||
model: cfg.model,
|
||||
useOAuth: true,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'OpenAI credentials not configured (auth_mode: auto). ' +
|
||||
'Set OPENAI_API_KEY (or run `flynn openai-key`), ' +
|
||||
'or run `flynn openai-auth` for OAuth.',
|
||||
);
|
||||
}
|
||||
case 'ollama':
|
||||
return new OllamaClient({
|
||||
model: cfg.model,
|
||||
|
||||
Reference in New Issue
Block a user