Skip to content

fix(provider-copilot): drop the obsolete Vertex structured-output strips - #271

Merged
Menci merged 1 commit into
mainfrom
copilot-bedrock-only-structured-outputs
Aug 1, 2026
Merged

fix(provider-copilot): drop the obsolete Vertex structured-output strips#271
Menci merged 1 commit into
mainfrom
copilot-bedrock-only-structured-outputs

Conversation

@Menci

@Menci Menci commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Deletes withStructuredOutputFormatStripped and withToolStrictStripped from the Copilot Messages boundary, along with their tests and their two registrations in the boundary chain.

What the strips did

Both interceptors deleted a field from every outbound Messages request: output_config.format, which carries the JSON Schema a reply is constrained to, and strict: true on tools, which compiles input_schema into a grammar. Anthropic serves both on the GA Messages wire with no beta header.

They existed because Copilot load-balances its Claude catalog across three Anthropic deployments — AWS Bedrock, Google Vertex, and Anthropic's own API — picking one per request. A Vertex-routed request tripped the GCP organization policy constraints/vertexai.allowedPartnerModelFeatures, which denies the structured_outputs partner feature by default and answers a 400 FAILED_PRECONDITION. Because the backend is chosen per request and is not visible beforehand, the only deterministic defence was to strip the fields from everything — so every caller on every model lost the grammar-constrained guarantee to insure against a backend it might not even draw.

Why the policy no longer requires it

A sampler ran every two hours from 2026-07-27 to 2026-08-01 against the live catalog, from two independent network egresses, sending output_config.format and a strict: true tool to every Claude model Copilot serves on /v1/messages. Each sample is attributed to its serving deployment by the Anthropic message-id infix — msg_vrtx_* for Vertex, msg_bdrk_* for Bedrock, bare msg_01* for Anthropic-direct — which is the only backend signal available, since Copilot strips Anthropic's request-id response header.

The policy change shows up as a single clean transition:

round Vertex-served format/strict
2026-07-31T12:35Z 16x org-policy rejection
2026-07-31T14:37Z 8x org-policy rejection
2026-07-31T16:38Z 8x 200
2026-07-31T23:00Z 8x 200
2026-08-01T03:03Z 8x 200
2026-08-01T05:04Z 8x 200

32 Vertex-served successes across four consecutive rounds on both egresses, with the rejection counters flat throughout. These are genuinely Vertex-served, not requests that happened to route elsewhere.

Why removal rather than a model-id gate

The failure never tracked the model. claude-opus-4.6 drew Vertex and Bedrock inside the same window and failed only on the Vertex samples; both egresses converged on the same per-model backend mix, so the client's source address is not the variable either. Any gate would have to predict a per-request routing decision that no client can observe before the response. With the upstream accepting the fields, there is nothing left to gate, and keeping a stripped path would only preserve the silent downgrade.

Risk and rollback

The organization policy lives in GitHub's own GCP project (projects/524636045653), so it can be flipped back without notice. The sampler continues to run the same matrix; a regression surfaces as the return of constraints/vertexai.allowedPartnerModelFeatures rejections. Reinstating the strips is a revert of this commit.

One unexplained failure mode is unrelated to this change and predates it: output_config.format: Extra inputs are not permitted on claude-opus-4.7 (12 samples) and claude-opus-4.8 (3 samples), last seen 2026-07-28T03:11Z, served by Anthropic-direct rather than Vertex. It has not recurred in over four days and its cause is unknown.

Test Plan

  • pnpm run typecheck — clean
  • pnpm run lint — clean
  • pnpm run test — 4947 passed / 421 files
  • Vertex accepts output_config.format and tool strict — 32 Vertex-attributed 200s across four rounds and two egresses since 2026-07-31T16:38Z, zero organization-policy rejections in that span
  • On-demand Vertex confirmation at finalize time — not performable: 48 consecutive requests to the two Vertex-prone models (claude-opus-4.6, claude-sonnet-4.6) all drew Bedrock. Backend selection is upstream-controlled, so a Vertex sample cannot be forced on demand; the item above covers the same claim from the sampler's accumulated draws.

@Menci

Menci commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

Agent note (Claude Code, Opus 5): the earlier claim in this PR that a live re-probe was impossible was wrong, and the reason is instructive — I probed with direct egress after excluding the upstream's HKG-scoped ss:// fallback by colo. GitHub gates the Claude catalog by egress, so direct egress returns a catalog with zero claude-* entries. Re-probed through the upstream's actual proxy (throwaway script on @floway-dev/proxy's dialer, per the probing-copilot skill's rule for ss://). Seven Claude models are present and /v1/messages works.

What upstream actually does today (2026-07-27, 42 samples + a 28-sample confirmation round)

model output_config.format tool strict: true
claude-opus-4.6 200 200
claude-opus-4.7 400 400
claude-opus-4.8 200 200
claude-opus-5 200 200
claude-sonnet-4.6 200 200
claude-sonnet-5 200 200
claude-haiku-4.5 200 200

Deterministic, not per-request flapping: every model was 6/6 consistent in the first deep run and 2/2 again in the confirmation round.

claude-opus-4.7 is the only model that fails, and it fails from two different backends:

  • Organization Policy constraint constraints/vertexai.allowedPartnerModelFeatures violated for 'projects/524636045653' attempting to use a disallowed feature — the Vertex org-policy gate this workaround was written for (12/12 in the deep run).
  • output_config.format: Extra inputs are not permitted — a schema-validation rejection, i.e. a backend that does not know the field at all (seen once in the first pass).

What this means for the gate in this PR

The >= 4.8 threshold is safe but too conservative. It permits exactly the models that work (4.8 / opus-5 / sonnet-5, all 200) and strips the one that must be stripped (4.7), so this PR is a strict improvement over unconditional stripping with no regression. But it also strips claude-opus-4.6, claude-sonnet-4.6 and claude-haiku-4.5, where structured outputs demonstrably work — value left on the table.

Two hypotheses are now dead:

  1. Version threshold. 4.6 works and 4.7 does not, so "newer = Bedrock-only = supported" does not describe the fleet. Whatever separates 4.7 is not its version ordinal.
  2. Copilot's own capability metadata. capabilities.supports.structured_outputs is true for 4.7 (which fails) and absent for claude-haiku-4.5 (which succeeds). It predicts neither direction, so it cannot be the gate either.

A 5-day sampler is now running every ~28 minutes across all catalog Claude models to establish whether the 4.7 carve-out is stable or a transient state of Copilot's fleet. I would rather not encode a one-model exception off a single day's data.

Holding this PR as-is: the conservative gate is correct and shippable today, and the sampler decides whether to widen it. Marking it draft until that data lands.

@Menci
Menci marked this pull request as draft July 27, 2026 18:51
The Messages boundary deleted `output_config.format` and tool `strict: true`
from every request. Both existed because a Vertex-routed request tripped the
GCP org policy `constraints/vertexai.allowedPartnerModelFeatures`, which denied
the `structured_outputs` partner feature with a 400 FAILED_PRECONDITION.
Copilot picks the backend per request, so callers lost the grammar-constrained
guarantee on every model to insure against a backend they might not even draw.

That policy no longer denies the feature. A sampler that has run every two
hours since Jul 27 against the live catalog, from two egresses, recorded the
change as a single clean transition: the last org-policy rejection landed at
2026-07-31T14:37Z and every Vertex-served request since 16:38Z has returned
200, on both egresses, with the rejection counters flat across the rounds that
followed. The message-id infix (`msg_vrtx_*` / `msg_bdrk_*` / bare `msg_01*`)
attributes each sample to its serving deployment, so these are Vertex-served
successes rather than requests that happened to route elsewhere.

With the upstream accepting the fields, stripping them is pure loss: clients
ask for a schema-constrained reply and silently receive an unconstrained one.
Remove both interceptors rather than gate them — a gate would still need a
model-id predicate, and the failure never tracked the model.

The policy lives in GitHub's own GCP project and can be flipped back without
notice, so the sampler keeps running against the same matrix. Reinstating the
strips is a revert of this commit.
@Menci
Menci force-pushed the copilot-bedrock-only-structured-outputs branch from b0eb7cb to d1beeba Compare August 1, 2026 07:12
@Menci Menci changed the title fix(provider-copilot): keep structured outputs on Bedrock-only Claude models fix(provider-copilot): drop the obsolete Vertex structured-output strips Aug 1, 2026
@Menci
Menci marked this pull request as ready for review August 1, 2026 07:30
@Menci
Menci merged commit 8117341 into main Aug 1, 2026
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.

1 participant