Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ plan sign-in. Retrying a failed summary refreshes its sidebar preview without re
activity. The macOS app includes the microphone permission description and audio-input entitlement
needed for dictation and voice calls.

### The Pydantic AI Bot answers on a plain OpenAI key or an Anthropic key

Picked with either key, the Pydantic AI Bot failed every run. Compose writes the endpoint the choice
did not need as empty (`OPENAI_BASE_URL` for a plain OpenAI key, `ANTHROPIC_BASE_URL` for an
Anthropic key), and Pydantic AI builds each provider's client from the environment, so the SDK was
given "" as the address. The Bot now removes an empty value before building the model, as
`agent-langgraph-agui` already does, so the SDK uses its own endpoint. A real endpoint is unchanged.

### Find older conversations and keep chat preferences across devices

The sidebar loads older conversations as the person scrolls. Settings save the choice to emphasize
Expand Down
20 changes: 20 additions & 0 deletions agent-pydantic-ai/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ def _model_id() -> str:
return model if model.startswith(f"{provider}:") else f"{provider}:{model}"


def _drop_blank_base_urls() -> None:
"""Compose exports missing overrides as ""; the SDKs only default an absent URL.

The model screen writes the endpoint it did not need as empty: `OPENAI_BASE_URL` for a plain
OpenAI key, `ANTHROPIC_BASE_URL` for an Anthropic key. Pydantic AI builds each provider's client
from the environment, so the empty value reached the SDK as the address and every run failed to
connect. Taking it out lets the SDK use its own endpoint; a real one is left as it is. The same
rule as `_normalize_openai_base_url` in `agent-langgraph-agui`.
"""
for name in ("OPENAI_BASE_URL", "ANTHROPIC_BASE_URL"):
value = os.environ.get(name)
if value is None:
continue
if value.strip():
os.environ[name] = value.strip()
else:
os.environ.pop(name, None)


_drop_blank_base_urls()
agent = Agent(_model_id())
app = FastAPI()

Expand Down
60 changes: 60 additions & 0 deletions agent-pydantic-ai/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import time
from pathlib import Path

import httpx
import httpx2
import pytest
import uvicorn
from fastapi import FastAPI, Request
Expand Down Expand Up @@ -201,3 +203,61 @@ def test_a_run_reaches_the_model_the_setup_screen_chose(monkeypatch, provider, c
assert '"RUN_ERROR"' not in response.text
assert "hello" in response.text
assert seen == [expected]


# What Compose passes for the two plain keys: the key, and the endpoint it did not need as "".
BLANK_URLS = {
"an OpenAI key": (
{"BOT_PROVIDER": "openai", "BOT_MODEL": "gpt-5.5", "OPENAI_API_KEY": "test-key", "ANTHROPIC_API_KEY": ""},
("https", "api.openai.com", "/v1/responses"),
("openai", "gpt-5.5"),
),
"an Anthropic key": (
{"BOT_PROVIDER": "anthropic", "BOT_MODEL": "claude-sonnet-4-5", "ANTHROPIC_API_KEY": "test-key", "OPENAI_API_KEY": ""},
("https", "api.anthropic.com", "/v1/messages"),
("anthropic", "claude-sonnet-4-5"),
),
}


@pytest.mark.parametrize("choice", list(BLANK_URLS))
def test_a_key_reaches_the_official_endpoint_when_compose_leaves_the_url_blank(monkeypatch, choice):
environment, destination, expected = BLANK_URLS[choice]
seen = []
provider_seen = []
provider_app = _provider_app(provider_seen)

def respond_with(module):
async def respond(transport, request):
seen.append((request.url.scheme, request.url.host, request.url.path))
async with module.ASGITransport(app=provider_app) as local_provider:
return await local_provider.handle_async_request(request)

return respond

# Keep the real Pydantic AI and SDK clients; replace only the network transport, on both HTTP
# stacks, because the OpenAI SDK sends through httpx2 rather than httpx.
monkeypatch.setattr(httpx.AsyncHTTPTransport, "handle_async_request", respond_with(httpx))
monkeypatch.setattr(httpx2.AsyncHTTPTransport, "handle_async_request", respond_with(httpx2))
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
monkeypatch.setenv("MANAGED_AGENT_TOKEN", TOKEN)
monkeypatch.setenv("PYDANTIC_AI_NO_BANNER", "1")
monkeypatch.setenv("OPENAI_BASE_URL", "")
monkeypatch.setenv("ANTHROPIC_BASE_URL", "")
for key, value in environment.items():
monkeypatch.setenv(key, value)

from src import main

main = importlib.reload(main)
response = TestClient(main.app).post(
"/", json=RUN, headers={"x-openbot-agent-token": TOKEN}
)

assert seen == [destination]
assert provider_seen == [expected]
assert response.status_code == 200
assert '"RUN_FINISHED"' in response.text
assert '"RUN_ERROR"' not in response.text
assert "hello" in response.text
Loading