Skip to content

Commit 1e4b610

Browse files
authored
Answer on a plain OpenAI key from the Langroid Bot (#678)
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 drops an empty OPENAI_BASE_URL before it builds its client, as it already does for an empty OPENAI_API_KEY.
1 parent f6a5dcc commit 1e4b610

3 files changed

Lines changed: 76 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,14 @@ Anthropic key), and Pydantic AI builds each provider's client from the environme
257257
given "" as the address. The Bot now removes an empty value before building the model, as
258258
`agent-langgraph-agui` already does, so the SDK uses its own endpoint. A real endpoint is unchanged.
259259

260+
### The Langroid Bot answers on a plain OpenAI key
261+
262+
Picked with an OpenAI key, the Langroid Bot failed every run with "Connection error.". Compose
263+
writes `OPENAI_BASE_URL` empty when the choice is a plain OpenAI key, and the OpenAI SDK only
264+
defaults an absent URL, so it was given "" as the address. The Bot now drops an empty
265+
`OPENAI_BASE_URL` before it builds its client, as it already does for an empty `OPENAI_API_KEY`.
266+
An OpenAI-compatible endpoint is unchanged.
267+
260268
### Find older conversations and keep chat preferences across devices
261269

262270
The sidebar loads older conversations as the person scrolls. Settings save the choice to emphasize

‎agent-langroid/src/main.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ def _model_id() -> str:
4242
if not os.environ.get("OPENAI_API_KEY"):
4343
os.environ.pop("OPENAI_API_KEY", None)
4444

45+
# And an empty `OPENAI_BASE_URL` is no address. Compose writes it empty when the model screen chose a
46+
# plain OpenAI key. Langroid builds its OpenAI client with no `base_url`, so the SDK reads this
47+
# variable itself, and it falls back to `https://api.openai.com/v1` only when the variable is absent:
48+
# given "", every request was built with no host and failed to connect. A real endpoint is left alone.
49+
if not os.environ.get("OPENAI_BASE_URL", "").strip():
50+
os.environ.pop("OPENAI_BASE_URL", None)
51+
4552
agent = ChatAgent(
4653
ChatAgentConfig(
4754
llm=OpenAIGPTConfig(chat_model=_model_id(), parallel_tool_calls=False),

‎agent-langroid/tests/test_main.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import time
77
from pathlib import Path
88

9+
import httpx
10+
import httpx2
911
import pytest
1012
import uvicorn
1113
from fastapi import FastAPI, Request
@@ -182,3 +184,62 @@ def test_a_run_reaches_the_model_the_setup_screen_chose(monkeypatch, provider, c
182184
assert '"RUN_FINISHED"' in response.text
183185
assert '"RUN_ERROR"' not in response.text
184186
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

Comments
 (0)