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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions