mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
feat(sdk-py): add sentinel to skip auto loading api key on sdk client create (#6500)
**Description:** There are times a user might want to create the client, but conditionally set the API key. For example, consider a complex auth situation where the system has user callers using jwts and system callers using API keys. This allows explicitly disabling the auto-loading behavior of API keys in the client today, so no key is set. **Issue:** N/A **Dependencies:** None **Twitter handle:** N/A
This commit is contained in:
@@ -38,7 +38,7 @@ def main():
|
||||
tree = ast.parse(file.read())
|
||||
|
||||
classes = find_classes(tree)
|
||||
|
||||
|
||||
def is_sync(class_spec: Tuple[str, List[str]]) -> bool:
|
||||
return class_spec[0].startswith("Sync")
|
||||
|
||||
|
||||
@@ -81,25 +81,37 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
RESERVED_HEADERS = ("x-api-key",)
|
||||
|
||||
NOT_PROVIDED = cast(None, object())
|
||||
|
||||
def _get_api_key(api_key: str | None = None) -> str | None:
|
||||
|
||||
def _get_api_key(api_key: str | None = NOT_PROVIDED) -> str | None:
|
||||
"""Get the API key from the environment.
|
||||
Precedence:
|
||||
1. explicit argument
|
||||
2. LANGGRAPH_API_KEY
|
||||
3. LANGSMITH_API_KEY
|
||||
4. LANGCHAIN_API_KEY
|
||||
1. explicit string argument
|
||||
2. LANGGRAPH_API_KEY (if api_key not provided)
|
||||
3. LANGSMITH_API_KEY (if api_key not provided)
|
||||
4. LANGCHAIN_API_KEY (if api_key not provided)
|
||||
|
||||
Args:
|
||||
api_key: The API key to use. Can be:
|
||||
- A string: use this exact API key
|
||||
- None: explicitly skip loading from environment
|
||||
- NOT_PROVIDED (default): auto-load from environment variables
|
||||
"""
|
||||
if api_key:
|
||||
if isinstance(api_key, str):
|
||||
return api_key
|
||||
for prefix in ["LANGGRAPH", "LANGSMITH", "LANGCHAIN"]:
|
||||
if env := os.getenv(f"{prefix}_API_KEY"):
|
||||
return env.strip().strip('"').strip("'")
|
||||
return None # type: ignore
|
||||
if api_key is NOT_PROVIDED:
|
||||
# api_key is not explicitly provided, try to load from environment
|
||||
for prefix in ["LANGGRAPH", "LANGSMITH", "LANGCHAIN"]:
|
||||
if env := os.getenv(f"{prefix}_API_KEY"):
|
||||
return env.strip().strip('"').strip("'")
|
||||
# api_key is explicitly None, don't load from environment
|
||||
return None
|
||||
|
||||
|
||||
def _get_headers(
|
||||
api_key: str | None, custom_headers: Mapping[str, str] | None
|
||||
api_key: str | None,
|
||||
custom_headers: Mapping[str, str] | None,
|
||||
) -> dict[str, str]:
|
||||
"""Combine api_key and custom user-provided headers."""
|
||||
custom_headers = custom_headers or {}
|
||||
@@ -111,9 +123,9 @@ def _get_headers(
|
||||
"User-Agent": f"langgraph-sdk-py/{langgraph_sdk.__version__}",
|
||||
**custom_headers,
|
||||
}
|
||||
api_key = _get_api_key(api_key)
|
||||
if api_key:
|
||||
headers["x-api-key"] = api_key
|
||||
resolved_api_key = _get_api_key(api_key)
|
||||
if resolved_api_key:
|
||||
headers["x-api-key"] = resolved_api_key
|
||||
|
||||
return headers
|
||||
|
||||
@@ -164,7 +176,7 @@ def _get_run_metadata_from_response(
|
||||
def get_client(
|
||||
*,
|
||||
url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
api_key: str | None = NOT_PROVIDED,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
timeout: TimeoutTypes | None = None,
|
||||
) -> LangGraphClient:
|
||||
@@ -179,12 +191,13 @@ def get_client(
|
||||
- If `None`, the client first attempts an in-process connection via ASGI transport.
|
||||
If that fails, it falls back to `http://localhost:8123`.
|
||||
api_key:
|
||||
API key for authentication. If omitted, the client reads from environment
|
||||
variables in the following order:
|
||||
1. Function argument
|
||||
2. `LANGGRAPH_API_KEY`
|
||||
3. `LANGSMITH_API_KEY`
|
||||
4. `LANGCHAIN_API_KEY`
|
||||
API key for authentication. Can be:
|
||||
- A string: use this exact API key
|
||||
- `None`: explicitly skip loading from environment variables
|
||||
- Not provided (default): auto-load from environment in this order:
|
||||
1. `LANGGRAPH_API_KEY`
|
||||
2. `LANGSMITH_API_KEY`
|
||||
3. `LANGCHAIN_API_KEY`
|
||||
headers:
|
||||
Additional HTTP headers to include in requests. Merged with authentication headers.
|
||||
timeout:
|
||||
@@ -225,6 +238,18 @@ def get_client(
|
||||
input={"messages": [{"role": "user", "content": "Foo"}]},
|
||||
)
|
||||
```
|
||||
|
||||
???+ example "Skip auto-loading API key from environment:"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
# Don't load API key from environment variables
|
||||
client = get_client(
|
||||
url="http://localhost:8123",
|
||||
api_key=None
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
transport: httpx.AsyncBaseTransport | None = None
|
||||
@@ -3471,7 +3496,7 @@ class StoreClient:
|
||||
def get_sync_client(
|
||||
*,
|
||||
url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
api_key: str | None = NOT_PROVIDED,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
timeout: TimeoutTypes | None = None,
|
||||
) -> SyncLangGraphClient:
|
||||
@@ -3479,12 +3504,13 @@ def get_sync_client(
|
||||
|
||||
Args:
|
||||
url: The URL of the LangGraph API.
|
||||
api_key: The API key. If not provided, it will be read from the environment.
|
||||
Precedence:
|
||||
1. explicit argument
|
||||
2. LANGGRAPH_API_KEY
|
||||
3. LANGSMITH_API_KEY
|
||||
4. LANGCHAIN_API_KEY
|
||||
api_key: API key for authentication. Can be:
|
||||
- A string: use this exact API key
|
||||
- `None`: explicitly skip loading from environment variables
|
||||
- Not provided (default): auto-load from environment in this order:
|
||||
1. `LANGGRAPH_API_KEY`
|
||||
2. `LANGSMITH_API_KEY`
|
||||
3. `LANGCHAIN_API_KEY`
|
||||
headers: Optional custom headers
|
||||
timeout: Optional timeout configuration for the HTTP client.
|
||||
Accepts an httpx.Timeout instance, a float (seconds), or a tuple of timeouts.
|
||||
@@ -3505,6 +3531,18 @@ def get_sync_client(
|
||||
# example usage: client.<model>.<method_name>()
|
||||
assistant = client.assistants.get(assistant_id="some_uuid")
|
||||
```
|
||||
|
||||
???+ example "Skip auto-loading API key from environment:"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_sync_client
|
||||
|
||||
# Don't load API key from environment variables
|
||||
client = get_sync_client(
|
||||
url="http://localhost:8123",
|
||||
api_key=None
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
if url is None:
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Tests for api_key parameter behavior."""
|
||||
|
||||
import pytest
|
||||
|
||||
from langgraph_sdk import get_client, get_sync_client
|
||||
|
||||
|
||||
class TestSkipAutoLoadApiKey:
|
||||
"""Test the api_key parameter's auto-loading behavior."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_client_loads_from_env_by_default(self, monkeypatch):
|
||||
"""Test that API key is loaded from environment by default."""
|
||||
monkeypatch.setenv("LANGGRAPH_API_KEY", "test-key-from-env")
|
||||
|
||||
client = get_client(url="http://localhost:8123")
|
||||
assert "x-api-key" in client.http.client.headers
|
||||
assert client.http.client.headers["x-api-key"] == "test-key-from-env"
|
||||
await client.aclose()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_client_skips_env_when_sentinel_used(self, monkeypatch):
|
||||
"""Test that API key is not loaded from environment when None is explicitly passed."""
|
||||
monkeypatch.setenv("LANGGRAPH_API_KEY", "test-key-from-env")
|
||||
|
||||
client = get_client(url="http://localhost:8123", api_key=None)
|
||||
assert "x-api-key" not in client.http.client.headers
|
||||
await client.aclose()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_client_uses_explicit_key_when_provided(self, monkeypatch):
|
||||
"""Test that explicit API key takes precedence over environment."""
|
||||
monkeypatch.setenv("LANGGRAPH_API_KEY", "test-key-from-env")
|
||||
|
||||
client = get_client(
|
||||
url="http://localhost:8123",
|
||||
api_key="explicit-key",
|
||||
)
|
||||
assert "x-api-key" in client.http.client.headers
|
||||
assert client.http.client.headers["x-api-key"] == "explicit-key"
|
||||
await client.aclose()
|
||||
|
||||
def test_get_sync_client_loads_from_env_by_default(self, monkeypatch):
|
||||
"""Test that sync client loads API key from environment by default."""
|
||||
monkeypatch.setenv("LANGGRAPH_API_KEY", "test-key-from-env")
|
||||
|
||||
client = get_sync_client(url="http://localhost:8123")
|
||||
assert "x-api-key" in client.http.client.headers
|
||||
assert client.http.client.headers["x-api-key"] == "test-key-from-env"
|
||||
client.close()
|
||||
|
||||
def test_get_sync_client_skips_env_when_sentinel_used(self, monkeypatch):
|
||||
"""Test that sync client doesn't load from environment when None is explicitly passed."""
|
||||
monkeypatch.setenv("LANGGRAPH_API_KEY", "test-key-from-env")
|
||||
|
||||
client = get_sync_client(url="http://localhost:8123", api_key=None)
|
||||
assert "x-api-key" not in client.http.client.headers
|
||||
client.close()
|
||||
|
||||
def test_get_sync_client_uses_explicit_key_when_provided(self, monkeypatch):
|
||||
"""Test that sync client uses explicit API key when provided."""
|
||||
monkeypatch.setenv("LANGGRAPH_API_KEY", "test-key-from-env")
|
||||
|
||||
client = get_sync_client(
|
||||
url="http://localhost:8123",
|
||||
api_key="explicit-key",
|
||||
)
|
||||
assert "x-api-key" in client.http.client.headers
|
||||
assert client.http.client.headers["x-api-key"] == "explicit-key"
|
||||
client.close()
|
||||
Reference in New Issue
Block a user