feat(agent): make Gemini and MLflow-route models usable through databricks_v2 - #3569
Merged
Merged
Conversation
… 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>
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
enabled auto-merge (squash)
July 29, 2026 16:37
wpfleger96
approved these changes
Jul 29, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 frombenchmark/harness-accounting-and-solo, lifted onto a clean base offmainso 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 threedatabricks_v2sub-routes — theluna/solconditions run the Responses route and theopusconditions 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_modelbuckets 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
thoughtSignatureon every tool call and requires it echoed back.openai_bodyreserialized each call as{id, type, function}only, dropping the field, so the next request 400'd: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 insidefunction{}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:
ToolCallgainsprovider_extra: Map<String, Value>.parse_openaicaptures every top-level wire key except the three we model (id,type,function);openai_bodyre-emits them besidefunction. Keeping whatever we did not model, rather than namingthoughtSignature, 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 passDefault::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_idssuffixes collisions (get_weather,get_weather-2). Safe because both halves of the pairing (the assistanttool_calls[].idand the result'stool_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_openaireadcontentwithas_str(), which returns""for anything that isn't a JSON string. Gemini (and Qwen35, gpt-oss) send an array of typed blocks: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_partsnow accepts either shape — string as before, or a block array wheretextblocks concatenate into text andreasoningblocks into reasoning (Gemini nests the prose one level down undersummary). Message-levelreasoning_content/reasoningstill win when present, so DeepSeek and vLLM-style hosts are unchanged; block reasoning is the last fallback.Also: a turn-start log line (
buzz-acppool.rs)Small, independent observability change that also rides in the non-benchmark delta:
run_prompt_tasknow emits apool::prompt"turn starting" line, labelled by the sameprompt_labelhelper aslog_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_modelto route by boundary-aware model-family segments. That change and this one touch different functions inllm.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 (thoughtSignaturebesidefunction, not inside it), and id de-duplication.block-lakehouse-staging(recorded in the design doc).🤖 Generated with Claude Code