Skip to content

fix: wire response model mapping into provider edit#553

Merged
ymkiux merged 1 commit intomainfrom
feat/provider-response-model-mapping
May 8, 2026
Merged

fix: wire response model mapping into provider edit#553
ymkiux merged 1 commit intomainfrom
feat/provider-response-model-mapping

Conversation

@dreamhunter2333
Copy link
Copy Markdown
Member

@dreamhunter2333 dreamhunter2333 commented May 8, 2026

Summary

  • move response model mapping UI to the common provider edit page only
  • persist custom provider responseModelMapping in provider config
  • apply response model modification for both standard routing and provider direct routing, including streaming responses

Tests

  • go test ./internal/handler ./internal/executor/responsemodifier ./internal/executor
  • pnpm --dir web typecheck

Summary by CodeRabbit

发布说明

  • 新功能
    • 为自定义提供商新增响应模型映射配置能力,支持在请求和响应层面进行模型转换
    • 在提供商编辑页面增加专属的响应模型映射配置界面,支持添加、编辑和删除映射规则
    • 新增中英文本地化支持,覆盖所有新增配置项

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

走查

该PR为自定义提供商配置添加响应模型映射功能。后端扩展domain模型并重构Claude响应修饰符以支持从自定义配置或Claude配置中选择映射;代理处理程序现在通过ResponseModifierWriter应用映射。前端添加了新的UI组件来编辑映射,并从claude-provider-view迁移到通用的provider-edit-flow。新增国际化支持和相应测试验证功能。

变更

响应模型映射功能

层级 / 文件 摘要
数据模型与类型
internal/domain/model.go, web/src/lib/transport/types.ts
添加ModelMapping和ResponseModelMapping字段到ProviderConfigCustom,支持请求和响应模型名称映射。
响应修饰符实现
internal/executor/responsemodifier/claude_response_modifier.go, internal/executor/responsemodifier/response_modifier_writer.go
重构修饰符以通过新的responseModelMapping辅助函数优先使用Custom.ResponseModelMapping,回退到Claude配置;统一response_modifier_writer总是调用Claude修饰符创建。
代理处理程序集成
internal/handler/provider_proxy.go
在代理执行中包装响应写入器为ResponseModifierWriter,在成功和错误路径上都调用Finalize()处理修饰符结果。
前端编辑界面
web/src/pages/providers/components/provider-edit-flow.tsx, web/src/pages/providers/components/claude-provider-view.tsx
添加ResponseModelMappings组件用于编辑映射对象;从claude-provider-view移除旧实现,统一由provider-edit-flow管理;扩展表单状态和save/clone有效负载。
测试与国际化
internal/executor/responsemodifier/response_modifier_writer_test.go, web/src/locales/en.json, web/src/locales/zh.json
添加customProvider测试辅助函数和新的测试用例验证自定义提供商映射;添加英文和中文国际化字符串用于UI标签和帮助文本。

预期代码审查工作量

🎯 3 (中等) | ⏱️ ~25 分钟

可能相关的PR

  • awsl-project/maxx#550:该PR直接在PR #550的基础上构建,扩展了自定义提供商响应模型映射支持,并统一了修饰符/写入器/处理程序实现以支持多来源配置。

建议审查人

  • awsl233777
  • ymkiux
  • whhjdi

诗歌

🐰 为映射而欢呼,模型从此重生,
自定义和Claude配置携手同行,
响应在处理中优雅变换,
前后端齐心协力,映射映射,配置流畅!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题清晰准确地反映了本PR的核心变更:将响应模型映射UI整合到提供商编辑流程中。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/provider-response-model-mapping

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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)
internal/handler/provider_proxy.go (1)

142-204: ⚠️ Potential issue | 🔴 Critical

标准路由处理器中缺少响应模型修改集成

PR 声称对"标准路由和 provider 直连路由"都应用了响应模型映射,但实际检查发现:

  • proxy.goProxyHandler.dispatch() 方法(第 ~262 行)调用 h.executor.ExecuteWith(c) 后直接进行简单的错误处理,未使用 ResponseModifierWriter
  • provider_proxy.goProviderProxyHandler 才集成了 responsemodifier.NewResponseModifierWriter 和响应捕获机制
  • 两个处理器的执行模式完全不同,这说明 PR 在标准路由路径上的修改并未完成

标准路由路径应补充以下内容以保持功能一致性:

  1. 导入 responsemodifier
  2. ExecuteWith() 前后添加 ResponseModifierWriter 封装和 responseCapture 机制
  3. 补充 proxyReq 跟踪和日志记录(状态、时长、响应头、响应体)
🤖 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 `@internal/handler/provider_proxy.go` around lines 142 - 204, The standard
route handler (ProxyHandler.dispatch) is missing the response modifier/capture
integration present in ProviderProxyHandler: wrap the original c.Writer with
responsemodifier.NewResponseModifierWriter (import responsemodifier), fall back
to the original when nil, create executor.NewResponseCapture around the chosen
writer, assign c.Writer = responseCapture before calling
h.executor.ExecuteWith(c) and restore it after, then populate proxyReq fields
(EndTime, Duration, StatusCode from responseCapture.StatusCode(), ResponseInfo
with CapturedHeaders/Body unless clearDetail) and invoke
modifierWriter.Finalize() on success or failure (handling errors like in
ProviderProxyHandler), and ensure proxyReq.Status is set to COMPLETED or FAILED
and proxyRequestRepo.Update(proxyReq) is called accordingly; use the same helper
functions writeProxyError/writeStreamError/writeError to render errors.
🧹 Nitpick comments (1)
internal/executor/responsemodifier/response_modifier_writer_test.go (1)

134-145: ⚡ Quick win

customProvider(nil) 未在 Disabled 测试中覆盖

TestResponseModifierWriterDisabled 覆盖了 claudeProvider(nil) 和非 Claude 客户端类型,但未覆盖 customProvider(nil) 应返回 nil 的情况。若后续修改 responseModelMapping 的 fallback 逻辑,此行为可能悄然回归。

✅ 建议补充测试用例
 func TestResponseModifierWriterDisabled(t *testing.T) {
 	rr := httptest.NewRecorder()
 	if NewResponseModifierWriter(rr, &domain.Provider{Type: "codex"}, domain.ClientTypeCodex, false) != nil {
 		t.Fatal("expected non-claude provider to be disabled")
 	}
 	if NewResponseModifierWriter(rr, claudeProvider(nil), domain.ClientTypeClaude, false) != nil {
 		t.Fatal("expected empty mapping to be disabled")
 	}
+	if NewResponseModifierWriter(rr, customProvider(nil), domain.ClientTypeClaude, false) != nil {
+		t.Fatal("expected custom provider with nil mapping to be disabled")
+	}
 	if NewResponseModifierWriter(rr, claudeProvider(map[string]string{"a": "b"}), domain.ClientTypeCodex, false) != nil {
 		t.Fatal("expected non-claude client shape to be disabled")
 	}
 }
🤖 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 `@internal/executor/responsemodifier/response_modifier_writer_test.go` around
lines 134 - 145, Add a missing assertion in TestResponseModifierWriterDisabled
to ensure NewResponseModifierWriter returns nil for a custom provider with no
mapping: call NewResponseModifierWriter(rr, customProvider(nil),
domain.ClientTypeCustom, false) and fail the test if it is non-nil; this mirrors
the existing checks for claudeProvider(nil) and non-claude providers and
prevents regressions if responseModelMapping fallback logic changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/pages/providers/components/provider-edit-flow.tsx`:
- Around line 217-231: handleUpdateMapping currently silently returns on invalid
target, drops entries when target is cleared, and overwrites existing patterns;
change it to validate and surface errors instead of returning silently by
invoking the existing error/validation handler (or set a local error state) when
isValidTarget(nextTarget) fails, do not remove the original entry if nextTarget
is empty (re-add the oldPattern -> value pair to next), and before inserting
nextPattern check for conflicts: if nextPattern !== oldPattern and nextPattern
already exists in entries, abort the update and surface a duplicate-key error
rather than overwriting; use the existing symbols handleUpdateMapping, entries,
onChange, and isValidTarget to implement these checks and error callbacks.
- Around line 259-286: The pattern input loses focus because each row uses
key={pattern} and handleUpdateMapping updates the parent on every keystroke;
change the row to a local controlled component (extract a Row component, e.g.,
ResponseModelMappingRow) that keeps pattern and target in useState, render rows
with a stable key (e.g., index or an id), wire ModelInput and Input to the local
state, call handleUpdateMapping only onBlur (or onEnter) to push changes to
formData.responseModelMapping, and keep handleDeleteMapping as-is; this
preserves focus while still syncing updates to the parent.

---

Outside diff comments:
In `@internal/handler/provider_proxy.go`:
- Around line 142-204: The standard route handler (ProxyHandler.dispatch) is
missing the response modifier/capture integration present in
ProviderProxyHandler: wrap the original c.Writer with
responsemodifier.NewResponseModifierWriter (import responsemodifier), fall back
to the original when nil, create executor.NewResponseCapture around the chosen
writer, assign c.Writer = responseCapture before calling
h.executor.ExecuteWith(c) and restore it after, then populate proxyReq fields
(EndTime, Duration, StatusCode from responseCapture.StatusCode(), ResponseInfo
with CapturedHeaders/Body unless clearDetail) and invoke
modifierWriter.Finalize() on success or failure (handling errors like in
ProviderProxyHandler), and ensure proxyReq.Status is set to COMPLETED or FAILED
and proxyRequestRepo.Update(proxyReq) is called accordingly; use the same helper
functions writeProxyError/writeStreamError/writeError to render errors.

---

Nitpick comments:
In `@internal/executor/responsemodifier/response_modifier_writer_test.go`:
- Around line 134-145: Add a missing assertion in
TestResponseModifierWriterDisabled to ensure NewResponseModifierWriter returns
nil for a custom provider with no mapping: call NewResponseModifierWriter(rr,
customProvider(nil), domain.ClientTypeCustom, false) and fail the test if it is
non-nil; this mirrors the existing checks for claudeProvider(nil) and non-claude
providers and prevents regressions if responseModelMapping fallback logic
changes.
🪄 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: e2d9f550-764c-4617-b9ac-90b9c6a7f9eb

📥 Commits

Reviewing files that changed from the base of the PR and between 20e4549 and b61aff1.

📒 Files selected for processing (10)
  • internal/domain/model.go
  • internal/executor/responsemodifier/claude_response_modifier.go
  • internal/executor/responsemodifier/response_modifier_writer.go
  • internal/executor/responsemodifier/response_modifier_writer_test.go
  • internal/handler/provider_proxy.go
  • web/src/lib/transport/types.ts
  • web/src/locales/en.json
  • web/src/locales/zh.json
  • web/src/pages/providers/components/claude-provider-view.tsx
  • web/src/pages/providers/components/provider-edit-flow.tsx
💤 Files with no reviewable changes (1)
  • web/src/pages/providers/components/claude-provider-view.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: playwright
  • GitHub Check: e2e

Comment on lines +217 to +231
const handleUpdateMapping = (oldPattern: string, pattern: string, target: string) => {
const next: Record<string, string> = {};
for (const [key, value] of entries) {
if (key === oldPattern) continue;
next[key] = value;
}

const nextPattern = pattern.trim();
const nextTarget = target.trim();
if (nextTarget && !isValidTarget(nextTarget)) return;
if (nextPattern && nextTarget) {
next[nextPattern] = nextTarget;
}
onChange(next);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

内联编辑时静默失败与意外删除条目

handleUpdateMapping 存在两个边界问题:

  1. 静默校验失败(line 226):当用户在 target 字段输入包含 * 的内容时,函数直接 return 而不显示任何错误提示。与新增按钮(通过禁用状态给出明确反馈)不同,内联编辑完全没有用户可见的错误指示。

  2. 意外删除条目(lines 227-229):当 nextTarget 为空字符串时,if (nextPattern && nextTarget) 为假,条目不会被重新加入 next,相当于在用户无意间清空 target 字段时静默删除了该条目。

  3. 重复 pattern 键冲突(lines 218-230):若用户将 pattern "A" 重命名为已存在的 "B",现有 "B" 的值会被覆盖,且没有任何警告。

🛡️ 建议的改进
 const handleUpdateMapping = (oldPattern: string, pattern: string, target: string) => {
   const next: Record<string, string> = {};
   for (const [key, value] of entries) {
     if (key === oldPattern) continue;
     next[key] = value;
   }

   const nextPattern = pattern.trim();
   const nextTarget = target.trim();
-  if (nextTarget && !isValidTarget(nextTarget)) return;
-  if (nextPattern && nextTarget) {
-    next[nextPattern] = nextTarget;
-  }
+  if (!nextPattern || !nextTarget) return; // 不允许空 pattern 或 target
+  if (!isValidTarget(nextTarget)) return;  // 无效 target 时不提交(可配合 UI 提示)
+  if (nextPattern in next) return;         // 防止覆盖已有 pattern
+  next[nextPattern] = nextTarget;
   onChange(next);
 };
🤖 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/src/pages/providers/components/provider-edit-flow.tsx` around lines 217 -
231, handleUpdateMapping currently silently returns on invalid target, drops
entries when target is cleared, and overwrites existing patterns; change it to
validate and surface errors instead of returning silently by invoking the
existing error/validation handler (or set a local error state) when
isValidTarget(nextTarget) fails, do not remove the original entry if nextTarget
is empty (re-add the oldPattern -> value pair to next), and before inserting
nextPattern check for conflicts: if nextPattern !== oldPattern and nextPattern
already exists in entries, abort the update and surface a duplicate-key error
rather than overwriting; use the existing symbols handleUpdateMapping, entries,
onChange, and isValidTarget to implement these checks and error callbacks.

Comment on lines +259 to +286
{entries.map(([pattern, target], index) => (
<div key={pattern} className="flex items-center gap-2">
<span className="text-xs text-muted-foreground w-6 shrink-0">{index + 1}.</span>
<ModelInput
value={pattern}
onChange={(value) => handleUpdateMapping(pattern, value, target)}
placeholder={t('responseModelMappings.matchPattern')}
disabled={disabled}
className="flex-1 min-w-0 h-8 text-sm"
/>
<ArrowRight className="h-4 w-4 text-muted-foreground shrink-0" />
<Input
value={target}
onChange={(event) => handleUpdateMapping(pattern, pattern, event.target.value)}
placeholder={t('responseModelMappings.targetModel')}
disabled={disabled}
className="flex-1 min-w-0 h-8 text-sm"
/>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteMapping(pattern)}
disabled={disabled}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
))}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

模式字段(pattern)编辑时每次按键都会丢失焦点

在编辑已有条目的模式(pattern)输入框时,key={pattern}handleUpdateMapping 的组合会触发以下循环:用户输入单个字符 → onChange 触发 → 父组件 formData.responseModelMapping 更新 → entries useMemo 重新计算(旧 key 消失、新 key 出现) → React 以新 key 卸载旧输入框并挂载新输入框 → 焦点丢失

实际效果是:pattern 输入框每输入一个字符后就会失去焦点,用户完全无法正常编辑该字段。target 的 Input(line 272)不受影响,因为其所在行的 key 不会因 target 变化而改变。

修复思路:将各行的 pattern 和 target 改为局部受控状态(useState),仅在 onBlur 时才调用 handleUpdateMapping 提交到父组件。示例:

🐛 建议的修复方式(抽取行组件,用 onBlur 提交)
+function MappingRow({
+  index,
+  pattern,
+  target,
+  disabled,
+  onUpdate,
+  onDelete,
+}: {
+  index: number;
+  pattern: string;
+  target: string;
+  disabled: boolean;
+  onUpdate: (oldPattern: string, pattern: string, target: string) => void;
+  onDelete: (pattern: string) => void;
+}) {
+  const { t } = useTranslation();
+  const [localPattern, setLocalPattern] = useState(pattern);
+  const [localTarget, setLocalTarget] = useState(target);
+
+  // 当外部 pattern/target 变更时同步(例如删除了其他行)
+  useEffect(() => { setLocalPattern(pattern); }, [pattern]);
+  useEffect(() => { setLocalTarget(target); }, [target]);
+
+  return (
+    <div className="flex items-center gap-2">
+      <span className="text-xs text-muted-foreground w-6 shrink-0">{index + 1}.</span>
+      <ModelInput
+        value={localPattern}
+        onChange={setLocalPattern}
+        onBlur={() => onUpdate(pattern, localPattern, localTarget)}
+        placeholder={t('responseModelMappings.matchPattern')}
+        disabled={disabled}
+        className="flex-1 min-w-0 h-8 text-sm"
+      />
+      <ArrowRight className="h-4 w-4 text-muted-foreground shrink-0" />
+      <Input
+        value={localTarget}
+        onChange={(e) => setLocalTarget(e.target.value)}
+        onBlur={() => onUpdate(pattern, localPattern, localTarget)}
+        placeholder={t('responseModelMappings.targetModel')}
+        disabled={disabled}
+        className="flex-1 min-w-0 h-8 text-sm"
+      />
+      <Button variant="ghost" size="sm" onClick={() => onDelete(pattern)} disabled={disabled}>
+        <Trash2 className="h-4 w-4 text-destructive" />
+      </Button>
+    </div>
+  );
+}

然后在 ResponseModelMappings 中:

-{entries.map(([pattern, target], index) => (
-  <div key={pattern} className="flex items-center gap-2">
-    ...
-  </div>
-))}
+{entries.map(([pattern, target], index) => (
+  <MappingRow
+    key={pattern}
+    index={index}
+    pattern={pattern}
+    target={target}
+    disabled={disabled}
+    onUpdate={handleUpdateMapping}
+    onDelete={handleDeleteMapping}
+  />
+))}
🤖 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/src/pages/providers/components/provider-edit-flow.tsx` around lines 259 -
286, The pattern input loses focus because each row uses key={pattern} and
handleUpdateMapping updates the parent on every keystroke; change the row to a
local controlled component (extract a Row component, e.g.,
ResponseModelMappingRow) that keeps pattern and target in useState, render rows
with a stable key (e.g., index or an id), wire ModelInput and Input to the local
state, call handleUpdateMapping only onBlur (or onEnter) to push changes to
formData.responseModelMapping, and keep handleDeleteMapping as-is; this
preserves focus while still syncing updates to the parent.

@ymkiux ymkiux merged commit c99f36c into main May 8, 2026
5 checks passed
@ymkiux ymkiux deleted the feat/provider-response-model-mapping branch May 8, 2026 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants