Skip to content

feat(flags): provider- and model-level extra_headers (backend) - #1590

Open
0x0079 wants to merge 3 commits into
mainfrom
claude/provider-extension-fields-2nmxvh
Open

0x0079 wants to merge 3 commits into
mainfrom
claude/provider-extension-fields-2nmxvh

Conversation

@0x0079

@0x0079 0x0079 commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follows #1589 (rule level, merged) and rebases onto #1592's unified rule-flag path: completes the three-level Custom Headers control (provider < model < rule). Backend only — the UI is a separate PR so the capability and its API can land independently. api_key providers only; design in .design/provider-flags.md.

Key Changes

  • Typed fields on ai.Provider: Flags / ModelFlags carry the values directly, so the service reads p.Flags.ExtraHeaders exactly the way it reads rule.Flags.ExtraHeaders — no container, no accessors, no well-known keys. ai documents an admission rule to keep that struct narrow: only "how to reach the upstream" belongs there, gateway product behaviour stays in rule flags.
  • Same shape as rule flags: ProviderFlagRegistry mirrors RuleFlagRegistry with no per-spec scope or merge axis, because merging is a property of the levels (model overrides provider), not of each flag.
  • Supply side rides the client, not the request: wrapWithRuleFlags takes the model and resolves typ.SupplyExtraHeaders(provider, model) once per client — clients are already keyed by provider + model — so probe / model-list / vision-proxy paths get upstream headers too and dispatch needs no changes at all.
  • Precedence from write order: the transport writes supply-side headers, then the rule's, then the UA, so provider < model < rule holds without any three-level merge, and the rule-flag context key stays strictly "the resolved RuleFlags" — X-Tingly-Applied-Flags never reports provider config as a rule flag.
  • Persistence: additive flags / model_flags columns (serializer:json, same pattern as credential / vmodel_detail), no migration script.
  • api_key scope: Config.AddProvider/UpdateProvider reject on save (HTTP, CLI, import), wrapWithRuleFlags loads no supply headers for a non-api_key provider, and vendor chains mount nothing at all.
  • Provider API: typed flags / model_flags with partial-update semantics (nil untouched, non-null replaces wholesale) plus GET /provider/flags/registry.

Notes

  • Review trail, recorded in the design doc's trade-off table: an opaque Extensions container and per-spec Scope/MergeMode axes were built first and then removed — the container had exactly one consumer and the axes had no production reader, so both were structure kept for an imagined second flag (~135 lines).
  • Until the UI PR lands, provider/model headers are configured through the API; rule-level headers already have their UI from feat(flags): rule-level extra_headers — custom HTTP headers per rule #1589.

@0x0079
0x0079 force-pushed the claude/provider-extension-fields-2nmxvh branch 2 times, most recently from 2c9c733 to fc87817 Compare August 18, 2026 06:19
@0x0079 0x0079 changed the title feat(flags): provider- and model-level extra_headers on the Extensions container feat(flags): provider- and model-level extra_headers on the Extensions container (backend) Aug 18, 2026
@0x0079
0x0079 changed the base branch from claude/rule-extra-headers-2nmxvh to main August 18, 2026 06:21
@0x0079
0x0079 force-pushed the claude/provider-extension-fields-2nmxvh branch 2 times, most recently from d528aa8 to ea2bebf Compare August 18, 2026 11:46
@0x0079 0x0079 changed the title feat(flags): provider- and model-level extra_headers on the Extensions container (backend) feat(flags): provider- and model-level extra_headers (backend) Aug 18, 2026
claude added 3 commits August 24, 2026 07:19
Backend half of the provider/model level (frontend follows in a separate
PR), completing the three-level Custom Headers control on top of the rule
level merged in #1589 and rebased onto the unified rule-flag path (#1592).

- ai.Provider gains Flags / ModelFlags, both typed ProviderFlags, so the
  service reads p.Flags.ExtraHeaders exactly the way it reads
  rule.Flags.ExtraHeaders. The package documents an admission rule to keep
  that struct narrow: only fields describing how to reach the upstream
  belong in ai; gateway product behaviour stays in rule flags.
- ProviderFlagRegistry is the same shape as RuleFlagRegistry — no scope or
  merge axis per spec, because merging is a property of the levels (model
  overrides provider), not of each flag.
- The supply side rides the client, not the request: wrapWithRuleFlags takes
  the model and resolves typ.SupplyExtraHeaders(provider, model) once per
  client — clients are already keyed by provider + model — so probe,
  model-list and vision-proxy paths get upstream headers too and dispatch
  needs no changes at all.
- Precedence provider < model < rule falls out of the transport write order
  (supply, then rule, then UA), so nothing merges all three levels and the
  rule-flag context key stays strictly the resolved RuleFlags — diagnostics
  never report provider config as a rule flag.
- Persistence: additive flags / model_flags columns (serializer:json, same
  pattern as credential and vmodel_detail), no migration script.
- api_key scope: Config.AddProvider/UpdateProvider reject on save, covering
  HTTP, CLI and import; wrapWithRuleFlags loads no supply headers for a
  non-api_key provider, and vendor chains mount nothing at all.
- Provider API exposes typed flags / model_flags with partial-update
  semantics (nil untouched, non-null replaces wholesale) plus
  GET /provider/flags/registry.
- The extra_headers e2e behavior case gains a three-level route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBYfVGevcaVk7PxCJRh69t
Provider flags shipped with exactly one knob (extra_headers), which left
every upstream quirk to be re-configured on each rule that touches that
provider -- even for flags whose own description names the provider or the
model family as the reason (use_max_tokens: "older OpenAI-compatible
providers"; claude_code_compat: "third-party Anthropic-compatible providers
reject that role"; context_1m: "supported models").

Replace the old admission rule ("only fields describing how to reach the
upstream") with a stronger, easier one: ProviderFlags mirrors the consumer's
request-side flag set. A knob means the same thing at every level; only the
scope changes, and no supply-side vocabulary gets invented. Twelve flags
are now configurable at the provider and model levels.

Four rule flags are deliberately not mirrored, with a test pinning it:
session_affinity and vision_proxy_service are consumed before an upstream is
chosen; openai_endpoint_override duplicates Provider.OpenAIEndpointMode;
claude_org_id is Claude OAuth only while provider flags are api_key only.

ProviderFlagRegistry() now derives each spec from the rule spec of the same
key and overrides only the description, so both surfaces render the same
control for the same knob and cannot drift apart.

extra_headers keeps its transport write-order precedence. The rest inject
through the transform chain and the SDK layer, where no write order exists,
so they fold in via typ.ApplyProviderFlags at the single point that already
resolves the effective flags for a request (ResolveRuleFlagsWithScenario,
which gains a model parameter). Precedence is provider < model < rule
throughout: bools OR across levels, scalars take the narrowest non-zero
value.

Per-flag validation stays out on purpose -- a custom upstream is exactly
where an unusual value is legitimate, so only extra_headers keeps the
structural check it needs to be putable on the wire.

The frontend Plugins catalog currently renders the headers control only;
the registry now returns bool/string/enum types too, so exposing this batch
is pure UI work on top of an already-settled contract.
The provider catalog was a second definition: a key -> supply-side wording
table that pulled control shape from the rule spec of the same key. Even
with the shape derived, that is two places to maintain and two texts to keep
aligned -- and "which levels can this flag be set at" is a property of the
flag itself, so it belongs on the spec.

FlagSpec gains Levels []FlagLevel (provider < model < rule, widest first).
RuleFlagRegistry is now the single definition -- 12 flags declare all three
levels, 4 declare rule only -- and ProviderFlagRegistry degenerates to the
provider-level slice of it with the rule-side scenario axis cleared. The
description table is gone.

This is not the Scope/MergeMode pair that was removed earlier for being
speculative: those had no production reader and no data behind them, while
Levels carries 16 real declarations, has one reader, and decides which
controls each surface renders. Merge semantics stay off the spec -- that
really is a property of the level, not the flag.

One flag now means one description, so the wording has to be level-neutral;
the surface a user is on already says which level they are editing. Only
extra_headers and custom_user_agent actually needed rewording, which is
itself a sign the old table was mostly copied text.

Also trims the comments this feature added. The rationale lives in
.design/provider-flags.md; the code now states what it does and points
there, rather than restating the design at each site.
@0x0079
0x0079 force-pushed the claude/provider-extension-fields-2nmxvh branch from cc12542 to 2534926 Compare August 24, 2026 07:27
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