diff --git a/CHANGELOG.md b/CHANGELOG.md index af2dece18..28daf3c7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,14 @@ now read, each on its own labelled line. The URI leads and the name and descript a long name cannot push the pointer past the result cap; a server's `title` is shown over its `name` when it gives one. A part that already carried text is unchanged. +### The Microsoft Agent Framework Bot answers on a plain OpenAI key + +Picked with an OpenAI key, the Microsoft Agent Framework Bot failed every run with "Connection +error.". 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. + ### Skill selection keeps capabilities named across multiple JSON replies When a model wraps its skill choice in prose or sends a revised JSON object, OpenBot reads each diff --git a/agent-microsoft/src/main.py b/agent-microsoft/src/main.py index 82af5a934..53e7190e8 100644 --- a/agent-microsoft/src/main.py +++ b/agent-microsoft/src/main.py @@ -23,7 +23,10 @@ def _client() -> AnthropicClient | OpenAIChatClient: # 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 AnthropicClient(model=model, base_url=base_url) - return OpenAIChatClient(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 OpenAIChatClient(model, base_url=base_url) agent = _client().as_agent( diff --git a/agent-microsoft/tests/test_main.py b/agent-microsoft/tests/test_main.py index e439493fa..bc0a03372 100644 --- a/agent-microsoft/tests/test_main.py +++ b/agent-microsoft/tests/test_main.py @@ -7,6 +7,7 @@ from pathlib import Path import httpx +import httpx2 import pytest import uvicorn from fastapi import FastAPI, Request @@ -217,3 +218,43 @@ 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( + (request.url.scheme, request.url.host, request.url.path, 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 framework and OpenAI clients; replace only the network transport. The OpenAI SDK + # sends through httpx2, not httpx, so patching httpx's transport here would let the request out. + 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/responses", "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