Skip to content

[Bug] gateway: message level output_config is stripped when the request carries the per-turn-control beta #8711

Open

Description

@Anankke

When a /v1/messages request carries anthropic-beta: per-turn-control-2026-07-01, the gateway removes output_config from the messages that have one. The client's mid session effort change never reaches Anthropic.

The check is in stripAnthropicMessageOutputConfigUnlessBeta in backend/internal/service/gateway_request.go:

func stripAnthropicMessageOutputConfigUnlessBeta(body []byte, anthropicBetaHeader string) ([]byte, bool) {
if anthropicBetaTokensContains(anthropicBetaHeader, claude.BetaMidConversationOutputConfig) {
return body, false
}

It returns the body untouched only when the header contains claude.BetaMidConversationOutputConfig, which is mid-conversation-output-config-2026-07-01, defined at

BetaMidConversationOutputConfig = "mid-conversation-output-config-2026-07-01"
. There is no constant for per-turn-control-2026-07-01, so with only that beta the field is stripped, and a system message with an empty content is dropped completely.

To reproduce, send this to /v1/messages and look at the body the gateway forwards:

anthropic-beta: per-turn-control-2026-07-01
{
  "model": "claude-opus-5-5",
  "max_tokens": 256,
  "messages": [
    {"role": "user", "content": "Hello"},
    {"role": "assistant", "content": "Hi, how can I help?"},
    {"role": "system", "content": [], "output_config": {"effort": "low"}},
    {"role": "user", "content": "Summarize that in one sentence."}
  ]
}

The forwarded body has no output_config on the third message, and the whole message is gone because its content is empty.

Expected: the body is forwarded unchanged, the same as with mid-conversation-output-config-2026-07-01. Claude Code sends per-turn-control-2026-07-01 in its anthropic-beta list, for example in the header capture described in #8394, and a message level output_config on a system message is the shape Anthropic documents for changing effort partway through a conversation: https://platform.claude.com/docs/en/build-with-claude/effort-per-message

Activity

  1. r266-tech commented on Nov 12, 2026

    @r266-tech
    Contributor

    核对了当前官方 per-message effort 文档:beta header、请求示例和可用性表列出的仍是 mid-conversation-output-config-2026-07-01。该页没有把 per-turn-control-2026-07-01 列为这个参数的别名。

    我用与 main@530c5e3a4 相同的请求构造代码做了 9 个独立本地对照,使用本 issue 的四条消息和顶层 effort: high:

    账号路径与客户端 beta 最终请求中的控制消息
    API Key 普通/透传,仅 per-turn-control-2026-07-01 删除,剩 3 条消息;顶层 effort 仍为 high
    API Key 普通/透传,改用官方的 mid-conversation-output-config-2026-07-01,或同时带两者 保留 4 条消息,控制消息 effort 为 low
    OAuth 默认构造,客户端仅带 per-turn-control-2026-07-01 最终 header 自带官方标记,控制消息保留

    这确认了 API Key 路径上的本地差异,也给出了按当前文档可用的最小对照;尚未验证真实上游是否接受 per-turn-control 单独授权这个字段。若已有该别名成功保留消息级 effort 的上游响应,请补充脱敏后的账号类型、是否透传、最终 beta header 及 HTTP 状态/错误,便于区分公开协议、账号专属行为或需要支持的别名。无需附密钥或完整会话。

    验证使用 Go 1.27.3/macOS arm64,未调用真实 Anthropic。AI 辅助源码核对和合成请求验证。

  2. Anankke commented on Nov 12, 2026

    @Anankke
    Author

    Tested on the real upstream through gateway that doesn't strip it with OAuth account not passthrough and claude-opus-5, sent on CC with anthropic-beta: claude-code-20250219,per-turn-control-2026-07-01 and a system message carrying output_configgot 200, so upstream accepts the field under that beta

    Row 3 in your table is the mimic path for clients that aren't Claude Code. Real Claude Code on OAuth isn't mimicked, so main drops the message there too

  3. r266-tech commented on Nov 12, 2026

    @r266-tech
    Contributor

    你说得对:我原表格的 OAuth 行只验证了非 Claude Code 客户端的 mimic 分支,应明确标注为 “OAuth mimic”。它不能概括真实 Claude Code 的 OAuth 请求;此前的结论漏掉了这条路径。

    已在 main@530c5e3a4 的真实 Forward 入口补充对照,用 Claude Code UA 和合法 metadata.user_id 触发非 mimic 路径。结果确认:仅带 claude-code-20250219,per-turn-control-2026-07-01 时,真实 Claude Code OAuth、API Key 普通和 API Key 透传三条路径都会删除该控制消息;mimic 因最终 header 自带旧 beta 而保留。顶层 effort 均不受影响。

    修复已提交为 https://github.com/Wei-Shaw/sub2api/pull/8742(`bd89d3d94`):按最终 header 中任一相关 beta 保留消息级 output_config;无相关 beta 或两者均被策略过滤时保持原行为。39 项相关 test/subtest、完整 unit(26,895 通过 / 41 跳过)、integration(16,143 通过 / 16 跳过)及 lint 均已通过。PR 中保留并披露了首轮计费内存断言失败、main 对照复现和后续复验结果。

    你的 OAuth + Claude Code + claude-opus-5 HTTP 200 是这里的真实上游接受证据;本账号完成的是本地合成请求验证,没有请求真实 Anthropic,也未验证模型实际 effort 效果或 API Key 的真实上游行为。当前官方文档仍列旧 beta,不能把上述实测写成官方文档已声明两者等价。

    AI 辅助实现、验证与回复。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions