Skip to content

fix(executors): route Claude-via-Vertex through native rawPredict with real streaming - #8909

Closed
wgordon17 wants to merge 14 commits into
cheaper-inference:release/v3.8.50from
wgordon17:fix/vertex-claude-rawpredict-streaming
Closed

wgordon17 wants to merge 14 commits into
cheaper-inference:release/v3.8.50from
wgordon17:fix/vertex-claude-rawpredict-streaming

Conversation

@wgordon17

@wgordon17 wgordon17 commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Route Claude models on Vertex AI through the native Anthropic rawPredict endpoint instead of the generic OpenAI-compatible partner endpoint (which 404s/errors for Claude on at least some projects), stripping the body-level model field rawPredict rejects and injecting the required anthropic_version.
  • Since rawPredict only ever returns a complete JSON body (never real SSE framing), streaming requests now get a genuine Anthropic-format SSE stream synthesized from that JSON so the existing claude-to-openai response translator can parse it correctly.
  • Fix two response-format resolution bugs that silently dropped a custom model's DB-stored targetFormat override whenever the model id also existed in the static provider registry (as claude-sonnet-4-6/claude-opus-4-7 do under vertex).

⚠️ base-red inherited: #9985 — the ESLint parsing error in @omniroute/opencode-plugin/src/index.ts, the no-new-func finding in tests/unit/cli-env-inline-comment-10100.test.ts, the 5 window.location.href warnings, and 4 fast-path unit test failures (combo-quota-exhaustion-only-fallback, free-tier-providers-wave1-a, combo-499-abort, combo-stream-readiness-fallback) are confirmed identical to release/v3.8.50 tip (zero diff) and independently red on the release branch's own CI — not introduced by this PR.

wgordon17 added a commit to wgordon17/OmniRoute that referenced this pull request Jul 29, 2026
@wgordon17
wgordon17 marked this pull request as ready for review July 30, 2026 18:14
@wgordon17
wgordon17 requested a review from diegosouzapw as a code owner July 30, 2026 18:14
…h real streaming

Claude models on Vertex AI were being sent through the generic OpenAI-
compatible partner endpoint, which 404s/errors for Claude on at least
some projects. Route them through Vertex's native Anthropic Messages
API (publishers/anthropic/.../rawPredict) instead, stripping the
body-level model field rawPredict rejects and injecting the required
anthropic_version field.

rawPredict only ever returns a complete JSON body, never real SSE
framing, so streaming requests now get a genuine Anthropic-format SSE
stream synthesized from that JSON (message_start/content_block_*/
message_delta/message_stop), which the existing claude-to-openai
response translator already knows how to parse.

Also fixes two response-format resolution bugs that silently dropped
a custom model's DB-stored targetFormat override whenever the model
id also existed in the static provider registry (as claude-sonnet-4-6
and claude-opus-4-7 do under vertex): resolveModelOrError had its own
ad-hoc resolution that never consulted the override, and even once
fixed, executeChatWithBreaker discarded the correctly-resolved format
before handleChatCore's own resolution ran a second time.
check:file-size counts via content.split("\n").length (not wc -l), so
the single modelTargetFormat: targetFormat field this PR adds to the
existing executeChatWithBreaker() options object pushes chat.ts from
1845 to 1846 by that measure. Irreducible call-site wiring at the
existing dispatch chokepoint; bump the frozen baseline with
justification, matching this file's established rebaseline precedent.
Same two categories of inherited base-branch breakage as cheaper-inference#9006 (this
PR shares the identical chatHelpers.ts diff), confirmed unrelated to
this PR's own diff:

- check:file-size: base.ts and chat.ts drifted further past their
  frozen caps via already-merged commits (7163081 and others) that
  didn't rebaseline after growing them.
- chat-helpers.test.ts: two gpt-5.5 routing assertions predate cheaper-inference#9275
  (fix(routing): bare model ids route to codex first), which
  deliberately made gpt-5.5 route to codex unconditionally. Updated
  both assertions and their names to match the new, intentional
  default.
@wgordon17
wgordon17 force-pushed the fix/vertex-claude-rawpredict-streaming branch from 2c01461 to 20828cc Compare August 4, 2026 13:43
…ex-claude-rawpredict-streaming

# Conflicts:
#	config/quality/file-size-baseline.json
…ex-claude-rawpredict-streaming

# Conflicts:
#	src/sse/handlers/chatHelpers.ts
#	tests/unit/executor-vertex-extended.test.ts
…ex-claude-rawpredict-streaming

# Conflicts:
#	config/quality/file-size-baseline.json
#	tests/unit/combo-routing-engine.test.ts
#	tests/unit/t23-t24-fallback-resilience.test.ts
…ions

Kept the assertion value fix (ALL_ACCOUNTS_INACTIVE -> ALL_TARGETS_SKIPPED); the comments were unnecessary. Reverts the file-size baseline bump these comments caused (combo-routing-engine.test.ts back to its original 3457).
diegosouzapw pushed a commit to Egorich-print/OmniRoute-Rust that referenced this pull request Aug 11, 2026
diegosouzapw pushed a commit to wgordon17/OmniRoute that referenced this pull request Aug 11, 2026
diegosouzapw pushed a commit that referenced this pull request Aug 11, 2026
…n every provider (#9006)

* fix(executors): route Claude-via-Vertex through native rawPredict with real streaming

Claude models on Vertex AI were being sent through the generic OpenAI-
compatible partner endpoint, which 404s/errors for Claude on at least
some projects. Route them through Vertex's native Anthropic Messages
API (publishers/anthropic/.../rawPredict) instead, stripping the
body-level model field rawPredict rejects and injecting the required
anthropic_version field.

rawPredict only ever returns a complete JSON body, never real SSE
framing, so streaming requests now get a genuine Anthropic-format SSE
stream synthesized from that JSON (message_start/content_block_*/
message_delta/message_stop), which the existing claude-to-openai
response translator already knows how to parse.

Also fixes two response-format resolution bugs that silently dropped
a custom model's DB-stored targetFormat override whenever the model
id also existed in the static provider registry (as claude-sonnet-4-6
and claude-opus-4-7 do under vertex): resolveModelOrError had its own
ad-hoc resolution that never consulted the override, and even once
fixed, executeChatWithBreaker discarded the correctly-resolved format
before handleChatCore's own resolution ran a second time.

* docs: add changelog fragment for #8909

* refactor(sse): extract shared Claude effort-model predicate

* fix(sse): strip Claude effort-suffix ids for any provider serving a real Claude model

* fix(sse): keep no-think and CC-discovery catalog variant roots unprefixed

* fix(dashboard): re-qualify no-think playground model ids correctly

* fix(sse): scope Vertex 404s to a per-model lockout via passthroughModels

* docs: add changelog fragment for the Claude catalog/dispatch fix

* fix(sse): align regex naming and changelog formatting

* fix(sse): clarify effort-variant strip comment and add cross-module drift guard

* fix(sse): disambiguate Vertex connection-wide vs per-model 403s

* docs: document Vertex 403 disambiguation in changelog fragment

* fix(sse): correlate reason and resource within the same ErrorInfo detail

* fix(sse): extract Vertex error classifier and rebaseline frozen file sizes

* test: register vertex-passthrough-model-lockout in stryker tap.testFiles

* fix(sse): reconciles rebase-onto-tip drift for 9006

Two categories of inherited base-branch breakage surfaced when
rebasing onto release/v3.8.50's latest tip, both confirmed unrelated
to this PR's own diff:

- check:file-size: base.ts and chat.ts drifted further past their
  frozen caps via already-merged commits (7163081 and others) that
  didn't rebaseline after growing them. Documented and bumped in
  file-size-baseline.json.
- chat-helpers.test.ts: two gpt-5.5 routing assertions predate #9275
  (fix(routing): bare model ids route to codex first), which
  deliberately made gpt-5.5 route to codex unconditionally, regardless
  of which other providers are active. Confirmed via #9275's own
  commit message and code comments this is intentional, not a
  regression; verified reproducible on the raw base tip alone, with
  no changes from this PR involved. Updated both assertions and their
  names to match the new, intentional default.

* ci: re-trigger checks after GitHub Actions incident (2026-08-07, resolved)

* ci: re-trigger checks (previous push event was dropped)

* fix(quality): rebaseline combo-routing-engine.test.ts own-comment growth

The ALL_ACCOUNTS_INACTIVE->ALL_TARGETS_SKIPPED fix (a32aed738) added explanatory comments (+7 lines), pushing the file past its frozen 3457 cap. CI's PR-mode check:file-size caught it; local check-file-size.mjs was not re-run after that specific commit.
@diegosouzapw diegosouzapw added the hold-vps PR verde, merge aguardando validação live (release-drain) label Aug 13, 2026
wgordon17 and others added 3 commits August 13, 2026 10:12
…ex-claude-rawpredict-streaming

# Conflicts:
#	config/quality/file-size-baseline.json
- open-sse/utils/stream.ts: drop duplicate sseCommentsEnabled import
  (both merge parents added it independently, no textual conflict) —
  fixes open-sse-typecheck TS2300 regression
- skills/omni-settings/SKILL.md: regenerate via check:agent-skills-sync
  --apply to match its catalog entry (merge integrity gate)
- config/quality/eslint-suppressions.json: prune 5 stale entries no
  longer occurring (npx eslint --prune-suppressions)
…-claude-rawpredict-streaming

# Conflicts:
#	config/quality/file-size-baseline.json
@diegosouzapw diegosouzapw added the needs-vps PR requires Hard Rule #18 VPS smoke test on 192.168.0.15 before merge label Aug 17, 2026
@diegosouzapw

Copy link
Copy Markdown
Collaborator

The PR remains in the release drain with needs-vps/hold-vps: the Vertex rawPredict streaming behavior requires the documented live validation before merge. It remains open and will not be bypassed with an admin merge.

1 similar comment
@diegosouzapw

Copy link
Copy Markdown
Collaborator

The PR remains in the release drain with needs-vps/hold-vps: the Vertex rawPredict streaming behavior requires the documented live validation before merge. It remains open and will not be bypassed with an admin merge.

@wgordon17

wgordon17 commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Live validation complete (Hard Rule #18 — the network-dependent rawPredict/streaming behavior TDD can't fully prove). Redacted below: GCP project id → <project>, service-account email → <service-account>, internal connection/account ids → <conn-id>, egress IP → <ip>. Nothing else altered.

Environment: isolated OmniRoute instance built from this branch (fresh throwaway DATA_DIR, no production data touched), wired to a real GCP service account with Anthropic Claude access enabled on Vertex AI. All calls below are real network round-trips to the live rawPredict endpoint — no mocks, no stubbed fetch.


1. Raw upstream proof — rawPredict itself, bypassing OmniRoute entirely

Direct fetch to https://aiplatform.googleapis.com/v1/projects/<project>/locations/{region}/publishers/anthropic/models/claude-haiku-4-5@20251001:rawPredict with a minted OAuth token, same body shape the executor sends (anthropic_version, max_tokens, messages, no model field):

=== us-east5 → 200 ===
{"model":"claude-haiku-4-5-20251001","id":"msg_vrtx_011Ce8XWP3ndv9gYvCxtjnHT","type":"message","role":"assistant","content":[{"type":"text","text":"Hey! 👋 How's it going"}],"stop_reason":"max_tokens","stop_sequence":null,"usage":{"input_tokens":8,"output_tokens":10}}

=== europe-west1 → 200 ===
{"model":"claude-haiku-4-5-20251001","id":"msg_vrtx_011Ce8XWTKmFMHbrc6Y9MWgR","type":"message","role":"assistant","content":[{"type":"text","text":"# Hello! 👋\n\nHow can I"}],"stop_reason":"max_tokens","stop_sequence":null,"usage":{"input_tokens":8,"output_tokens":10}}

=== global → 200 ===
{"model":"claude-haiku-4-5-20251001","id":"msg_vrtx_011Ce8XWfMokRu2jzX5XFuJU","type":"message","role":"assistant","content":[{"type":"text","text":"Hello! 👋 How can I help"}],"stop_reason":"max_tokens","stop_sequence":null,"usage":{"input_tokens":8,"output_tokens":10}}

Confirms rawPredict genuinely works and returns real Anthropic-message-shaped JSON — when the request is servable in that region. us-central1 (this executor's hardcoded region default when no providerSpecificData.region is set) is not a valid region for this model on this project; global/us-east5/europe-west1 are. Not this PR's bug, but worth documenting here since it'll otherwise look like a "model not found" code issue to the next person debugging this path.

2. Full pipeline, non-streaming — /v1/chat/completions → routing → VertexExecutor → live upstream

Request:

{"model":"vertex/claude-haiku-4-5@20251001","stream":false,"max_tokens":30,"temperature":0,"messages":[{"role":"user","content":"Reply with exactly the token: RAWPREDICT_OK"}]}

Relevant log lines (format resolution + request):

[sse] Provider: vertex, Model: claude-haiku-4-5@20251001                 tag=ROUTING
[sse] Using vertex account: <conn-id>                                    tag=AUTH
[sse] openai → claude | stream=false                                     tag=FORMAT
[sse] VERTEX | claude-haiku-4-5@20251001 | 1 msgs                        tag=REQUEST

Response — 200, full body:

{
  "id": "chatcmpl-msg_vrtx_011Ce8Y2SBe1Yr3ub1dv2fSA",
  "object": "chat.completion",
  "created": 1786979518,
  "model": "claude-haiku-4-5@20251001",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": { "role": "assistant", "content": "RAWPREDICT_OK" }
    }
  ],
  "usage": { "prompt_tokens": 19, "completion_tokens": 9, "total_tokens": 28 }
}

The completion is the exact literal token requested — not a cached/mocked value, a real generation.

3. Full pipeline, streaming — synthesized SSE consumed by the downstream translator

Request:

{"model":"vertex/claude-haiku-4-5@20251001","stream":true,"max_tokens":30,"temperature":0,"messages":[{"role":"user","content":"Reply with exactly the token: STREAM_OK"}]}

Relevant log lines:

[sse] openai → claude | stream=true                                      tag=FORMAT
[sse] VERTEX | claude-haiku-4-5@20251001 | 1 msgs                        tag=REQUEST
[ProxyEgress] vertex/<conn-id> in=? out=<ip> proxy=direct status=success
[sse] Stream readiness confirmed in 8ms (vertex/claude-haiku-4-5@20251001)  tag=STREAM
[sse] Translation mode: claude → openai                                  tag=STREAM
[USAGE] VERTEX | in=17 | out=7 | account=<conn-id>
[STREAM] VERTEX | claude-haiku-4-5@20251001 | 1273ms | complete

Response — 200, content-type: text/event-stream, full raw body as received by the client:

data: {"id":"chatcmpl-msg_vrtx_011Ce8Y2WzN2ApV6JzhmS5iM","object":"chat.completion.chunk","created":1786979519,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-msg_vrtx_011Ce8Y2WzN2ApV6JzhmS5iM","object":"chat.completion.chunk","created":1786979519,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{"content":"STREAM_OK"},"finish_reason":null}]}

data: {"id":"chatcmpl-msg_vrtx_011Ce8Y2WzN2ApV6JzhmS5iM","object":"chat.completion.chunk","created":1786979519,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":17,"completion_tokens":7,"total_tokens":24}}

: x-omniroute-cache-hit=false
: x-omniroute-latency-ms=153
: x-omniroute-tokens-in=17
: x-omniroute-tokens-out=7
: x-omniroute-model=claude-haiku-4-5@20251001
: x-omniroute-provider=vertex
data: [DONE]

This is the chain the PR actually adds: rawPredict returns one complete JSON body (confirmed in §1) → synthesizeClaudeSse() turns it into a real Anthropic message_start/content_block_delta/message_stop event sequence internally → the existing claude→openai stream translator parses that correctly (Translation mode: claude → openai above) and re-emits valid OpenAI chunk-shaped SSE with the actual streamed content, correct finish_reason, correct token usage, clean [DONE].

4. Baseline without a format override (context, not a regression)

Same model, same connection, but without registering it as a custom model with targetFormat: "claude" — i.e. relying only on the provider's base format:

[sse] openai → gemini | stream=false                                     tag=FORMAT
...
[MALFORMED-200] mode=nonstream provider=vertex model=claude-haiku-4-5@20251001 reason=empty_choices recvBytes=457
status: 502
{"error":{"message":"[vertex/claude-haiku-4-5@20251001] upstream returned an empty response without usable output", ...}}

Expected: without targetFormat: "claude" set, format resolution defaults to the provider's base "gemini" and the response parser can't read Claude's response shape → empty. Confirms the override in §2/§3 is load-bearing, not incidental — this is the exact mechanism bullet 3 of the PR description is about.


No code changes from this validation — ready to merge on this evidence.

@diegosouzapw diegosouzapw removed hold-vps PR verde, merge aguardando validação live (release-drain) needs-vps PR requires Hard Rule #18 VPS smoke test on 192.168.0.15 before merge labels Aug 17, 2026
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Hi @wgordon17 — thanks for the detailed diagnosis here. Looking at the current release branch, the rawPredict routing + real-streaming-synthesis fix you describe is already live in open-sse/executors/vertex.ts (merged via a separate fix for issue #8994). The remaining diff on this PR is just whitespace/formatting noise with no functional change left.

We're evaluating this as a likely-subsumed PR — really appreciate you tracking down the root cause, and we want to make sure you get credit for the diagnosis before deciding how to close this out. Will follow up here once that's settled.

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks for the diagnosis and fix — verified the substance already landed on the release tip: open-sse/executors/vertex.ts already routes Claude models through the native Anthropic rawPredict endpoint (with the anthropic_version injection and stripped model field) and already synthesizes a genuine SSE stream from the complete JSON body rawPredict returns.

Auditing this PR's actual current diff against the release tip: it's now down to config/quality/eslint-suppressions.json (trailing-newline only), and pure Prettier reformatting in open-sse/utils/stream.ts and tests/unit/executor-vertex-extended.test.ts (line wrapping, no behavior change) — the functional Vertex/Claude routing change itself isn't in this diff anymore, presumably because it already merged separately and this branch never got a chance to rebase past it.

Closing as subsumed — thanks again for finding and root-causing the original bug.

tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…n every provider (cheaper-inference#9006)

* fix(executors): route Claude-via-Vertex through native rawPredict with real streaming

Claude models on Vertex AI were being sent through the generic OpenAI-
compatible partner endpoint, which 404s/errors for Claude on at least
some projects. Route them through Vertex's native Anthropic Messages
API (publishers/anthropic/.../rawPredict) instead, stripping the
body-level model field rawPredict rejects and injecting the required
anthropic_version field.

rawPredict only ever returns a complete JSON body, never real SSE
framing, so streaming requests now get a genuine Anthropic-format SSE
stream synthesized from that JSON (message_start/content_block_*/
message_delta/message_stop), which the existing claude-to-openai
response translator already knows how to parse.

Also fixes two response-format resolution bugs that silently dropped
a custom model's DB-stored targetFormat override whenever the model
id also existed in the static provider registry (as claude-sonnet-4-6
and claude-opus-4-7 do under vertex): resolveModelOrError had its own
ad-hoc resolution that never consulted the override, and even once
fixed, executeChatWithBreaker discarded the correctly-resolved format
before handleChatCore's own resolution ran a second time.

* docs: add changelog fragment for cheaper-inference#8909

* refactor(sse): extract shared Claude effort-model predicate

* fix(sse): strip Claude effort-suffix ids for any provider serving a real Claude model

* fix(sse): keep no-think and CC-discovery catalog variant roots unprefixed

* fix(dashboard): re-qualify no-think playground model ids correctly

* fix(sse): scope Vertex 404s to a per-model lockout via passthroughModels

* docs: add changelog fragment for the Claude catalog/dispatch fix

* fix(sse): align regex naming and changelog formatting

* fix(sse): clarify effort-variant strip comment and add cross-module drift guard

* fix(sse): disambiguate Vertex connection-wide vs per-model 403s

* docs: document Vertex 403 disambiguation in changelog fragment

* fix(sse): correlate reason and resource within the same ErrorInfo detail

* fix(sse): extract Vertex error classifier and rebaseline frozen file sizes

* test: register vertex-passthrough-model-lockout in stryker tap.testFiles

* fix(sse): reconciles rebase-onto-tip drift for 9006

Two categories of inherited base-branch breakage surfaced when
rebasing onto release/v3.8.50's latest tip, both confirmed unrelated
to this PR's own diff:

- check:file-size: base.ts and chat.ts drifted further past their
  frozen caps via already-merged commits (174e08b and others) that
  didn't rebaseline after growing them. Documented and bumped in
  file-size-baseline.json.
- chat-helpers.test.ts: two gpt-5.5 routing assertions predate cheaper-inference#9275
  (fix(routing): bare model ids route to codex first), which
  deliberately made gpt-5.5 route to codex unconditionally, regardless
  of which other providers are active. Confirmed via cheaper-inference#9275's own
  commit message and code comments this is intentional, not a
  regression; verified reproducible on the raw base tip alone, with
  no changes from this PR involved. Updated both assertions and their
  names to match the new, intentional default.

* ci: re-trigger checks after GitHub Actions incident (2026-08-07, resolved)

* ci: re-trigger checks (previous push event was dropped)

* fix(quality): rebaseline combo-routing-engine.test.ts own-comment growth

The ALL_ACCOUNTS_INACTIVE->ALL_TARGETS_SKIPPED fix (a32aed738) added explanatory comments (+7 lines), pushing the file past its frozen 3457 cap. CI's PR-mode check:file-size caught it; local check-file-size.mjs was not re-run after that specific commit.
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