Skip to content

The callable api_key feature seems to be broken for Azure OpenAI clients #2626

@anujhydrabadi

Description

@anujhydrabadi

Confirm this is an issue with the Python library and not an underlying OpenAI API

  • This is an issue with the Python library

Describe the bug

The callable api_key feature seems to be broken for Azure OpenAI clients. Please let me know if I am missing something. Happy to contribute a PR if needed.

How It's Supposed to Work

  1. OpenAI Client (Working)

    • When you pass a callable as api_key, the client stores it in _api_key_provider and sets api_key to empty string.
    • During request preparation, _prepare_options() calls _refresh_api_key() (src/openai/_client.py:310).
    • _refresh_api_key() invokes the callable and updates self.api_key with the fresh value.
    • The fresh API key is then used in the auth_headers property.
  2. Azure OpenAI Client (Broken)

    • Inherits from OpenAI, so it gets the _api_key_provider storage mechanism.
    • However, Azure overrides _prepare_options() without calling the parent implementation.
    • Azure's _prepare_options() (src/openai/lib/azure.py:322-339) directly uses self.api_key without refreshing it.
    • Since api_key is initialized to an empty string when using a callable, Azure sends an empty API key.

The Bug Location

  • File: src/openai/lib/azure.py
  • Lines: 322-339 (sync) and 605-622 (async)

The Azure implementation checks self.api_key directly:

elif self.api_key is not API_KEY_SENTINEL:
    if headers.get("api-key") is None:
        headers["api-key"] = self.api_key  # ← Uses stale/empty value!

It should either:

  1. Call super()._prepare_options() first to trigger the refresh, OR
  2. Manually call self._refresh_api_key() before using self.api_key.

Impact

Anyone trying to use dynamic API key generation (e.g., from a secrets manager, key rotation, etc.) with Azure OpenAI will find it doesn't work — the client will always use an empty API key.

Additional Context

To Reproduce

Try this out:

from openai import OpenAI
  from openai.lib.azure import AzureOpenAI

  def get_api_key():
      print("Getting API key...")
      return "sk-..."

  # Works: callable is invoked
  client = OpenAI(api_key=get_api_key)
  client.models.list()  # Prints "Getting API key..."

  # Broken: callable is never invoked
  azure_client = AzureOpenAI(
      api_key=get_api_key,
      azure_endpoint="https://...",
      api_version="2024-02-01"
  )
  azure_client.models.list()  # Callable never invoked, auth fails

Code snippets

#!/usr/bin/env python3
"""Test script to verify callable api_key functionality in OpenAI and Azure OpenAI clients"""

import os
from openai import OpenAI, AsyncOpenAI
from openai.lib.azure import AzureOpenAI, AsyncAzureOpenAI

def test_openai_callable():
    """Test callable api_key with regular OpenAI client"""
    
    call_count = 0
    
    def get_api_key():
        nonlocal call_count
        call_count += 1
        print(f"OpenAI: get_api_key called (call #{call_count})")
        return os.environ.get("OPENAI_API_KEY", "test-key")
    
    client = OpenAI(api_key=get_api_key)
    
    # Check that callable is stored properly
    print(f"OpenAI client api_key: '{client.api_key}'")
    print(f"OpenAI client _api_key_provider: {client._api_key_provider}")
    
    # Try to make a request (will fail without real key, but we can see if callable is invoked)
    try:
        # This should trigger the callable
        client.models.list()
    except Exception as e:
        print(f"OpenAI request failed (expected): {e}")
    
    print(f"OpenAI: Total calls to get_api_key: {call_count}")
    print()

def test_azure_callable():
    """Test callable api_key with Azure OpenAI client"""
    
    call_count = 0
    
    def get_api_key():
        nonlocal call_count
        call_count += 1
        print(f"Azure: get_api_key called (call #{call_count})")
        return os.environ.get("AZURE_OPENAI_API_KEY", "test-key")
    
    client = AzureOpenAI(
        api_key=get_api_key,
        api_version="2024-02-01",
        azure_endpoint="https://test.openai.azure.com"
    )
    
    # Check that callable is stored properly  
    print(f"Azure client api_key: '{client.api_key}'")
    # Azure client inherits from OpenAI, so it should have _api_key_provider
    print(f"Azure client _api_key_provider: {getattr(client, '_api_key_provider', 'NOT FOUND')}")
    
    # Try to make a request (will fail without real key, but we can see if callable is invoked)
    try:
        # This should trigger the callable
        client.models.list()
    except Exception as e:
        print(f"Azure request failed (expected): {e}")
    
    print(f"Azure: Total calls to get_api_key: {call_count}")
    print()

def test_azure_prepare_options():
    """Test to see what happens in _prepare_options for Azure"""
    
    def get_api_key():
        print("Azure _prepare_options test: get_api_key called")
        return "dynamic-key-12345"
    
    client = AzureOpenAI(
        api_key=get_api_key,
        api_version="2024-02-01", 
        azure_endpoint="https://test.openai.azure.com"
    )
    
    print(f"Initial api_key value: '{client.api_key}'")
    
    # Simulate what happens when a request is made
    # The OpenAI parent class should call _refresh_api_key in its _prepare_options
    # But Azure overrides _prepare_options and doesn't call the parent
    
    from openai._models import FinalRequestOptions
    options = FinalRequestOptions(
        method="GET",
        url="/models",
    )
    
    # This should call Azure's _prepare_options
    prepared = client._prepare_options(options)
    
    print(f"After _prepare_options, api_key value: '{client.api_key}'")
    print()

if __name__ == "__main__":
    print("Testing Callable API Key Support\n")
    print("=" * 50)
    
    test_openai_callable()
    test_azure_callable()
    test_azure_prepare_options()

OS

macOS

Python version

Python v3.13.7

Library version

v1.107.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions