Skip to content

Commit 6ef2968

Browse files
kevin9327claude
authored andcommitted
Answer on an Anthropic key from the AG2 Bot, not only on an OpenAI one
The AG2 Bot built `OpenAIConfig(model=BOT_MODEL)` whatever the setup screen chose and never read `BOT_PROVIDER`. The catalogue offers every harness it installs with any model the screen offers, so AG2 can be picked with an Anthropic key: the desktop then writes `BOT_PROVIDER=anthropic`, `BOT_MODEL=claude-sonnet-4-5` and an empty `OPENAI_API_KEY`, and every run failed inside the OpenAI client with `OpenAIError: Missing credentials` before any request was made. AG2 reaches Anthropic through its own `AnthropicConfig`, from the `anthropic` extra, which reads `ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL` the way the Anthropic SDK does. The harness now picks that config when the provider is `anthropic` and `OpenAIConfig` otherwise, so an OpenAI key and an OpenAI-compatible endpoint behave as they did. AG2 has no LiteLLM config to route through, which is why this is a branch rather than one `provider/model` string as in the LlamaIndex Bot. The test follows the LlamaIndex Bot's. Before this change the Anthropic key choice fails and the other two pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 9bd594c commit 6ef2968

6 files changed

Lines changed: 218 additions & 3 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,13 @@ jobs:
318318
. .venv-langroid/bin/activate
319319
python -m pip install --requirement agent-langroid/requirements.txt --requirement agent-langroid/requirements-test.txt
320320
python -m pytest agent-langroid/tests -q
321+
- name: AG2 model-choice regression
322+
run: |
323+
set -euo pipefail
324+
python -m venv .venv-ag2
325+
. .venv-ag2/bin/activate
326+
python -m pip install --requirement agent-ag2/requirements.txt --requirement agent-ag2/requirements-test.txt
327+
python -m pytest agent-ag2/tests -q
321328
- run: bun install --frozen-lockfile
322329
- run: bun test tests/compose.test.ts
323330
- 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
@@ -32,6 +32,14 @@ builds an OpenAI client for such a model all the same, from the `OPENAI_API_KEY`
3232
empty when the choice was not OpenAI. The image now installs Langroid's litellm extra and an empty
3333
OpenAI key is treated as none, so the Bot starts and answers with the Anthropic model chosen.
3434

35+
### The AG2 Bot answers on an Anthropic key
36+
37+
The AG2 Bot built an OpenAI client whatever the setup screen chose, and read `BOT_MODEL` but never
38+
`BOT_PROVIDER`. Picked with an Anthropic key, every run failed asking for an OpenAI key, because
39+
Compose writes that one empty when the choice was Anthropic. It now reaches Anthropic through AG2's
40+
own Anthropic client when that is the provider chosen, and OpenAI or an OpenAI-compatible endpoint
41+
as before otherwise.
42+
3543
## 0.0.13
3644

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

‎agent-ag2/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-ag2/requirements.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ag2[ag-ui,openai]
1+
ag2[ag-ui,anthropic,openai]
22
fastapi
33
python-multipart
44
uvicorn[standard]

‎agent-ag2/src/main.py‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,30 @@
44

55
from ag2 import Agent
66
from ag2.ag_ui import AGUIStream
7-
from ag2.config import OpenAIConfig
7+
from ag2.config import AnthropicConfig, OpenAIConfig
88
from fastapi import FastAPI, Request
99
from fastapi.responses import JSONResponse
1010

1111
TOKEN_HEADER = "x-openbot-agent-token"
1212

13+
14+
def _config() -> AnthropicConfig | OpenAIConfig:
15+
"""The provider the model screen chose, which AG2 reaches through a config of its own.
16+
17+
`BOT_PROVIDER` is `anthropic` for an Anthropic key and `openai` otherwise, an OpenAI-compatible
18+
endpoint included. Each SDK reads its own key and base URL from the environment.
19+
"""
20+
provider = (os.environ.get("BOT_PROVIDER") or "openai").strip()
21+
model = (os.environ.get("BOT_MODEL") or "gpt-4o-mini").strip()
22+
if provider == "anthropic":
23+
return AnthropicConfig(model=model)
24+
return OpenAIConfig(model=model)
25+
26+
1327
agent = Agent(
1428
name="openbot",
1529
prompt="Answer the question you are asked, briefly and correctly.",
16-
config=OpenAIConfig(model=(os.environ.get("BOT_MODEL") or "gpt-4o-mini").strip()),
30+
config=_config(),
1731
)
1832
stream = AGUIStream(agent)
1933

‎agent-ag2/tests/test_main.py‎

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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+
"an OpenAI key": (
153+
lambda base: {
154+
"BOT_PROVIDER": "",
155+
"BOT_MODEL": "gpt-5.5",
156+
"OPENAI_API_KEY": "test-key",
157+
"OPENAI_BASE_URL": f"{base}/v1",
158+
"ANTHROPIC_API_KEY": "",
159+
},
160+
("openai", "gpt-5.5"),
161+
),
162+
}
163+
164+
165+
@pytest.mark.parametrize("choice", list(CHOICES))
166+
def test_a_run_reaches_the_model_the_setup_screen_chose(monkeypatch, provider, choice):
167+
base, seen = provider
168+
environment, expected = CHOICES[choice]
169+
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
170+
monkeypatch.setenv("MANAGED_AGENT_TOKEN", TOKEN)
171+
for key, value in environment(base).items():
172+
monkeypatch.setenv(key, value)
173+
174+
from src import main
175+
176+
main = importlib.reload(main)
177+
response = TestClient(main.app).post(
178+
"/", json=RUN, headers={"x-openbot-agent-token": TOKEN}
179+
)
180+
181+
assert response.status_code == 200
182+
assert '"RUN_FINISHED"' in response.text
183+
assert '"RUN_ERROR"' not in response.text
184+
assert seen == [expected]

0 commit comments

Comments
 (0)