-
Notifications
You must be signed in to change notification settings - Fork 0
Description
OpenAI can pass apiKey in clientConfig
Original Request
Our OpenAI model can take in an apiKey as a member of clientConfig, but right now we throw an exception if we try to do this (because neither the environment variable nor the explicit apiKey at the top level config was passed in). However, the clientConfig.apiKey is useful because it can be a function instead of a string.
Update the OpenAI model config to allow the same type for apiKey as the client does, so that you can pass a function to the openAI model provider.
Implementation Requirements
Problem Analysis
The current OpenAIModelOptions interface (line 173 in src/models/openai.ts) defines apiKey as only accepting a string:
export interface OpenAIModelOptions extends OpenAIModelConfig {
apiKey?: string // ❌ Only accepts string
// ...
}However, the OpenAI SDK's ClientOptions interface supports apiKey as either a string OR an async function that returns a string:
export type ApiKeySetter = () => Promise<string>
export interface ClientOptions {
apiKey?: string | ApiKeySetter | undefined
// ...
}This function-based API key is useful for:
- Dynamic API key rotation
- Runtime credential refresh
- Fetching keys from secret managers
- Per-request authentication logic
Technical Approach
File to Modify: src/models/openai.ts
Change 1: Import the ApiKeySetter Type
Location: Line 10 (import statement)
Current:
import OpenAI, { type ClientOptions } from 'openai'Required:
import OpenAI, { type ClientOptions, type ApiKeySetter } from 'openai'Change 2: Update Interface Type Definition
Location: Line 169-173 (OpenAIModelOptions interface)
Current:
export interface OpenAIModelOptions extends OpenAIModelConfig {
/**
* OpenAI API key (falls back to OPENAI_API_KEY environment variable).
*/
apiKey?: stringRequired:
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 | ApiKeySetterChange 3: Update Constructor Validation Logic
Location: Lines 265-272 (constructor validation)
Current:
const hasEnvKey =
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."
)
}Required:
const hasEnvKey =
typeof process !== 'undefined' && typeof process.env !== 'undefined' && process.env.OPENAI_API_KEY
// Check if apiKey is provided (string or function are both truthy)
if (!apiKey && !hasEnvKey) {
throw new Error(
"OpenAI API key is required. Provide it via the 'apiKey' option (string or function) or set the OPENAI_API_KEY environment variable."
)
}Note: No additional validation is needed for function-based keys - the OpenAI SDK will validate that the function returns a non-empty string and throw appropriate errors.
Change 4: Update Constructor TSDoc Examples
Location: Lines 218-250 (constructor documentation)
Add example demonstrating function-based API key usage:
/**
* Creates a new OpenAIModel instance.
*
* @param options - Configuration for model and client
*
* @example
* ```typescript
* // Using static API key
* const provider = new OpenAIModel({
* modelId: 'gpt-4o',
* apiKey: 'sk-...'
* })
*
* // Using function-based API key for dynamic rotation
* const provider = new OpenAIModel({
* modelId: 'gpt-4o',
* apiKey: async () => await getRotatingApiKey()
* })
*
* // Using environment variable
* const provider = new OpenAIModel({
* modelId: 'gpt-3.5-turbo'
* })
* ```
*/Test Requirements
File to Modify: src/models/__tests__/openai.test.ts
Add test cases in the constructor describe block (after line 147):
Test 1: String-based API key (already exists)
Verify existing test at line 51 still passes.
Test 2: Function-based API key
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
})
)
})Test 3: Async function API key
it('accepts async function-based API key', async () => {
const apiKeyFn = async () => {
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 10))
return 'sk-async-key'
}
new OpenAIModel({
modelId: 'gpt-4o',
apiKey: apiKeyFn
})
expect(OpenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: apiKeyFn
})
)
})Test 4: Function API key takes precedence over env var
if (isNode) {
it('function-based API key takes precedence over environment variable', () => {
vi.stubEnv('OPENAI_API_KEY', 'sk-from-env')
const apiKeyFn = async () => 'sk-from-function'
new OpenAIModel({
modelId: 'gpt-4o',
apiKey: apiKeyFn
})
expect(OpenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: apiKeyFn
})
)
})
}Test 5: Error when neither string nor function API key provided
it('throws error when no API key is available (no function or string)', () => {
if (isNode) {
vi.stubEnv('OPENAI_API_KEY', '')
}
expect(() =>
new OpenAIModel({ modelId: 'gpt-4o' })
).toThrow(
"OpenAI API key is required. Provide it via the 'apiKey' option (string or function) or set the OPENAI_API_KEY environment variable."
)
})Acceptance Criteria
-
ApiKeySettertype is imported from 'openai' package -
OpenAIModelOptions.apiKeyacceptsstring | ApiKeySetter - Constructor accepts string-based
apiKeywithout error (existing behavior) - Constructor accepts function-based
apiKeywithout error (new behavior) - Constructor passes function-based
apiKeydirectly to OpenAI client - Validation logic treats function as valid (truthy check)
- Error message updated to mention "string or function"
- TSDoc comments document function-based API key capability
- TSDoc includes example of function-based usage
- All existing tests continue to pass
- New tests added for function-based
apiKeyscenarios - Test coverage remains at 80%+
- Code passes linting and formatting checks
- No breaking changes to existing API
Estimated Scope
- Complexity: Low
- Files Modified: 2 files (
openai.tsandopenai.test.ts) - Lines Changed:
- Source: ~15 lines (import, type, validation, docs)
- Tests: ~70 lines (5 new test cases)
- Implementation Time: 1-2 hours
- Testing Time: 1 hour
Additional Notes
- No breaking changes - This is purely additive functionality that expands the accepted type
- Backward compatible - All existing string-based usage patterns continue to work
- OpenAI SDK handles validation - No need to validate function return values; OpenAI SDK will throw appropriate errors
- Type safety - Using OpenAI's native
ApiKeySettertype ensures compatibility - Use case alignment - Matches real-world needs for credential rotation and dynamic key management