Skip to content

Commit a8aafcd

Browse files
committed
Make vercel dep optional
1 parent 80f3b01 commit a8aafcd

5 files changed

Lines changed: 91 additions & 39 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ A toolkit for building LLM-powered applications and agent loops.
88
uv add ai
99
```
1010

11-
AI Gateway usage works with the base package. Direct providers that use an
12-
OpenAI-compatible or Anthropic-compatible adapter load the corresponding
13-
official SDK lazily and require optional extras:
11+
AI Gateway API-key usage works with the base package. Direct providers that
12+
use an OpenAI-compatible or Anthropic-compatible adapter load the corresponding
13+
official SDK lazily. Vercel OIDC for AI Gateway also uses an optional extra:
1414

1515
```bash
1616
uv add "ai[openai]" # OpenAI-compatible providers
1717
uv add "ai[anthropic]" # Anthropic-compatible providers
18+
uv add "ai[vercel]" # Vercel OIDC for AI Gateway
1819
```
1920

2021
```python

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ dependencies = [
3232
"modelsdotdev==0.*",
3333
"pydantic>=2.12.5",
3434
"typing-extensions>=4.15.0",
35-
"vercel>=0.5.9",
3635
]
3736

3837
[project.optional-dependencies]
3938
anthropic = ["anthropic>=0.83.0"]
4039
mcp = ["mcp>=1.18.0"]
4140
openai = ["openai>=2.14.0"]
41+
vercel = [
42+
"vercel>=0.5.9",
43+
]
4244

4345
[build-system]
4446
requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"]

src/ai/providers/ai_gateway/provider.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77

88
from typing import TYPE_CHECKING, Any, ClassVar
99

10-
from vercel import oidc
11-
1210
from ... import errors as ai_errors
1311
from .. import base
1412
from . import client as gateway_client
@@ -17,7 +15,7 @@
1715
from .client import errors as client_errors
1816

1917
if TYPE_CHECKING:
20-
from collections.abc import AsyncGenerator, Mapping, Sequence
18+
from collections.abc import AsyncGenerator, Callable, Mapping, Sequence
2119
from types import ModuleType
2220

2321
import httpx
@@ -35,6 +33,27 @@
3533
_VERCEL_ENV = "VERCEL"
3634
_OIDC_TOKEN_ENV = "VERCEL_OIDC_TOKEN"
3735

36+
_get_vercel_oidc_token_impl: Callable[[], str] | None
37+
try:
38+
from vercel import oidc as _vercel_oidc
39+
except ModuleNotFoundError as exc:
40+
if exc.name != "vercel":
41+
raise
42+
_get_vercel_oidc_token_impl = None
43+
else:
44+
_get_vercel_oidc_token_impl = _vercel_oidc.get_vercel_oidc_token
45+
46+
47+
def _get_vercel_oidc_token() -> str:
48+
if _get_vercel_oidc_token_impl is None:
49+
raise ai_errors.InstallationError(
50+
"AI Gateway OIDC authentication requires the optional `vercel` "
51+
'package. Install it with `pip install "ai[vercel]"` or '
52+
'`uv add "ai[vercel]"`, or set `AI_GATEWAY_API_KEY` to use '
53+
"API key authentication."
54+
)
55+
return _get_vercel_oidc_token_impl()
56+
3857

3958
class GatewayProvider(base.Provider[gateway_client.GatewayClient]):
4059
"""Provider configuration for the Vercel AI Gateway."""
@@ -87,7 +106,7 @@ def _gateway_auth(
87106
if self._config_value(_VERCEL_ENV) == "1" or self._config_value(
88107
_OIDC_TOKEN_ENV
89108
):
90-
return oidc.get_vercel_oidc_token(), "oidc"
109+
return _get_vercel_oidc_token(), "oidc"
91110
return None, None
92111

93112
def is_configured(self) -> bool:

tests/providers/ai_gateway/test_provider.py

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
11
from __future__ import annotations
22

3+
from collections.abc import Callable
4+
35
import httpx
46
import pytest
5-
from vercel import oidc as vercel_oidc
67

78
import ai
9+
from ai.providers.ai_gateway import provider as gateway_provider
810
from ai.providers.ai_gateway.client import errors
911

1012

13+
def _set_oidc_token(
14+
monkeypatch: pytest.MonkeyPatch,
15+
get_token: Callable[[], str],
16+
) -> None:
17+
monkeypatch.setattr(
18+
gateway_provider,
19+
"_get_vercel_oidc_token_impl",
20+
get_token,
21+
)
22+
23+
24+
def _fail_oidc_import(monkeypatch: pytest.MonkeyPatch) -> None:
25+
monkeypatch.setattr(
26+
gateway_provider,
27+
"_get_vercel_oidc_token_impl",
28+
lambda: pytest.fail("OIDC should not be used when an API key is set"),
29+
)
30+
31+
1132
async def test_list_models_gets_config_with_gateway_headers_and_sorts_ids() -> (
1233
None
1334
):
@@ -79,11 +100,7 @@ async def test_list_models_uses_oidc_on_vercel_when_no_api_key(
79100
) -> None:
80101
monkeypatch.delenv("AI_GATEWAY_API_KEY", raising=False)
81102
monkeypatch.setenv("VERCEL", "1")
82-
monkeypatch.setattr(
83-
vercel_oidc,
84-
"get_vercel_oidc_token",
85-
lambda: "oidc-test-token",
86-
)
103+
_set_oidc_token(monkeypatch, lambda: "oidc-test-token")
87104
captured_headers: dict[str, str] = {}
88105

89106
def _handler(request: httpx.Request) -> httpx.Response:
@@ -115,11 +132,7 @@ async def test_list_models_uses_oidc_token_env_without_vercel_flag(
115132
monkeypatch.delenv("AI_GATEWAY_API_KEY", raising=False)
116133
monkeypatch.delenv("VERCEL", raising=False)
117134
monkeypatch.setenv("VERCEL_OIDC_TOKEN", "pulled-oidc-token")
118-
monkeypatch.setattr(
119-
vercel_oidc,
120-
"get_vercel_oidc_token",
121-
lambda: "pulled-oidc-token",
122-
)
135+
_set_oidc_token(monkeypatch, lambda: "pulled-oidc-token")
123136
captured_headers: dict[str, str] = {}
124137

125138
def _handler(request: httpx.Request) -> httpx.Response:
@@ -141,20 +154,43 @@ def _handler(request: httpx.Request) -> httpx.Response:
141154
assert captured_headers["ai-gateway-auth-method"] == "oidc"
142155

143156

144-
async def test_api_key_env_takes_precedence_over_oidc(
157+
async def test_oidc_expected_without_vercel_extra_raises_installation_error(
145158
monkeypatch: pytest.MonkeyPatch,
146159
) -> None:
147-
monkeypatch.setenv("AI_GATEWAY_API_KEY", "env-test-key")
160+
monkeypatch.delenv("AI_GATEWAY_API_KEY", raising=False)
148161
monkeypatch.setenv("VERCEL", "1")
162+
monkeypatch.setattr(
163+
gateway_provider,
164+
"_get_vercel_oidc_token_impl",
165+
None,
166+
)
149167

150-
def _fail_oidc() -> str:
151-
pytest.fail("OIDC should not be fetched when an API key is set")
168+
def _handler(request: httpx.Request) -> httpx.Response:
169+
pytest.fail("Gateway should not be called without the OIDC helper")
152170

153-
monkeypatch.setattr(
154-
vercel_oidc,
155-
"get_vercel_oidc_token",
156-
_fail_oidc,
171+
provider = ai.get_provider(
172+
"vercel",
173+
base_url="https://gateway.test/v3/ai",
174+
client=httpx.AsyncClient(transport=httpx.MockTransport(_handler)),
157175
)
176+
177+
try:
178+
with pytest.raises(ai.InstallationError) as exc_info:
179+
await provider.list_models()
180+
finally:
181+
await provider.aclose()
182+
183+
assert "AI Gateway OIDC authentication requires" in str(exc_info.value)
184+
assert "ai[vercel]" in str(exc_info.value)
185+
assert "AI_GATEWAY_API_KEY" in str(exc_info.value)
186+
187+
188+
async def test_api_key_env_takes_precedence_over_oidc(
189+
monkeypatch: pytest.MonkeyPatch,
190+
) -> None:
191+
monkeypatch.setenv("AI_GATEWAY_API_KEY", "env-test-key")
192+
monkeypatch.setenv("VERCEL", "1")
193+
_fail_oidc_import(monkeypatch)
158194
captured_headers: dict[str, str] = {}
159195

160196
def _handler(request: httpx.Request) -> httpx.Response:
@@ -181,15 +217,7 @@ async def test_explicit_api_key_takes_precedence_over_oidc(
181217
) -> None:
182218
monkeypatch.setenv("AI_GATEWAY_API_KEY", "env-test-key")
183219
monkeypatch.setenv("VERCEL", "1")
184-
185-
def _fail_oidc() -> str:
186-
pytest.fail("OIDC should not be fetched when an API key is set")
187-
188-
monkeypatch.setattr(
189-
vercel_oidc,
190-
"get_vercel_oidc_token",
191-
_fail_oidc,
192-
)
220+
_fail_oidc_import(monkeypatch)
193221
captured_headers: dict[str, str] = {}
194222

195223
def _handler(request: httpx.Request) -> httpx.Response:

uv.lock

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)