Skip to content

feat: improve channel override ui/ux - #3009

Merged
seefs001 merged 14 commits into
QuantumNous:mainfrom
seefs001:feature/improve-param-override
Feb 28, 2026
Merged

feat: improve channel override ui/ux#3009
seefs001 merged 14 commits into
QuantumNous:mainfrom
seefs001:feature/improve-param-override

Conversation

@seefs001

@seefs001 seefs001 commented Feb 25, 2026

Copy link
Copy Markdown
Collaborator

参数覆盖增加更多操作项,支持请求体和请求头同时操作。
引入新的UI编辑界面。
渠道亲和规则支持绑定参数覆盖,可适用于claude code/codex cli自动透传请求头。
支持对anthropic-beta类似的请求头进行深度的操作,预设模版

Summary by CodeRabbit

  • New Features

    • Expanded parameter-override system: header pass-through/templates, runtime header overrides, object pruning, field sync, and per-rule channel-affinity templates.
    • UI: richer Param Override editor with visual editing, JSON preview, copy, and template helpers; CLI header passthrough templates surfaced.
  • Bug Fixes

    • Improved error mapping and retry/last-error tracking for clearer override failure responses.
  • Tests

    • Large test suite added covering override operations, header handling, templates, and merge behaviors.

@coderabbitai

coderabbitai Bot commented Feb 25, 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

Replaces ApplyParamOverride usage with ApplyParamOverrideWithRelayInfo across handlers; significantly expands the param-override subsystem (new modes, header/runtime override handling, pruning, sync_fields, structured return_error); enriches RelayInfo with headers, retry/last-error and runtime header overrides; wires per-rule channel-affinity param templates and UI editors; updates middleware, tests, and header override normalization.

Changes

Cohort / File(s) Summary
Param override core & tests
relay/common/override.go, relay/common/override_test.go
Major expansion: new ParamOperation modes (return_error, prune_objects, set/delete/copy/move_header, pass_headers, sync_fields), ParamOverrideReturnError type + helpers, ApplyParamOverrideWithRelayInfo public API, rich header/runtime-context helpers, object-prune/sync implementations, and many unit tests.
Relay info & retry state
relay/common/relay_info.go, controller/relay.go
RelayInfo gains RequestHeaders, RetryIndex, LastError, RuntimeHeadersOverride, UseRuntimeHeadersOverride; relay initializes/updates RetryIndex and LastError during retry loops.
Handler callsites switched to RelayInfo-aware overrides
relay/chat_completions_via_responses.go, relay/claude_handler.go, relay/compatible_handler.go, relay/embedding_handler.go, relay/gemini_handler.go, relay/image_handler.go, relay/rerank_handler.go, relay/responses_handler.go
Replaced ApplyParamOverride(..., BuildParamOverrideContext(...)) with ApplyParamOverrideWithRelayInfo(jsonData, info) and standardized error translation via newAPIErrorFromParamOverride.
Param override error translation
relay/param_override_error.go
New helper newAPIErrorFromParamOverride that maps ParamOverrideReturnError to types.NewAPIError or falls back to ChannelParamOverrideInvalid with skip-retry behavior.
Channel header override processing & tests
relay/channel/api_request.go, relay/channel/api_request_test.go
Nil-safe header override sourcing via GetEffectiveHeaderOverride, normalize/lowercase header keys, iterate headerOverrideSource consistently; tests updated/added for lowercase keys, runtime header override precedence, and pass-headers behavior.
Channel test path
controller/channel-test.go
Use common.Marshal and ApplyParamOverrideWithRelayInfo; added branch mapping ParamOverrideReturnError to testResult.localErr and a translated API error.
Channel-affinity templates & middleware
service/channel_affinity.go, service/channel_affinity_template_test.go, middleware/distributor.go, setting/operation_setting/channel_affinity_setting.go
Add ParamTemplate/ParamOverrideTemplate to affinity meta and settings; clone/merge helpers, ApplyChannelAffinityOverrideTemplate to merge per-rule templates into param overrides; middleware conditionally applies template; tests for template application and logging.
Frontend: param override UI & templates
web/src/components/.../EditChannelModal.jsx, web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx, web/src/constants/channel-affinity-template.constants.js, web/src/constants/index.js, web/package.json
Add ParamOverrideEditorModal, preview/copy UI, parsing/merge helpers, constants/templates for channel-affinity header passthrough, UI to edit/preview param_override_template; small package.json axios version change.
Tests & coverage
relay/common/override_test.go, relay/channel/api_request_test.go, service/channel_affinity_template_test.go
Extensive new tests covering return_error, prune_objects, header ops, sync_fields, runtime header merging, and channel-affinity template merging.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(52,120,246,0.5)
    participant Client
  end
  rect rgba(34,197,94,0.5)
    participant Middleware
  end
  rect rgba(236,72,153,0.5)
    participant Relay
  end
  rect rgba(249,115,22,0.5)
    participant ParamOverrideEngine
  end
  rect rgba(107,114,128,0.5)
    participant Upstream
  end

  Client->>Middleware: HTTP request (headers, body)
  Middleware->>Relay: Setup RelayInfo (RequestHeaders, Channel overrides, ParamTemplate)
  Relay->>ParamOverrideEngine: ApplyParamOverrideWithRelayInfo(jsonData, RelayInfo)
  ParamOverrideEngine-->>Relay: modified jsonData OR ParamOverrideReturnError
  alt override returned API error
    Relay->>Client: respond with translated API error (newAPIErrorFromParamOverride)
  else success
    Relay->>Upstream: forward modified request (headers/body)
    Upstream-->>Client: upstream response
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • #2568 — expands ParamOperation modes and override engine in relay/common/override.go (direct overlap).
  • #2669 — modifies channel-affinity subsystem and templates (strongly related to ParamTemplate/affinity changes).
  • #2627 — touches controller/channel-test.go and param-override error handling (overlapping edits to test path).

Suggested reviewers

  • creamlike1024
  • Calcium-Ion

Poem

🐰 I nibble headers, tuck them tight,
Templates hum beneath moonlight,
Overrides hop from rule to relay,
Retry counts mark the rabbit's way,
Sync, prune, and pass — I celebrate today! 🎋

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: improve channel override ui/ux' is vague and generic—it refers to improving UI/UX but doesn't clearly convey the specific, substantial changes made across the codebase (new param override operations, header override normalization, channel affinity templates, etc.). Consider a more specific title that highlights the main technical changes, such as 'feat: add parameter override operations and channel affinity templates' or 'feat: expand parameter override capabilities with new operations and UI' to better reflect the scope.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
relay/common/override.go (1)

116-130: ⚠️ Potential issue | 🟠 Major

Invalid operations config is silently treated as legacy override.

When operations exists but parse fails (e.g., Line [201]-Line [204]), tryParseOperations returns ok=false, and ApplyParamOverride falls back to applyOperationsLegacy on Line [129]. This masks misconfiguration and mutates payload in an unintended way instead of failing fast.

🛠️ Proposed fix
-func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, conditionContext map[string]interface{}) ([]byte, error) {
+func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, conditionContext map[string]interface{}) ([]byte, error) {
 	if len(paramOverride) == 0 {
 		return jsonData, nil
 	}

-	// 尝试断言为操作格式
-	if operations, ok := tryParseOperations(paramOverride); ok {
+	operations, ok, parseErr := tryParseOperations(paramOverride)
+	if parseErr != nil {
+		return nil, parseErr
+	}
+	if ok {
 		// 使用新方法
 		result, err := applyOperations(string(jsonData), operations, conditionContext)
 		return []byte(result), err
 	}

 	// 直接使用旧方法
 	return applyOperationsLegacy(jsonData, paramOverride)
 }

-func tryParseOperations(paramOverride map[string]interface{}) ([]ParamOperation, bool) {
+func tryParseOperations(paramOverride map[string]interface{}) ([]ParamOperation, bool, error) {
 	// 检查是否包含 "operations" 字段
 	if opsValue, exists := paramOverride["operations"]; exists {
 		if opsSlice, ok := opsValue.([]interface{}); ok {
 			var operations []ParamOperation
 			for _, op := range opsSlice {
 				if opMap, ok := op.(map[string]interface{}); ok {
 					operation := ParamOperation{}
@@
 					if conditions, exists := opMap["conditions"]; exists {
 						parsedConditions, err := parseConditionOperations(conditions)
 						if err != nil {
-							return nil, false
+							return nil, false, fmt.Errorf("invalid operations.conditions: %w", err)
 						}
 						operation.Conditions = append(operation.Conditions, parsedConditions...)
 					}

 					operations = append(operations, operation)
 				} else {
-					return nil, false
+					return nil, false, fmt.Errorf("operation must be object")
 				}
 			}
-			return operations, true
+			return operations, true, nil
 		}
+		return nil, false, fmt.Errorf("operations must be array")
 	}

-	return nil, false
+	return nil, false, nil
 }

Also applies to: 161-218

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/common/override.go` around lines 116 - 130, The function
ApplyParamOverride currently falls back to applyOperationsLegacy when
tryParseOperations fails, silently masking malformed "operations" configs;
update ApplyParamOverride to detect if paramOverride contains the "operations"
key and, when tryParseOperations returns ok==false, return a descriptive error
instead of falling back to applyOperationsLegacy; specifically, in
ApplyParamOverride check for _, exists := paramOverride["operations"] (or
equivalent), and if exists && !ok return an error mentioning invalid operations
payload, otherwise proceed to call applyOperations (on success) or
applyOperationsLegacy (when no "operations" key).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/channel/api_request.go`:
- Around line 176-179: The code sets headerOverrideSource to
info.RuntimeHeadersOverride when info.UseRuntimeHeadersOverride is true, but if
RuntimeHeadersOverride is nil that silently discards info.HeadersOverride;
change the selection to nil-guard and fallback: when
info.UseRuntimeHeadersOverride is true and info.RuntimeHeadersOverride is
non-nil use it, otherwise fall back to info.HeadersOverride (and ensure
headerOverrideSource is never left nil before iterating). Update the logic
around headerOverrideSource (the variables info.HeadersOverride,
info.RuntimeHeadersOverride and flag UseRuntimeHeadersOverride) to perform this
guarded selection so static overrides are preserved when runtime overrides are
nil.

In `@web/package.json`:
- Line 13: Update the axios dependency in package.json from "1.12.0" to a
patched release (at least "1.13.5") to address CVE-2026-25639; modify the
"axios" entry so the version string is "1.13.5" (or a later compatible semver
range) and run npm/yarn install to update lockfile and verify builds/tests that
reference axios in the project.

In `@web/src/components/table/channels/modals/EditChannelModal.jsx`:
- Around line 707-760: In applyParamOverrideTemplate, parseParamOverrideInput()
is called unconditionally which causes 'fill' operations to fail when current
JSON is invalid; modify applyParamOverrideTemplate so it checks applyMode (and
templateType for 'legacy') first and returns the fill content via
handleInputChange('param_override', JSON.stringify(...)) without calling
parseParamOverrideInput(), and only call parseParamOverrideInput() when
applyMode !== 'fill' and you need to merge with existing content (used in the
legacy merge branch and operations-merge branch) so parsing is only attempted
when actually required.

---

Outside diff comments:
In `@relay/common/override.go`:
- Around line 116-130: The function ApplyParamOverride currently falls back to
applyOperationsLegacy when tryParseOperations fails, silently masking malformed
"operations" configs; update ApplyParamOverride to detect if paramOverride
contains the "operations" key and, when tryParseOperations returns ok==false,
return a descriptive error instead of falling back to applyOperationsLegacy;
specifically, in ApplyParamOverride check for _, exists :=
paramOverride["operations"] (or equivalent), and if exists && !ok return an
error mentioning invalid operations payload, otherwise proceed to call
applyOperations (on success) or applyOperationsLegacy (when no "operations"
key).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a4cf0a and db0b452.

📒 Files selected for processing (19)
  • controller/channel-test.go
  • controller/relay.go
  • relay/channel/api_request.go
  • relay/channel/api_request_test.go
  • relay/chat_completions_via_responses.go
  • relay/claude_handler.go
  • relay/common/override.go
  • relay/common/override_test.go
  • relay/common/relay_info.go
  • relay/compatible_handler.go
  • relay/embedding_handler.go
  • relay/gemini_handler.go
  • relay/image_handler.go
  • relay/param_override_error.go
  • relay/rerank_handler.go
  • relay/responses_handler.go
  • web/package.json
  • web/src/components/table/channels/modals/EditChannelModal.jsx
  • web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx

Comment thread relay/channel/api_request.go Outdated
Comment thread web/package.json
"@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8",
"axios": "1.13.5",
"axios": "1.12.0",

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

🧩 Analysis chain

🏁 Script executed:

cat -n web/package.json | head -20

Repository: QuantumNous/new-api

Length of output: 768


🏁 Script executed:

cd web && git log -p --follow -S "axios" -- package.json | head -100

Repository: QuantumNous/new-api

Length of output: 45


🌐 Web query:

axios 1.13.5 security fix DoS mergeConfig __proto__

💡 Result:

Axios v1.13.5 includes a security fix for CVE-2026-25639 / GHSA-43fc-jf86-j433: a Denial of Service (DoS) crash triggered when mergeConfig processes a config object that has an own __proto__ property (commonly introduced via JSON.parse('{"__proto__": {...}}')). In vulnerable versions, mergeMap['__proto__'] resolves via the prototype chain to Object.prototype and is then invoked as a function, throwing TypeError: merge is not a function and potentially crashing a Node.js process. [1][2]

Affected: axios <= 1.13.4 (also <= 0.30.2)
Patched: 1.13.5 (also 0.30.3) [1][3]

What changed in the fix (PR #7369 / commit 28c7215):

  • mergeConfig now skips keys __proto__, constructor, and prototype.
  • It also ensures mergeMap[prop] is only used when prop is an own property of mergeMap (avoids prototype-chain lookups). [2][4]

Action: upgrade to axios 1.13.5+ (or 0.30.3+ if you’re on that line). [1][3]

Sources

  1. GitHub Security Advisory GHSA-43fc-jf86-j433 (axios)
  2. Axios v1.13.5 release notes
  3. NVD entry for CVE-2026-25639
  4. Fix commit (axios/axios@28c7215)

🌐 Web query:

axios 1.12.0 1.13.5 release notes changelog

💡 Result:

Axios v1.12.0 (released 2025-09-11) — release notes / changelog

Bug fixes: fetch-adapter Content-Type for Node FormData; enforce maxContentLength for data: URLs; package exports; URL param encoding change; type-guard change for isCancel; plus release/build housekeeping. [2]
Features: surface low-level network error details via cause; fetch adapter env config (fetch, Request, Response); JSON.parse reviver support; extend AxiosResponse to allow custom headers type. [2]
Security context: 1.12.0 is the patched version for GHSA-4hjh-wcwx-xvwj (DoS via large data: URL / lack of size check). [3]

Axios v1.13.5 (released 2026-02-08) — release notes / changelog

Highlights: security fix for DoS via __proto__ key in mergeConfig; fix missing AxiosError.status regression (on/after v1.13.3). [1]
Other changes: input validation for isAbsoluteURL; dependency bumps/refactors; docs + CI maintenance items. [1]

GitHub “Full Changelog” compare links (official):

v1.12.0: https://github.com/axios/axios/compare/v1.11.0...v1.12.0
v1.13.5: https://github.com/axios/axios/compare/v1.13.4...v1.13.5

Upgrade axios to a patched version to fix DoS vulnerability.

At Line 13, the pinned version 1.12.0 is vulnerable to CVE-2026-25639 (DoS crash via __proto__ key in mergeConfig), which was patched in 1.13.5. Upgrade to 1.13.5 or later.

🔧 Proposed change
-    "axios": "1.12.0",
+    "axios": "1.13.5",
📝 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
"axios": "1.12.0",
"axios": "1.13.5",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/package.json` at line 13, Update the axios dependency in package.json
from "1.12.0" to a patched release (at least "1.13.5") to address
CVE-2026-25639; modify the "axios" entry so the version string is "1.13.5" (or a
later compatible semver range) and run npm/yarn install to update lockfile and
verify builds/tests that reference axios in the project.

Comment on lines +707 to +760
const applyParamOverrideTemplate = (
templateType = 'operations',
applyMode = 'fill',
) => {
try {
const parsedCurrent = parseParamOverrideInput();
if (templateType === 'legacy') {
if (applyMode === 'fill') {
handleInputChange(
'param_override',
JSON.stringify(PARAM_OVERRIDE_LEGACY_TEMPLATE, null, 2),
);
return;
}
const currentLegacy =
parsedCurrent &&
typeof parsedCurrent === 'object' &&
!Array.isArray(parsedCurrent) &&
!Array.isArray(parsedCurrent.operations)
? parsedCurrent
: {};
const merged = {
...PARAM_OVERRIDE_LEGACY_TEMPLATE,
...currentLegacy,
};
handleInputChange('param_override', JSON.stringify(merged, null, 2));
return;
}

if (applyMode === 'fill') {
handleInputChange(
'param_override',
JSON.stringify(PARAM_OVERRIDE_OPERATIONS_TEMPLATE, null, 2),
);
return;
}
const currentOperations =
parsedCurrent &&
typeof parsedCurrent === 'object' &&
!Array.isArray(parsedCurrent) &&
Array.isArray(parsedCurrent.operations)
? parsedCurrent.operations
: [];
const merged = {
operations: [
...currentOperations,
...PARAM_OVERRIDE_OPERATIONS_TEMPLATE.operations,
],
};
handleInputChange('param_override', JSON.stringify(merged, null, 2));
} catch (error) {
showError(error.message || t('模板应用失败'));
}
};

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

Template “fill” should bypass current JSON parsing.

On Line [712], parseParamOverrideInput() runs before checking applyMode. With invalid current JSON, the fill actions on Line [736] and Line [713] fail instead of replacing content. Fill should work regardless of current validity.

💡 Proposed fix
 const applyParamOverrideTemplate = (
   templateType = 'operations',
   applyMode = 'fill',
 ) => {
   try {
-    const parsedCurrent = parseParamOverrideInput();
+    const parsedCurrent =
+      applyMode === 'fill' ? null : parseParamOverrideInput();
     if (templateType === 'legacy') {
       if (applyMode === 'fill') {
         handleInputChange(
           'param_override',
           JSON.stringify(PARAM_OVERRIDE_LEGACY_TEMPLATE, null, 2),
         );
         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.

Suggested change
const applyParamOverrideTemplate = (
templateType = 'operations',
applyMode = 'fill',
) => {
try {
const parsedCurrent = parseParamOverrideInput();
if (templateType === 'legacy') {
if (applyMode === 'fill') {
handleInputChange(
'param_override',
JSON.stringify(PARAM_OVERRIDE_LEGACY_TEMPLATE, null, 2),
);
return;
}
const currentLegacy =
parsedCurrent &&
typeof parsedCurrent === 'object' &&
!Array.isArray(parsedCurrent) &&
!Array.isArray(parsedCurrent.operations)
? parsedCurrent
: {};
const merged = {
...PARAM_OVERRIDE_LEGACY_TEMPLATE,
...currentLegacy,
};
handleInputChange('param_override', JSON.stringify(merged, null, 2));
return;
}
if (applyMode === 'fill') {
handleInputChange(
'param_override',
JSON.stringify(PARAM_OVERRIDE_OPERATIONS_TEMPLATE, null, 2),
);
return;
}
const currentOperations =
parsedCurrent &&
typeof parsedCurrent === 'object' &&
!Array.isArray(parsedCurrent) &&
Array.isArray(parsedCurrent.operations)
? parsedCurrent.operations
: [];
const merged = {
operations: [
...currentOperations,
...PARAM_OVERRIDE_OPERATIONS_TEMPLATE.operations,
],
};
handleInputChange('param_override', JSON.stringify(merged, null, 2));
} catch (error) {
showError(error.message || t('模板应用失败'));
}
};
const applyParamOverrideTemplate = (
templateType = 'operations',
applyMode = 'fill',
) => {
try {
const parsedCurrent =
applyMode === 'fill' ? null : parseParamOverrideInput();
if (templateType === 'legacy') {
if (applyMode === 'fill') {
handleInputChange(
'param_override',
JSON.stringify(PARAM_OVERRIDE_LEGACY_TEMPLATE, null, 2),
);
return;
}
const currentLegacy =
parsedCurrent &&
typeof parsedCurrent === 'object' &&
!Array.isArray(parsedCurrent) &&
!Array.isArray(parsedCurrent.operations)
? parsedCurrent
: {};
const merged = {
...PARAM_OVERRIDE_LEGACY_TEMPLATE,
...currentLegacy,
};
handleInputChange('param_override', JSON.stringify(merged, null, 2));
return;
}
if (applyMode === 'fill') {
handleInputChange(
'param_override',
JSON.stringify(PARAM_OVERRIDE_OPERATIONS_TEMPLATE, null, 2),
);
return;
}
const currentOperations =
parsedCurrent &&
typeof parsedCurrent === 'object' &&
!Array.isArray(parsedCurrent) &&
Array.isArray(parsedCurrent.operations)
? parsedCurrent.operations
: [];
const merged = {
operations: [
...currentOperations,
...PARAM_OVERRIDE_OPERATIONS_TEMPLATE.operations,
],
};
handleInputChange('param_override', JSON.stringify(merged, null, 2));
} catch (error) {
showError(error.message || t('模板应用失败'));
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/table/channels/modals/EditChannelModal.jsx` around lines
707 - 760, In applyParamOverrideTemplate, parseParamOverrideInput() is called
unconditionally which causes 'fill' operations to fail when current JSON is
invalid; modify applyParamOverrideTemplate so it checks applyMode (and
templateType for 'legacy') first and returns the fill content via
handleInputChange('param_override', JSON.stringify(...)) without calling
parseParamOverrideInput(), and only call parseParamOverrideInput() when
applyMode !== 'fill' and you need to merge with existing content (used in the
legacy merge branch and operations-merge branch) so parsing is only attempted
when actually required.

@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: 1

🧹 Nitpick comments (1)
setting/operation_setting/channel_affinity_setting.go (1)

37-59: Prevent backend/frontend template drift.

These header lists and rule templates are duplicated in web/src/constants/channel-affinity-template.constants.js. Consider sourcing defaults from one authoritative place (e.g., backend-provided defaults) to avoid silent divergence later.

Also applies to: 82-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setting/operation_setting/channel_affinity_setting.go` around lines 37 - 59,
The duplicated header lists codexCliPassThroughHeaders and
claudeCliPassThroughHeaders are causing template drift between backend and
frontend; centralize these defaults by exporting a single source of truth from
the backend and updating the frontend to consume it (or load them from a shared
config/endpoint) instead of hardcoding; modify the code around
codexCliPassThroughHeaders and claudeCliPassThroughHeaders to load values from
the new backend-provided constant or API (and remove the duplicate arrays), and
ensure the corresponding frontend constant in
channel-affinity-template.constants.js references that backend source.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx`:
- Around line 177-191: parseOptionalObjectJson currently returns dynamic Chinese
strings using the label; change it to return fixed i18n keys (and optional
params) instead of interpolated messages so translators remain stable.
Specifically, update parseOptionalObjectJson (and places using verifyJSON) to
return something like { ok: false, key: 'settings.json_invalid' } or { ok:false,
key: 'settings.json_must_be_object' , params: { label } } and then update the
caller that wraps results with t(...) to call t(result.key, result.params)
(using useTranslation in the component) rather than t(result.message). This
preserves the label as a param for interpolation while ensuring message keys are
static for translation.

---

Nitpick comments:
In `@setting/operation_setting/channel_affinity_setting.go`:
- Around line 37-59: The duplicated header lists codexCliPassThroughHeaders and
claudeCliPassThroughHeaders are causing template drift between backend and
frontend; centralize these defaults by exporting a single source of truth from
the backend and updating the frontend to consume it (or load them from a shared
config/endpoint) instead of hardcoding; modify the code around
codexCliPassThroughHeaders and claudeCliPassThroughHeaders to load values from
the new backend-provided constant or API (and remove the duplicate arrays), and
ensure the corresponding frontend constant in
channel-affinity-template.constants.js references that backend source.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db0b452 and 0519446.

📒 Files selected for processing (8)
  • middleware/distributor.go
  • service/channel_affinity.go
  • service/channel_affinity_template_test.go
  • setting/operation_setting/channel_affinity_setting.go
  • web/src/components/table/channels/modals/ParamOverrideEditorModal.jsx
  • web/src/constants/channel-affinity-template.constants.js
  • web/src/constants/index.js
  • web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx

Comment on lines +177 to +191
const parseOptionalObjectJson = (jsonString, label) => {
const raw = (jsonString || '').trim();
if (!raw) return { ok: true, value: null };
if (!verifyJSON(raw)) {
return { ok: false, message: `${label} JSON 格式不正确` };
}
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return { ok: false, message: `${label} 必须是 JSON 对象` };
}
return { ok: true, value: parsed };
} catch (error) {
return { ok: false, message: `${label} JSON 格式不正确` };
}

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

Use static i18n keys for validation errors.

Line 181, Line 186, and Line 190 compose messages dynamically (${label}...) and Line 703 sends them into t(...). Prefer fixed message keys/constants to keep translations stable and maintainable.

♻️ Suggested adjustment
+const MSG_PARAM_TEMPLATE_JSON_INVALID = '参数覆盖模板 JSON 格式不正确';
+const MSG_PARAM_TEMPLATE_JSON_OBJECT = '参数覆盖模板 必须是 JSON 对象';
+
-const parseOptionalObjectJson = (jsonString, label) => {
+const parseOptionalObjectJson = (jsonString) => {
   const raw = (jsonString || '').trim();
   if (!raw) return { ok: true, value: null };
   if (!verifyJSON(raw)) {
-    return { ok: false, message: `${label} JSON 格式不正确` };
+    return { ok: false, message: MSG_PARAM_TEMPLATE_JSON_INVALID };
   }
   try {
     const parsed = JSON.parse(raw);
     if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
-      return { ok: false, message: `${label} 必须是 JSON 对象` };
+      return { ok: false, message: MSG_PARAM_TEMPLATE_JSON_OBJECT };
     }
     return { ok: true, value: parsed };
   } catch (error) {
-    return { ok: false, message: `${label} JSON 格式不正确` };
+    return { ok: false, message: MSG_PARAM_TEMPLATE_JSON_INVALID };
   }
 };
...
-const paramTemplateValidation = parseOptionalObjectJson(
-  paramTemplateDraft,
-  '参数覆盖模板',
-);
+const paramTemplateValidation = parseOptionalObjectJson(paramTemplateDraft);

Based on learnings: Applies to web/src/**/*.{ts,tsx} : Use useTranslation() hook and call t('中文key') in React components for translation lookup.

Also applies to: 698-704

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx` around lines 177
- 191, parseOptionalObjectJson currently returns dynamic Chinese strings using
the label; change it to return fixed i18n keys (and optional params) instead of
interpolated messages so translators remain stable. Specifically, update
parseOptionalObjectJson (and places using verifyJSON) to return something like {
ok: false, key: 'settings.json_invalid' } or { ok:false, key:
'settings.json_must_be_object' , params: { label } } and then update the caller
that wraps results with t(...) to call t(result.key, result.params) (using
useTranslation in the component) rather than t(result.message). This preserves
the label as a param for interpolation while ensuring message keys are static
for translation.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
relay/common/override.go (1)

118-131: ⚠️ Potential issue | 🟠 Major

Do not silently fall back to legacy mode when operations parsing fails.

When condition parsing fails (Line 203), tryParseOperations returns false, and ApplyParamOverride falls back to applyOperationsLegacy (Line 130). That turns an invalid override config into a payload merge instead of a hard failure, which can bypass intended override behavior.

Proposed fix
-func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, conditionContext map[string]interface{}) ([]byte, error) {
+func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, conditionContext map[string]interface{}) ([]byte, error) {
 	if len(paramOverride) == 0 {
 		return jsonData, nil
 	}
-
-	// 尝试断言为操作格式
-	if operations, ok := tryParseOperations(paramOverride); ok {
-		// 使用新方法
-		result, err := applyOperations(string(jsonData), operations, conditionContext)
-		return []byte(result), err
-	}
+	if opsValue, exists := paramOverride["operations"]; exists {
+		operations, err := tryParseOperationsValue(opsValue)
+		if err != nil {
+			return nil, err
+		}
+		result, err := applyOperations(string(jsonData), operations, conditionContext)
+		return []byte(result), err
+	}

 	// 直接使用旧方法
 	return applyOperationsLegacy(jsonData, paramOverride)
 }
-func tryParseOperations(paramOverride map[string]interface{}) ([]ParamOperation, bool) {
-	if opsValue, exists := paramOverride["operations"]; exists {
+func tryParseOperationsValue(opsValue interface{}) ([]ParamOperation, error) {
+	if opsValue != nil {
 		if opsSlice, ok := opsValue.([]interface{}); ok {
 			var operations []ParamOperation
 			for _, op := range opsSlice {
 				if opMap, ok := op.(map[string]interface{}); ok {
 					operation := ParamOperation{}
@@
-					if mode, ok := opMap["mode"].(string); ok {
+					if mode, ok := opMap["mode"].(string); ok {
 						operation.Mode = mode
 					} else {
-						return nil, false // mode 是必需的
+						return nil, fmt.Errorf("operation mode is required")
 					}
@@
 					if conditions, exists := opMap["conditions"]; exists {
 						parsedConditions, err := parseConditionOperations(conditions)
 						if err != nil {
-							return nil, false
+							return nil, err
 						}
 						operation.Conditions = append(operation.Conditions, parsedConditions...)
 					}
@@
-					return nil, false
+					return nil, fmt.Errorf("operation item must be object")
 				}
 			}
-			return operations, true
+			return operations, nil
 		}
+		return nil, fmt.Errorf("operations must be array")
 	}
-
-	return nil, false
+	return nil, fmt.Errorf("operations value is required")
 }

Also applies to: 203-206

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/common/override.go` around lines 118 - 131, The ApplyParamOverride
function currently silently falls back to applyOperationsLegacy when
tryParseOperations fails; change this so a failed parse returns a clear error
instead of treating the override as legacy mode. Update ApplyParamOverride to
detect a false ok from tryParseOperations and return a descriptive error
(including the original paramOverride or reason) rather than calling
applyOperationsLegacy; alternatively adjust tryParseOperations to return
(operations, error) and propagate that error into ApplyParamOverride so callers
see a hard failure instead of a silent payload merge (referencing
ApplyParamOverride, tryParseOperations, applyOperations, and
applyOperationsLegacy).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/channel/api_request_test.go`:
- Around line 148-156: The test never supplies the "X-Codex-Beta-Features"
header so the exclusion branch for that header in the pass_headers check isn't
exercised; update the test to set
ctx.Request.Header.Set("X-Codex-Beta-Features", "some-value") and include the
same key/value in info.RequestHeaders (the RelayInfo struct) before calling the
code that uses pass_headers, then assert that the header is excluded as expected
(or that code paths 177-178 and 184-185 behave accordingly) to validate the
exclusion logic.

In `@relay/common/override.go`:
- Around line 668-680: moveHeaderInContext currently deletes the source header
only from override maps (via deleteHeaderOverrideInContext) so
getHeaderValueFromContext can still read the original value from
request_headers/request_headers_raw; update moveHeaderInContext to, after
calling copyHeaderInContext and when fromHeader != toHeader, also remove the
source header keys from any request header storage (request_headers and
request_headers_raw) in the context so the header is truly moved. Ensure you use
the same header normalization as copyHeaderInContext/getHeaderValueFromContext
when deleting, and apply the same fix to the other move-header codepaths that
mirror this logic (the other move_header blocks that call
copyHeaderInContext/deleteHeaderOverrideInContext) so all request-sourced
headers are removed from request_* maps when moved.

---

Outside diff comments:
In `@relay/common/override.go`:
- Around line 118-131: The ApplyParamOverride function currently silently falls
back to applyOperationsLegacy when tryParseOperations fails; change this so a
failed parse returns a clear error instead of treating the override as legacy
mode. Update ApplyParamOverride to detect a false ok from tryParseOperations and
return a descriptive error (including the original paramOverride or reason)
rather than calling applyOperationsLegacy; alternatively adjust
tryParseOperations to return (operations, error) and propagate that error into
ApplyParamOverride so callers see a hard failure instead of a silent payload
merge (referencing ApplyParamOverride, tryParseOperations, applyOperations, and
applyOperationsLegacy).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0519446 and bb0c663.

📒 Files selected for processing (3)
  • relay/channel/api_request_test.go
  • relay/common/override.go
  • relay/common/override_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • relay/common/override_test.go

Comment thread relay/channel/api_request_test.go
Comment thread relay/common/override.go
@seefs001
seefs001 marked this pull request as draft February 25, 2026 08:11

@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

♻️ Duplicate comments (1)
relay/common/override.go (1)

758-770: ⚠️ Potential issue | 🟠 Major

move_header still behaves like copy_header for request-sourced headers.

After move, the source header is removed from override maps only. It remains readable from request_headers / request_headers_raw, so subsequent operations can still consume it.

💡 Proposed fix
 func moveHeaderInContext(context map[string]interface{}, fromHeader, toHeader string, keepOrigin bool) error {
@@
 	if strings.EqualFold(fromHeader, toHeader) {
 		return nil
 	}
-	return deleteHeaderOverrideInContext(context, fromHeader)
+	if err := deleteHeaderOverrideInContext(context, fromHeader); err != nil {
+		return err
+	}
+	deleteHeaderFromRequestContext(context, fromHeader)
+	return nil
 }
+
+func deleteHeaderFromRequestContext(context map[string]interface{}, headerName string) {
+	for key := range ensureMapKeyInContext(context, paramOverrideContextRequestHeadersRaw) {
+		if strings.EqualFold(strings.TrimSpace(key), headerName) {
+			delete(ensureMapKeyInContext(context, paramOverrideContextRequestHeadersRaw), key)
+		}
+	}
+	normalized := normalizeHeaderContextKey(headerName)
+	if normalized != "" {
+		delete(ensureMapKeyInContext(context, paramOverrideContextRequestHeaders), normalized)
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/common/override.go` around lines 758 - 770, moveHeaderInContext
currently only removes the source header from override maps (via
deleteHeaderOverrideInContext) so headers coming from
request_headers/request_headers_raw remain readable; update moveHeaderInContext
to also remove the source header from request-sourced maps when a true move is
performed: after copyHeaderInContext and the EqualFold check, delete the header
key from context["request_headers"] and context["request_headers_raw"] (handle
case-insensitive keys or both raw/normalized forms as used elsewhere) so the
header is fully moved; reference moveHeaderInContext, copyHeaderInContext,
deleteHeaderOverrideInContext, request_headers and request_headers_raw.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/common/override.go`:
- Around line 732-734: When setting header_override in override.go,
ensureMapKeyInContext returns rawHeaders but you must deduplicate existing keys
case-insensitively before inserting headerName/headerValue; scan rawHeaders for
an existingKey where strings.EqualFold(existingKey, headerName) and if found and
existingKey != headerName delete rawHeaders[existingKey] then set
rawHeaders[headerName] = headerValue. This preserves deterministic overwrite
semantics for subsequent reads/merges while still using ensureMapKeyInContext
and paramOverrideContextHeaderOverride.
- Around line 282-285: The current code silently falls back to legacy override
behavior when parseConditionOperations(conditions) errors, allowing the raw
operations payload to be injected; change this to fail fast by returning the
parse error (or a wrapped error) instead of returning nil, false so callers
cannot continue into legacy processing—specifically update the handling around
parseConditionOperations/parsedConditions to return the error to the caller (or
propagate it via the function's error return) and ensure any code path that
would inject the original operations payload is skipped when parsing fails.

---

Duplicate comments:
In `@relay/common/override.go`:
- Around line 758-770: moveHeaderInContext currently only removes the source
header from override maps (via deleteHeaderOverrideInContext) so headers coming
from request_headers/request_headers_raw remain readable; update
moveHeaderInContext to also remove the source header from request-sourced maps
when a true move is performed: after copyHeaderInContext and the EqualFold
check, delete the header key from context["request_headers"] and
context["request_headers_raw"] (handle case-insensitive keys or both
raw/normalized forms as used elsewhere) so the header is fully moved; reference
moveHeaderInContext, copyHeaderInContext, deleteHeaderOverrideInContext,
request_headers and request_headers_raw.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb0c663 and 305dbce.

📒 Files selected for processing (5)
  • relay/channel/api_request.go
  • relay/channel/api_request_test.go
  • relay/common/override.go
  • relay/common/override_test.go
  • relay/common/relay_info.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • relay/channel/api_request.go
  • relay/channel/api_request_test.go

Comment thread relay/common/override.go Outdated
Comment on lines 282 to 285
parsedConditions, err := parseConditionOperations(conditions)
if err != nil {
return nil, false
}

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

Do not silently downgrade invalid operations to legacy override behavior.

When condition parsing fails here, control falls back to legacy mode, which can inject the raw operations payload into the request body instead of failing fast.

💡 Proposed fix
 func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, conditionContext map[string]interface{}) ([]byte, error) {
 	if len(paramOverride) == 0 {
 		return jsonData, nil
 	}
+	_, hasOperations := paramOverride["operations"]

 	// 尝试断言为操作格式
 	if operations, ok := tryParseOperations(paramOverride); ok {
 		// 使用新方法
 		result, err := applyOperations(string(jsonData), operations, conditionContext)
 		return []byte(result), err
 	}
+	if hasOperations {
+		return nil, fmt.Errorf("invalid param_override.operations format")
+	}

 	// 直接使用旧方法
 	return applyOperationsLegacy(jsonData, paramOverride)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/common/override.go` around lines 282 - 285, The current code silently
falls back to legacy override behavior when parseConditionOperations(conditions)
errors, allowing the raw operations payload to be injected; change this to fail
fast by returning the parse error (or a wrapped error) instead of returning nil,
false so callers cannot continue into legacy processing—specifically update the
handling around parseConditionOperations/parsedConditions to return the error to
the caller (or propagate it via the function's error return) and ensure any code
path that would inject the original operations payload is skipped when parsing
fails.

Comment thread relay/common/override.go Outdated
Comment on lines +732 to +734
rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
rawHeaders[headerName] = headerValue

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

Deduplicate case-insensitive header keys when setting header_override.

This write path can leave both X-Trace and x-trace in the raw map, which breaks deterministic overwrite semantics for later reads/merges.

💡 Proposed fix
 	rawHeaders := ensureMapKeyInContext(context, paramOverrideContextHeaderOverride)
-	rawHeaders[headerName] = headerValue
+	setHeaderOverrideEntry(rawHeaders, headerName, headerValue)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/common/override.go` around lines 732 - 734, When setting
header_override in override.go, ensureMapKeyInContext returns rawHeaders but you
must deduplicate existing keys case-insensitively before inserting
headerName/headerValue; scan rawHeaders for an existingKey where
strings.EqualFold(existingKey, headerName) and if found and existingKey !=
headerName delete rawHeaders[existingKey] then set rawHeaders[headerName] =
headerValue. This preserves deterministic overwrite semantics for subsequent
reads/merges while still using ensureMapKeyInContext and
paramOverrideContextHeaderOverride.

@seefs001
seefs001 marked this pull request as ready for review February 25, 2026 09:32

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
relay/channel/api_request.go (1)

181-196: ⚠️ Potential issue | 🟠 Major

Regex passthrough can miss matching headers due case mismatch.

Rule keys are normalized to lowercase, but regex matching is done against the original header name (X-Trace-Id style). This breaks many lowercase regex rules unless callers explicitly add case-insensitive regex flags.

💡 Proposed fix
 		for name := range c.Request.Header {
+			nameLower := strings.ToLower(strings.TrimSpace(name))
 			if shouldSkipPassthroughHeader(name) {
 				continue
 			}
 			if !passAll {
 				matched := false
 				for _, re := range passthroughRegex {
-					if re.MatchString(name) {
+					if re.MatchString(nameLower) {
 						matched = true
 						break
 					}
 				}
@@
-			headerOverride[strings.ToLower(strings.TrimSpace(name))] = value
+			headerOverride[nameLower] = value
 		}
#!/bin/bash
# Verify that regex rules are lowercased, while matching still uses raw header names.
rg -n 'for k := range headerOverrideSource|strings.ToLower\(k\)|MatchString\(name\)|strings.ToLower\(strings.TrimSpace\(name\)\)' relay/channel/api_request.go

Expected: matches showing key normalization and MatchString(name) in the same flow.

Also applies to: 223-224, 236-236

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/api_request.go` around lines 181 - 196, The regex passthrough
keys are normalized to lowercase (headerOverrideSource loop,
headerPassthroughRegexPrefix/headerPassthroughRegexPrefixV2 -> pattern) but
matching still uses the raw header name (MatchString(name)), causing
case-mismatches; fix by normalizing the header name before regex matching (e.g.,
run strings.ToLower(strings.TrimSpace(name)) prior to calling the compiled
regex's MatchString) or ensure the compiled pattern is case-insensitive (prepend
(?i) when building the regex from pattern) wherever pattern is used (including
the other occurrences around the same flow/variables).
♻️ Duplicate comments (4)
relay/common/override.go (3)

198-201: ⚠️ Potential issue | 🟠 Major

Guard runtime-header mode when runtime map is nil.

When UseRuntimeHeadersOverride is true and RuntimeHeadersOverride is nil, this returns an empty effective map and silently drops static channel header overrides.

💡 Proposed fix
 func GetEffectiveHeaderOverride(info *RelayInfo) map[string]interface{} {
 	if info == nil {
 		return map[string]interface{}{}
 	}
-	if info.UseRuntimeHeadersOverride {
+	if info.UseRuntimeHeadersOverride && info.RuntimeHeadersOverride != nil {
 		return sanitizeHeaderOverrideMap(info.RuntimeHeadersOverride)
 	}
 	return sanitizeHeaderOverrideMap(getHeaderOverrideMap(info))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/common/override.go` around lines 198 - 201, If
UseRuntimeHeadersOverride is true but RuntimeHeadersOverride is nil the current
branch returns sanitizeHeaderOverrideMap(nil) and silently drops static
overrides; update the conditional in the code that checks
info.UseRuntimeHeadersOverride to also guard for a non-nil
info.RuntimeHeadersOverride (i.e. only call
sanitizeHeaderOverrideMap(info.RuntimeHeadersOverride) when
RuntimeHeadersOverride != nil), otherwise fall back to
sanitizeHeaderOverrideMap(getHeaderOverrideMap(info)) so static channel
overrides are preserved; reference the symbols UseRuntimeHeadersOverride,
RuntimeHeadersOverride, sanitizeHeaderOverrideMap, and getHeaderOverrideMap to
locate and change the branch logic.

727-739: ⚠️ Potential issue | 🟠 Major

move_header still behaves like copy_header for request-sourced headers.

The source key is removed only from header_override. Subsequent reads still resolve it from request_headers, so later operations can keep consuming a header that should have been moved.

💡 Proposed fix
 func moveHeaderInContext(context map[string]interface{}, fromHeader, toHeader string, keepOrigin bool) error {
@@
 	if strings.EqualFold(fromHeader, toHeader) {
 		return nil
 	}
-	return deleteHeaderOverrideInContext(context, fromHeader)
+	if err := deleteHeaderOverrideInContext(context, fromHeader); err != nil {
+		return err
+	}
+	delete(ensureMapKeyInContext(context, paramOverrideContextRequestHeaders), fromHeader)
+	return nil
 }

Also applies to: 966-983

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/common/override.go` around lines 727 - 739, The moveHeaderInContext
currently only deletes the key from header_override, leaving the original value
in request_headers so consumers still see it; update moveHeaderInContext (and/or
deleteHeaderOverrideInContext) so that when moving (i.e., keepOrigin == false
and fromHeader != toHeader) you also remove the normalized fromHeader key from
the request_headers map in context (look up context["request_headers"] as
map[string]interface{} and delete the key if present) after copyHeaderInContext
succeeds, ensuring the header is truly moved rather than still readable from
request_headers; keep the current behavior when keepOrigin == true.

116-129: ⚠️ Potential issue | 🟠 Major

Fail fast when operations exists but parsing fails.

If operations is present and condition parsing fails, this returns (nil, false) and falls back to legacy merge, which can inject raw operations into the request instead of rejecting invalid config.

💡 Proposed fix
-func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, conditionContext map[string]interface{}) ([]byte, error) {
+func ApplyParamOverride(jsonData []byte, paramOverride map[string]interface{}, conditionContext map[string]interface{}) ([]byte, error) {
 	if len(paramOverride) == 0 {
 		return jsonData, nil
 	}

-	// 尝试断言为操作格式
-	if operations, ok := tryParseOperations(paramOverride); ok {
+	operations, ok, parseErr := tryParseOperations(paramOverride)
+	if parseErr != nil {
+		return nil, parseErr
+	}
+	if ok {
 		result, err := applyOperations(string(jsonData), operations, conditionContext)
 		return []byte(result), err
 	}

 	return applyOperationsLegacy(jsonData, paramOverride)
 }

-func tryParseOperations(paramOverride map[string]interface{}) ([]ParamOperation, bool) {
+func tryParseOperations(paramOverride map[string]interface{}) ([]ParamOperation, bool, error) {
 	opsValue, exists := paramOverride["operations"]
 	if !exists {
-		return nil, false
+		return nil, false, nil
 	}
@@
-			if err != nil {
-				return nil, false
+			if err != nil {
+				return nil, true, fmt.Errorf("invalid operations.conditions: %w", err)
 			}

Also applies to: 263-266

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/common/override.go` around lines 116 - 129, The current flow in
ApplyParamOverride falls back to legacy merge when tryParseOperations returns
false, which lets malformed "operations" slip through; change logic to first
check if the "operations" key exists in paramOverride (e.g., _, hasOps :=
paramOverride["operations"]), then if hasOps call tryParseOperations and if
parsing returns failure/invalid, return an error immediately (do not call
applyOperationsLegacy); only call applyOperations when parsing succeeds, and
otherwise when "operations" key is absent continue to use applyOperationsLegacy;
apply the same presence+fail-fast behavior for the other occurrence referenced
(lines ~263-266) so malformed operations are rejected rather than merged.
relay/channel/api_request_test.go (1)

148-156: ⚠️ Potential issue | 🟡 Minor

X-Codex-Beta-Features exclusion path is still not exercised.

This test never supplies X-Codex-Beta-Features, so the “excluded/not forwarded” assertions pass even if exclusion logic regresses.

💡 Proposed fix
 	ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
 	ctx.Request.Header.Set("Originator", "Codex CLI")
 	ctx.Request.Header.Set("Session_id", "sess-123")
+	ctx.Request.Header.Set("X-Codex-Beta-Features", "beta-flag")
@@
 		RequestHeaders: map[string]string{
 			"Originator": "Codex CLI",
 			"Session_id": "sess-123",
+			"X-Codex-Beta-Features": "beta-flag",
 		},

Also applies to: 177-186

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/api_request_test.go` around lines 148 - 156, The test currently
never sets the "X-Codex-Beta-Features" header so the exclusion code path isn't
exercised; update the test to set
ctx.Request.Header.Set("X-Codex-Beta-Features", "<some-feature>") and also
include the same key/value in the info.RequestHeaders map used to build
relaycommon.RelayInfo, then assert the expected exclusion/forwarding behavior
(i.e., that the header is handled according to the exclusion logic) for both the
block around ctx.Request.Header and the similar block at lines 177-186 so the
exclusion path is actually tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@relay/channel/api_request.go`:
- Around line 181-196: The regex passthrough keys are normalized to lowercase
(headerOverrideSource loop,
headerPassthroughRegexPrefix/headerPassthroughRegexPrefixV2 -> pattern) but
matching still uses the raw header name (MatchString(name)), causing
case-mismatches; fix by normalizing the header name before regex matching (e.g.,
run strings.ToLower(strings.TrimSpace(name)) prior to calling the compiled
regex's MatchString) or ensure the compiled pattern is case-insensitive (prepend
(?i) when building the regex from pattern) wherever pattern is used (including
the other occurrences around the same flow/variables).

---

Duplicate comments:
In `@relay/channel/api_request_test.go`:
- Around line 148-156: The test currently never sets the "X-Codex-Beta-Features"
header so the exclusion code path isn't exercised; update the test to set
ctx.Request.Header.Set("X-Codex-Beta-Features", "<some-feature>") and also
include the same key/value in the info.RequestHeaders map used to build
relaycommon.RelayInfo, then assert the expected exclusion/forwarding behavior
(i.e., that the header is handled according to the exclusion logic) for both the
block around ctx.Request.Header and the similar block at lines 177-186 so the
exclusion path is actually tested.

In `@relay/common/override.go`:
- Around line 198-201: If UseRuntimeHeadersOverride is true but
RuntimeHeadersOverride is nil the current branch returns
sanitizeHeaderOverrideMap(nil) and silently drops static overrides; update the
conditional in the code that checks info.UseRuntimeHeadersOverride to also guard
for a non-nil info.RuntimeHeadersOverride (i.e. only call
sanitizeHeaderOverrideMap(info.RuntimeHeadersOverride) when
RuntimeHeadersOverride != nil), otherwise fall back to
sanitizeHeaderOverrideMap(getHeaderOverrideMap(info)) so static channel
overrides are preserved; reference the symbols UseRuntimeHeadersOverride,
RuntimeHeadersOverride, sanitizeHeaderOverrideMap, and getHeaderOverrideMap to
locate and change the branch logic.
- Around line 727-739: The moveHeaderInContext currently only deletes the key
from header_override, leaving the original value in request_headers so consumers
still see it; update moveHeaderInContext (and/or deleteHeaderOverrideInContext)
so that when moving (i.e., keepOrigin == false and fromHeader != toHeader) you
also remove the normalized fromHeader key from the request_headers map in
context (look up context["request_headers"] as map[string]interface{} and delete
the key if present) after copyHeaderInContext succeeds, ensuring the header is
truly moved rather than still readable from request_headers; keep the current
behavior when keepOrigin == true.
- Around line 116-129: The current flow in ApplyParamOverride falls back to
legacy merge when tryParseOperations returns false, which lets malformed
"operations" slip through; change logic to first check if the "operations" key
exists in paramOverride (e.g., _, hasOps := paramOverride["operations"]), then
if hasOps call tryParseOperations and if parsing returns failure/invalid, return
an error immediately (do not call applyOperationsLegacy); only call
applyOperations when parsing succeeds, and otherwise when "operations" key is
absent continue to use applyOperationsLegacy; apply the same presence+fail-fast
behavior for the other occurrence referenced (lines ~263-266) so malformed
operations are rejected rather than merged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 305dbce and 6c6096f.

📒 Files selected for processing (6)
  • relay/channel/api_request.go
  • relay/channel/api_request_test.go
  • relay/common/override.go
  • relay/common/override_test.go
  • relay/common/relay_info.go
  • service/channel_affinity_template_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • service/channel_affinity_template_test.go

@seefs001
seefs001 merged commit 99bb41e into QuantumNous:main Feb 28, 2026
1 check passed
ennnnny pushed a commit to ennnnny/new-api that referenced this pull request Mar 17, 2026
…am-override

feat: improve channel override ui/ux
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.

Claude Code使用非Anthropic渠道出现报错“invalid beta flag”

2 participants