[eric] browser: stream the loop LLM call so tool-use works on ALL providers (gemini/openrouter/antigravity were silently broken via 9router SSE)

This commit is contained in:
ciregenz
2026-07-03 04:21:15 -07:00
parent 240d1a4313
commit 20834d7c11
2 changed files with 28 additions and 9 deletions
+12 -8
View File
@@ -1178,14 +1178,18 @@ async def run_browser_agent(
browser_history.prune_stale_page_state(messages)
browser_history.place_cache_marker(messages)
p_llm_t0 = time.monotonic()
response = await p_cancellable(client.messages.create(
model=api_model,
max_tokens=4096,
# Cache the ~4k-token fixed prefix (system + tool schema) so it's reprocessed once, not on every turn: big TTFT + cost win on the first run, which is dominated by turns x per-turn prefill. The trailing cache_control marker is what Anthropic keys on; on non-Anthropic routes (9router) the marker is harmlessly ignored.
system=p_cached_system,
tools=p_cached_tools,
messages=messages,
))
# STREAM, don't .create(): 9Router returns non-Anthropic lanes (Gemini/OpenRouter/Antigravity) as a REAL multi-event SSE stream that the non-streaming client parses to empty content, silently breaking tool-use on every non-Claude provider (measured: only cc/ emitted tool_use before this). The streaming parser reconstructs tool_use identically for ALL providers, so this is the model-independence fix, not a UX tweak. Cache marker still rides p_cached_system (Anthropic keys on it; other routes ignore it harmlessly).
async def p_stream_turn():
async with client.messages.stream(
model=api_model,
max_tokens=4096,
system=p_cached_system,
tools=p_cached_tools,
messages=messages,
) as p_s:
return await p_s.get_final_message()
response = await p_cancellable(p_stream_turn())
if response is None:
break
p_llm_ms = int((time.monotonic() - p_llm_t0) * 1000)
+16 -1
View File
@@ -30,17 +30,32 @@ class Resp:
self.usage = type("U", (), {"input_tokens": 1, "output_tokens": 1})()
class FakeStream:
# mirrors anthropic's messages.stream(): async CM whose get_final_message() returns the turn
def __init__(self, resp): self.resp = resp
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
async def get_final_message(self): return self.resp
class FakeLLM:
def __init__(self, scripted):
self.scripted = scripted; self.turn = 0; self.calls = []
self.messages = self
async def create(self, **kw):
def p_next(self, kw):
self.calls.append(kw)
i = min(self.turn, len(self.scripted) - 1)
self.turn += 1
return self.scripted[i]
async def create(self, **kw):
return self.p_next(kw)
def stream(self, **kw):
# the loop now streams; return an async-CM yielding the scripted turn
return FakeStream(self.p_next(kw))
class FakeAux:
def __init__(self):