Skip to content

OpenAI can pass apiKey in clientConfig #4

@zastrowm

Description

@zastrowm

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?: string

Required:

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 | ApiKeySetter

Change 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

  • ApiKeySetter type is imported from 'openai' package
  • OpenAIModelOptions.apiKey accepts string | ApiKeySetter
  • Constructor accepts string-based apiKey without error (existing behavior)
  • Constructor accepts function-based apiKey without error (new behavior)
  • Constructor passes function-based apiKey directly 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 apiKey scenarios
  • 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.ts and openai.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 ApiKeySetter type ensures compatibility
  • Use case alignment - Matches real-world needs for credential rotation and dynamic key management

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions