Skip to content

feat(channel): add Cloudflare AI Gateway channel (type 58) - #4814

Open
xn030523 wants to merge 7 commits into
QuantumNous:mainfrom
xn030523:feat/cloudflare-ai-gateway-channel
Open

feat(channel): add Cloudflare AI Gateway channel (type 58)#4814
xn030523 wants to merge 7 commits into
QuantumNous:mainfrom
xn030523:feat/cloudflare-ai-gateway-channel

Conversation

@xn030523

@xn030523 xn030523 commented May 12, 2026

Copy link
Copy Markdown

Summary

Adds a new channel type Cloudflare AI Gateway (type 58) that targets Cloudflare's OpenAI-compatible aggregated endpoint:

https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/compat/chat/completions

The Other field on the channel stores {account_id}/{gateway_id} and is composed into the upstream URL. /compat/embeddings and /compat/responses are also supported.

Docs: https://developers.cloudflare.com/ai-gateway/usage/chat-completion/

Backend

  • constant/channel.go / constant/api_type.go / common/api_type.go / middleware/distributor.go / relay/relay_adaptor.go: register channel type 58 and API type, default base URL https://gateway.ai.cloudflare.com.
  • relay/channel/cloudflare_aig/adaptor.go: new adapter reusing OpenAI handlers and service.ClaudeToOpenAIRequest for Claude relay format.
  • relay/common/relay_info.go: added to streamSupportedChannels so stream_options.include_usage works.
  • autoPrefixModel auto-prepends the required {provider}/ segment (anthropic, openai, google-ai-studio, grok, deepseek, cohere, mistral, workers-ai) when the channel-configured model name does not already contain a /. This allows users to keep using bare model names (claude-sonnet-4-5, gpt-4o, etc.) that match upstream SDK expectations, while still allowing explicit overrides like vertex/claude-sonnet-4-5.

Frontend

  • web/default: channel constants, channel-type-config, mutate drawer with a new Other field (account_id/gateway_id), and i18n strings added for en/fr/ja/ru/vi/zh.
  • web/classic: CHANNEL_OPTIONS entry, icon mapping, EditChannelModal Other field for type 58.

Usage

  1. Create channel, type = Cloudflare AI Gateway.
  2. Key = your Cloudflare AI Gateway token (BYOK / unified billing) or upstream provider key.
  3. Other = {account_id}/{gateway_id} (e.g. abcd1234.../default).
  4. Add models with bare names (claude-sonnet-4-5, gpt-4o, gemini-2.5-flash) — the adapter will auto-prefix. Or use explicit provider/model to override.

Testing

  • Built go build successfully.
  • Validated via OpenAI SDK with claude-sonnet-4-5 / anthropic/claude-sonnet-4-5 and Claude Code (/v1/messages) with model mapping path.

Notes

  • AutoPrefix is a thin best-effort helper; users can disable it by simply prefixing the provider themselves.
  • Beta query (?beta=true) on Claude relay format is not appended for the compat endpoint; anthropic-beta header would be the correct path if needed in future iterations.

Summary by CodeRabbit

  • New Features

    • Added Cloudflare AI Gateway channel with UI fields for Account ID/Gateway ID, localized labels (EN/FR/JA/RU/VI/ZH), and channel listing with streaming support and model-name normalization.
  • Bug Fixes

    • Improved handling and logging of aborted Claude streams to prevent incorrect usage reporting and reduce false error indications.

Review Change Stack

Adds a new channel adapter cloudflare_aig that targets Cloudflare AI
Gateway's OpenAI-compatible endpoint (/compat/chat/completions,
/compat/embeddings, /compat/responses). The Other field stores
{account_id}/{gateway_id} which is composed into the upstream URL
https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/compat/...

Backend:
- New channel type 58 (Cloudflare AI Gateway) and API type registered
  in constant/channel.go, constant/api_type.go, common/api_type.go,
  middleware/distributor.go and relay/relay_adaptor.go.
- relay/channel/cloudflare_aig/adaptor.go reuses openai handlers for
  responses and supports Claude relay format via ClaudeToOpenAIRequest.
- Channel is added to streamSupportedChannels so stream_options work.
- autoPrefixModel auto-prepends the required {provider}/ prefix
  (anthropic/, openai/, google-ai-studio/, grok/, deepseek/, cohere/,
  mistral/, workers-ai/) when the configured model name does not
  already contain a '/' separator, so users can keep using bare model
  names that match SDK expectations.

Frontend:
- web/default: channel constants, channel-type-config, mutate drawer
  (new Other field with account_id/gateway_id placeholder) and i18n
  entries for en/fr/ja/ru/vi/zh.
- web/classic: channel options entry, icon mapping, EditChannelModal
  Other field for type 58.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Cloudflare AI Gateway (channel type 58): constants, backend relay adaptor and registration, middleware/stream support, frontend UI/config updates, data/Claude handling, and translations.

Changes

Cloudflare AI Gateway Channel Integration

Layer / File(s) Summary
Constants and type system mapping
constant/api_type.go, constant/channel.go, common/api_type.go
New APITypeCloudflareAIGateway and ChannelTypeCloudflareAIGateway (58) added with base URL/name entries; channel→API mapping updated.
Cloudflare AI Gateway adaptor
relay/channel/cloudflare_aig/adaptor.go
New adaptor implements model normalization, request URL selection, header setup (Bearer or Claude-native x-api-key + anthropic headers), request conversions for OpenAI/Responses/embeddings, DoRequest delegation, response handling, and cf-aig header passthrough.
Adaptor registration and request processing
relay/relay_adaptor.go, middleware/distributor.go, relay/common/relay_info.go
Adaptor imported/returned by GetAdaptor for the new API type; middleware reads api_version from channel.Other for the channel; channel marked as stream-supported.
Classic web UI
web/classic/src/constants/channel.constants.js, web/classic/src/components/table/channels/modals/EditChannelModal.jsx, web/classic/src/helpers/render.jsx
Adds Cloudflare AI Gateway option, Account ID / Gateway ID input in channel edit modal, and Cloudflare icon rendering.
Default web configuration
web/default/src/features/channels/constants.ts, web/default/src/features/channels/lib/channel-type-config.ts, web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
Adds channel type entry, config (icon, default base URL, hints), display ordering, and drawer input for {account_id}/{gateway_id}.
Internationalization
web/default/src/i18n/locales/{en,fr,ja,ru,vi,zh}.json
Adds localized labels and field/format strings for Cloudflare AI Gateway across six locales.
Claude stream handling
relay/channel/claude/relay-claude.go
Detects client-aborted Claude streams with no upstream bytes, sets usage tokens to zero, sets UsageSemantic to "anthropic", and returns early.
Claude thinking-mode params
relay/claude_handler.go
Ensures Temperature, TopP, and TopK are stripped when request.Thinking is non-nil.
Stream logging and status
relay/helper/stream_scanner.go, service/log_info_generate.go
Logs client-aborted streams as informational and omits stream_status for ClientGone non-error ends.
Claude DTO changes
dto/claude.go
Adds data and is_error fields to ClaudeMediaMessage for tool-result and redacted-thinking round-tripping.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RelayDispatcher
  participant Adaptor
  participant DoApiRequest
  participant CloudflareAPI
  participant ResponseHandler
  Client->>RelayDispatcher: inbound relay request (APITypeCloudflareAIGateway)
  RelayDispatcher->>Adaptor: select cloudflare_aig.Adaptor
  Adaptor->>Adaptor: ConvertRequest (autoPrefixModel, validate)
  Adaptor->>Adaptor: SetupRequestHeader (cf-aig headers, auth)
  Adaptor->>Adaptor: GetRequestURL (mode & format)
  Adaptor->>DoApiRequest: DoApiRequest (HTTP)
  DoApiRequest->>CloudflareAPI: HTTP call
  CloudflareAPI->>ResponseHandler: response + cf-aig headers
  ResponseHandler->>Client: parsed response + forwarded cf-aig headers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • seefs001
  • Calcium-Ion
  • creamlike1024

Poem

🐰 I hop through gateways, Cloudflare in sight,
Prefixing models, routing requests right,
Headers and locales now snug and bright,
UI fields and streams all dancing light,
Hooray — the relay hops into flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding a new Cloudflare AI Gateway channel type (type 58). It is specific, actionable, and directly reflects the primary objective of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (5)
web/classic/src/helpers/render.jsx (1)

354-355: 💤 Low value

Consider using a distinct icon for Cloudflare AI Gateway.

Channel type 58 (Cloudflare AI Gateway) reuses the same Cloudflare.Color icon as channel type 39 (Cloudflare). While this might be intentional since both are Cloudflare services, using distinct icons could improve UX by making it easier to visually differentiate between the two channel types in the UI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/classic/src/helpers/render.jsx` around lines 354 - 355, The Cloudflare AI
Gateway case reuses Cloudflare.Color (case 58), making it indistinguishable from
Cloudflare (case 39); update the switch in render.jsx to render a distinct icon
for channel type 58 by replacing Cloudflare.Color with a new identifier (e.g.,
Cloudflare.AIGateway or Cloudflare.Gateway) and ensure that new icon is
added/exported from the Cloudflare icon module (or create a new SVG component in
the icons set) so the component reference resolves correctly; keep case 39 using
Cloudflare.Color and update any exports/imports so the new icon is available to
the render switch.
web/default/src/i18n/locales/ja.json (1)

751-753: 🏗️ Heavy lift

Use hierarchical i18n keys for new entries

These newly added keys are raw UI strings. Please switch to semantic hierarchical keys (for example under a channels.cloudflareAiGateway.* namespace) and update references accordingly.

As per coding guidelines, "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/ja.json` around lines 751 - 753, The three new
raw UI entries ("Cloudflare AI Gateway", "Account ID / Gateway ID *", and the
format string "Format: {account_id}/{gateway_id}. The default gateway is
\"default\".") should be converted to hierarchical i18n keys (e.g.
channels.cloudflareAiGateway.title, channels.cloudflareAiGateway.accountLabel,
channels.cloudflareAiGateway.formatHint) in the ja.json file; add those keys
under a channels.cloudflareAiGateway namespace, move the Japanese strings to
those keys, and then update all code references that directly used the raw
strings to use the new i18n keys (e.g., lookup
channels.cloudflareAiGateway.title/accountLabel/formatHint) so naming is
consistent with the existing translation structure.
web/default/src/i18n/locales/ru.json (1)

751-753: 🏗️ Heavy lift

Use hierarchical i18n keys for new entries.

These new translation keys are phrase-based; please switch to semantic hierarchical keys (for example under a channels.cloudflareAiGateway.* namespace) and update usages accordingly to match project i18n standards.

As per coding guidelines, "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/ru.json` around lines 751 - 753, Replace the
phrase-based keys ("Cloudflare AI Gateway", "Account ID / Gateway ID *",
"Format: {account_id}/{gateway_id}. The default gateway is \"default\".") with
hierarchical keys under a channels.cloudflareAiGateway namespace (e.g.,
channels.cloudflareAiGateway.title, channels.cloudflareAiGateway.accountLabel,
channels.cloudflareAiGateway.formatHint), move the Russian strings to those keys
in ru.json, remove the old phrase keys, and update all code references that
currently use the phrase keys to use channels.cloudflareAiGateway.title /
.accountLabel / .formatHint so the project follows the semantic hierarchical
i18n convention.
web/default/src/i18n/locales/en.json (1)

751-753: ⚡ Quick win

Use hierarchical i18n keys for new Cloudflare AI Gateway entries.

These new keys are phrase-based; please switch to semantic hierarchical keys (for example, channel.cloudflareAigateway.name, channel.cloudflareAigateway.accountGatewayIdLabel, channel.cloudflareAigateway.accountGatewayIdHint) and update the corresponding references.

As per coding guidelines, "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/en.json` around lines 751 - 753, Replace the
phrase-based translation keys with semantic hierarchical keys and update their
usages: create keys like channel.cloudflareAigateway.name,
channel.cloudflareAigateway.accountGatewayIdLabel, and
channel.cloudflareAigateway.accountGatewayIdHint in the en.json (replacing
"Cloudflare AI Gateway", "Account ID / Gateway ID *", and the format hint string
respectively) and then update any code references that currently use the old
phrase strings to use these new keys (search for the literal strings or existing
lookups and replace them with the new hierarchical key names in UI
components/forms).
web/default/src/i18n/locales/vi.json (1)

751-753: ⚡ Quick win

Use hierarchical i18n keys for the new Cloudflare strings.

These new entries use raw English sentence keys. Please switch to semantic keys (for example, channel.cloudflareAiGateway.title, channel.cloudflareAiGateway.accountGatewayId, channel.cloudflareAiGateway.formatHint) to align with project i18n conventions.

As per coding guidelines, use “hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/vi.json` around lines 751 - 753, The three new
raw-English i18n entries ("Cloudflare AI Gateway", "Account ID / Gateway ID *",
and "Format: {account_id}/{gateway_id}. The default gateway is \"default\".")
should be converted to hierarchical semantic keys; replace them with
channel.cloudflareAiGateway.title, channel.cloudflareAiGateway.accountGatewayId,
and channel.cloudflareAiGateway.formatHint respectively in vi.json and ensure
any code/UI that reads the old literal keys is updated to use these new keys;
keep the Vietnamese translations as-is but move them under the new hierarchical
keys to match project i18n conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 1538-1559: The "other" field (used when currentType === 58) lacks
format validation; update the channelFormSchema to validate the "other" string
contains a '/' with non-empty parts on both sides (e.g., account_id/gateway_id).
Add a refinement (e.g., z.string().refine(...) or equivalent validator for your
schema) that runs only when currentType === 58, splits the value on '/', ensures
at least two parts and that parts[0].trim() and parts[1].trim() are non-empty,
and returns a clear error message like "Format must be
{account_id}/{gateway_id}" to surface via the FormMessage for the form field
named "other".

In `@web/default/src/i18n/locales/fr.json`:
- Line 752: The French locale entry for the key "Account ID / Gateway ID *" in
fr.json is still in English; replace the value with the correct French
translation (e.g., "ID de compte / ID de passerelle *" or the preferred French
phrasing) so the JSON pair becomes "Account ID / Gateway ID *": "ID de compte /
ID de passerelle *"; ensure you keep the same JSON key and punctuation/asterisk
exactly and validate the JSON remains well-formed after editing.

In `@web/default/src/i18n/locales/ja.json`:
- Line 752: Replace the English value for the JSON key "Account ID / Gateway ID
*" in the ja locale with an appropriate Japanese translation; change the value
to "アカウントID / ゲートウェイID *" so the entry reads "Account ID / Gateway ID *":
"アカウントID / ゲートウェイID *".

In `@web/default/src/i18n/locales/ru.json`:
- Line 752: The value for the localization key "Account ID / Gateway ID *" in
ru.json is still English; replace the English string with a proper Russian
translation (for example: "ID аккаунта / ID шлюза *" or "Идентификатор аккаунта
/ Идентификатор шлюза *") so the Russian UI shows a fully localized label;
update the value for the exact key "Account ID / Gateway ID *" in the ru.json
locale file.

In `@web/default/src/i18n/locales/vi.json`:
- Line 752: The value for the key "Account ID / Gateway ID *" in vi.json is
still in English; replace the English string with the Vietnamese translation
(e.g., "ID tài khoản / ID cổng *") while preserving the exact key, punctuation
and the trailing asterisk so the JSON structure and usage by any consumers (the
"Account ID / Gateway ID *" entry) remain unchanged.

---

Nitpick comments:
In `@web/classic/src/helpers/render.jsx`:
- Around line 354-355: The Cloudflare AI Gateway case reuses Cloudflare.Color
(case 58), making it indistinguishable from Cloudflare (case 39); update the
switch in render.jsx to render a distinct icon for channel type 58 by replacing
Cloudflare.Color with a new identifier (e.g., Cloudflare.AIGateway or
Cloudflare.Gateway) and ensure that new icon is added/exported from the
Cloudflare icon module (or create a new SVG component in the icons set) so the
component reference resolves correctly; keep case 39 using Cloudflare.Color and
update any exports/imports so the new icon is available to the render switch.

In `@web/default/src/i18n/locales/en.json`:
- Around line 751-753: Replace the phrase-based translation keys with semantic
hierarchical keys and update their usages: create keys like
channel.cloudflareAigateway.name,
channel.cloudflareAigateway.accountGatewayIdLabel, and
channel.cloudflareAigateway.accountGatewayIdHint in the en.json (replacing
"Cloudflare AI Gateway", "Account ID / Gateway ID *", and the format hint string
respectively) and then update any code references that currently use the old
phrase strings to use these new keys (search for the literal strings or existing
lookups and replace them with the new hierarchical key names in UI
components/forms).

In `@web/default/src/i18n/locales/ja.json`:
- Around line 751-753: The three new raw UI entries ("Cloudflare AI Gateway",
"Account ID / Gateway ID *", and the format string "Format:
{account_id}/{gateway_id}. The default gateway is \"default\".") should be
converted to hierarchical i18n keys (e.g. channels.cloudflareAiGateway.title,
channels.cloudflareAiGateway.accountLabel,
channels.cloudflareAiGateway.formatHint) in the ja.json file; add those keys
under a channels.cloudflareAiGateway namespace, move the Japanese strings to
those keys, and then update all code references that directly used the raw
strings to use the new i18n keys (e.g., lookup
channels.cloudflareAiGateway.title/accountLabel/formatHint) so naming is
consistent with the existing translation structure.

In `@web/default/src/i18n/locales/ru.json`:
- Around line 751-753: Replace the phrase-based keys ("Cloudflare AI Gateway",
"Account ID / Gateway ID *", "Format: {account_id}/{gateway_id}. The default
gateway is \"default\".") with hierarchical keys under a
channels.cloudflareAiGateway namespace (e.g.,
channels.cloudflareAiGateway.title, channels.cloudflareAiGateway.accountLabel,
channels.cloudflareAiGateway.formatHint), move the Russian strings to those keys
in ru.json, remove the old phrase keys, and update all code references that
currently use the phrase keys to use channels.cloudflareAiGateway.title /
.accountLabel / .formatHint so the project follows the semantic hierarchical
i18n convention.

In `@web/default/src/i18n/locales/vi.json`:
- Around line 751-753: The three new raw-English i18n entries ("Cloudflare AI
Gateway", "Account ID / Gateway ID *", and "Format: {account_id}/{gateway_id}.
The default gateway is \"default\".") should be converted to hierarchical
semantic keys; replace them with channel.cloudflareAiGateway.title,
channel.cloudflareAiGateway.accountGatewayId, and
channel.cloudflareAiGateway.formatHint respectively in vi.json and ensure any
code/UI that reads the old literal keys is updated to use these new keys; keep
the Vietnamese translations as-is but move them under the new hierarchical keys
to match project i18n conventions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58012e83-a099-4beb-bee6-25fca30aa70d

📥 Commits

Reviewing files that changed from the base of the PR and between aa56667 and 36bf212.

📒 Files selected for processing (19)
  • common/api_type.go
  • constant/api_type.go
  • constant/channel.go
  • middleware/distributor.go
  • relay/channel/cloudflare_aig/adaptor.go
  • relay/common/relay_info.go
  • relay/relay_adaptor.go
  • web/classic/src/components/table/channels/modals/EditChannelModal.jsx
  • web/classic/src/constants/channel.constants.js
  • web/classic/src/helpers/render.jsx
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/constants.ts
  • web/default/src/features/channels/lib/channel-type-config.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json

Comment on lines +1538 to +1559
{/* Cloudflare AI Gateway (type 58) */}
{currentType === 58 && (
<FormField
control={form.control}
name='other'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Account ID / Gateway ID *')}</FormLabel>
<FormControl>
<Input
placeholder={t('e.g., d6b5da8hk1awo8nap34ube6gh/default')}
{...field}
/>
</FormControl>
<FormDescription>
{t('Format: {account_id}/{gateway_id}. The default gateway is "default".')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add format validation for the "other" field.

The "other" field expects the format {account_id}/{gateway_id} (e.g., abcd1234.../default), but there's no frontend validation to enforce this. Consider adding a validation rule to the form schema to ensure the field contains at least one / separator and non-empty parts before and after it.

✨ Example validation pattern

In the schema definition (likely in channelFormSchema), add validation for type 58's "other" field:

other: z.string()
  .refine(
    (val) => {
      if (currentType === 58) {
        const parts = val.split('/')
        return parts.length >= 2 && parts[0].trim() && parts[1].trim()
      }
      return true
    },
    { message: 'Format must be {account_id}/{gateway_id}' }
  )

This catches malformed input before submission, improving UX and reducing backend errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`
around lines 1538 - 1559, The "other" field (used when currentType === 58) lacks
format validation; update the channelFormSchema to validate the "other" string
contains a '/' with non-empty parts on both sides (e.g., account_id/gateway_id).
Add a refinement (e.g., z.string().refine(...) or equivalent validator for your
schema) that runs only when currentType === 58, splits the value on '/', ensures
at least two parts and that parts[0].trim() and parts[1].trim() are non-empty,
and returns a clear error message like "Format must be
{account_id}/{gateway_id}" to surface via the FormMessage for the form field
named "other".

"Codes copied!": "Codes copiés !",
"Codex": "Codex",
"Cloudflare AI Gateway": "Cloudflare AI Gateway",
"Account ID / Gateway ID *": "Account ID / Gateway ID *",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

French locale string is not translated on Line 752

"Account ID / Gateway ID *" is still English in fr.json, which causes mixed-language UI in French locale.

💡 Proposed fix
-    "Account ID / Gateway ID *": "Account ID / Gateway ID *",
+    "Account ID / Gateway ID *": "ID de compte / ID de passerelle *",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/fr.json` at line 752, The French locale entry
for the key "Account ID / Gateway ID *" in fr.json is still in English; replace
the value with the correct French translation (e.g., "ID de compte / ID de
passerelle *" or the preferred French phrasing) so the JSON pair becomes
"Account ID / Gateway ID *": "ID de compte / ID de passerelle *"; ensure you
keep the same JSON key and punctuation/asterisk exactly and validate the JSON
remains well-formed after editing.

"Codes copied!": "コードをコピーしました!",
"Codex": "Codex",
"Cloudflare AI Gateway": "Cloudflare AI Gateway",
"Account ID / Gateway ID *": "Account ID / Gateway ID *",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the Japanese value at Line 752

The value is still English in the ja locale. Please translate it to Japanese for UI consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/ja.json` at line 752, Replace the English value
for the JSON key "Account ID / Gateway ID *" in the ja locale with an
appropriate Japanese translation; change the value to "アカウントID / ゲートウェイID *" so
the entry reads "Account ID / Gateway ID *": "アカウントID / ゲートウェイID *".

"Codes copied!": "Коды скопированы!",
"Codex": "Codex",
"Cloudflare AI Gateway": "Cloudflare AI Gateway",
"Account ID / Gateway ID *": "Account ID / Gateway ID *",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Russian locale value is still English.

"Account ID / Gateway ID *" is not localized in ru.json, so Russian UI will show mixed language text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/ru.json` at line 752, The value for the
localization key "Account ID / Gateway ID *" in ru.json is still English;
replace the English string with a proper Russian translation (for example: "ID
аккаунта / ID шлюза *" or "Идентификатор аккаунта / Идентификатор шлюза *") so
the Russian UI shows a fully localized label; update the value for the exact key
"Account ID / Gateway ID *" in the ru.json locale file.

"Codes copied!": "Đã sao chép mã!",
"Codex": "Codex",
"Cloudflare AI Gateway": "Cloudflare AI Gateway",
"Account ID / Gateway ID *": "Account ID / Gateway ID *",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate the Vietnamese locale value on Line 752.

"Account ID / Gateway ID *" is still in English in vi.json, so users on Vietnamese locale will see mixed-language UI text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/vi.json` at line 752, The value for the key
"Account ID / Gateway ID *" in vi.json is still in English; replace the English
string with the Vietnamese translation (e.g., "ID tài khoản / ID cổng *") while
preserving the exact key, punctuation and the trailing asterisk so the JSON
structure and usage by any consumers (the "Account ID / Gateway ID *" entry)
remain unchanged.

xn030523 and others added 2 commits May 13, 2026 15:26
…nd forward cf-aig-* headers

- Route RelayFormatClaude to CF's native Anthropic endpoint instead of /compat/chat/completions, preserving cache_control and full anthropic usage detail (cache_creation_input_tokens / cache_read_input_tokens / ephemeral_5m/1h).
- Forward client-supplied cf-aig-* request headers (cache-ttl/skip-cache/cache-key) so users can control CF AI Gateway response caching per-request.
- Passthrough cf-aig-cache-status/cache-ttl/event-id/log-id/request-id and cf-ray response headers back to clients for observability.
- Switch auth header per route: x-api-key + anthropic-version (+optional anthropic-beta) for Claude native, Authorization: Bearer for OpenAI compat.
- ConvertClaudeRequest now passes the request through untouched; Claude responses are handled by claude.ClaudeHandler / ClaudeStreamHandler so prompt-cache fields survive end-to-end.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… before any data received

When a Claude streaming request ends with received=0 + client_gone (HTTP/2 client cancelled before the first SSE chunk arrived), HandleStreamFinalResponse used to fill the empty Usage with locally-estimated prompt tokens via ResponseText2Usage, which then caused PostTextConsumeQuota to bill the user for the full estimated quota even though no upstream bytes were consumed.

Detect this case (info.IsStream && info.ReceivedResponseCount == 0 && info.StreamStatus.EndReason == StreamEndReasonClientGone) and leave Usage zeroed so summary.TotalTokens == 0 causes the quota path to settle at 0, refunding any pre-consumed quota. Other abort reasons (EOF / scanner_error / timeout) still go through the existing fallback so genuine upstream failures continue to be billed against the local estimate as before.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/channel/cloudflare_aig/adaptor.go`:
- Around line 124-128: The log currently prints raw cf-aig-* header values by
serializing the variable forwarded via fmt.Sprintf in the logger.LogInfo calls;
replace this with non-sensitive output (e.g., log only the header names, their
count, or a redacted placeholder) wherever forwarded is used (including the
other occurrence around lines 227-230). Update the logger.LogInfo(c, ...) calls
to compute a safe summary from forwarded (like len(forwarded) or a slice of
header names or values replaced with "<redacted>") and log that summary instead
of the raw forwarded value.
- Around line 68-75: The ApiVersion (info.ApiVersion) is currently only checked
for non-empty and can contain slashes that produce bad URLs; update the logic in
adaptor.go (around apiVersion variable and the isClaudeNativeRoute branch) to
validate that apiVersion matches the expected "account_id/gateway_id" pattern
(exactly one slash, non-empty segments) and return a descriptive error if it
does not; keep using isClaudeNativeRoute(info) to decide the anthropic path but
only after the apiVersion format check passes so malformed values like
"account", "/gateway", or "a/b/c" are rejected rather than interpolated into the
URL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 82b42ba6-3a93-4432-b1af-858d9d18f2c6

📥 Commits

Reviewing files that changed from the base of the PR and between 36bf212 and d1a2f43.

📒 Files selected for processing (2)
  • relay/channel/claude/relay-claude.go
  • relay/channel/cloudflare_aig/adaptor.go

Comment on lines +68 to +75
apiVersion := info.ApiVersion
if apiVersion == "" {
return "", errors.New("account_id/gateway_id is required (set in Other field, format: {account_id}/{gateway_id})")
}

if isClaudeNativeRoute(info) {
return fmt.Sprintf("%s/v1/%s/anthropic/v1/messages", info.ChannelBaseUrl, apiVersion), nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate the account_id/gateway_id format before composing the URL.

Any non-empty ApiVersion currently gets interpolated into the path, so values like account, /gateway, or account/gateway/extra silently produce malformed upstream routes.

Suggested fix
 func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
 	apiVersion := info.ApiVersion
-	if apiVersion == "" {
+	if apiVersion == "" ||
+		strings.Count(apiVersion, "/") != 1 ||
+		strings.HasPrefix(apiVersion, "/") ||
+		strings.HasSuffix(apiVersion, "/") {
 		return "", errors.New("account_id/gateway_id is required (set in Other field, format: {account_id}/{gateway_id})")
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
apiVersion := info.ApiVersion
if apiVersion == "" {
return "", errors.New("account_id/gateway_id is required (set in Other field, format: {account_id}/{gateway_id})")
}
if isClaudeNativeRoute(info) {
return fmt.Sprintf("%s/v1/%s/anthropic/v1/messages", info.ChannelBaseUrl, apiVersion), nil
}
apiVersion := info.ApiVersion
if apiVersion == "" ||
strings.Count(apiVersion, "/") != 1 ||
strings.HasPrefix(apiVersion, "/") ||
strings.HasSuffix(apiVersion, "/") {
return "", errors.New("account_id/gateway_id is required (set in Other field, format: {account_id}/{gateway_id})")
}
if isClaudeNativeRoute(info) {
return fmt.Sprintf("%s/v1/%s/anthropic/v1/messages", info.ChannelBaseUrl, apiVersion), nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/cloudflare_aig/adaptor.go` around lines 68 - 75, The ApiVersion
(info.ApiVersion) is currently only checked for non-empty and can contain
slashes that produce bad URLs; update the logic in adaptor.go (around apiVersion
variable and the isClaudeNativeRoute branch) to validate that apiVersion matches
the expected "account_id/gateway_id" pattern (exactly one slash, non-empty
segments) and return a descriptive error if it does not; keep using
isClaudeNativeRoute(info) to decide the anthropic path but only after the
apiVersion format check passes so malformed values like "account", "/gateway",
or "a/b/c" are rejected rather than interpolated into the URL.

Comment on lines +124 to +128
if len(forwarded) > 0 {
logger.LogInfo(c, fmt.Sprintf("[cloudflare_aig] forwarding cf-aig-* request headers: %v", forwarded))
} else {
logger.LogInfo(c, "[cloudflare_aig] no cf-aig-* request headers from client (cache will fall back to gateway default TTL)")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop logging raw cf-aig-* header values.

These logs serialize user/upstream-controlled header contents verbatim. That can leak cache metadata or other sensitive values into logs and also creates very high-cardinality log lines. Log only header names or counts, or redact the values first.

Suggested fix
-	if len(forwarded) > 0 {
-		logger.LogInfo(c, fmt.Sprintf("[cloudflare_aig] forwarding cf-aig-* request headers: %v", forwarded))
+	if len(forwarded) > 0 {
+		keys := make([]string, 0, len(forwarded))
+		for k := range forwarded {
+			keys = append(keys, k)
+		}
+		logger.LogInfo(c, fmt.Sprintf("[cloudflare_aig] forwarding cf-aig-* request headers: %v", keys))
 	} else {
 		logger.LogInfo(c, "[cloudflare_aig] no cf-aig-* request headers from client (cache will fall back to gateway default TTL)")
 	}
@@
-	if len(seen) > 0 {
-		logger.LogInfo(c, fmt.Sprintf("[cloudflare_aig] upstream cf-aig-* response headers: %v", seen))
+	if len(seen) > 0 {
+		keys := make([]string, 0, len(seen))
+		for k := range seen {
+			keys = append(keys, k)
+		}
+		logger.LogInfo(c, fmt.Sprintf("[cloudflare_aig] upstream cf-aig-* response headers: %v", keys))
 	} else {
 		logger.LogInfo(c, "[cloudflare_aig] upstream returned no cf-aig-* response headers")
 	}

Also applies to: 227-230

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/cloudflare_aig/adaptor.go` around lines 124 - 128, The log
currently prints raw cf-aig-* header values by serializing the variable
forwarded via fmt.Sprintf in the logger.LogInfo calls; replace this with
non-sensitive output (e.g., log only the header names, their count, or a
redacted placeholder) wherever forwarded is used (including the other occurrence
around lines 227-230). Update the logger.LogInfo(c, ...) calls to compute a safe
summary from forwarded (like len(forwarded) or a slice of header names or values
replaced with "<redacted>") and log that summary instead of the raw forwarded
value.

xn030523 and others added 3 commits May 13, 2026 15:35
…er logging behind DebugEnabled

Previously the adaptor logged `forwarding cf-aig-* request headers` and `upstream cf-aig-* response headers` (or the `no headers` variants) on EVERY request, polluting production logs. Move these behind common.DebugEnabled so they only appear when explicitly running in debug mode. The map for collecting forwarded headers is also skipped entirely when debug is off to avoid the extra allocation.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… it from usage log status panel

* relay/helper/stream_scanner.go: when a stream ends with reason=client_gone and no upstream errors recorded, write a single [INFO] `stream aborted by client` line instead of [ERR] `stream ended` — client-initiated disconnects (e.g. Claude CLI cancel/retry over HTTP/2) are not server-side faults and should not flood production error logs.
* service/log_info_generate.go: skip emitting the `other.stream_status` block entirely on the same condition (client_gone + no errors). The frontend usage-logs detail panel only renders the red Badge when `other.stream_status && other.stream_status.status !== 'ok'`, so omitting the field makes those entries look like normal completions in the UI.
* No billing / quota logic touched. Real `HasErrors` cases and other abnormal end reasons (timeout, scanner_error, panic, ping_fail) still log at [ERR] and still set `stream_status.status = 'error'` as before.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…s when thinking enabled

Two Anthropic 400 errors surfaced after routing more traffic through native /v1/messages:

1. dto/claude.go: ClaudeMediaMessage had no Data field, so the opaque payload of redacted_thinking content blocks was lost on unmarshal and never sent back upstream, producing "Invalid data in redacted_thinking block". Added Data string with json tag data,omitempty, plus IsError pointer for tool_result blocks so error markers round-trip.

2. relay/claude_handler.go: Anthropic now rejects temperature, top_p, top_k whenever extended thinking is enabled ("temperature is deprecated for this model"). The previous logic only zeroed them on the claude-opus-4-7 effort branch. After all model-specific thinking setup, unconditionally set Temperature, TopP, TopK to nil when request.Thinking is non-nil. Applies to every claude-format channel (claude, cf_aig, aws, vertex, etc.) because the cleanup runs before adaptor.ConvertClaudeRequest.

Note: channels with pass_through_body_enabled=true bypass both fixes since the raw client body is forwarded verbatim.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
relay/claude_handler.go (2)

66-68: 💤 Low value

Optional: consolidate redundant parameter stripping.

Lines 66-68 and 81-83 strip the same sampling parameters that are now unconditionally stripped at lines 111-113 when Thinking != nil. Since the centralized stripping at lines 106-114 runs after all model-specific branches and covers all thinking-enabled requests, these earlier stripping operations are redundant.

♻️ Optional cleanup to remove redundant stripping

Remove the parameter stripping from the model-specific branches since the centralized logic now handles all cases:

Lines 66-68 (Opus 4.7 with effort suffix):

 		if strings.HasPrefix(request.Model, "claude-opus-4-7") {
 			// Opus 4.7 rejects non-default temperature/top_p/top_k with 400
 			// and defaults display to "omitted"; restore the 4.6 visible summary.
 			request.Thinking.Display = "summarized"
-			request.Temperature = nil
-			request.TopP = nil
-			request.TopK = nil

Lines 81-83 (Opus 4.7 with -thinking suffix):

 			if strings.HasPrefix(baseModel, "claude-opus-4-7") {
 				// Opus 4.7 rejects thinking.type="enabled"; use adaptive at high effort.
 				request.Thinking = &dto.Thinking{Type: "adaptive", Display: "summarized"}
 				request.OutputConfig = json.RawMessage(`{"effort":"high"}`)
-				request.Temperature = nil
-				request.TopP = nil
-				request.TopK = nil

Also applies to: 81-83

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/claude_handler.go` around lines 66 - 68, Remove the redundant early
stripping of sampling parameters in the Opus model-specific branches: do not set
request.Temperature, request.TopP or request.TopK to nil inside the Opus 4.7
branches (the ones handling the effort and -thinking suffix cases); rely on the
centralized stripping that runs when request.Thinking != nil (the logic that
nils these fields later) so behavior is unchanged—delete the duplicate nil
assignments in the model-specific branches and keep the centralized removal
under the Thinking check.

70-70: 💤 Low value

Optional: remove Temperature assignments that are immediately stripped.

Line 70 (Opus 4.6 with effort suffix) and line 97 (non-Opus-4.7 with -thinking suffix) set Temperature = 1.0, but this value is immediately stripped by the centralized logic at line 111 since Thinking != nil in both branches. These assignments have no effect.

♻️ Optional cleanup to remove dead Temperature assignments

Remove the Temperature assignments that are overridden by the centralized stripping:

Line 70 (Opus 4.6 branch):

 		} else {
-			request.Temperature = common.GetPointer[float64](1.0)
 		}

Line 97 (non-Opus-4.7 thinking branch):

 				request.Thinking = &dto.Thinking{
 					Type:         "enabled",
 					BudgetTokens: common.GetPointer[int](int(float64(*request.MaxTokens) * model_setting.GetClaudeSettings().ThinkingAdapterBudgetTokensPercentage)),
 				}
-				// TODO: 临时处理
-				// https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking
-				request.Temperature = common.GetPointer[float64](1.0)

Also applies to: 97-97

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/claude_handler.go` at line 70, Remove the dead assignments of
request.Temperature = common.GetPointer[float64](1.0) in the Opus 4.6 branch and
the non-Opus-4.7 thinking branch since the centralized logic later checks
Thinking != nil and always strips/overrides Temperature; specifically, delete
the two temperature set lines where request.Temperature is immediately made
irrelevant, leaving the Thinking-related assignments and centralized stripping
logic intact (look for the request.Temperature assignments adjacent to the Opus
4.6 effort-suffix branch and the non-Opus-4.7 "-thinking" branch).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@relay/claude_handler.go`:
- Around line 66-68: Remove the redundant early stripping of sampling parameters
in the Opus model-specific branches: do not set request.Temperature,
request.TopP or request.TopK to nil inside the Opus 4.7 branches (the ones
handling the effort and -thinking suffix cases); rely on the centralized
stripping that runs when request.Thinking != nil (the logic that nils these
fields later) so behavior is unchanged—delete the duplicate nil assignments in
the model-specific branches and keep the centralized removal under the Thinking
check.
- Line 70: Remove the dead assignments of request.Temperature =
common.GetPointer[float64](1.0) in the Opus 4.6 branch and the non-Opus-4.7
thinking branch since the centralized logic later checks Thinking != nil and
always strips/overrides Temperature; specifically, delete the two temperature
set lines where request.Temperature is immediately made irrelevant, leaving the
Thinking-related assignments and centralized stripping logic intact (look for
the request.Temperature assignments adjacent to the Opus 4.6 effort-suffix
branch and the non-Opus-4.7 "-thinking" branch).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cd9b7895-c342-49f9-bf78-2665d59afec6

📥 Commits

Reviewing files that changed from the base of the PR and between 819e6d4 and c1fc894.

📒 Files selected for processing (2)
  • dto/claude.go
  • relay/claude_handler.go

…prefix-order 500s

Anthropic now enforces strict prefix ordering for prompt-cache scopes: a cache_control block with scope:"global" is only valid when every preceding rendered block (tools, then earlier system entries, then earlier messages) also uses scope:"global". When any preceding block has a narrower scope or none, the request is rejected with HTTP 500:

  cache_control.scope: "global" is only valid when every preceding block is also globally scoped. A block with scope: "global" was found after content with a narrower cache scope...

Many clients (Claude CLI variants, custom param_override rules, etc.) generate layouts that violate this in the common case where tools render before a system[0] block tagged scope:"global". Production traffic shows this firing on hundreds of requests per hour, with no observable workaround on the request path.

This commit:

- Adds AllowGlobalCacheScope to dto.ChannelOtherSettings (default false). When false, all cache_control.scope keys are recursively stripped from the request JSON in relay/common.RemoveDisabledFields, degrading the cache scope to the user-scoped default. Cache hit rates within a single account are unaffected; only cross-account global cache is given up.
- Adds a stripCacheControlScope helper that walks any nested map/slice tree (covers system[], messages[].content[], tools[], and top-level cache_control) and deletes the scope key from every cache_control object encountered.

Channels that genuinely need cross-account global caching can opt in by setting allow_global_cache_scope=true; pass_through_body_enabled=true continues to bypass all sanitization.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@Calcium-Ion
Calcium-Ion force-pushed the main branch 2 times, most recently from 51fdfc5 to 2b6f1df Compare August 30, 2026 15:03
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