Skip to content

feat(agent): make Gemini and MLflow-route models usable through databricks_v2 - #3569

Merged
atishpatel merged 2 commits into
mainfrom
buzz-agent/gemini-databricks-v2-provider
Jul 29, 2026
Merged

feat(agent): make Gemini and MLflow-route models usable through databricks_v2#3569
atishpatel merged 2 commits into
mainfrom
buzz-agent/gemini-databricks-v2-provider

Conversation

@atishpatel

@atishpatel atishpatel commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes Gemini — and every other non-Claude, non-GPT-5 model on the Databricks MLflow route (databricks_v2) — usable in an agent loop. These are the non-benchmarks/ changes from benchmark/harness-accounting-and-solo, lifted onto a clean base off main so they can land independently while the harness work continues.

Two defects made these models unusable, one fatal and one silent. Both live only in openai_body / parse_openai, which is the least-exercised of the three databricks_v2 sub-routes — the luna/sol conditions run the Responses route and the opus conditions run the Anthropic route, so this change is inert for every model already in use and only lights up the MLflow path.

Why a third route at all

databricks_v2_route_for_model buckets by model family: claude* → Anthropic Messages, gpt-5/code-names → OpenAI Responses, everything else → MLflow chat-completions. Gemini, Qwen, gpt-oss, and friends all fall through to that third pair — and both bugs below live only there.

D1 — dropped thought signatures (fatal)

Gemini returns a thoughtSignature on every tool call and requires it echoed back. openai_body reserialized each call as {id, type, function} only, dropping the field, so the next request 400'd:

HTTP 400  Function call is missing a thought_signature in functionCall parts.

For a coding agent this fires on the first tool call, so the model never completes a single turn.

Position is load-bearing. A four-shape replay probe against the live gateway established that the signature must sit as a sibling of function — nesting it inside function{} fails with the same 400 as omitting it. A fix that "preserves the field" without preserving its position passes a unit test and still 400s.

The fix: ToolCall gains provider_extra: Map<String, Value>. parse_openai captures every top-level wire key except the three we model (id, type, function); openai_body re-emits them beside function. Keeping whatever we did not model, rather than naming thoughtSignature, means the next provider with an opaque per-call token needs no change here. The Responses and Anthropic replay shapes are fully modelled, so they pass Default::default() and stay byte-identical to before.

D1b — duplicate tool-call ids (same root cause)

Gemini returns the function name as the id, so two parallel calls to one function arrive sharing an id — and that id is what pairs a role:"tool" result back to its call, making two results indistinguishable. dedupe_provider_ids suffixes collisions (get_weather, get_weather-2). Safe because both halves of the pairing (the assistant tool_calls[].id and the result's tool_call_id) are re-emitted from this same value; the provider never sees its original id again.

D2 — block-array content discarded (silent, worse than a crash)

parse_openai read content with as_str(), which returns "" for anything that isn't a JSON string. Gemini (and Qwen35, gpt-oss) send an array of typed blocks:

"content": [
  {"type": "reasoning", "summary": [{"type": "summary_text", "text": ""}]},
  {"type": "text", "text": "391"}
]

So the model answered and the answer was thrown away — no error, no warning, just a turn that looked like the model had said nothing. On a benchmark this reads as "Gemini is bad at the task" rather than "buzz dropped the reply."

openai_content_parts now accepts either shape — string as before, or a block array where text blocks concatenate into text and reasoning blocks into reasoning (Gemini nests the prose one level down under summary). Message-level reasoning_content / reasoning still win when present, so DeepSeek and vLLM-style hosts are unchanged; block reasoning is the last fallback.

Also: a turn-start log line (buzz-acp pool.rs)

Small, independent observability change that also rides in the non-benchmark delta: run_prompt_task now emits a pool::prompt "turn starting" line, labelled by the same prompt_label helper as log_stop_reason, so a log reads as start/stop pairs. An unpaired start is the only durable evidence that a turn was entered and never returned — without it, a stalled agent and an agent nobody woke leave identical (zero-completion) logs.

Interaction with #3538

#3538 (already merged) rewrote databricks_v2_route_for_model to route by boundary-aware model-family segments. That change and this one touch different functions in llm.rs — routing vs. body/parse — and compose cleanly; the family routing decides which pair runs, and this fixes the MLflow pair it can now select.

Testing

  • cargo fmt --all -- --check, cargo clippy -p buzz-agent -p buzz-acp --all-targets -- -D warnings — clean.
  • cargo test -p buzz-agent -p buzz-acp — all green (304 + 632 lib tests plus integration suites, 0 failures). Five new tests cover: block-array text extraction, plain-string regression, passthrough capture (and non-duplication of the modelled keys), replay position (thoughtSignature beside function, not inside it), and id de-duplication.
  • Wire evidence: the four-shape replay table and the reasoning-effort probe were run against block-lakehouse-staging (recorded in the design doc).

🤖 Generated with Claude Code

… databricks_v2

Non-benchmark changes lifted off benchmark/harness-accounting-and-solo so they
can land independently. Two defects made every non-Claude, non-GPT-5 model on
the Databricks MLflow route (databricks_v2) unusable in an agent loop; both live
only in openai_body/parse_openai, which the luna (Responses) and opus (Anthropic)
conditions never reach, so this is inert for models already in use.

D1 — dropped thought signatures (fatal). Gemini returns a `thoughtSignature` on
every tool call and requires it echoed back verbatim, as a sibling of `function`
(nesting it inside `function{}` 400s with the same error as omitting it). buzz
reserialized calls as `{id, type, function}` only, dropping the field, so the
first tool call of every run failed with "Function call is missing a
thought_signature". ToolCall now carries `provider_extra: Map<String, Value>`;
parse_openai captures every unmodelled top-level key and openai_body re-emits
them beside `function`, preserving position. Keeping whatever we did not model
(rather than naming `thoughtSignature`) means the next provider with an opaque
per-call token needs no change. The Responses and Anthropic replay shapes are
fully modelled, so they pass an empty map and stay byte-identical.

D1b — duplicate tool-call ids. Gemini returns the function name as the call id,
so parallel calls to one function collide, and that id is what pairs a tool
result back to its call. dedupe_provider_ids suffixes collisions
(`get_weather`, `get_weather-2`); safe because both halves of the pairing are
re-emitted from this same value.

D2 — block-array content discarded (silent). parse_openai read `content` with
`as_str()`, which yields "" for the typed-block arrays Gemini/Qwen35/gpt-oss
send, so the model's answer vanished with no error. openai_content_parts now
accepts a string or a block array, splitting text and reasoning (Gemini nests
prose under `summary`); message-level reasoning_content/reasoning still win, so
DeepSeek and vLLM hosts are unchanged.

Also adds a turn-start log line to buzz-acp pool.rs, labelled to pair with
log_stop_reason, so a stalled turn (start with no stop) is distinguishable from
one that was never entered.

Design write-up: docs/08-gemini-provider-fixes.md on the benchmark branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Atish Patel <atish@squareup.com>
@atishpatel
atishpatel requested a review from a team as a code owner July 29, 2026 15:39
Addresses codex review (P2). `HistoryItem::size_with` summed only
provider_id + name + arguments, so a Gemini `thoughtSignature` — which is
re-serialized into every replayed tool call — was invisible to
`truncate_history` (request-body sizing) and the context-pressure/handoff
gate. A large or repeated signature could push the real request past the
configured byte/context budget before truncation kicked in. Count the
serialized `provider_extra` in both measures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Atish Patel <atish@squareup.com>
@atishpatel
atishpatel enabled auto-merge (squash) July 29, 2026 16:37
@atishpatel
atishpatel merged commit 4a1ebf2 into main Jul 29, 2026
32 checks passed
@atishpatel
atishpatel deleted the buzz-agent/gemini-databricks-v2-provider branch July 29, 2026 16:49
joahg added a commit to joahg/buzz that referenced this pull request Jul 29, 2026
…-style

* origin/main:
  revert(acp): remove dead GOOSE_ACP_SCHEDULER_DISABLED env injection (block#3576)
  feat(agent): make Gemini and MLflow-route models usable through databricks_v2 (block#3569)

Signed-off-by: Joah Gerstenberg <joah@squareup.com>
wpfleger96 added a commit that referenced this pull request Jul 29, 2026
…r findings

Restructure build_thinking_field to accept persona_effort and global_effort
as explicit tier parameters, resolving via the existing resolve_with_override
machinery at the correct precedence:

  record env (BuzzExplicit) > ACP configOption > persona > global > config file

The previous inject+retag approach planted persona/global values in the
record-env tier, which outranks ACP; a live ACP effort (e.g. Goose effort=low
set post-spawn) would be shadowed by the spawn baseline. The tiered reader
approach fixes this: ACP wins as primary with the inherited value as the
overridden secondary, exactly as resolve_with_override produces.

Remove all effort injection from resolve_config_surface and the five duplicate
inline tests, reducing agent_config.rs from 1365 to 1109 lines (gate: ≤1112).
Move the five acceptance-criteria tests to reader_tests.rs and add the
conflicting-ACP test Thufir required. Rebase onto origin/main to pick up
AgentDefinition.shared/catalog_source fields added in #3569.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
loganj pushed a commit that referenced this pull request Jul 29, 2026
…dance

* origin/main:
  fix(mobile): keep TLS on relays joined by invite (#3139)
  Improve emoji autocomplete matching (#3571)
  Fix shared agent avatar import profiles (#3578)
  Fix inline raster avatars in agent catalog (#3581)
  revert(acp): remove dead GOOSE_ACP_SCHEDULER_DISABLED env injection (#3576)
  feat(agent): make Gemini and MLflow-route models usable through databricks_v2 (#3569)
  fix(cli): mask credential env values in --help output (#3570)
  Serialize Tauri pre-push checks (#3567)
  chore(release): release Buzz Desktop version 0.5.1 (#3566)
  Run Tauri clippy in pre-push (#3555)
  perf(desktop): move observer-feed archive and decrypt commands off main thread (#3415)
  fix(desktop): preserve shared agent fidelity (#3553)
  Polish mobile navigation and menus (#3486)
  feat(agent): route Claude/GPT model families to their native gateway wire (#3538)

Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants