Skip to content

fix(transformer): support DeepSeek thinking parameter passthrough - #296

Open
Hureru wants to merge 284 commits into
bestruirui:devfrom
Hureru:fix/issue-88-deepseek-thinking-parameter
Open

fix(transformer): support DeepSeek thinking parameter passthrough#296
Hureru wants to merge 284 commits into
bestruirui:devfrom
Hureru:fix/issue-88-deepseek-thinking-parameter

Conversation

@Hureru

@Hureru Hureru commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

问题描述

修复 #88 - 用户在 Cherry Studio 中关闭思维链设置后,DeepSeek 模型仍然自动思考。

根本原因

客户端(Cherry Studio)正确发送了 {"thinking": {"type": "disabled"}} 参数,但 Octopus 在处理请求时:

  1. InternalLLMRequest 结构体中没有 thinking 字段
  2. JSON 反序列化时该参数被静默丢弃
  3. DeepSeek API 收到的请求中缺少此参数
  4. DeepSeek 使用默认值 thinking.type = "enabled"
  5. 模型开始思考 ❌

修复方案

添加对 DeepSeek thinking 参数的透传支持:

修改的文件

  • internal/transformer/model/model.go

    • 新增 ThinkingConfig 结构体
    • InternalLLMRequest 中添加 Thinking 字段
  • internal/transformer/outbound/openai/chat.go

    • ChatCompletionsRequest 中添加 Thinking 字段
    • buildChatCompletionsRequest() 中透传该参数

新增测试

  • chat_thinking_test.go - 单元测试(3 个测试用例)
  • chat_issue88_test.go - 端到端回归测试

设计理念

  • 协议中立:仅透传参数,不做智能转换或默认值处理
  • 向后兼容thinking 为可选参数(omitempty),不影响现有功能
  • 供应商隔离:不影响 Anthropic、Gemini 等其他供应商的处理逻辑

测试结果

✅ 所有现有测试通过
✅ 新增回归测试通过
✅ 完整构建成功

验证方式

修复后,当 Cherry Studio 发送以下请求:

{
  "model": "deepseek-v4-flash",
  "thinking": {"type": "disabled"},
  "messages": [...]
}

thinking 参数会正确传递到 DeepSeek API,模型将不再进行自动思考。


Closes #88

Hureru and others added 30 commits March 31, 2026 13:32
…ough

- Add redacted_thinking block support throughout Anthropic transformer chain
- Fix convertMultiplePartContent missing thinking blocks for MultipleContent messages
- Remove invalid magic string signature fallback that caused upstream rejection
- Add RedactedThinkingBlocks field to internal Message model with proper cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ChannelEnabled() rejects managed channels due to the managed-binding
check, causing ProjectAccount's early-exit path to silently fail.
Add ChannelEnabledManaged() that bypasses the managed check for
internal projection use, and use it in ProjectAccount.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove backend checkinExternal() and the external_checkin_url dispatch
in sync.go. Add a "打开手动签到" button in CheckinPanel that opens all
configured external checkin URLs in the browser for manual completion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove "打开站点" and "查看站点渠道" buttons from the card header,
move them into the popover menu. Show "新增账号" externally only when
the site has no accounts. Make base_url a clickable link. Change the
outer container from <button> to <div role="button"> to avoid invalid
nested <a> inside <button>.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add min-w-0 and overflow-hidden to constrain flex children within
column boundaries. Prevent username input row from overflowing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add per-account enable/disable toggle button in the dialog header
  with Power icon and pending spinner state.
- Merge bulk model action bar into filter card's status alert row,
  showing "已选N个" + endpoint select + move/enable/disable/clear
  buttons inline when models are selected.
- Remove standalone SiteChannelBulkActionBar component.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…esponses API

Implement bidirectional WebSocket support for the relay layer:
- WS connection pool with idle/max-age cleanup for upstream providers
- Client-facing WS handler at GET /v1/responses with response.create message loop
- StreamWriter abstraction to unify HTTP SSE and WS client writes
- Configurable WS upstream upgrade setting (proactive or follow-client)
- Pass-through fields for OpenAI Responses API (previous_response_id, prompt, etc.)
- Reasoning signature/encrypted_content support in inbound/outbound transformers
- Handle reasoning-only messages in Anthropic outbound

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
调整 internal/sitesync 的错误优先级,在分组无可用模型或可用 Key 时返回稳定且可理解的提示,避免直接暴露 decode response failed: New API 这类摘要报错。

更新站点相关前端错误翻译与展示入口,统一将该场景映射为‘获取模型失败:当前分组没有可用模型或可用 Key’,并精简账号卡片按钮,将查看站点渠道收纳进更多菜单,同时让同步/签到信息占满卡片宽度。

补充回归测试,并为站点卡片菜单中的停用/启用站点项添加 Power 图标,保持启停语义与现有界面一致。
后端改为输出分组级同步状态,区分 synced/empty/failed/unresolved/removed,并按 authoritative 分组落库,避免上游删模或单分组失败时误删整账号历史模型。

同步结果新增 partial 状态与 group_results,前端站点页补充部分同步展示与提示,手动同步 toast 改为展示后端汇总消息。

补充 management/storage 测试,覆盖全组清空、部分分组未解析保留历史模型、仅替换 authoritative 分组等场景。
修改文件: web/src/components/modules/site-channel/index.tsx

目的: 为站点渠道管理面板头部的账号选择 Tab 容器增加右外边距,给右上角关闭按钮预留空间。

效果: 避免账号 Tab 与关闭页面按钮部分重叠,保持账号切换区域的可读性与可点击性。
修改文件: internal/model/setting.go, web/src/components/modules/setting/System.tsx, web/src/components/modules/log/Item.tsx。

目的与效果: 将主动 WebSocket 上游升级默认值改为关闭;设置页改为与 CORS 一致的问号提示说明;日志页与日志详情中的 WS 标识移动到模型标题行最右侧,放到卡片右上角位置。
修改文件: internal/relay/ws_client.go, internal/relay/ws_pool.go, internal/relay/transport_ws.go, internal/relay/relay.go, internal/relay/ws_error.go, internal/relay/balancer/iterator.go, internal/relay/balancer/session.go, internal/model/channel.go 以及对应测试。

目的: 将 Responses 连续会话状态从上游连接池中剥离,避免 ws->http 错误降级导致 previous_response_id/call_id 上下文失配,并在粘性会话中优先复用上次成功的 key。

效果: 上游返回 please restart the conversation、no available account、blocked_invalid_request 等错误时会被明确分类;需要连续会话的请求在 WS 不可用时不再偷偷降级到 HTTP;重启对话场景会同时清理本地缓存的 response_id 和 sticky key;WS unsupported 标记只在明确不支持握手时写入;新增 model/relay/balancer 测试覆盖关键路径。
修改文件: internal/relay/ws_client.go, internal/relay/ws_session.go, internal/relay/ws_error.go, internal/relay/ws_state_test.go。

目的: 当上游返回 restart the conversation 且当前请求尚未向下游写出任何内容时,自动用本地缓存的 transcript 重建 fresh Responses 请求,尽量做到客户端无感恢复。

效果: 下游 WS 会话现在会缓存 transcript 与 last response id;增量请求会在需要时自动拼接历史并移除 previous_response_id 后重放;tool output 只有在缺少对应 assistant tool_call 上下文时才会被判定为依赖上游连续状态;补充测试覆盖注入、判定、重建与 transcript 更新逻辑。
修改文件: internal/transformer/inbound/openai/response.go, internal/transformer/outbound/openai/response.go, internal/transformer/model/model.go, internal/relay/ws_session.go 及对应测试。

目的: 将数组型 Responses input 原样保存到 RawInputItems,并在 WS 连续会话自动重建时把 transcript 生成的历史 items 与当前请求的原始 raw items 合并回放,减少原生字段在重建过程中被归一化丢失。

效果: OpenAI Responses 数组 input 的原始字段会被保留并优先用于 outbound 序列化;含 RawInputItems 的 Responses 请求即使暂时无法完整映射到 Messages 也能通过 chat 校验;新增 transformer/relay 测试覆盖 raw input 保存、优先序列化和 replay merge 行为。
修改文件: internal/relay/ws_client.go, internal/relay/ws_warmup_test.go。

目的: 解决客户端启用 WS 后,新会话首次正式请求仍需承担上游 WS 冷拨号成本,导致第一次请求降级到 HTTP/SSE 且首字极慢的问题。

效果: generate:false warmup 不再只在本地伪造完成事件,而是会尝试建立真实的上游 WS 连接并放回连接池,同时为命中的 channel/key 设置 sticky;新增 relay 集成测试验证 warmup 会预热连接并写入会话保持。
后端为 RelayLog 新增传输输入、计费输入、缓存读写和 ws_mode 字段。

在实际发往上游的 HTTP/WS 请求体处估算 transport_input_tokens,并根据请求路径标记 fresh、continuation、replay 三种 WS 状态。

前端日志卡片保持原有输入数字不变,在输入旁新增明细 Popover,同时把原来的 Wifi 徽标改为链路状态徽标,支持 WS/续传/回放三态展示。

补充 relay 相关测试覆盖 OpenAI 兼容缓存口径、Anthropic 缓存口径和默认 WS 模式判定。
把日志卡片中的 WS/续传/回放状态徽标图标从 Waypoints 调整为 Link。

同时将系统设置里 WebSocket 上游升级开关的图标从 Wifi 调整为 Link,统一连接语义。

本次只变更图标,不调整现有状态文案、交互和布局。
修改 web/src/components/modules/log/Item.tsx,为展开态标题增加右侧内边距,给绝对定位的关闭按钮预留稳定安全区。

效果是 WS 徽标在展开后不再与右上角关闭 X 部分重叠,窄宽度下的标题布局也更稳定。
- 将上游 WS stale 连接导致的 broken pipe/EOF/closed connection 识别为显式连接失效

- 对 continuation 请求在发送阶段命中失效连接时立即清理 sticky 并短路返回重开对话信号,停止后续 failover

- 对 fresh 请求在复用到 stale 池连接时先强制同通道重拨一次,避免首次直接降级到 HTTP/SSE

- 避免将这类连续会话失效误记入熔断,减少随后出现 no available key 的误伤

- 补充 relay 回归测试,覆盖续传短路和 fresh stale redial 两条路径
persist websocket conversation state by api key and model so a new client connection can recover previous response context after disconnect

prefer the previously successful channel and key when rebuilding websocket relay requests to improve continuation and replay success

add relay tests for stored websocket state resolution, preferred iterator ordering, and final channel key capture
Hureru and others added 30 commits June 4, 2026 10:39
* fix: suspend site projection on model sync failure

* fix: keep stale site projections on sync failure

* fix: restore suspended site projections after manual repair

* fix: address review feedback on site projection suspend/recovery

- align group projection activation with channel key availability via shared
  isUsableSiteToken/hasUsableToken instead of len(tokens) > 0
- preserve unset projection_suspend_reason/model_sync_message as undefined
- extract STALE_MODEL_SYNC_STATUSES constant and getGroupStatusBadge helper
- handle sync result status explicitly with a default fallback branch
- unify duplicate ToolbarSortField with the shared toolbar type

* fix: remove obsolete account projection suspend path

* feat: passive outlier retirement for site channels

Retire site-projected channels with sustained failures in a rolling
window, then recover them after probe-success streaks.

- outlierwindow: in-memory per-channel relay outcome window
- SiteChannelOutlierState model + op for retired-state persistence
- relay success/failure reporting hooks in relay and compact paths
- export IsCloudflareProtectionResponse for POR Cloudflare gate reuse
- keep retired channels disabled across site re-projection
- 10 configurable settings, disabled by default

* feat: outlier retirement settings panel

Add the POR settings UI with enable toggle and threshold inputs, wire
the new setting keys, and add en/zh-Hans/zh-Hant translations.

* fix: address outlier retirement review findings

- outlierwindow.Reap: hold the window lock across the stale recheck and
  delete so a window a concurrent Report just refreshed is not reaped (TOCTOU)
- setting.Validate: enforce bounds for POR settings (window capacity 1..20,
  fail-rate 1..100, others >=1); task-interval keys stay unbounded since
  0 disables the task via task.Update
- OutlierRetirement.tsx: roll back the switch and toast on save failure;
  only hydrate the form on first load so the 30s refetch/invalidation no
  longer clobbers in-progress edits
- locale: add setting.saveFailed (en/zh_hans/zh_hant)

* fix: mirror POR setting bounds in number inputs

Add step/min/max to the OutlierRetirement number inputs so the UI enforces
the same integer bounds as model.Setting.Validate() (fail-rate 1..100,
window capacity 1..20, others >=1) before blur/save.

* chore: stop tracking SITE_PROJECTION_SUSPEND_PROGRESS.md

Progress scratch doc should not live in version control; keep it local only.
* fix: make startup cache init more robust

* fix: keep cached channel stats empty

* fix: avoid preloading discarded channel stats
#65) (#68)

- guard ChatInbound.TransformStreamEvents against nil aggregates, matching
  the existing pattern in ResponseInbound and MessagesInbound
- emit Responses function_call events on choice index 0 with densely
  renumbered tool_calls indices (output_index counts all output items, so
  reasoning models placed function calls at index >= 1, collapsing the
  aggregate to nil and panicking the chat inbound)
- key tool call events from StreamEventsFromInternalResponse by choice
  index instead of tool call index (same defect for chat-to-chat parallel
  tool calls)
- rebuild choices by sorted map keys in InternalResponseFromStreamEvents
  and StreamAggregator.Response so non-contiguous indices are never
  silently dropped
#69)

A 200 SSE stream that ended without forwarding any payload used to return
nil from the stream handlers, so attempt() recorded a zero-token success,
reset the circuit breaker, and pinned session stickiness to the
misbehaving channel while the client received an empty body.

Return an error from the clean-end paths of handleStreamResponse and both
passthrough finishStream closures when streamPayloadWritten is still
false, so empty streams fail the attempt and the relay can fail over
(nothing has been written to the client at that point). Client
disconnects are unaffected: they already return context errors and are
classified by isClientCancellation.
When a site does not report endpoint types that map to a known route
bucket (or reports none at all), models used to be marked as unknown
and required manual route assignment. Route detection now falls back
to guessing the route type from the model name as the final step, and
records route_guessed in the route metadata. Detected routes still
take precedence over guessed ones when merging, and legacy unknown
routes are converted on the next sync. The UI shows a hint badge for
guessed routes so users can correct them if needed.
* feat: batch update site custom headers

Add POST /api/v1/site/batch/header to merge custom headers across multiple
sites in one call:
- case-insensitive upsert keeps the existing key casing and updates value
- delete-keys remove by key and win over upserts on the same key
- per-site failures are collected/reported, affected sites re-projected async

Frontend adds a "批量编辑 Header" batch action and BatchHeaderDialog with
set/delete rows applied to the selected sites.

* fix: reset batch header dialog rows on every close

Route all close paths (esc/overlay, X button, cancel) through a single handleOpenChange that clears rows, so stale input no longer persists when reopening.

Also drop the dead parent batchHeader mutation whose isPending never reflected the dialog's own submission.

* fix: keep site batch actions sticky
* feat: add site tags with filtering and batch operations

* refactor: unify site batch operations layering and toolbar UX

- move per-site batch loop from handler into op.SiteBatchApply (delete injected to avoid op->sitesync cycle), symmetric with SiteBatchUpdateHeader
- share one projectSitesAsync helper between /batch and /batch/header handlers
- merge batchTagDialog/batchHeaderOpen into single batchDialog state
- collapse tag/header/delete batch buttons into a More Actions popover menu
- require confirmation before batch delete via existing deleteConfirm dialog
- unify batch result toast wording; keep selection after non-delete actions

* feat: unify batch tag and header edits into one batch edit dialog

- add SiteBatchEditRequest and op.SiteBatchEdit engine: one load + one
  write per site applying tag add/remove and header upsert/delete together
- SiteBatchUpdateHeader becomes a thin wrapper over the engine
- new POST /api/v1/site/batch/edit; /batch and /batch/header kept for
  backward compatibility
- rework BatchHeaderDialog into BatchEditDialog with tag add/remove
  inputs plus the existing header row editor; submit only filled parts
- flatten batch toolbar back to direct buttons (enable/disable/edit/
  delete) now that secondary entries shrank to two

* refactor: drop unreleased batch tag actions superseded by /batch/edit

add_tags/remove_tags on /batch never shipped on dev (introduced on this
branch) and are fully replaced by POST /api/v1/site/batch/edit. Remove the
actions, SiteBatchRequest.Tags, SiteTagsAdd/SiteTagsRemove/siteTagsModify
and the stale tags param on the frontend hook. /batch (enable/disable/
delete) and /batch/header stay untouched for backward compatibility.

* refactor: drop unreleased /batch/header endpoint superseded by /batch/edit

/batch/header (#63) never shipped: it is not in any release tag (latest
v0.8.33 predates it) and releases are cut from master only. Remove the
endpoint, SiteBatchHeaderRequest and the SiteBatchUpdateHeader wrapper;
its tests now exercise SiteBatchEdit directly with identical assertions.

* fix: make site tag filter badges keyboard-accessible

Tag badges sat inside the card's role=button expand area as plain spans:
not focusable, and Enter/Space would have been hijacked by the ancestor's
keydown handler. Render them as native buttons via Badge asChild (matching
the CheckinPanel filter chips) with aria-pressed, and stop click/keydown
propagation so activation does not toggle card expansion.
…) (#70)

* feat: one-click export API key config to CC Switch / Cherry Studio (#60)

* fix: surface system setting save failure with toast and revert input

* fix: dedupe /v1 in export endpoint and add Escape close with dialog a11y
#71)

* fix: treat client cancel after terminal SSE event as normal stream end

When a client disconnects right after receiving the terminal event
(response.completed / message_stop) but before upstream EOF arrives,
the cancellation propagates through the outbound request context and
aborts the blocked body read with context.Canceled. Stream handlers
only recognized io.EOF as graceful end, so such requests were recorded
as "failed to read stream event: context canceled" with zero tokens,
because passthrough usage collection is deferred to stream end.

Route cancellation-induced read errors into the client-disconnect path
in all three stream handlers. In passthrough handlers, when the
buffered raw stream already contains a protocol terminal event, finish
the stream as success and collect usage normally, matching the
existing behavior when upstream EOF wins the race.

* refactor: return early from streamReachedTerminalEvent on terminal match
* fix: portal API key setting overlays to body to avoid header clipping

* refactor: merge setting page cards into themed groups

* refactor: keep API key card at top of setting page

* refactor: swap account and network card order on setting page

* feat: show last run time for site full sync and check-in

* refactor: split logs export into dedicated ZIP archive button, fix regular export to JSON

* refactor: enable include-stats export option by default

* docs: rewrite passive outlier retirement descriptions in plain language

* style: wrap long setting tooltips with max width

* style: use normal text wrapping in setting tooltips to remove right gap

* fix: address setting page review findings

- guard formatTime against malformed timestamps in sync tasks
- parallelize mirror-key setting writes with Promise.all
- surface API error details on WS mode save failure
- extract shared CORS origin parsing helper
- add dialog ARIA attributes to API key overlays, aria-hidden on backdrop
- validate backup import file type client-side with localized message
* fix: 修复站点弹窗高度和代理池按钮样式问题

- 站点编辑弹窗:将 max-h 改为 h 并使用 dvh 单位,修复手机端内容不显示
- 代理池按钮:改用原生 button 元素并匹配 toolbar 按钮样式,保持视觉一致性

* refactor: 将代理池按钮集成到 Toolbar 内部

- 将代理池按钮从 app.tsx 移到 toolbar/index.tsx
- 按钮现在位于搜索框左侧,避免搜索展开时被遮挡
- 使用 store 模式而非独立组件,保持架构一致性
- 所有工具按钮统一在 Toolbar 容器内,布局更统一

* refactor: 调整代理池按钮位置并添加动画

- 将代理池按钮移到搜索框右侧,搜索展开时向左扩展不会遮挡
- 使用 motion.button 添加进入/退出动画,与 Toolbar 整体动画保持一致
- 动画参数与 Toolbar 容器一致 (opacity + scale, duration 0.2s)

* fix: 工具栏代理池按钮改用本地化标签

aria-label/title 此前硬编码中文“代理池”,改用已有的 proxyPool.name 翻译键(与 ProxyPoolHeaderAction 一致),可随语言切换。

* refactor: 删除未使用的 ProxyPoolHeaderAction 组件

代理池按钮已内联到 Toolbar,该导出组件不再被任何地方引用。
…tion (#76)

* feat: 实现响应式工具栏折叠

核心改动:
- 新增 ToolbarMenu 组件,支持按优先级响应式显示/隐藏按钮
- 新增 completion-store 管理统一补全 Key 状态
- 重构 Toolbar 组件,使用统一的按钮配置系统
- 从 app.tsx 移除独立的 ChannelHeaderActions 和 ProxyPoolHeaderAction

响应式策略:
- 大屏(≥1280px): 所有按钮展开
- 中屏(768-1279px): 核心按钮展开 + 更多菜单
- 小屏(<768px): 仅搜索 + 更多菜单

按钮优先级:
- always: 始终可见(搜索)
- desktop: md以上可见(新增)
- large: xl以上可见(代理池/统一补全)
- menu-only: 只在菜单(设置/页面操作)

解决问题:
- 搜索框展开不再与标题重叠
- 统一补全按钮改为图标模式,不再挤压标题
- 所有工具按钮在窄屏下收入更多菜单

* fix: 恢复原有工具栏风格,设置按钮独立可见

修复问题:
- 设置按钮(SlidersHorizontal)改回始终可见(原始设计)
- 移除不必要的"更多"菜单嵌套
- 站点页面的全局操作(导入/同步/签到/归档)放回设置 Popover 内
- 保持与原有代码一致的视觉风格和交互方式

当前布局:
- 搜索框 + 响应式按钮(代理池/补全/自动分组/新增)+ 设置按钮
- 设置 Popover 包含:布局选项 + 排序选项 + 全局操作(站点页面)

响应式仅应用于:
- large (xl): 代理池/统一补全/自动分组
- desktop (md): 新增按钮

* fix: 移除搜索框 maxWidth 限制,恢复原始展开动画

- 移除 style maxWidth 限制,让搜索框自然展开
- 保持 absolute right-0 定位,从右向左展开
- 展开动画与原始代码完全一致

* fix: 修复 completionDialogOpen 未定义错误

- 将 UnifiedCompletionDialog 从 SiteChannelGrid 移到 SiteChannelSection
- SiteChannelGrid 是子组件,不应访问父组件的状态变量
- 对话框应该在 SiteChannelSection 层级渲染

* feat: 渠道页面 Tab 响应式布局优化

- 小屏幕(<640px): 标题和 Tab 垂直排列
- 大屏幕(≥640px): 标题和 Tab 水平排列(保持原样式)
- 使用 flex-col sm:flex-row 实现响应式切换
- 调整间距:小屏幕 gap-2,大屏幕 gap-6

解决问题:
- 移动端标题和 Tab 在同一行显得拥挤
- 给予 Tab 更多垂直空间,提升移动端体验

* fix: 修复移动端搜索框展开与 Tab 重叠问题

- header 改为 items-start 对齐,避免垂直居中导致的重叠
- 标题容器添加 pb-2 (小屏幕),为搜索框展开留出空间
- 工具栏容器添加 relative 定位和 min-h-[36px] 最小高度
- 确保搜索框展开时不会覆盖下方的 Tab

* fix: 修复"更多"按钮响应式显隐逻辑

核心问题:
- 之前"更多"按钮在所有屏幕尺寸下都显示,即使内容已经全部展开
- 导致日志页面等没有折叠内容的页面仍显示空的"更多"菜单

解决方案:
- 使用 CSS 响应式类控制"更多"按钮显隐
- 有 large 按钮时:xl:hidden (xl以下显示)
- 有 desktop 按钮时:md:hidden (md以下显示)
- 没有可折叠按钮时:hidden (完全隐藏)

效果:
- 大屏幕(xl): 所有按钮展开,"更多"按钮隐藏
- 中屏幕(md-xl): desktop 按钮展开,large 按钮在"更多"中
- 小屏幕(<md): 只有搜索和设置,其他都在"更多"中
- 日志页面刷新按钮展开后,"更多"按钮自动隐藏

* fix: 修复工具栏按钮间距/排序与对话框无法弹出

- 将创建/自动分组对话框移出 flex 容器(包进隐藏容器),消除其隐藏触发器作为 flex 子项在设置按钮右侧产生的逐页不同间隔
- 新增(+)按钮调整到工具栏最右侧(ToolbarMenu 中 desktop 项渲染到最后)
- 修复"更多"菜单中 large→desktop 分割线在 md~xl 区间的孤立显示
- MorphingDialog 支持可选受控 open/onOpenChange,修复新增/自动分组对话框无法打开(向后兼容非受控用法)

* fix: 修复工具栏“更多”按钮仅折叠单个选项的问题

折叠成“更多”只在收纳 ≥2 项时才有意义。此前 large 按钮在中等屏(md~xl)、desktop 按钮在小屏(<md)会各自单独进入“更多”,导致只装一个选项——占位相同却多一次点击。改为按各断点被折叠的项数决定是否显示“更多”,仅 ≥2 项时折叠,否则该项直接平铺。

* fix: 修复工具栏 Hooks 条件调用与未使用导入的 lint 报错

useMemo 此前位于 early return 之后,违反 react-hooks/rules-of-hooks;将 return 移到 useMemo 之后使其无条件调用。同时删除未使用的 Archive/CalendarCheck2/Upload 图标导入。

* fix: 修复补全对话框可达性与 MorphingDialog 非受控状态陈旧

- site-channel:移除 cards 为空时的 early-return,改为条件渲染 SiteChannelGrid,使 UnifiedCompletionDialog 始终挂载(过滤后无卡片仍可从工具栏打开补全);待补全归零时关闭对话框,避免新任务到来时自动重开。

- morphing-dialog:非受控模式直接将值交给 setUncontrolledOpen,由 React 基于最新 state 计算,消除陈旧 uncontrolledOpen 快照并移除该依赖。
* fix: gate site sync token sk- prefix by platform

Stop force-prepending sk- to every site sync token. The prefix is a
new-api family convention; direct providers (openai/claude/gemini) must
use their keys verbatim, so forcing sk- on them caused 401s.

- model: add NormalizeSiteSyncTokenValueForPlatform / usesSyncTokenSkPrefix
- sitesync: apply prefix by platform at projection (buildChannelKeys) and
  at model-list fetch (fetchModelsForSiteToken), keeping both consistent
- op: store source tokens verbatim (no forced sk- on save)
- keep NormalizeComparableSiteTokenValue (sk--insensitive masked matching)
- tests: cover platform gating; update management mocks to sk-prefixed keys

* test: correct verbatim-storage assertion message in site_channel_test
)

* feat: per-protocol base URL override for sites

Some upstreams expose different protocols under different path prefixes
(e.g. OpenAI responses at <base>/v1 but Anthropic messages at
<base>/anthropic/v1). A single site base URL cannot serve both.

Since projection already splits aggregator sites into one channel per
outbound route type, give each route its own base URL:

- model: add SiteRouteBaseURL + Site.RouteBaseURLs (json column),
  ResolveRouteBaseURL / NormalizeSiteRouteBaseURLs, wired into Normalize
- op: SiteUpdate merges RouteBaseURLs (empty slice clears)
- sitesync: resolveProjectedChannelBaseURL picks the per-route override
  (verbatim) or falls back to the default /v1 base; relay unchanged
- web: site edit dialog gains a per-protocol base URL editor; types and
  normalization updated

GORM AutoMigrate adds the nullable JSON column; no manual migration.
Stacks on the sk--prefix-by-platform branch (shares buildChannelKeys).

* feat: validate site route base URL overrides

Reject overrides whose route type is not a projectable outbound route or
whose base URL is not a valid http/https URL (mirrors Site.BaseURL
validation), so malformed overrides surface to the caller instead of
silently breaking projection. Validation runs in Site.Validate (create +
update); Normalize keeps doing trim/dedup only.
* feat: add per-API-key RPM rate limiting

Add optional MaxRPM field to API keys for request-per-minute rate
limiting. Uses a fixed-window counter stored in a dedicated in-memory
cache (no DB persistence needed). Enforced in APIKeyAuth middleware
before channel routing, covering all proxy traffic (both regular and
site-projected channels). Returns HTTP 429 with Retry-After header
when exceeded. Includes non-negative validation in handlers and
frontend form with i18n support (en/zh_hans/zh_hant).

* fix: rate limit race condition, delete ordering, and RPM input state

- Move RateLimitDel after successful DB deletion in APIKeyDelete
- Add atomic GetOrSet to cache to prevent concurrent entry creation
- Reset maxRPM input to empty when user enters 0 (matches unlimited state)

* fix: use structured error code for max_rpm validation

Replace plain text resp.Error with ErrorWithAppError using
CodeCommonInvalidParam for consistent localization and client handling.
* feat: display cache tokens inline in log card

- Merge firstToken + totalTime into single "duration" column (TTFT/Total)
- Show cache_read inline after input count (sky-blue "R 148K") when present
- Change input icon color to sky-blue when cache exists
- Input shows non-cache portion: input_tokens - cache_read + cache_write
- Remove InputTokenDetailsPopover component
- Add compact duration formatter for large values (≥100s → no decimals)
- Add compact token formatter (K/M units)
- Reduce grid from 7 to 6 columns to fill space
- Add i18n "duration" key for zh_hans/zh_hant/en

* chore: remove unused formatOptionalTokenCount

* fix: clamp headline input tokens to non-negative value

* style: render cache-read tokens as badge and align stat row

- Show cache-read tokens as a sky-tinted Badge matching channel badges
- Weight the headline stat grid columns (minmax) so input no longer wraps
- Put input label/value/badge in a flex row so the badge aligns with the
  token number instead of floating on the text baseline
Deduct cache_read only when the channel is non-Anthropic and input
already includes the cached portion (input >= cache_read). Anthropic
input_tokens exclude cache_read by spec, so subtracting them collapsed
the headline to 0 on cache-heavy requests and mis-reduced recovered
conversations where input >= cache_read. The size guard also catches
malformed upstreams whose prompt excludes cached tokens.
The legacy "[Site] site / account / group (key) [endpoint]" channel name
stopped being created in f4a29d0 (kept only as a reuse fallback). With no
"[Site] %" channels left in the DB, the fallback and its recognition are
dead weight.

- Remove buildLegacyManagedChannelName and the legacyName reuse fallback in
  reuseManagedChannelByName; reuse now keys solely on the compact new name.
- Switch the orphan-reuse test to buildManagedChannelName.
- Drop the /\[Anthropic\]/ branch in isAnthropicChannel, keep /-Anthropic$/
  only; the headline-input cache semantics stay intact.
* feat: 统一 API 平台,支持多协议路由配置

## 功能变更

### 后端
- 新增 `SitePlatformAPI` 常量,统一 OpenAI/Claude/Gemini 为「API 直连」平台
- 新增 `Site.DefaultRouteType` 字段,指定默认协议类型
- 向后兼容:自动迁移旧平台值 (openai→api+openai_chat, claude→api+anthropic, gemini→api+gemini)
- 支持按协议配置独立 Base URL (`RouteBaseURLs`),自动拆分渠道
- 修复 APIKey 账户路由探测时 token 传递错误
- 优化平台检测,返回推荐的 `default_route_type`

### 前端
- 站点编辑:新增「默认协议」选择器(OpenAI Chat / Anthropic / Gemini)
- 平台检测时自动填充推荐协议
- 优化日志详情页显示(手动调整)
- 更新所有平台标签和凭据类型选项

### 数据库
- 迁移 017:添加 `default_route_type` 列,批量更新现有站点

## 测试
- ✅ 所有单元测试通过 (model/sitesync/op)
- ✅ 前端 TypeScript/ESLint 检查通过
- ✅ 构建验证通过

## 兼容性
- 旧平台值通过 `Normalize()` 自动转换
- 现有站点无缝升级,行为保持一致

* fix: add migration error checks and fix detect race in site form

- Check .Error on each Updates call in migration 017 to surface DB failures
- Use local variable for default_route_type in submit handler to avoid
  reading stale React state after setSiteForm
…#84)

* docs: update usage guide for new features and add English translation

- Update USAGE_zh.md: unified API Direct platform, default protocol,
  per-protocol Base URL, per-key RPM rate limiting, cache token display
- Create USAGE.md: full English translation of the usage guide
- Add prominent links to usage guides in both README files

* Update API documentation for OpenAI/Claude/Gemini

* Update API token instructions in USAGE_zh.md

Clarified instructions for API token usage and site URL format.
主代理路径 (relay.go copyHeaders) 已在出站请求显式置空 User-Agent,
阻止 net/http 注入默认的 Go-http-client/1.1。但有四条独立的出站请求
路径绕过了该逻辑,仍会向上游泄露 Go 标识:

- images.go copyHeadersToUpstream (OpenAI 图像 API 代理)
- compact.go copyProxyHeaders (OpenAI Responses Compact 代理)
- grouphealth/probe.go (分组健康探测 / 站点离群检测)
- helper/delay.go GetUrlDelay (渠道 BaseUrl 延迟测速)

这四处在客户端未携带 User-Agent 时均补齐显式置空,与主路径行为一致。

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

- Widen duration/input/output columns to 1.2fr (others stay 1fr)
- Drop "R " prefix from the cache-read badge
- Show one extra decimal and truncate (not round) compact token counts

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ons (#94)

Previously, ClearHelpFields() cleared ReasoningContent and Reasoning fields
before sending to OpenAI-compatible providers. This caused DeepSeek and other
providers that use reasoning_content in their OpenAI Chat format to fail with
400 errors when the content wasn't passed back in multi-turn conversations.

Root cause: DeepSeek's thinking mode requires reasoning_content to be echoed
back in subsequent turns. The field is a legitimate part of the OpenAI Chat
format (DeepSeek extension), and clearing it broke multi-turn thinking flows.

Changes:
- Keep ReasoningContent and Reasoning fields in ClearHelpFields()
- Only clear provider-specific help fields (ReasoningSignature, ReasoningBlocks)
- Providers that don't support reasoning_content will ignore the field
- Providers that require it (DeepSeek) now receive it correctly

This fixes the error:
"The \`reasoning_content\` in the thinking mode must be passed back to the API"
…through (#95)

* feat(relay): add StreamProcessor abstraction for unified stream handling

Create new internal/relay/stream package with:
- StreamProcessor: Unified stream processing loop handling heartbeat,
  first-token timeout, context cancellation, and terminal event detection
- StreamSource interface with 3 implementations:
  - SSESource: wraps tmaxmax/go-sse for HTTP SSE streams
  - WSSource: wraps WebSocket upstream reader
  - RawSource: raw chunked reads for passthrough (32KB buffers)

Benefits:
- Eliminates ~400 lines of duplicated stream loop code across 4 handlers
- Single source of truth for timeout/heartbeat/disconnect logic
- Comprehensive unit test coverage (10 tests, all passing)

This is Phase 2 Step 1 of the stream unification refactor. Next steps
will migrate existing handlers to use these abstractions.

Related: Phase 2 (Stream Processor Unification) from architecture improvement plan

* feat(relay): migrate standard HTTP SSE path to StreamProcessor

Replace handleStreamResponse with handleStreamResponseV2 that uses the
new StreamProcessor abstraction. This eliminates ~150 lines of duplicated
stream handling logic while maintaining identical behavior.

Changes:
- Add handleStreamResponseV2() using StreamProcessor + SSESource
- Expose PayloadWritten() method on StreamProcessor for metrics tracking
- Update forwardViaHTTP() to use V2 implementation for streaming responses
- Keep old handleStreamResponse() temporarily for rollback capability

Benefits:
- Unified timeout/heartbeat/disconnect handling via StreamProcessor
- Transform logic reused via ra.transformStreamData()
- All existing relay tests pass without modification

This is Phase 2 Step 2 of the stream unification refactor. The old
handleStreamResponse will be removed in the final cleanup step after
all paths are migrated and validated.

* feat(relay): migrate WebSocket path to StreamProcessor

Replace handleWSStreamResponse with handleWSStreamResponseV2 that uses
the StreamProcessor abstraction. This eliminates another ~110 lines of
duplicated stream handling logic.

Changes:
- Add handleWSStreamResponseV2() using StreamProcessor + WSSource
- Fix WSUpstreamReader interface: Close() returns error (not void)
- Update both WS call sites to use V2 implementation:
  - forwardViaWS() for fresh connections
  - retryViaFreshUpstreamWS() for reconnect recovery
- Keep old handleWSStreamResponse() for rollback capability

Benefits:
- Unified timeout/heartbeat/disconnect handling via StreamProcessor
- Transform logic reused via ra.transformStreamData()
- WebSocket upstream reader now uses same abstraction as HTTP SSE
- All existing relay tests pass without modification

This is Phase 2 Step 3 of the stream unification refactor. Next step
will migrate passthrough paths (Anthropic/OpenAI Responses).

* feat(transformer): add PassthroughCapable interface for same-format optimization

Add PassthroughCapable interface to enable outbound transformers to declare
support for same-format passthrough (bypassing Internal Model round-trip).

Changes:
- Add PassthroughCapable interface to model/interface.go with methods:
  - CanPassthrough(inboundFormat) bool
  - TransformRequestRaw(...) (*http.Request, error)  [already existed]
  - PassthroughConfig() PassthroughConfig

- Add PassthroughConfig struct with:
  - TerminalEvents: protocol-specific terminal event types
  - CollectMetrics: whether to aggregate response for cost tracking

- Implement PassthroughCapable on Anthropic MessageOutbound:
  - CanPassthrough: true for APIFormatAnthropicMessage
  - TerminalEvents: {"message_stop", "error"}
  - CollectMetrics: true (requires full aggregation)

- Implement PassthroughCapable on OpenAI ResponseOutbound:
  - CanPassthrough: true for APIFormatOpenAIResponse
  - TerminalEvents: {"response.completed", "response.failed",
                     "response.incomplete", "error"}
  - CollectMetrics: false (different metrics semantics)

Benefits:
- Interface-driven extensibility: new providers implement interface
- Protocol-specific settings encapsulated in transformer, not relay
- Terminal event detection logic pushed to transformers
- No hardcoded relay.go type checks for passthrough capability

This is Phase 5 Step 1 of the passthrough generalization refactor.
Next step will refactor relay.go to use this interface.

* feat(relay): migrate passthrough paths to StreamProcessor

Replace Anthropic and OpenAI Responses passthrough stream handlers with
unified handleStreamResponsePassthroughV2 using StreamProcessor + RawSource.

Changes:
- Add handleStreamResponsePassthroughV2() using StreamProcessor with:
  - RawSource for 32KB chunked reads (preserves byte fidelity)
  - BufferRawStream enabled for metrics collection
  - TerminalEvents from PassthroughConfig for early completion detection
  - Unified OnFinish callback for metrics aggregation

- Add collectPassthroughMetrics() unified metrics collector:
  - Replaces collectAnthropicPassthroughMetrics()
  - Replaces collectOpenAIResponsesPassthroughMetrics()
  - Tries OutboundStreamEventTransformer first (preferred)
  - Falls back to TransformStream for compatibility

- Update forwardViaHTTPPassthroughAnthropic() to use V2
- Update forwardViaHTTPPassthroughOpenAIResponses() to use V2

- Keep old handlers for rollback:
  - handleStreamResponsePassthroughAnthropic()
  - handleStreamResponsePassthroughOpenAIResponses()

Benefits:
- Eliminates ~340 lines of duplicated passthrough stream logic
- Unified timeout/heartbeat/disconnect/terminal-event handling
- Protocol-specific settings (terminal events, metrics collection)
  driven by PassthroughConfig from transformer
- All existing relay tests pass without modification

This is Phase 2 Step 4 of the stream unification refactor. All 4 stream
paths (HTTP SSE, WebSocket, Anthropic passthrough, OpenAI Responses
passthrough) now use StreamProcessor.

* feat(relay): unify passthrough logic with interface-driven dispatch

Replace hardcoded passthrough detection (shouldPassthroughAnthropic,
shouldPassthroughOpenAIResponses) with interface-driven unified logic.

Changes:
- Refactor forwardViaHTTP() to use PassthroughCapable interface:
  - Single type check: outAdapter.(model.PassthroughCapable)
  - Calls CanPassthrough(inboundFormat) for protocol matching
  - No hardcoded channel type checks

- Add forwardViaHTTPPassthrough() unified handler:
  - Works with any PassthroughCapable transformer
  - Uses TransformRequestRaw() from interface
  - Gets PassthroughConfig() for protocol-specific settings
  - Delegates to handleStreamResponsePassthroughV2 for streams
  - Delegates to handleResponsePassthrough for non-streams

- Add handleResponsePassthrough() for non-streaming passthrough:
  - Unified logic for Anthropic and OpenAI Responses
  - Sidecar metrics parsing via TransformResponse
  - Respects PassthroughConfig.CollectMetrics

- Remove duplicate forwardViaHTTPStandard() definition
- Keep old passthrough functions for rollback:
  - shouldPassthroughAnthropic()
  - shouldPassthroughOpenAIResponses()
  - forwardViaHTTPPassthroughAnthropic()
  - forwardViaHTTPPassthroughOpenAIResponses()

Benefits:
- ~150 lines of hardcoded logic eliminated
- New protocols add passthrough by implementing interface
- Protocol-specific logic in transformer, not relay
- Single passthrough code path for all protocols
- All existing relay tests pass without modification

This is Phase 5 Step 2 of the passthrough generalization refactor.
Combined with Phase 2, this completes the core refactoring.

* refactor(relay): unify error handling and update tests for V2

Changes:
- Export ErrEmptyUpstreamStream from stream package
- Update relay.go to use stream.ErrEmptyUpstreamStream
- Update empty_stream_test.go to use V2 handlers
- All empty stream detection tests pass

Note: Two WebSocket continuation tests fail due to slightly different
error detection timing in V2. These are edge cases for WebSocket
continuation recovery that need further investigation.

All other tests (including all HTTP SSE, standard WebSocket, and
passthrough tests) pass without issues.

* fix(relay): properly handle ErrEmptyUpstreamStream in WS continuation logic

Fix WebSocket continuation tests by updating error detection functions to
recognize stream.ErrEmptyUpstreamStream.

Changes:
- Update isContinuationTransportFailure() to check errors.Is(err, stream.ErrEmptyUpstreamStream)
- Update shouldReconnectUpstreamWSBeforeReplay() to recognize ErrEmptyUpstreamStream
- Add stream package import to ws_error.go

Fixes:
- TestHandlerStopsFailoverWhenContinuationTransportIsUnavailable now passes
- TestForwardViaWSReconnectsContinuationAfterReadFailureBeforeFirstEvent now passes

Behavior:
- Empty streams before first event in continuation requests trigger reconnect
- Empty streams after send failure in continuation requests return 409
- All relay tests (60+) now pass without failures

* refactor(relay): remove obsolete passthrough checks

Remove obsolete shouldPassthroughAnthropic/OpenAIResponses checks that are
now redundant with the unified PassthroughCapable interface-driven logic.

Changes:
- Remove shouldPassthroughAnthropic() check in collectResponse()
  - V2 passthrough handlers now handle metrics collection via PassthroughConfig.CollectMetrics
  - All paths now call collectResponse() consistently

- Remove shouldPassthroughOpenAIResponses() check in WS decision logic
  - Passthrough is now handled by forwardViaHTTP via PassthroughCapable interface
  - WS logic no longer needs special passthrough awareness

Old handler functions are kept for reference and rollback safety:
- handleWSStreamResponse (superseded by handleWSStreamResponseV2)
- handleStreamResponse (superseded by handleStreamResponseV2)
- handleStreamResponsePassthrough* (superseded by handleStreamResponsePassthroughV2)
- handleResponsePassthrough* (superseded by handleResponsePassthrough)
- forwardViaHTTPPassthrough* (superseded by forwardViaHTTPPassthrough)
- shouldPassthrough* (superseded by PassthroughCapable.CanPassthrough)

These can be removed in a future cleanup PR once the V2 implementation
is fully validated in production.

* refactor(test): update all passthrough tests to use V2 handlers

Update all passthrough-related tests in empty_stream_test.go and relay_test.go
to use the new handleStreamResponsePassthroughV2 with PassthroughConfig.

Changes:
- TestPassthroughOpenAIResponsesEmptyStreamFails: use V2 + PassthroughConfig
- TestPassthroughAnthropicEmptyStreamFails: use V2 + PassthroughConfig
- TestHandleStreamResponsePassthroughAnthropicPreservesRawSSE: use V2
- TestHandleStreamResponsePassthroughOpenAIResponsesPreservesRawSSE: use V2
- TestHandleStreamResponsePassthroughOpenAIResponsesClientCancelAfterTerminal: use V2
- TestHandleStreamResponsePassthroughOpenAIResponsesClientCancelMidStream: use V2
- TestHandleStreamResponsePassthroughAnthropicClientCancelAfterTerminal: use V2

All tests retrieve PassthroughConfig via:
  pt := ra.outAdapter.(transformerModel.PassthroughCapable)
  cfg := pt.PassthroughConfig()

All tests continue to pass with identical behavior.

* refactor(relay): remove all deprecated old handler functions

Delete 807 lines of deprecated handler functions that have been fully
replaced by the V2 unified StreamProcessor implementation.

Deleted functions:
- handleWSStreamResponse (117 lines) → replaced by handleWSStreamResponseV2
- handleStreamResponse (149 lines) → replaced by handleStreamResponseV2
- handleStreamResponsePassthroughAnthropic (169 lines) → replaced by handleStreamResponsePassthroughV2
- handleStreamResponsePassthroughOpenAIResponses (162 lines) → replaced by handleStreamResponsePassthroughV2
- handleResponsePassthroughAnthropic (27 lines) → replaced by handleResponsePassthrough
- handleResponsePassthroughOpenAIResponses (22 lines) → replaced by handleResponsePassthrough
- forwardViaHTTPPassthroughAnthropic (64 lines) → replaced by forwardViaHTTPPassthrough
- forwardViaHTTPPassthroughOpenAIResponses (58 lines) → replaced by forwardViaHTTPPassthrough
- shouldPassthroughAnthropic (14 lines) → replaced by PassthroughCapable.CanPassthrough
- shouldPassthroughOpenAIResponses (18 lines) → replaced by PassthroughCapable.CanPassthrough

Also removed unused imports:
- outAnthropic (no longer referenced)
- safe (no longer used after removing old handlers)

Impact:
- 807 lines deleted
- All 60+ tests pass
- No behavioral changes
- Cleaner, more maintainable codebase

* feat(relay): enhance stream processor robustness and WS handling

Improve stream processing lifecycle management, context cancellation handling,
and first token timeout detection for WebSocket streams.

Key changes:
- Stream processor now uses derived context to unblock ReadEvent on exit
- Add sync.Once protection to SSESource.Close to prevent double-close
- WebSocket handler checks first token timeout on context cancellation
- Add atomic flag to prevent duplicate response collection
- Fix race condition in context cancellation tests
- Downgrade usage_missing log from Warn to Debug

These changes resolve edge cases where stream cleanup could hang or timeout
detection could miss early cancellations.

* chore: Delete debug-site-channel.md

* fix(relay): correct mojibake in rawBody comment

* chore: fix gofmt violations and remove deprecated errEmptyUpstreamStream

- Run gofmt on processor.go, sse_source.go, relay.go (struct alignment, comment alignment, blank line)
- Remove errEmptyUpstreamStream alias; update empty_stream_test.go to use stream.ErrEmptyUpstreamStream directly
* fix(site-channel): virtualize projected models table

The models table inside a site's projected-channel dialog rendered every
row in a real <table> with manual pagination (15 initial + 30 per page)
driven by an IntersectionObserver sentinel. With 1000+ models the
load-more fired once then stalled at 45 rows, so the list could not be
scrolled any further.

Replace the pagination with full vertical virtualization
(@tanstack/react-virtual): a self-contained scroll container, a sticky
header and body rows sharing one grid-template-columns so columns stay
aligned, horizontal scroll via min-w, and only visible rows in the DOM.
Jump-to-model now uses the virtualizer's scrollToIndex instead of
per-row scrollIntoView, so the old registerModelRef/modelElementRefs
plumbing is removed.

Also stop overlong model names from widening the row: the truncate span
was a flex child without min-w-0 and never clipped. Add min-w-0/flex-1
along the name chain and shrink-0 to the custom badge.

* fix(site-channel): keep jump target pinned until highlight clears

When jumping to a model, onJumpHandled cleared jumpRequest at +80ms,
which dropped forcedModelKey and removed the target row from
filteredModels well before the +1800ms highlight finished. A target
excluded by an active search / quick-filter therefore flashed and
vanished mid-jump, leaving the highlight ring on an unmounted row.

Also pin filteredModels on highlightedModelKey (set at +80ms, cleared
at +1800ms) so the target stays visible for the full highlight dwell
regardless of when jumpRequest is cleared.
)

Native DB import ran site base_url through normalizeImportBaseURL, which
keeps only scheme://host and drops the path. A site exported as
https://opencode.ai/zen/v1 came back as https://opencode.ai, and because
dedup keys on platform+base_url, the mutated URL no longer matched the
original, creating a duplicate site (e.g. "Name (2)").

That helper is meant for third-party imports (new-api etc., where the
base is the domain root). Native backups already hold full, canonical
URLs, so trim only like Site.Normalize and keep the path intact.
问题:API 直连站点修改模型端点类型后不生效

根因:API 平台默认不拆分渠道,导致用户手动修改的 RouteType 被投影逻辑忽略

修复:实现智能拆分逻辑 shouldSplitForAccount
- 检测账号内是否有多种手动覆盖的 RouteType
- 如检测到混合端点类型,自动启用拆分模式
- 不同端点格式的模型分配到不同的投影渠道
- 自动迁移 GroupItem 到正确渠道

影响:
- 用户修改端点类型后立即生效
- 无需手动配置 RouteBaseURLs
- 自动适应混合端点场景
- 现有混合端点站点升级后会自动拆分(无感迁移)

测试:新增 10 个测试用例,所有现有测试通过

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add support for DeepSeek's thinking parameter to fix issue where
thinking mode could not be disabled from client applications.

Root cause: The thinking parameter ({"thinking": {"type": "disabled"}})
sent by clients like Cherry Studio was silently dropped during JSON
deserialization because InternalLLMRequest lacked this field. This
caused DeepSeek API to use its default value (thinking.type = "enabled"),
making the model think even when users explicitly disabled it.

Changes:
- Add ThinkingConfig type to model package
- Add Thinking field to InternalLLMRequest
- Pass through thinking parameter in OpenAI chat outbound transformer
- Add unit tests and end-to-end regression test

Design:
- Protocol-neutral: only passes through the parameter without transformation
- Backward compatible: thinking is optional (omitempty)
- Isolated: does not affect other providers (Anthropic, Gemini, etc.)

Fixes: #88
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.

[Feature] 增加 “渠道请求超时”功能

2 participants