feat: improve Claude Responses cache affinity support#4981
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
WalkthroughThis PR implements a complete Claude-to-OpenAI Responses relay system with prompt cache key derivation, web search tool support, streaming response handling, and operational filtering. The backend introduces request conversion, response aggregation, and tool normalization; the frontend adds excluded-channel-ID configuration and prompt-cache-key selection in channel affinity rules with full i18n support. ChangesClaude-Responses Relay System
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (1)
web/default/src/i18n/locales/en.json (1)
1437-1437: ⚡ Quick winUse hierarchical i18n keys for the newly added monitoring strings.
These new entries use literal sentence keys instead of semantic hierarchical keys. Please switch to scoped keys (for example under
monitoring.autoTest.*) and update the correspondingt(...)lookups.💡 Suggested key shape
- "Enter positive channel IDs separated by commas or line breaks": "Enter positive channel IDs separated by commas or line breaks", - "Excluded channel IDs": "Excluded channel IDs", - "Scheduled tests skip these channel IDs. Leave empty to test all eligible channels.": "Scheduled tests skip these channel IDs. Leave empty to test all eligible channels.", + "monitoring.autoTest.excludedChannelIds.placeholder": "Enter positive channel IDs separated by commas or line breaks", + "monitoring.autoTest.excludedChannelIds.label": "Excluded channel IDs", + "monitoring.autoTest.excludedChannelIds.description": "Scheduled tests skip these channel IDs. Leave empty to test all eligible channels.",As per coding guidelines, use “hierarchical and semantically clear translation key names such as
dashboard.overview.titleand maintain naming consistency”.Also applies to: 1532-1532, 3463-3463
🤖 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` at line 1437, Summary: The new i18n entry uses a full sentence as the key instead of a hierarchical semantic key; replace it with a scoped key and update code to use the new key. Fix: in en.json replace the literal key "Enter positive channel IDs separated by commas or line breaks" with a hierarchical key such as "monitoring.autoTest.enterPositiveChannelIds" (and do the same for the other similar literal keys flagged), then update all t(...) lookups that reference the literal sentence to use the new hierarchical key string, ensuring naming consistency under the monitoring.autoTest namespace.
🤖 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 `@controller/channel-test.go`:
- Around line 874-882: The helper shouldSkipAutomaticChannelTest currently
always skips channels with Status == common.ChannelStatusManuallyDisabled, which
prevents manual TestAllChannels when useAutomaticTestFilter is false; change
shouldSkipAutomaticChannelTest to accept a useAutomaticTestFilter bool and only
apply the channel.Status == common.ChannelStatusManuallyDisabled check when
useAutomaticTestFilter is true, update all callers (including the manual
TestAllChannels path that invokes shouldSkipAutomaticChannelTest) to pass the
appropriate flag, and ensure the nil and excludedChannelIDs checks remain
unchanged.
In `@controller/usage_stats.go`:
- Around line 131-138: The response object is using usedUSD for weekUsed and
totalUsed which is inconsistent with period-specific fields (todayUsed uses
todayStats.Quota); update weekUsed to quotaToUSD(weekStats.Quota) and
weekUsedFormatted to formatUSD(quotaToUSD(weekStats.Quota)), and update
totalUsed to quotaToUSD(totalStats.Quota) and totalUsedFormatted to
formatUSD(quotaToUSD(totalStats.Quota)); keep other fields (e.g., usedUSD,
analyticsTotalUsed) unchanged unless you intend to rename them to clarify they
represent cumulative account/subscription usage.
In `@dto/openai_request.go`:
- Around line 230-234: Change the optional scalar struct fields ID and
SearchContextSize from plain string to pointer types (e.g., *string) and keep
`omitempty` on their json tags so omitted vs explicit-empty values are preserved
during relay conversion; update the struct that contains the fields ID, Type,
Function, Custom, SearchContextSize (the OpenAI request DTO) to use pointer
types for those optional scalars and adjust any callers/constructors to handle
nil vs non-nil values accordingly.
In `@relay/channel/openai/chat_via_responses.go`:
- Around line 590-591: The call to helper.ClaudeData(c, *claudeResp) currently
ignores any returned error; change it to capture the error (err :=
helper.ClaudeData(...)) and handle it by logging the error and terminating
further parsing/streaming (e.g., return the error or break out of the event loop
and close the connection) so upstream event processing stops on downstream write
failures; update the surrounding function in chat_via_responses.go to propagate
or handle that error appropriately using the c and claudeResp symbols.
- Around line 229-243: readResponsesStreamFinal only accumulates text from
"response.output_text.delta", so if upstream sends text via
"response.output_item.delta" or only a terminal "response.output_item.done" you
end up with an empty synthesized body; update the switch inside
readResponsesStreamFinal to also handle "response.output_item.delta" (append
streamResp.Delta to outputText) and handle "response.output_item.done" (append
any final piece of text from streamResp.Content/Delta or streamResp.Response
choice text into outputText) before using outputText as the stream-final
fallback, using the existing variables streamResp, outputText and finalResp to
aggregate those pieces.
In `@router/api-router.go`:
- Around line 284-288: The new v1UsageRoute group lacks rate limiting which
exposes the public GET /v1/usage/stats (controller.GetUsageStats) to
brute-force/DoS; add the same middleware used by the existing /api/usage route
by calling v1UsageRoute.Use(middleware.CriticalRateLimit()) (or the appropriate
rate limit middleware) when constructing the group so the /v1/usage route is
protected.
In `@service/channel_affinity.go`:
- Around line 329-343: The current extraction returns the literal "null" when
res represents JSON null; change the logic in the switch that inspects
res.Type/res.Raw so that JSON null is considered missing: treat gjson.Null
(and/or res.Raw == "null") as empty and do not return it, allowing the fallback
that checks src.Path == "prompt_cache_key" and calls
BuildOpenAIResponsesPromptCacheKeyFromContext(c).Key to execute; update the
conditional around res.String()/res.Raw (the switch handling res.Type and the
subsequent if) to explicitly exclude gjson.Null or the "null" raw value before
returning.
In `@service/claude_prompt_cache.go`:
- Around line 39-44: extractClaudeMetadataUserID currently returns a raw user ID
and the code in the Claude prompt-cache branch returns that raw value as
ClaudePromptCacheKeyResult.Key; instead of forwarding the raw ID, compute a
cryptographic hash (e.g., SHA‑256 hex) of the userID and return that hashed
string as Key while leaving Source as "metadata.user_id" and OK true. Update the
branch that constructs ClaudePromptCacheKeyResult for
extractClaudeMetadataUserID to replace the plain userID with the hashed
representation so upstream/cache paths never see the raw identifier.
In `@service/claude_responses.go`:
- Around line 367-370: The switch is matching strings.TrimSpace(effort) but
returns the original effort, so inputs like " high " can pass the case yet still
emit an invalid token; change the code to first normalize the token (e.g.,
normalized := strings.ToLower(strings.TrimSpace(effort))) and switch on that
normalized value, and return the normalized string in the matching cases
(reference the effort variable and the switch block in claude_responses.go),
leaving the existing "max"/default handling intact.
In `@web/classic/src/i18n/locales/en.json`:
- Line 2820: Update the English translation for the key "定时测试排除渠道 ID 格式不正确" so
the message reads naturally in English; change "Invalid scheduled-test excluded
channel ID format" to use "scheduled test" (e.g., "Invalid scheduled test
excluded channel ID format") to remove the awkward hyphen and improve clarity in
the UI.
In `@web/classic/src/pages/Setting/Operation/SettingsChannelAffinity.jsx`:
- Around line 73-74: The new option 'claude_prompt_cache_key' was added to the
key source list but validateKeySources currently treats it as invalid,
preventing saves; update the validation logic in SettingsChannelAffinity.jsx so
validateKeySources recognizes 'claude_prompt_cache_key' as a valid source (and
any related allow-list or switch handling used by the save routine), ensuring
the same symbol name ('claude_prompt_cache_key') is accepted wherever key
sources are validated before saving.
In
`@web/default/src/features/system-settings/general/channel-affinity/rule-editor-dialog.tsx`:
- Around line 115-119: The placeholder string in getKeySourceInputPlaceholder is
hardcoded and must be localized; update getKeySourceInputPlaceholder to not
return raw user-facing strings but either accept a translation function (t:
TFunction) as a parameter or return stable translation keys (e.g.,
'placeholder.noParam', 'placeholder.metadataConversationId',
'placeholder.userId') and then call t(...) from the React component that uses
getKeySourceInputPlaceholder; update callers of getKeySourceInputPlaceholder
(where it's used in the rule-editor-dialog UI) to pass useTranslation().t or to
translate the returned keys before rendering so all user-facing text uses t().
In
`@web/default/src/features/system-settings/integrations/monitoring-settings-section.tsx`:
- Around line 49-57: The hardcoded Zod error message in channelIdListString (the
z.string().refine call) must be replaced with a localized message supplied via
i18n; refactor this by creating a schema factory or a small helper that accepts
the t function (from useTranslation) and returns the zod string schema with the
refine error message set to t('...') (or map Zod errors to localized strings),
then use that factory where channelIdListString is currently defined so all
validation messages call t(...) instead of the hardcoded English string.
---
Nitpick comments:
In `@web/default/src/i18n/locales/en.json`:
- Line 1437: Summary: The new i18n entry uses a full sentence as the key instead
of a hierarchical semantic key; replace it with a scoped key and update code to
use the new key. Fix: in en.json replace the literal key "Enter positive channel
IDs separated by commas or line breaks" with a hierarchical key such as
"monitoring.autoTest.enterPositiveChannelIds" (and do the same for the other
similar literal keys flagged), then update all t(...) lookups that reference the
literal sentence to use the new hierarchical key string, ensuring naming
consistency under the monitoring.autoTest namespace.
🪄 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: ec6a8453-acac-4917-b283-674457a4cb60
📒 Files selected for processing (36)
controller/channel-test.gocontroller/usage_stats.godto/openai_request.godto/openai_response.gorelay/channel/codex/adaptor.gorelay/channel/openai/chat_via_responses.gorelay/channel/openai/relay_responses.gorelay/chat_completions_via_responses.gorelay/claude_handler.gorelay/common/relay_info.gorelay/responses_handler.gorouter/api-router.goservice/channel_affinity.goservice/claude_prompt_cache.goservice/claude_responses.goservice/convert.goservice/openai_responses_prompt_cache.goservice/openaicompat/chat_to_responses.goservice/text_quota.gosetting/operation_setting/channel_affinity_setting.gosetting/operation_setting/monitor_setting.goweb/classic/src/components/settings/OperationSetting.jsxweb/classic/src/i18n/locales/en.jsonweb/classic/src/i18n/locales/zh-CN.jsonweb/classic/src/i18n/locales/zh.jsonweb/classic/src/pages/Setting/Operation/SettingsChannelAffinity.jsxweb/classic/src/pages/Setting/Operation/SettingsMonitoring.jsxweb/default/src/features/system-settings/general/channel-affinity/constants.tsweb/default/src/features/system-settings/general/channel-affinity/rule-editor-dialog.tsxweb/default/src/features/system-settings/general/channel-affinity/types.tsweb/default/src/features/system-settings/integrations/monitoring-settings-section.tsxweb/default/src/features/system-settings/operations/index.tsxweb/default/src/features/system-settings/operations/section-registry.tsxweb/default/src/features/system-settings/types.tsweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/zh.json
| func shouldSkipAutomaticChannelTest(channel *model.Channel, excludedChannelIDs map[int]struct{}) bool { | ||
| if channel == nil { | ||
| return true | ||
| } | ||
| if _, excluded := excludedChannelIDs[channel.Id]; excluded { | ||
| return true | ||
| } | ||
| return channel.Status == common.ChannelStatusManuallyDisabled | ||
| } |
There was a problem hiding this comment.
Manual channel tests are still filtered for manually disabled channels.
With useAutomaticTestFilter=false, manual TestAllChannels should not apply automatic skip rules, but shouldSkipAutomaticChannelTest still skips common.ChannelStatusManuallyDisabled.
Suggested fix
-func shouldSkipAutomaticChannelTest(channel *model.Channel, excludedChannelIDs map[int]struct{}) bool {
+func shouldSkipAutomaticChannelTest(channel *model.Channel, excludedChannelIDs map[int]struct{}, useAutomaticTestFilter bool) bool {
if channel == nil {
return true
}
+ if !useAutomaticTestFilter {
+ return false
+ }
if _, excluded := excludedChannelIDs[channel.Id]; excluded {
return true
}
return channel.Status == common.ChannelStatusManuallyDisabled
}
@@
- if shouldSkipAutomaticChannelTest(channel, excludedChannelIDs) {
+ if shouldSkipAutomaticChannelTest(channel, excludedChannelIDs, useAutomaticTestFilter) {
continue
}Also applies to: 917-918
🤖 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 `@controller/channel-test.go` around lines 874 - 882, The helper
shouldSkipAutomaticChannelTest currently always skips channels with Status ==
common.ChannelStatusManuallyDisabled, which prevents manual TestAllChannels when
useAutomaticTestFilter is false; change shouldSkipAutomaticChannelTest to accept
a useAutomaticTestFilter bool and only apply the channel.Status ==
common.ChannelStatusManuallyDisabled check when useAutomaticTestFilter is true,
update all callers (including the manual TestAllChannels path that invokes
shouldSkipAutomaticChannelTest) to pass the appropriate flag, and ensure the nil
and excludedChannelIDs checks remain unchanged.
| "weekUsed": usedUSD, | ||
| "weekUsedFormatted": formatUSD(usedUSD), | ||
| "weekCalls": weekStats.Calls, | ||
| "weekTokens": weekStats.Tokens, | ||
| "totalUsed": usedUSD, | ||
| "totalUsedFormatted": formatUSD(usedUSD), | ||
| "analyticsTotalUsed": usedUSD, | ||
| "analyticsTotalUsedFormatted": formatUSD(usedUSD), |
There was a problem hiding this comment.
weekUsed and totalUsed values are inconsistent with their period-specific counterparts.
todayUsed correctly uses todayStats.Quota (the actual quota consumed today), but weekUsed and totalUsed both use usedUSD (subscription/wallet cumulative used quota) rather than weekStats.Quota and totalStats.Quota respectively. Meanwhile, weekCalls/weekTokens and totalCalls/totalTokens do use the period-specific stats.
This semantic inconsistency may confuse API consumers expecting period-specific usage. Consider either:
- Using
quotaToUSD(weekStats.Quota)forweekUsedandquotaToUSD(totalStats.Quota)fortotalUsed, or - Renaming these fields to clarify they represent cumulative subscription/account usage (e.g.,
accountUsed)
🤖 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 `@controller/usage_stats.go` around lines 131 - 138, The response object is
using usedUSD for weekUsed and totalUsed which is inconsistent with
period-specific fields (todayUsed uses todayStats.Quota); update weekUsed to
quotaToUSD(weekStats.Quota) and weekUsedFormatted to
formatUSD(quotaToUSD(weekStats.Quota)), and update totalUsed to
quotaToUSD(totalStats.Quota) and totalUsedFormatted to
formatUSD(quotaToUSD(totalStats.Quota)); keep other fields (e.g., usedUSD,
analyticsTotalUsed) unchanged unless you intend to rename them to clarify they
represent cumulative account/subscription usage.
| ID string `json:"id,omitempty"` | ||
| Type string `json:"type"` | ||
| Function FunctionRequest `json:"function,omitempty"` | ||
| Custom json.RawMessage `json:"custom,omitempty"` | ||
| SearchContextSize string `json:"search_context_size,omitempty"` |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use pointer types for new optional scalar tool fields.
Line 230 and Line 234 add optional scalar request fields as plain string, which drops omitted-vs-explicit-empty semantics during relay conversion.
Proposed fix
type ToolCallRequest struct {
- ID string `json:"id,omitempty"`
+ ID *string `json:"id,omitempty"`
Type string `json:"type"`
Function FunctionRequest `json:"function,omitempty"`
Custom json.RawMessage `json:"custom,omitempty"`
- SearchContextSize string `json:"search_context_size,omitempty"`
+ SearchContextSize *string `json:"search_context_size,omitempty"`
}📝 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.
| ID string `json:"id,omitempty"` | |
| Type string `json:"type"` | |
| Function FunctionRequest `json:"function,omitempty"` | |
| Custom json.RawMessage `json:"custom,omitempty"` | |
| SearchContextSize string `json:"search_context_size,omitempty"` | |
| ID *string `json:"id,omitempty"` | |
| Type string `json:"type"` | |
| Function FunctionRequest `json:"function,omitempty"` | |
| Custom json.RawMessage `json:"custom,omitempty"` | |
| SearchContextSize *string `json:"search_context_size,omitempty"` |
🤖 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 `@dto/openai_request.go` around lines 230 - 234, Change the optional scalar
struct fields ID and SearchContextSize from plain string to pointer types (e.g.,
*string) and keep `omitempty` on their json tags so omitted vs explicit-empty
values are preserved during relay conversion; update the struct that contains
the fields ID, Type, Function, Custom, SearchContextSize (the OpenAI request
DTO) to use pointer types for those optional scalars and adjust any
callers/constructors to handle nil vs non-nil values accordingly.
| case "response.output_text.delta": | ||
| outputText.WriteString(streamResp.Delta) | ||
| case "response.completed": | ||
| if streamResp.Response != nil { | ||
| finalResp = streamResp.Response | ||
| } | ||
| case "response.error", "response.failed": | ||
| if streamResp.Response != nil { | ||
| if oaiErr := streamResp.Response.GetOpenAIError(); oaiErr != nil && oaiErr.Type != "" { | ||
| return nil, types.WithOpenAIError(*oaiErr, http.StatusInternalServerError) | ||
| } | ||
| } | ||
| return nil, types.NewOpenAIError(fmt.Errorf("responses stream error: %s", streamResp.Type), types.ErrorCodeBadResponse, http.StatusInternalServerError) | ||
| default: | ||
| } |
There was a problem hiding this comment.
Aggregate response.output_item.done message text in stream-final fallback.
readResponsesStreamFinal only appends response.output_text.delta. If upstream emits message text only via response.output_item.done and response.completed is missing, the synthesized response body becomes empty.
💡 Suggested patch
switch streamResp.Type {
case "response.created":
if streamResp.Response != nil {
@@
case "response.output_text.delta":
outputText.WriteString(streamResp.Delta)
+ case "response.output_item.done":
+ if streamResp.Item != nil && streamResp.Item.Type == "message" {
+ text := responsesOutputItemText(streamResp.Item)
+ if delta := stringDeltaFromPrefix(outputText.String(), text); delta != "" {
+ outputText.WriteString(delta)
+ }
+ }
case "response.completed":
if streamResp.Response != nil {
finalResp = streamResp.Response🤖 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/openai/chat_via_responses.go` around lines 229 - 243,
readResponsesStreamFinal only accumulates text from
"response.output_text.delta", so if upstream sends text via
"response.output_item.delta" or only a terminal "response.output_item.done" you
end up with an empty synthesized body; update the switch inside
readResponsesStreamFinal to also handle "response.output_item.delta" (append
streamResp.Delta to outputText) and handle "response.output_item.done" (append
any final piece of text from streamResp.Content/Delta or streamResp.Response
choice text into outputText) before using outputText as the stream-final
fallback, using the existing variables streamResp, outputText and finalResp to
aggregate those pieces.
| _ = helper.ClaudeData(c, *claudeResp) | ||
| } |
There was a problem hiding this comment.
Handle helper.ClaudeData write errors instead of ignoring them.
Ignoring this error can keep parsing upstream events after downstream write failures, causing inconsistent stream termination behavior.
💡 Suggested patch
- _ = helper.ClaudeData(c, *claudeResp)
+ if err := helper.ClaudeData(c, *claudeResp); err != nil {
+ streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError)
+ sr.Stop(streamErr)
+ return
+ }📝 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.
| _ = helper.ClaudeData(c, *claudeResp) | |
| } | |
| if err := helper.ClaudeData(c, *claudeResp); err != nil { | |
| streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError) | |
| sr.Stop(streamErr) | |
| return | |
| } |
🤖 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/openai/chat_via_responses.go` around lines 590 - 591, The call
to helper.ClaudeData(c, *claudeResp) currently ignores any returned error;
change it to capture the error (err := helper.ClaudeData(...)) and handle it by
logging the error and terminating further parsing/streaming (e.g., return the
error or break out of the event loop and close the connection) so upstream event
processing stops on downstream write failures; update the surrounding function
in chat_via_responses.go to propagate or handle that error appropriately using
the c and claudeResp symbols.
| switch strings.TrimSpace(effort) { | ||
| case "low", "medium", "high", "xhigh": | ||
| return effort | ||
| case "max": |
There was a problem hiding this comment.
Return normalized effort token from the switch.
Line 367 matches on strings.TrimSpace(effort) but Line 369 returns the original effort, so values like " high " can pass the switch and still emit an invalid token.
Proposed fix
func mapClaudeResponsesReasoningEffort(effort string) string {
- switch strings.TrimSpace(effort) {
+ normalized := strings.TrimSpace(effort)
+ switch normalized {
case "low", "medium", "high", "xhigh":
- return effort
+ return normalized
case "max":
return "xhigh"
default:
return ""
}
}📝 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.
| switch strings.TrimSpace(effort) { | |
| case "low", "medium", "high", "xhigh": | |
| return effort | |
| case "max": | |
| func mapClaudeResponsesReasoningEffort(effort string) string { | |
| normalized := strings.TrimSpace(effort) | |
| switch normalized { | |
| case "low", "medium", "high", "xhigh": | |
| return normalized | |
| case "max": | |
| return "xhigh" | |
| default: | |
| return "" | |
| } | |
| } |
🤖 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 `@service/claude_responses.go` around lines 367 - 370, The switch is matching
strings.TrimSpace(effort) but returns the original effort, so inputs like " high
" can pass the case yet still emit an invalid token; change the code to first
normalize the token (e.g., normalized :=
strings.ToLower(strings.TrimSpace(effort))) and switch on that normalized value,
and return the normalized string in the matching cases (reference the effort
variable and the switch block in claude_responses.go), leaving the existing
"max"/default handling intact.
| "自动禁用关键词": "Automatic disable keywords", | ||
| "自动禁用状态码": "Auto-disable status codes", | ||
| "自动禁用状态码格式不正确": "Invalid auto-disable status code format", | ||
| "定时测试排除渠道 ID 格式不正确": "Invalid scheduled-test excluded channel ID format", |
There was a problem hiding this comment.
Use natural wording in validation message
“scheduled-test” reads awkwardly in English UI. Prefer “scheduled test” for clarity.
✏️ Proposed text fix
- "定时测试排除渠道 ID 格式不正确": "Invalid scheduled-test excluded channel ID format",
+ "定时测试排除渠道 ID 格式不正确": "Invalid excluded channel ID format for scheduled tests",📝 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.
| "定时测试排除渠道 ID 格式不正确": "Invalid scheduled-test excluded channel ID format", | |
| "定时测试排除渠道 ID 格式不正确": "Invalid excluded channel ID format for scheduled tests", |
🤖 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/i18n/locales/en.json` at line 2820, Update the English
translation for the key "定时测试排除渠道 ID 格式不正确" so the message reads naturally in
English; change "Invalid scheduled-test excluded channel ID format" to use
"scheduled test" (e.g., "Invalid scheduled test excluded channel ID format") to
remove the awkward hyphen and improve clarity in the UI.
| { label: 'claude_prompt_cache_key', value: 'claude_prompt_cache_key' }, | ||
| { label: 'gjson', value: 'gjson' }, |
There was a problem hiding this comment.
New key source type is currently unsavable in classic UI.
Line 73 adds claude_prompt_cache_key, but validateKeySources later classifies it as invalid, so users can select it but cannot save rules.
Suggested fix
const normalizeKeySource = (src) => {
const type = (src?.type || '').trim();
const key = (src?.key || '').trim();
const path = (src?.path || '').trim();
+ if (type === 'claude_prompt_cache_key') {
+ return { type, key: '', path: '' };
+ }
+
if (type === 'gjson') {
return { type, key: '', path };
}
return { type, key, path: '' };
};
@@
for (const x of xs) {
- if (
+ if (x.type === 'claude_prompt_cache_key') {
+ continue;
+ } else if (
x.type === 'context_int' ||
x.type === 'context_string' ||
x.type === 'request_header'
) {
if (!x.key) return { ok: false, message: 'Key 不能为空' };
@@
const src = normalizeKeySource(
editingRule?.key_sources?.[idx],
);
const isGjson = src.type === 'gjson';
+ const noParam = src.type === 'claude_prompt_cache_key';
return (
<Input
- placeholder={
- isGjson ? 'metadata.conversation_id' : 'X-Affinity-Key'
- }
+ placeholder={noParam ? '无需参数' : (isGjson ? 'metadata.conversation_id' : 'X-Affinity-Key')}
aria-label={t('Key 或 Path')}
- value={isGjson ? src.path : src.key}
+ value={noParam ? '' : (isGjson ? src.path : src.key)}
+ disabled={noParam}
onChange={(value) =>
+ noParam ? undefined :
updateKeySource(
idx,
isGjson ? { path: value } : { key: value },
)
}🤖 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/pages/Setting/Operation/SettingsChannelAffinity.jsx` around
lines 73 - 74, The new option 'claude_prompt_cache_key' was added to the key
source list but validateKeySources currently treats it as invalid, preventing
saves; update the validation logic in SettingsChannelAffinity.jsx so
validateKeySources recognizes 'claude_prompt_cache_key' as a valid source (and
any related allow-list or switch handling used by the save routine), ensuring
the same symbol name ('claude_prompt_cache_key') is accepted wherever key
sources are validated before saving.
| function getKeySourceInputPlaceholder(src: KeySource): string { | ||
| if (src.type === 'claude_prompt_cache_key') return 'No parameter required' | ||
| if (src.type === 'gjson') return 'metadata.conversation_id' | ||
| return 'user_id' | ||
| } |
There was a problem hiding this comment.
Translate the new placeholder text.
Line 116 hardcodes a user-facing string (No parameter required) and bypasses i18n.
Suggested fix
-function getKeySourceInputPlaceholder(src: KeySource): string {
- if (src.type === 'claude_prompt_cache_key') return 'No parameter required'
+function getKeySourceInputPlaceholder(
+ src: KeySource,
+ translate: (key: string) => string
+): string {
+ if (src.type === 'claude_prompt_cache_key')
+ return translate('No parameter required')
if (src.type === 'gjson') return 'metadata.conversation_id'
return 'user_id'
}
@@
- placeholder={getKeySourceInputPlaceholder(src)}
+ placeholder={getKeySourceInputPlaceholder(src, t)}As per coding guidelines: web/default/**/*.{tsx,ts} requires all user-facing text content to support i18n using t() from useTranslation() in React components.
🤖 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/system-settings/general/channel-affinity/rule-editor-dialog.tsx`
around lines 115 - 119, The placeholder string in getKeySourceInputPlaceholder
is hardcoded and must be localized; update getKeySourceInputPlaceholder to not
return raw user-facing strings but either accept a translation function (t:
TFunction) as a parameter or return stable translation keys (e.g.,
'placeholder.noParam', 'placeholder.metadataConversationId',
'placeholder.userId') and then call t(...) from the React component that uses
getKeySourceInputPlaceholder; update callers of getKeySourceInputPlaceholder
(where it's used in the rule-editor-dialog UI) to pass useTranslation().t or to
translate the returned keys before rendering so all user-facing text uses t().
| const channelIdListString = z.string().refine((value) => { | ||
| const trimmed = value.trim() | ||
| if (!trimmed) return true | ||
| return trimmed | ||
| .split(/[,\s;]+/) | ||
| .filter(Boolean) | ||
| .every((item) => /^[1-9]\d*$/.test(item)) | ||
| }, 'Enter positive channel IDs separated by commas or line breaks') | ||
|
|
There was a problem hiding this comment.
Localize the new excluded-channel-ID validation message.
The new Zod error string is hardcoded in English, so non-English locales won’t get translated validation feedback. Please source this message from i18n (t(...)) via a schema factory or localized error mapping.
As per coding guidelines web/default/**/*.{tsx,ts}: “All user-facing text content must support i18n using the t() function from useTranslation() in React components”.
🤖 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/system-settings/integrations/monitoring-settings-section.tsx`
around lines 49 - 57, The hardcoded Zod error message in channelIdListString
(the z.string().refine call) must be replaced with a localized message supplied
via i18n; refactor this by creating a schema factory or a small helper that
accepts the t function (from useTranslation) and returns the zod string schema
with the refine error message set to t('...') (or map Zod errors to localized
strings), then use that factory where channelIdListString is currently defined
so all validation messages call t(...) instead of the hardcoded English string.
|
@seefs001 你好,这个 PR 现在 CodeRabbit 检查已通过,最新 review 也没有新的 actionable comments,目前只卡在 required approving review。方便的时候可以帮忙看一下吗?我是第一次向这个项目提交 PR,如果有什么需要做的请告知我,谢谢! CodeRabbit checks are passing, and the PR is currently blocked only by the required approving review. |
Important
This description is manually organized around the code changes and runtime behavior. It is not a raw AI-generated dump.
📝 变更描述 / Description
This PR improves the Claude-compatible -> OpenAI Responses relay path and the cache-affinity behavior around it.
Main changes:
/v1/responsesrequests.web_searchsemantics across Claude/Responses compatibility flows instead of degrading it into a normal function tool.claude_prompt_cache_keyas a channel-affinity key source, so Claude/Codex-style traces can stay on the same successful channel.Why it works:
prompt_cache_key, stable metadata, headers, or Claude cache-control prefix information where available.🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
#4150is related to configurable Claude Code cache affinity. This PR is broader and also includes Claude -> Responses direct conversion, Responses prompt cache key derivation, server-side web_search preservation, and scheduled channel test exclusions.✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
Usage examples
Model mapping
A Claude-compatible model can be mapped to the actual upstream Responses model. For example:
cc-gpt-5.5gpt-5.5ChatCompletions -> Responses compatibility
Channels can opt into the Responses compatibility path with a policy like:
{ "enabled": true, "all_channels": false, "channel_ids": [1], "model_patterns": ["^cc-gpt-5[.]5$"] }Channel affinity
Example Claude CLI trace rule:
^claude-.*$/v1/messagesgjson: metadata.user_idclaude_prompt_cache_keyThe UI now exposes
claude_prompt_cache_keyas a key source option.OpenAI Responses prompt cache key
For
/v1/responses, the relay can derive a prompt cache key from stable request metadata or headers when the request does not already provideprompt_cache_key.Scheduled channel test exclusions
The monitoring settings page now supports excluding specific channel IDs from scheduled channel tests. Manual channel tests are not affected.
Local verification
Passed after removing the out-of-scope plugin usage endpoint:
go test ./controller ./relay ./relay/channel/codex ./relay/channel/openai ./relay/common ./service ./service/openaicompat ./setting/operation_settingPreviously passed before this scope cleanup:
Known baseline note:
The broader relay test suite has existing failures on
origin/maininrelay/channel/claudeandrelay/helper; this PR did not introduce those failures.Screenshots
Screenshots show:

cc-gpt-5.5togpt-5.5.metadata.user_idandclaude_prompt_cache_key.claude_prompt_cache_keyoption in the rule editor key-source dropdown.Summary by CodeRabbit
New Features
UI & Settings
Bug Fixes