Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ A toolkit for building LLM-powered applications and agent loops.
uv add ai
```

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

```bash
uv add "ai[openai]" # OpenAI-compatible providers
uv add "ai[anthropic]" # Anthropic-compatible providers
uv add "ai[vercel]" # Vercel OIDC for AI Gateway
```

```python
Expand Down
59 changes: 58 additions & 1 deletion examples/fastapi-vite/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

from __future__ import annotations

import importlib
import sys
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Protocol, cast

import agent as agent_
import fastapi
Expand All @@ -17,11 +18,67 @@
if TYPE_CHECKING:
from collections.abc import AsyncGenerator

import starlette.types


class _VercelHeaders(Protocol):
def set_headers(self, headers: dict[str, str] | None) -> None: ...


class VercelOIDCHeadersMiddleware:
def __init__(self, app: starlette.types.ASGIApp) -> None:
self.app = app

async def __call__(
self,
scope: starlette.types.Scope,
receive: starlette.types.Receive,
send: starlette.types.Send,
) -> None:
headers = _vercel_headers()
if scope.get("type") != "http" or headers is None:
await self.app(scope, receive, send)
return

headers.set_headers(_scope_headers(scope))
try:
await self.app(scope, receive, send)
finally:
headers.set_headers(None)


def _vercel_headers() -> _VercelHeaders | None:
try:
return cast(
"_VercelHeaders",
importlib.import_module("vercel.headers"),
)
except ModuleNotFoundError as exc:
if exc.name not in {"vercel", "vercel.headers"}:
raise
return None


def _scope_headers(scope: starlette.types.Scope) -> dict[str, str]:
return {
_header_text(key): _header_text(value)
for key, value in scope.get("headers", [])
}


def _header_text(value: object) -> str:
if isinstance(value, bytes | bytearray):
return bytes(value).decode("latin1")
return str(value)


app = fastapi.FastAPI(
title="py-ai-fastapi-chat",
description="Chat demo using Python Vercel AI SDK",
)

app.add_middleware(VercelOIDCHeadersMiddleware)

app.add_middleware(
fastapi.middleware.cors.CORSMiddleware,
allow_origins=["*"],
Expand Down
2 changes: 1 addition & 1 deletion examples/fastapi-vite/e2e-test/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ trap cleanup EXIT
echo "Starting backend on :$BACKEND_PORT..."
(
cd "$ROOT/backend"
uv run --frozen --with-editable ~/src/py-ai/ fastapi dev main.py --port "$BACKEND_PORT"
uv run --frozen --with-editable "$ROOT/../.." fastapi dev main.py --port "$BACKEND_PORT"
) > "$LOGS/backend.log" 2>&1 &
BACKEND_PID=$!

Expand Down
127 changes: 21 additions & 106 deletions examples/fastapi-vite/frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ dependencies = [
anthropic = ["anthropic>=0.83.0"]
mcp = ["mcp>=1.18.0"]
openai = ["openai>=2.14.0"]
vercel = [
"vercel>=0.5.9",
]

[build-system]
requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"]
Expand Down
4 changes: 2 additions & 2 deletions src/ai/providers/ai_gateway/client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Async client for the AI Gateway provider protocol."""

from . import errors
from ._client import GatewayClient, ModelType
from ._client import AuthMethod, GatewayClient, ModelType

__all__ = ["GatewayClient", "ModelType", "errors"]
__all__ = ["AuthMethod", "GatewayClient", "ModelType", "errors"]
Loading
Loading