Skip to content

Commit b9876da

Browse files
kevin9327claude
andcommitted
Answer on an OpenAI key from the Agno Bot, not only on Anthropic or an endpoint
Agno's LiteLLM model sends `temperature=0.7` and `top_p=1.0` with every request. LiteLLM refuses both for a reasoning model, and `gpt-5.5` is one: it is the model Compose passes to the harness when the setup screen names none, which is what the OpenAI key choice does. So with an OpenAI key every run of the Agno Bot ended in `UnsupportedParamsError` before a request left the container. The model is now built with `drop_params`, scoped to this one client, so a parameter the model does not take is dropped instead of refused. That is how the LlamaIndex Bot already handles the temperature LlamaIndex sends, for the same reason. A model that takes both, such as the one behind an OpenAI-compatible endpoint, still receives them. The new test follows the LlamaIndex Bot's: a local fake provider, the environment the desktop writes for each of the three choices, and an AG-UI run through `/agui`. Before this change the OpenAI key choice fails and the other two pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 2d09a08 commit b9876da

5 files changed

Lines changed: 208 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,13 @@ jobs:
297297
. .venv-llamaindex/bin/activate
298298
python -m pip install --requirement agent-llamaindex/requirements.txt --requirement agent-llamaindex/requirements-test.txt
299299
python -m pytest agent-llamaindex/tests -q
300+
- name: Agno model-choice regression
301+
run: |
302+
set -euo pipefail
303+
python -m venv .venv-agno
304+
. .venv-agno/bin/activate
305+
python -m pip install --requirement agent-agno/requirements.txt --requirement agent-agno/requirements-test.txt
306+
python -m pytest agent-agno/tests -q
300307
- run: bun install --frozen-lockfile
301308
- run: bun test tests/compose.test.ts
302309
- run: docker compose --env-file /dev/null --profile harness config --format json >/dev/null

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
88

99
## Unreleased
1010

11+
### The Agno Bot answers on an OpenAI key
12+
13+
Picked with an OpenAI key, the Agno Bot failed every run before reaching OpenAI. Agno sends a
14+
temperature and a `top_p` with each request, and LiteLLM refuses both for `gpt-5.5`, the model
15+
Compose passes when the setup screen names none, so the run ended in `UnsupportedParamsError`. A
16+
parameter the model does not take is now dropped instead, the way the LlamaIndex Bot already does
17+
it. The Anthropic key and an OpenAI-compatible endpoint behave as before.
18+
1119
## 0.0.13
1220

1321
### Fresh desktop setup installs its runtime before sign-in

agent-agno/requirements-test.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
httpx==0.28.1
2+
pytest==9.0.2

agent-agno/src/main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ def _model_id() -> str:
2929
# In memory, because a Bot's history lives in OpenBot's database and not in the harness. Two
3030
# places remembering the same conversation is how they come to disagree.
3131
db=InMemoryDb(),
32-
model=LiteLLM(id=_model_id()),
32+
# `drop_params`, because Agno sends a temperature and a `top_p` on every request and LiteLLM
33+
# refuses both for a reasoning model, the default `gpt-5.5` among them: every run on an OpenAI
34+
# key failed before it reached OpenAI. A parameter a model does not take is dropped instead,
35+
# for this one client, as the LlamaIndex Bot does.
36+
model=LiteLLM(id=_model_id(), request_params={"drop_params": True}),
3337
# No role, goal or backstory invented on somebody's behalf. A Bot answers the question it is
3438
# asked, and anybody who wants a persona sets one in OpenBot where the rest of them live.
3539
instructions="Answer the question you are asked, briefly and correctly.",

agent-agno/tests/test_main.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import importlib
2+
import json
3+
import socket
4+
import sys
5+
import threading
6+
import time
7+
from pathlib import Path
8+
9+
import pytest
10+
import uvicorn
11+
from fastapi import FastAPI, Request
12+
from fastapi.responses import JSONResponse, StreamingResponse
13+
from fastapi.testclient import TestClient
14+
15+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
16+
17+
TOKEN = "test-token"
18+
RUN = {
19+
"threadId": "thread-1",
20+
"runId": "run-1",
21+
"state": {},
22+
"messages": [{"id": "m1", "role": "user", "content": "Say hello"}],
23+
"tools": [],
24+
"context": [],
25+
"forwardedProps": {},
26+
}
27+
28+
29+
def _sse(events):
30+
async def stream():
31+
for event in events:
32+
yield event
33+
34+
return StreamingResponse(stream(), media_type="text/event-stream")
35+
36+
37+
def _provider_app(seen):
38+
app = FastAPI()
39+
40+
@app.post("/v1/chat/completions")
41+
async def openai_chat(request: Request):
42+
body = await request.json()
43+
seen.append(("openai", body["model"]))
44+
if not body.get("stream"):
45+
return JSONResponse(
46+
{
47+
"id": "c",
48+
"object": "chat.completion",
49+
"created": 0,
50+
"model": body["model"],
51+
"choices": [
52+
{
53+
"index": 0,
54+
"finish_reason": "stop",
55+
"message": {"role": "assistant", "content": "hello"},
56+
}
57+
],
58+
}
59+
)
60+
chunk = {
61+
"id": "c",
62+
"object": "chat.completion.chunk",
63+
"created": 0,
64+
"model": body["model"],
65+
"choices": [
66+
{
67+
"index": 0,
68+
"delta": {"role": "assistant", "content": "hello"},
69+
"finish_reason": None,
70+
}
71+
],
72+
}
73+
done = {**chunk, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]}
74+
return _sse(
75+
[f"data: {json.dumps(chunk)}\n\n", f"data: {json.dumps(done)}\n\n", "data: [DONE]\n\n"]
76+
)
77+
78+
@app.post("/v1/messages")
79+
async def anthropic_messages(request: Request):
80+
body = await request.json()
81+
seen.append(("anthropic", body["model"]))
82+
message = {
83+
"id": "msg",
84+
"type": "message",
85+
"role": "assistant",
86+
"model": body["model"],
87+
"stop_sequence": None,
88+
}
89+
if not body.get("stream"):
90+
return JSONResponse(
91+
{
92+
**message,
93+
"content": [{"type": "text", "text": "hello"}],
94+
"stop_reason": "end_turn",
95+
"usage": {"input_tokens": 1, "output_tokens": 1},
96+
}
97+
)
98+
events = [
99+
("message_start", {"type": "message_start", "message": {**message, "content": [], "stop_reason": None, "usage": {"input_tokens": 1, "output_tokens": 0}}}),
100+
("content_block_start", {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
101+
("content_block_delta", {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "hello"}}),
102+
("content_block_stop", {"type": "content_block_stop", "index": 0}),
103+
("message_delta", {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": None}, "usage": {"output_tokens": 1}}),
104+
("message_stop", {"type": "message_stop"}),
105+
]
106+
return _sse([f"event: {name}\ndata: {json.dumps(data)}\n\n" for name, data in events])
107+
108+
return app
109+
110+
111+
@pytest.fixture
112+
def provider():
113+
seen = []
114+
with socket.socket() as probe:
115+
probe.bind(("127.0.0.1", 0))
116+
port = probe.getsockname()[1]
117+
server = uvicorn.Server(
118+
uvicorn.Config(_provider_app(seen), host="127.0.0.1", port=port, log_level="error")
119+
)
120+
thread = threading.Thread(target=server.run, daemon=True)
121+
thread.start()
122+
deadline = time.monotonic() + 10
123+
while not server.started and time.monotonic() < deadline:
124+
time.sleep(0.01)
125+
yield f"http://127.0.0.1:{port}", seen
126+
server.should_exit = True
127+
thread.join(timeout=10)
128+
129+
130+
CHOICES = {
131+
"an Anthropic key": (
132+
lambda base: {
133+
"BOT_PROVIDER": "anthropic",
134+
"BOT_MODEL": "claude-sonnet-4-5",
135+
"ANTHROPIC_API_KEY": "test-key",
136+
"ANTHROPIC_BASE_URL": base,
137+
"OPENAI_API_KEY": "",
138+
"OPENAI_BASE_URL": "",
139+
},
140+
("anthropic", "claude-sonnet-4-5"),
141+
),
142+
"an OpenAI-compatible endpoint": (
143+
lambda base: {
144+
"BOT_PROVIDER": "",
145+
"BOT_MODEL": "local-model",
146+
"OPENAI_API_KEY": "no-key-needed",
147+
"OPENAI_BASE_URL": f"{base}/v1",
148+
"ANTHROPIC_API_KEY": "",
149+
},
150+
("openai", "local-model"),
151+
),
152+
# The default: Compose passes `gpt-5.5` when the model screen names no model, and Agno sends a
153+
# temperature and a `top_p` on every request, which LiteLLM refuses for that reasoning model.
154+
"an OpenAI key": (
155+
lambda base: {
156+
"BOT_PROVIDER": "",
157+
"BOT_MODEL": "gpt-5.5",
158+
"OPENAI_API_KEY": "test-key",
159+
"OPENAI_BASE_URL": f"{base}/v1",
160+
"ANTHROPIC_API_KEY": "",
161+
},
162+
("openai", "gpt-5.5"),
163+
),
164+
}
165+
166+
167+
@pytest.mark.parametrize("choice", list(CHOICES))
168+
def test_a_run_reaches_the_model_the_setup_screen_chose(monkeypatch, provider, choice):
169+
base, seen = provider
170+
environment, expected = CHOICES[choice]
171+
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
172+
monkeypatch.setenv("MANAGED_AGENT_TOKEN", TOKEN)
173+
for key, value in environment(base).items():
174+
monkeypatch.setenv(key, value)
175+
176+
from src import main
177+
178+
main = importlib.reload(main)
179+
response = TestClient(main.app).post(
180+
"/agui", json=RUN, headers={"x-openbot-agent-token": TOKEN}
181+
)
182+
183+
assert response.status_code == 200
184+
assert '"RUN_FINISHED"' in response.text
185+
assert '"RUN_ERROR"' not in response.text
186+
assert seen == [expected]

0 commit comments

Comments
 (0)