From 953f946a3ab1076225d85962ad3bf583601f9d9f Mon Sep 17 00:00:00 2001 From: asemabdallah Date: Mon, 28 Sep 2026 15:43:29 +0300 Subject: [PATCH] Answer on a plain OpenAI key from the Microsoft Agent Framework Bot Compose writes OPENAI_BASE_URL empty when the model screen chose a plain OpenAI key, and the OpenAI SDK only defaults an absent URL, so the Bot's client was given "" as its address and every run failed with "Connection error.". It now falls back to https://api.openai.com/v1 for an empty value, as its Anthropic branch already does for ANTHROPIC_BASE_URL. --- CHANGELOG.md | 8 ++++++ agent-microsoft/src/main.py | 5 +++- agent-microsoft/tests/test_main.py | 41 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) 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