Skip to content

feat(agent): route Claude/GPT model families to their native gateway wire - #3538

Merged
atishpatel merged 2 commits into
mainfrom
buzz-agent/databricks-v2-model-family-routing
Jul 29, 2026
Merged

feat(agent): route Claude/GPT model families to their native gateway wire#3538
atishpatel merged 2 commits into
mainfrom
buzz-agent/databricks-v2-model-family-routing

Conversation

@atishpatel

Copy link
Copy Markdown
Contributor

Summary

Databricks v2 chooses the gateway wire format — OpenAI Responses, Anthropic Messages, or MLflow chat — purely from substrings in the endpoint name. There is no family field on the endpoint to key off, so the substring set is the routing contract. The matcher only recognised gpt-5/gpt5 and claude, which makes correct billing depend on every Claude endpoint happening to be named with the literal string "claude".

Why this matters

Getting a Claude model onto the Anthropic Messages route is exactly what lets buzz attach the cache_control breakpoint (the fix in #3463). If a Claude endpoint's catalog name omits "claude" — an alias, a bare opus-5, a goose-opus-5 — it silently falls through to the MLflow (OpenAI-wire) path, where Anthropic prompt caching is structurally impossible. The result is the same failure #3463 fixed: 0% cache reads, the full ~10x read discount lost, and no error — a naming convention quietly holding up a billing-correctness invariant.

What changed

databricks_v2_route_for_model (crates/buzz-agent/src/llm.rs) now matches broader, case-insensitive marker sets:

  • Claude → Anthropic Messages: claude, opus, sonnet, haiku, mythos, fable — the Claude family names and release code names, so a Claude endpoint reaches the cache-capable route regardless of how it's named.
  • GPT → OpenAI Responses: the gpt family (now gpt on its own, not just gpt-5) plus the GPT-5 launch code names sol, luna, terra.

OpenAI markers are evaluated first, preserving the prior gpt-5-first precedence for any name that could carry both. Names matching neither set still fall through to the MLflow chat route.

Testing

  • cargo fmt, cargo clippy -p buzz-agent --all-targets -- -D warnings — clean.
  • cargo test -p buzz-agent — all green (299 lib + integration suites, 0 failures). The databricks_v2_routes_by_model_family test was expanded to cover each new marker, the GPT-5 code names, case-insensitivity, and the unchanged MLflow fallback (including gemini).

Relationship to #3463

#3463 taught the Anthropic path to request caching; this makes sure Claude models actually land on that path. Follow-up still open: surfacing cache_creation_input_tokens end-to-end so a persistent reads == 0 && writes == 0 reveals a disabled cache regardless of which wire a model takes — happy to do that next.

🤖 Generated with Claude Code

…wire

Databricks v2 picks the gateway wire format (OpenAI Responses vs. Anthropic
Messages vs. MLflow chat) purely from substrings in the endpoint name, with no
family field to rely on. The matcher only knew `gpt-5`/`gpt5` and `claude`, so
any Claude endpoint whose catalog name omits the literal "claude" — an alias, a
bare `opus-5`, a `goose-opus-5` — silently fell through to the MLflow
(OpenAI-wire) path, where Anthropic prompt caching is structurally impossible
and the ~10x cache-read discount is lost with no error.

Broaden both marker sets:

- Claude (Anthropic Messages): `claude`, `opus`, `sonnet`, `haiku`, `mythos`,
  `fable` — the family names and release code names, so a Claude endpoint
  reaches the cache-capable route regardless of naming.
- OpenAI (Responses): the `gpt` family plus the GPT-5 launch code names `sol`,
  `luna`, `terra`.

OpenAI markers are checked first, preserving the prior `gpt-5`-first
precedence. Unrecognised names still fall through to the MLflow chat route.

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 14:06
@atishpatel
atishpatel enabled auto-merge (squash) July 29, 2026 14:13

@tlongwell-block tlongwell-block left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking correctness issue: the new classifier uses unrestricted substring matching for very short markers (sol, terra, opus, etc.). Because endpoint names are arbitrary aliases, unrelated names can now change wire protocols: for example, consolidated-llama contains sol and routes to OpenAI Responses, while octopus-model contains opus and routes to Anthropic Messages instead of the documented MLflow fallback. That can turn a cache optimization into a request-shape/parse failure.

Please make these family/code-name matches token/boundary-aware (while retaining arbitrary prefixes such as goose-opus-5) and add negative collision cases. The current positive matrix and custom-tool-model case don't exercise this regression.

@tlongwell-block tlongwell-block left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on one routing-safety issue:

contains makes the newly added short/common markers match unrelated custom endpoint names. For example, consolidated-llama matches sol, terraform-coder matches terra, and corpus-reranker matches opus; each is then sent an OpenAI Responses or Anthropic Messages envelope instead of the existing MLflow chat fallback. V2 discovery intentionally retains unrecognised custom names, so this is a reachable regression rather than a theoretical input shape.

Please match model-name segments (or another boundary-aware form) instead of arbitrary substrings, while preserving the intentional gpt5 spelling. Negative tests for collision names would lock this down. The positive goose-opus-5 fix itself is correct.

@tlongwell-block tlongwell-block left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concurring with both prior reviews — changes requested — and adding one collision case that is strictly worse than the ones already flagged:

claude-console-5 regresses. The name contains the literal claude, which the old matcher routed correctly to Anthropic Messages. But console contains sol, and OpenAI markers are checked first, so the new matcher sends it to OpenAI Responses. The PR doesn't just make unrecognised names misroute — it can flip a name the previous code classified correctly. Verified against the exact marker sets + precedence at fe548d349:

claude-console-5:   new=OpenAiResponses  old=AnthropicMessages   <-- regression
consolidated-llama: new=OpenAiResponses  old=Mlflow
terraform-coder:    new=OpenAiResponses  old=Mlflow
octopus-model:      new=AnthropicMessages old=Mlflow
databricks-solstice: new=OpenAiResponses old=Mlflow

Suggested shape: split the name into segments on -/_/. and match the short code names (opus, sonnet, haiku, sol, luna, terra, mythos, fable) as whole segments only, keeping claude/gpt as segment prefixes (so gpt5, claude3 still hit). Segment matching alone fixes claude-console-5console is not the segment sol — without needing precedence surgery. Please add negative collision tests including the claude-name-with-embedded-marker case.

The motivating fix (goose-opus-5 → Anthropic Messages) is right and worth landing once the matcher is boundary-aware.

Address review: the family classifier used unrestricted `contains`, so short
markers matched unrelated custom aliases. V2 discovery keeps arbitrary endpoint
names, so this was reachable, not theoretical: `consolidated-llama` (`sol`) and
`terraform-coder` (`terra`) misrouted to OpenAI Responses, and
`corpus-reranker`/`octopus-model` (`opus`) to Anthropic Messages — sending each
a wire shape its backend can't parse, turning a caching optimization into a hard
request/parse failure.

Split the name into alphanumeric segments (breaking on any non-alphanumeric
delimiter) and match whole segments. Code/family names (`sol`, `luna`, `terra`,
`claude`, `opus`, `sonnet`, `haiku`, `mythos`, `fable`) must equal a segment;
the `gpt` family matches a segment beginning with `gpt`, preserving the dashless
`gpt5` spelling and the `gpt` of a split `gpt-5`. Prefixed real names like
`goose-opus-5` still match.

Add negative collision cases (`consolidated-llama`, `terraform-coder`,
`corpus-reranker`, `octopus-model`) and the `gpt5` spelling to the routing test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Atish Patel <atish@squareup.com>
@atishpatel

Copy link
Copy Markdown
Contributor Author

🤖 Good catch — fixed in f992d34.

The classifier no longer uses contains. It now splits the endpoint name into alphanumeric segments (breaking on any non-alphanumeric delimiter) and matches whole segments:

  • Code/family names (sol, luna, terra, claude, opus, sonnet, haiku, mythos, fable) must equal a segment, so consolidated-llama, terraform-coder, corpus-reranker, and octopus-model all stay on the MLflow fallback.
  • The gpt family matches a segment beginning with gpt, which preserves the intentional dashless gpt5 spelling and the gpt of a split gpt-5 without matching mid-word.
  • Prefixed real names like goose-opus-5 still route correctly.

Added the four collision names plus gpt5 as explicit cases in databricks_v2_routes_by_model_family. Full cargo test -p buzz-agent green.

@tlongwell-block tlongwell-block left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-tested the revised head f992d3428129b6e9fe2766f978bd4fb9e73ba703; my prior collision blocker is resolved.

In addition to the committed route table, I temporarily added request-capture integration probes through the real buzz-agent Databricks v2 prompt path and verified:

  • goose-opus-5 posts to /ai-gateway/anthropic/v1/messages and carries the system cache_control: {type: ephemeral} marker.
  • claude-console-5 remains on Anthropic Messages.
  • consolidated-llama, terraform-coder, corpus-reranker, and octopus-model all remain on MLflow chat and do not receive a Responses envelope.

Those probes passed and were reverted; the worktree is clean. Full package fmt, clippy, and tests also pass at that exact SHA. Approving.

@tlongwell-block tlongwell-block left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One collision class remains in the fix: segments.iter().any(|seg| seg.starts_with("gpt")) still classifies every arbitrary segment beginning with those three letters as OpenAI. For example, gptosis-reranker (or gptproxy-custom) now routes to Responses instead of MLflow. This has the same request-shape failure mode as the fixed sol/opus collisions, and the comment's claim that it avoids matching “mid-word” is not a sufficient family boundary when arbitrary aliases are retained.

Please constrain the compact GPT spelling to gpt itself or gpt followed by a digit (preserving gpt, gpt5, gpt4o, and split gpt-5), and add a negative gpt... word collision. Please also lock Eva's old-route regression (claude-console-5 → Anthropic) into the matrix; the new segmentation does fix it, but that precedence regression should stay covered.

@tlongwell-block tlongwell-block left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at f992d3428129b6e9fe2766f978bd4fb9e73ba703 — the octopus problem is dead. Approving.

Verified the segment matcher against every collision case from round 1, including the regression I flagged:

name route
claude-console-5 AnthropicMessages ✅ regression fixed (console no longer trips sol)
consolidated-llama Mlflow
terraform-coder Mlflow
octopus-model Mlflow
corpus-reranker Mlflow
goose-opus-5 AnthropicMessages ✅ motivating fix intact
gpt5 OpenAiResponses ✅ dashless spelling preserved via segment-prefix

Local at exact SHA (clean worktree, HEAD confirmed in-shell): cargo fmt --all -- --check ✅, cargo clippy -p buzz-agent --all-targets -- -D warnings ✅, full cargo test -p buzz-agent ✅ (299 unit + all integration/doc suites).

Two non-blocking notes:

  1. Test gap (nit): the collision-guard block covers the four Mlflow negatives but not claude-console-5 — the one case where the old code was right and substring matching flipped it. The mechanism guarantees it, but it's the sharpest regression sentinel this table could carry. Worth adding whenever this file is next touched.
  2. Asymmetry (deliberate-looking, just naming it): gpt is matched as a segment prefix (so gpt5 works) but claude only as a whole segment, so a hypothetical dashless claude5 falls to Mlflow. That miss degrades to lost cache discount, not a broken wire — the safe direction — so fine as-is.

Unrelated CI: the Windows failure is buzz-dev-mcp shell::tests::basic_echo (exit 124 timeout) — different crate, untouched by this PR; runner flake. Locally I also saw fake_llm::cancelled_turn_with_usage_emits_notification_before_response flake once — it flakes at the same rate on base c405ad1d4, pre-existing.

@atishpatel
atishpatel merged commit 6438ded into main Jul 29, 2026
54 of 55 checks passed
@atishpatel
atishpatel deleted the buzz-agent/databricks-v2-model-family-routing branch July 29, 2026 15:02
wesbillman added a commit that referenced this pull request Jul 29, 2026
## Buzz Desktop release v0.5.1

### Changes since v0.5.0:

- perf(desktop): move observer-feed archive and decrypt commands off
main thread ([#3415](#3415))
([`294c8c821`](294c8c8))
- fix(desktop): preserve shared agent fidelity
([#3553](#3553))
([`f7a3988ba`](f7a3988))
- feat(agent): route Claude/GPT model families to their native gateway
wire ([#3538](#3538))
([`6438dedf8`](6438ded))
- Refine community invite limits
([#3529](#3529))
([`24d90d128`](24d90d1))
- feat(agent): fix Anthropic prompt caching with Databricks (+ MCP
proxy/TLS passthrough)
([#3463](#3463))
([`c405ad1d4`](c405ad1))
- feat: add explicit entry for claude-opus-5 in model config
([#2831](#2831))
([`90e058ebf`](90e058e))
- fix(desktop): clear stale thread new-message pill
([#3411](#3411))
([`55a3ed7b9`](55a3ed7))
- fix(ci): ratchet file sizes against the base tree
([#3352](#3352))
([`9227bdf58`](9227bdf))
- feat(desktop): apply WebKit rendering workarounds at startup on Linux
([#3271](#3271))
([`3ece4461d`](3ece446))
- fix(desktop): stabilize flaky DM expansion E2E ordering assertions
([#2004](#2004))
([`913d564ce`](913d564))
- fix(desktop): paint community rail full height
([#3382](#3382))
([`1d3b810ad`](1d3b810))
- feat(desktop): add custom harness inline from agent dialogs
([#3252](#3252))
([`b0503d80c`](b0503d8))
- feat(desktop): refine agent catalog sharing
([#2439](#2439))
([`a35771fc4`](a35771f))
- fix(desktop): keep drafts out of the Inbox All view
([#3217](#3217))
([`3afa129ee`](3afa129))
- fix(desktop): restore the inbox icon in the sidebar
([#3341](#3341))
([`00ede2e7a`](00ede2e))
- fix(desktop): gate codex-acp on a minimum supported version
([#3254](#3254))
([`4e3998f36`](4e3998f))
- feat(cli): add users set-status command for NIP-38 profile status
([#3253](#3253))
([`60158fce3`](60158fc))
- fix(composer): scope multiline block formatting
([#3246](#3246))
([`5457c947a`](5457c94))

**To release:** merge this PR. The tag and build will happen
automatically.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
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