Skip to content

feat: add api_headers parameter and callable api_key support#307

Open
cpsievert wants to merge 3 commits into
mainfrom
feat/api-headers-190
Open

feat: add api_headers parameter and callable api_key support#307
cpsievert wants to merge 3 commits into
mainfrom
feat/api-headers-190

Conversation

@cpsievert
Copy link
Copy Markdown
Collaborator

Closes #190

Why

Users integrating with internal APIs or services that rotate Bearer tokens couldn't easily authenticate with chatlas. The existing kwargs={"default_headers": {...}} workaround was hard to discover and couldn't handle token refresh without recreating the chat object.

What's new

api_headers parameter — a new top-level parameter on all OpenAI-compatible Chat functions. Accepts a dict[str, str] of HTTP headers or a zero-argument callable returning one. Callables are invoked on every request, enabling dynamic patterns like token refresh:

from chatlas import ChatOpenAICompletions

# Static headers
chat = ChatOpenAICompletions(
    base_url="https://internal-api.example.com/v1",
    api_key="",
    api_headers={"Authorization": "Bearer my-token"},
)

# Dynamic/refreshable headers
chat = ChatOpenAICompletions(
    base_url="https://internal-api.example.com/v1",
    api_key="",
    api_headers=lambda: {"Authorization": f"Bearer {get_fresh_token()}"},
)

Callable api_keyapi_key now accepts Callable[[], str] in addition to str, matching the openai SDK's native support. The SDK calls it before every request for per-request key resolution:

chat = ChatOpenAI(api_key=lambda: get_rotated_key())

Both features are available on: ChatOpenAI, ChatOpenAICompletions, ChatGithub, ChatGroq, ChatPerplexity, ChatDeepSeek, ChatOpenRouter, ChatHuggingFace, ChatPortkey, ChatLMStudio, ChatMistral, ChatCloudflare, ChatAzureOpenAI, ChatAzureOpenAICompletions.

Test plan

  • Unit tests for resolve_api_headers() (static dict, callable, error cases)
  • Integration tests verifying api_headers flows through provider construction
  • Tests for callable api_key on both Responses and Completions APIs
  • Manual test with a real token-refreshing endpoint

This comment was marked as resolved.

cpsievert added 2 commits May 12, 2026 15:10
Two improvements for custom authentication on OpenAI-compatible providers:

1. New `api_headers` parameter — accepts a dict of HTTP headers or a
   zero-argument callable returning one. Applied via `extra_headers` on
   every API call, enabling token refresh and other dynamic auth patterns.

2. Widen `api_key` to accept `Callable[[], str]` — passed straight
   through to the openai SDK, which natively supports callable API keys
   with per-request resolution.

Both parameters are added to all OpenAI-compatible Chat functions
(ChatOpenAI, ChatOpenAICompletions, ChatGithub, ChatGroq, ChatPerplexity,
ChatDeepSeek, ChatOpenRouter, ChatHuggingFace, ChatPortkey, ChatLMStudio,
ChatMistral, ChatCloudflare, ChatAzureOpenAI, ChatAzureOpenAICompletions).

Closes #190
…rs scope

- Sync callable api_key is now wrapped with wrap_async() before passing
  to AsyncOpenAI/AsyncAzureOpenAI, which expect Callable[[], Awaitable[str]].
  Without this, chat_async() would fail when api_key is a sync callable.

- Clarified api_headers docstring: applies to chat API requests, not all
  API requests (list_models, batch, etc. use the client's default auth).
@cpsievert cpsievert force-pushed the feat/api-headers-190 branch from d3e5bbf to 1535c9a Compare May 12, 2026 20:13
ChatPortkey needs to serialize the API key into the x-portkey-api-key
header at construction time, so callable api_key is not supported —
raise a clear TypeError directing users to api_headers instead.

Also adds test coverage for callable api_key with the async client,
verifying the sync→async wrapping works correctly.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Comment on lines +223 to +226
return self._client.responses.create( # type: ignore
**kwargs,
extra_headers=self._get_extra_headers(),
)
Comment on lines +169 to +172
return self._client.chat.completions.create( # type: ignore
**kwargs,
extra_headers=self._get_extra_headers(),
)
Comment thread tests/test_api_headers.py
Comment on lines +49 to +67
class TestProviderApiHeaders:
"""Test that api_headers flow through to the provider correctly."""

def test_openai_provider_stores_api_headers(self, monkeypatch):
from chatlas._provider_openai_completions import OpenAICompletionsProvider

for k in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"):
monkeypatch.delenv(k, raising=False)
monkeypatch.delenv(k.lower(), raising=False)

headers = {"Authorization": "Bearer dynamic-key"}
provider = OpenAICompletionsProvider(
api_key="dummy",
model="gpt-4o",
api_headers=headers,
)
assert provider._api_headers is not None
assert provider._get_extra_headers() == headers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Consider exposing extra API headers as argument

2 participants