|
6 | 6 | import time |
7 | 7 | from pathlib import Path |
8 | 8 |
|
| 9 | +import httpx |
| 10 | +import httpx2 |
9 | 11 | import pytest |
10 | 12 | import uvicorn |
11 | 13 | from fastapi import FastAPI, Request |
@@ -182,3 +184,62 @@ def test_a_run_reaches_the_model_the_setup_screen_chose(monkeypatch, provider, c |
182 | 184 | assert '"RUN_FINISHED"' in response.text |
183 | 185 | assert '"RUN_ERROR"' not in response.text |
184 | 186 | assert seen == [expected] |
| 187 | + |
| 188 | + |
| 189 | +def test_an_openai_key_uses_the_official_endpoint_when_compose_sets_a_blank_url(monkeypatch, provider): |
| 190 | + # The "an OpenAI key" choice above points OPENAI_BASE_URL at the fake provider, which is how the |
| 191 | + # empty value Compose actually writes for a plain OpenAI key went untested: the OpenAI SDK only |
| 192 | + # defaults an absent URL, so "" left every request without a host. |
| 193 | + base, provider_seen = provider |
| 194 | + port = int(base.rsplit(":", 1)[1]) |
| 195 | + destinations = [] |
| 196 | + |
| 197 | + def reroute(transport_class, method_name): |
| 198 | + original = getattr(transport_class, method_name) |
| 199 | + |
| 200 | + if method_name.startswith("handle_async"): |
| 201 | + |
| 202 | + async def forward(transport, request): |
| 203 | + destinations.append((request.url.scheme, request.url.host, request.url.path)) |
| 204 | + request.url = request.url.copy_with(scheme="http", host="127.0.0.1", port=port) |
| 205 | + return await original(transport, request) |
| 206 | + |
| 207 | + else: |
| 208 | + |
| 209 | + def forward(transport, request): |
| 210 | + destinations.append((request.url.scheme, request.url.host, request.url.path)) |
| 211 | + request.url = request.url.copy_with(scheme="http", host="127.0.0.1", port=port) |
| 212 | + return original(transport, request) |
| 213 | + |
| 214 | + monkeypatch.setattr(transport_class, method_name, forward) |
| 215 | + |
| 216 | + # Keep the real Langroid and OpenAI clients and send what they build to the fake provider, noting |
| 217 | + # where it was addressed. Both HTTP stacks and both client kinds, because the OpenAI SDK sends |
| 218 | + # through httpx2 rather than httpx and Langroid may use either the sync or the async client. |
| 219 | + for stack in (httpx, httpx2): |
| 220 | + reroute(stack.HTTPTransport, "handle_request") |
| 221 | + reroute(stack.AsyncHTTPTransport, "handle_async_request") |
| 222 | + monkeypatch.delenv("OPENAI_API_BASE", raising=False) |
| 223 | + monkeypatch.setenv("MANAGED_AGENT_TOKEN", TOKEN) |
| 224 | + monkeypatch.setenv("BOT_PROVIDER", "") |
| 225 | + monkeypatch.setenv("BOT_MODEL", "gpt-5.5") |
| 226 | + # A key no other test uses: Langroid caches the OpenAI client it builds by API key, so with the |
| 227 | + # "test-key" above this test would be handed that test's client, already pointed at the fake. |
| 228 | + monkeypatch.setenv("OPENAI_API_KEY", "blank-url-test-key") |
| 229 | + monkeypatch.setenv("OPENAI_BASE_URL", "") |
| 230 | + monkeypatch.setenv("ANTHROPIC_API_KEY", "") |
| 231 | + monkeypatch.setenv("ANTHROPIC_BASE_URL", "") |
| 232 | + |
| 233 | + from src import main |
| 234 | + |
| 235 | + main = importlib.reload(main) |
| 236 | + response = TestClient(main.app).post( |
| 237 | + "/", json=RUN, headers={"x-openbot-agent-token": TOKEN} |
| 238 | + ) |
| 239 | + |
| 240 | + assert destinations == [("https", "api.openai.com", "/v1/chat/completions")] |
| 241 | + assert provider_seen == [("openai", "gpt-5.5")] |
| 242 | + assert response.status_code == 200 |
| 243 | + assert '"RUN_FINISHED"' in response.text |
| 244 | + assert '"RUN_ERROR"' not in response.text |
| 245 | + assert "hello" in response.text |
0 commit comments