diff --git a/src/models/__tests__/openai.test.ts b/src/models/__tests__/openai.test.ts index 01614b7f..9419fd1a 100644 --- a/src/models/__tests__/openai.test.ts +++ b/src/models/__tests__/openai.test.ts @@ -100,7 +100,7 @@ describe('OpenAIModel', () => { vi.stubEnv('OPENAI_API_KEY', '') } expect(() => new OpenAIModel({ modelId: 'gpt-4o' })).toThrow( - "OpenAI API key is required. Provide it via the 'apiKey' option or set the OPENAI_API_KEY environment variable." + "OpenAI API key is required. Provide it via the 'apiKey' option (string or function) or set the OPENAI_API_KEY environment variable." ) }) @@ -144,6 +144,37 @@ describe('OpenAIModel', () => { const mockClient = {} as OpenAI expect(() => new OpenAIModel({ modelId: 'gpt-4o', client: mockClient })).not.toThrow() }) + + it('accepts function-based API key', () => { + const apiKeyFn = vi.fn(async () => 'sk-dynamic') + new OpenAIModel({ + modelId: 'gpt-4o', + apiKey: apiKeyFn, + }) + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: apiKeyFn, + }) + ) + }) + + it('accepts async function-based API key', () => { + const apiKeyFn = async (): Promise => { + await new Promise((resolve) => globalThis.setTimeout(resolve, 10)) + return 'sk-async-key' + } + + new OpenAIModel({ + modelId: 'gpt-4o', + apiKey: apiKeyFn, + }) + + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: apiKeyFn, + }) + ) + }) }) describe('updateConfig', () => { diff --git a/src/models/openai.ts b/src/models/openai.ts index 3e325a22..85d3a18b 100644 --- a/src/models/openai.ts +++ b/src/models/openai.ts @@ -8,6 +8,7 @@ */ import OpenAI, { type ClientOptions } from 'openai' +import type { ApiKeySetter } from 'openai/client' import { Model } from '../models/model.js' import type { BaseModelConfig, StreamOptions } from '../models/model.js' import type { Message } from '../types/messages.js' @@ -169,8 +170,12 @@ export interface OpenAIModelConfig extends BaseModelConfig { export interface OpenAIModelOptions extends OpenAIModelConfig { /** * OpenAI API key (falls back to OPENAI_API_KEY environment variable). + * + * Accepts either a static string or an async function that resolves to a string. + * When a function is provided, it is invoked before each request, allowing for + * dynamic API key rotation or runtime credential refresh. */ - apiKey?: string + apiKey?: string | ApiKeySetter /** * Pre-configured OpenAI client instance. @@ -241,6 +246,12 @@ export class OpenAIModel extends Model { * modelId: 'gpt-3.5-turbo' * }) * + * // Using function-based API key for dynamic key retrieval + * const provider = new OpenAIModel({ + * modelId: 'gpt-4o', + * apiKey: async () => await getRotatingApiKey() + * }) + * * // Using a pre-configured client instance * const client = new OpenAI({ apiKey: 'sk-...', timeout: 60000 }) * const provider = new OpenAIModel({ @@ -267,7 +278,7 @@ export class OpenAIModel extends Model { typeof process !== 'undefined' && typeof process.env !== 'undefined' && process.env.OPENAI_API_KEY if (!apiKey && !hasEnvKey) { throw new Error( - "OpenAI API key is required. Provide it via the 'apiKey' option or set the OPENAI_API_KEY environment variable." + "OpenAI API key is required. Provide it via the 'apiKey' option (string or function) or set the OPENAI_API_KEY environment variable." ) }