diff --git a/CHANGELOG.md b/CHANGELOG.md index af2dece18..340c08908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,13 @@ Anthropic's OpenAI-compatible endpoint, which may ignore the request for bare JS reply containing multiple objects fell back to offering every tool; replies with no valid `skills` list still do. +### The AG2 Bot answers on a plain OpenAI key + +Picked with an OpenAI key, the AG2 Bot failed every run. Compose writes `OPENAI_BASE_URL` empty when +the choice is a plain OpenAI key, and the OpenAI SDK only defaults an absent URL, so it was given "" +as the address. The Bot now falls back to `https://api.openai.com/v1` for an empty value, as its +Anthropic branch already did for `ANTHROPIC_BASE_URL`. An OpenAI-compatible endpoint is unchanged. + ### Dictate messages and talk to a coworker in a live voice call Deployments can configure transcription separately from their Bots' models, with a waveform composer diff --git a/agent-ag2/src/main.py b/agent-ag2/src/main.py index 5828e4ab9..796bc31bb 100644 --- a/agent-ag2/src/main.py +++ b/agent-ag2/src/main.py @@ -23,7 +23,10 @@ def _config() -> AnthropicConfig | OpenAIConfig: # Compose exports missing overrides as ""; the SDK only defaults an absent URL. base_url = (os.environ.get("ANTHROPIC_BASE_URL") or "").strip() or "https://api.anthropic.com" return AnthropicConfig(model=model, base_url=base_url) - return OpenAIConfig(model=model) + # The same "" for OPENAI_BASE_URL, which Compose writes whenever the model screen chose a plain + # OpenAI key. The SDK only defaults an absent URL; given "", every request fails to connect. + base_url = (os.environ.get("OPENAI_BASE_URL") or "").strip() or "https://api.openai.com/v1" + return OpenAIConfig(model=model, base_url=base_url) agent = Agent( diff --git a/agent-ag2/tests/test_main.py b/agent-ag2/tests/test_main.py index 7f7341d3c..93cf26a8c 100644 --- a/agent-ag2/tests/test_main.py +++ b/agent-ag2/tests/test_main.py @@ -229,3 +229,40 @@ async def respond(transport, request): assert '"RUN_FINISHED"' in response.text assert '"RUN_ERROR"' not in response.text assert "hello" in response.text + + +def test_an_openai_key_uses_the_official_endpoint_when_compose_sets_a_blank_url(monkeypatch): + # The same "" Compose writes for ANTHROPIC_BASE_URL above, on the other provider: the model screen + # sets only the key for a plain OpenAI key, and the SDK treats "" as an address, not as absent. + seen = [] + provider_seen = [] + provider_app = _provider_app(provider_seen) + + async def respond(transport, request): + seen.append((str(request.url), request.headers.get("authorization"))) + async with httpx2.ASGITransport(app=provider_app) as local_provider: + return await local_provider.handle_async_request(request) + + # Keep the real AG2 and OpenAI clients; replace only the network transport. + monkeypatch.setattr(httpx2.AsyncHTTPTransport, "handle_async_request", respond) + monkeypatch.setenv("MANAGED_AGENT_TOKEN", TOKEN) + monkeypatch.setenv("BOT_PROVIDER", "openai") + monkeypatch.setenv("BOT_MODEL", "gpt-5.5") + monkeypatch.setenv("OPENAI_API_KEY", "test-key") + monkeypatch.setenv("OPENAI_BASE_URL", "") + monkeypatch.setenv("ANTHROPIC_API_KEY", "") + monkeypatch.setenv("ANTHROPIC_BASE_URL", "") + + from src import main + + main = importlib.reload(main) + response = TestClient(main.app).post( + "/", json=RUN, headers={"x-openbot-agent-token": TOKEN} + ) + + assert seen == [("https://api.openai.com/v1/chat/completions", "Bearer test-key")] + assert provider_seen == [("openai", "gpt-5.5")] + assert response.status_code == 200 + assert '"RUN_FINISHED"' in response.text + assert '"RUN_ERROR"' not in response.text + assert "hello" in response.text