Skip to content

refactor: extract protocol conversion layer into standalone relaykit module - #6369

Merged
Calcium-Ion merged 17 commits into
mainfrom
refactor/relaykit
Jul 27, 2026
Merged

refactor: extract protocol conversion layer into standalone relaykit module#6369
Calcium-Ion merged 17 commits into
mainfrom
refactor/relaykit

Conversation

@Calcium-Ion

@Calcium-Ion Calcium-Ion commented Jul 21, 2026

Copy link
Copy Markdown
Member

📝 变更描述 / Description

目标:把协议转换层(OpenAI / Claude / Gemini / Responses 四种格式的请求、响应、流式互转,即 service/relayconvert + dto + types + reasonmap)抽为独立 Go 子模块 relaykit,可被其他项目独立引用。异步任务链路(task/suno/midjourney/video)不在本次范围,留在宿主。

为什么这样改能生效(主体按四步推进,review 修复以 follow-up commits 收口,行为全程被快照测试锁定):

  1. 回归基线先行:为互转矩阵全部 12 条 (from, to) 路由录制 golden 快照(请求/响应/流式共 36 个 testdata),之后每一步都验证转换产物字节级不变;另加边界测试禁止 kit 包新增宿主 import,越界会直接编译失败。
  2. 签名净化:实测 gin.Context 在转换器内部零实际使用(纯穿透参数),从全部签名删除,改为 context.Context;转换层只读 RelayInfo 的 12 个协议字段,且半数本就是转换器私有流式状态——为此新建小接口 convmeta.Meta,由 RelayInfo 直接实现,调用方无感;约 10 处 model_setting/reasoning 全局读取改为 convmeta.Options 配置快照,宿主在 ConvOptions() 里装配(跨渠道重试时缓存会失效重建)。
  3. 工具下沉:kit 用到的 common 纯工具(JSON 封装、GetPointer、MaskSensitiveInfo 等)下沉为 kitutilEndpointType/FinishReason 迁入 types。宿主侧 common/constant/setting/reasoning 全部保留转发别名,宿主既有代码基本无需改动。
  4. 物理拆分git mvrelaykit/ 并建立独立 go.mod;宿主通过 require relaykit v0.0.0 + replace => ./relaykit 消费本地源码。仓库不提交 go.work,宿主和 relaykit 的构建/测试均可显式使用 GOWORK=off

附带修复:

  • Docker Go 阶段显式 GOWORK=off,并在 go mod download 前复制 relaykit/go.mod,确保本地 replace 可解析。
  • Claude 默认 max_tokens 区分“未配置 callback”和“callback 显式返回 0”:前者省略,后者保留 "max_tokens": 0;正数正常注入,客户端显式非零值优先。
  • relaykit 保持 Gemini safety callback 空值即省略 category;宿主对缺失/空 default 在读取期回填 OFF,空 override 回退 default。历史非法非空值保持原样并继续由上游拒绝;管理端新写入的非法非空枚举会在落库前被拒绝。
  • RelayInfo 与 relaykit 原生 convmeta.Values 均支持 typed-nil Meta,转换 facade 不再因 nil receiver panic。
  • kit 普通数据错误经 logger.LogError(nil, ...) 保持 [ERR] 路由,内部 converter 异常经独立 system-error hook 进入 common.SysError[SYS] 路由;hook 在 main() 开始时安装,Debug 状态在环境加载后设置。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature)
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

宿主与 relaykit 全量测试:

$ make test
Testing root Go module...
(全部 ok,无失败用例)
Testing relaykit Go module...
(全部 ok,无失败用例)

relaykit 独立构建与测试:

$ cd relaykit
$ GOWORK=off go build ./...
$ GOWORK=off go test ./...
(全部 ok,无失败用例)

golden 快照保持字节不变,未使用 -update、未重新录制:

$ GOWORK=off go test ./relayconvert -run 'TestGolden|TestRelaykitBoundary' -count=1
ok  github.com/QuantumNous/new-api/relaykit/relayconvert
$ git diff --exit-code -- relaykit/relayconvert/testdata/golden
(零 diff)

两模块 tidy 在提交后复跑,四个 module 文件保持零 diff:

$ GOWORK=off go mod tidy
$ cd relaykit && GOWORK=off go mod tidy
$ git diff --exit-code -- go.mod go.sum relaykit/go.mod relaykit/go.sum
(零 diff)

本地 replace 与依赖闭包验证:

$ GOWORK=off go list -m github.com/QuantumNous/new-api/relaykit
github.com/QuantumNous/new-api/relaykit v0.0.0 => ./relaykit
$ cd relaykit && GOWORK=off go list -m all | grep -c QuantumNous
1   # 仅 relaykit 自身

Docker 完整构建:

$ docker build --tag new-api-pr6369-check .
#30 naming to docker.io/library/new-api-pr6369-check done
#30 DONE

…guard

Phase 0 of the relaykit extraction plan: pin byte-level output of every
registered (from,to) request/response/stream conversion route, and
forbid kit-bound packages from growing host-only imports.
…nvmeta draft

Phase 1 in progress: relayconvert now takes context.Context; host media
resolver adapts gin.Context back at the service boundary.
…settings

Phase 1 of the relaykit extraction plan:
- converters now depend on convmeta.Meta (implemented by RelayInfo) instead
  of *relaycommon.RelayInfo; ClaudeConvertInfo and the format guesser move
  to convmeta with aliases left behind
- host settings reach converters via a convmeta.Options snapshot built in
  RelayInfo.ConvOptions; no more model_setting/reasoning global reads inside
  the conversion layer
- effort-suffix helpers move to service/relayconvert/reasoning (old package
  forwards); chat-to-responses upgrade policy moves to service (host routing
  logic, not conversion)
- golden conversion matrix unchanged
Phase 2 (part 1): dto.Request.IsStream now takes *http.Request instead of
*gin.Context (Gemini's impl reads query/path off the std request); dto's
three logger calls become common.SysError. Boundary test allowlist is now
empty — kit-bound packages import no gin/setting/logger/model.
…t stop importing common

Phase 2 of the relaykit extraction plan:
- new service/relayconvert/kitutil holds the pure helpers the kit needs
  (JSON wrappers, pointer/string/uuid/timestamp utils, MaskSensitiveInfo,
  pluggable LogInfo/LogError hooks, Debug flag)
- dto, types, and all relayconvert packages now use kitutil; their only
  remaining internal deps are dto/types/constant
- common keeps every original symbol (MaskSensitiveInfo delegates to
  kitutil) so host code is untouched; main.go routes kit logging into
  common.SysLog/SysError and mirrors DebugEnabled
- golden conversion matrix unchanged
…alect via Options

Kit packages (dto/types/relayconvert/reasonmap) no longer import constant:
- EndpointType and finish-reason values live in types; constant re-exports
- the OpenRouter special-case in claude->openai request conversion reads
  Options.OpenRouterDialect, set by the host from the channel type;
  InitChannelMeta invalidates the cached snapshot on channel switch
Phase 3 of the relaykit extraction plan:
- new go module github.com/QuantumNous/new-api/relaykit containing dto
  (minus task family), types, relayconvert (with convmeta/kitutil/reasoning),
  and reasonmap; host consumes it via require + replace, go.work for dev
- task-family dto (task/suno/midjourney/video) stays in the host dto
  package; dual-consumer host files alias it as taskdto
- relaykit builds and tests standalone (GOWORK=off): no host imports,
  no gin, no DB, no settings
- golden conversion matrix unchanged
The local-replace submodule's go.mod must exist inside the build context
for the main module graph to resolve.
@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 362 files, which is 62 over the limit of 300.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3c618a3-b6b6-4e96-a96f-160ad78da210

📥 Commits

Reviewing files that changed from the base of the PR and between f51dd4d and 2700389.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • relaykit/go.sum is excluded by !**/*.sum
📒 Files selected for processing (366)
  • .gitignore
  • Dockerfile
  • common/str.go
  • constant/endpoint_type.go
  • constant/finish_reason.go
  • constant/waffo_pay_method.go
  • controller/billing.go
  • controller/channel-billing.go
  • controller/channel-test.go
  • controller/channel.go
  • controller/channel_test_internal_test.go
  • controller/channel_upstream_update.go
  • controller/channel_upstream_update_test.go
  • controller/model.go
  • controller/model_list_test.go
  • controller/option.go
  • controller/option_claude_test.go
  • controller/option_gemini_test.go
  • controller/playground.go
  • controller/ratio_sync.go
  • controller/relay.go
  • controller/task.go
  • controller/user.go
  • go.mod
  • i18n/i18n.go
  • main.go
  • makefile
  • middleware/auth.go
  • middleware/distributor.go
  • middleware/i18n.go
  • middleware/performance.go
  • middleware/utils.go
  • model/ability.go
  • model/channel.go
  • model/channel_cache.go
  • model/channel_settings_test.go
  • model/log.go
  • model/pricing.go
  • model/pricing_endpoint_test.go
  • model/task.go
  • model/user.go
  • model/user_cache.go
  • model/user_update_test.go
  • pkg/billingexpr/compile.go
  • pkg/billingexpr/run.go
  • relay/alpha_search_handler.go
  • relay/audio_handler.go
  • relay/channel/adapter.go
  • relay/channel/advancedcustom/adaptor.go
  • relay/channel/advancedcustom/adaptor_test.go
  • relay/channel/ali/adaptor.go
  • relay/channel/ali/dto.go
  • relay/channel/ali/image.go
  • relay/channel/ali/image_wan.go
  • relay/channel/ali/rerank.go
  • relay/channel/ali/text.go
  • relay/channel/api_request.go
  • relay/channel/aws/adaptor.go
  • relay/channel/aws/dto.go
  • relay/channel/aws/relay-aws.go
  • relay/channel/baidu/adaptor.go
  • relay/channel/baidu/dto.go
  • relay/channel/baidu/relay-baidu.go
  • relay/channel/baidu_v2/adaptor.go
  • relay/channel/claude/adaptor.go
  • relay/channel/claude/message_delta_usage_patch_test.go
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_claude_test.go
  • relay/channel/claude/tool_billing_test.go
  • relay/channel/cloudflare/adaptor.go
  • relay/channel/cloudflare/dto.go
  • relay/channel/cloudflare/relay_cloudflare.go
  • relay/channel/codex/adaptor.go
  • relay/channel/cohere/adaptor.go
  • relay/channel/cohere/dto.go
  • relay/channel/cohere/relay-cohere.go
  • relay/channel/coze/adaptor.go
  • relay/channel/coze/relay-coze.go
  • relay/channel/deepseek/adaptor.go
  • relay/channel/dify/adaptor.go
  • relay/channel/dify/dto.go
  • relay/channel/dify/relay-dify.go
  • relay/channel/gemini/adaptor.go
  • relay/channel/gemini/adaptor_responses_test.go
  • relay/channel/gemini/relay-gemini-native.go
  • relay/channel/gemini/relay-gemini.go
  • relay/channel/gemini/relay_gemini_usage_test.go
  • relay/channel/gemini/relay_responses.go
  • relay/channel/gemini/relay_responses_test.go
  • relay/channel/jimeng/adaptor.go
  • relay/channel/jimeng/image.go
  • relay/channel/jina/adaptor.go
  • relay/channel/minimax/adaptor.go
  • relay/channel/minimax/adaptor_test.go
  • relay/channel/minimax/image.go
  • relay/channel/minimax/relay-minimax.go
  • relay/channel/minimax/tts.go
  • relay/channel/mistral/adaptor.go
  • relay/channel/mistral/text.go
  • relay/channel/mokaai/adaptor.go
  • relay/channel/mokaai/relay-mokaai.go
  • relay/channel/moonshot/adaptor.go
  • relay/channel/moonshot/adaptor_test.go
  • relay/channel/ollama/adaptor.go
  • relay/channel/ollama/relay-ollama.go
  • relay/channel/ollama/stream.go
  • relay/channel/ollama/stream_test.go
  • relay/channel/openai/adaptor.go
  • relay/channel/openai/audio.go
  • relay/channel/openai/chat_via_responses.go
  • relay/channel/openai/chat_via_responses_test.go
  • relay/channel/openai/helper.go
  • relay/channel/openai/image_edit_test.go
  • relay/channel/openai/relay-openai.go
  • relay/channel/openai/relay_image.go
  • relay/channel/openai/relay_realtime.go
  • relay/channel/openai/relay_responses.go
  • relay/channel/openai/relay_responses_billing_test.go
  • relay/channel/openai/relay_responses_compact.go
  • relay/channel/openai/responses_via_chat.go
  • relay/channel/openai/usage.go
  • relay/channel/palm/adaptor.go
  • relay/channel/palm/dto.go
  • relay/channel/palm/relay-palm.go
  • relay/channel/perplexity/adaptor.go
  • relay/channel/perplexity/relay-perplexity.go
  • relay/channel/replicate/adaptor.go
  • relay/channel/siliconflow/adaptor.go
  • relay/channel/siliconflow/dto.go
  • relay/channel/siliconflow/relay-siliconflow.go
  • relay/channel/submodel/adaptor.go
  • relay/channel/task/ali/adaptor.go
  • relay/channel/task/doubao/adaptor.go
  • relay/channel/task/gemini/adaptor.go
  • relay/channel/task/hailuo/adaptor.go
  • relay/channel/task/jimeng/adaptor.go
  • relay/channel/task/kling/adaptor.go
  • relay/channel/task/vertex/adaptor.go
  • relay/channel/task/vidu/adaptor.go
  • relay/channel/tencent/adaptor.go
  • relay/channel/tencent/relay-tencent.go
  • relay/channel/vertex/adaptor.go
  • relay/channel/vertex/dto.go
  • relay/channel/volcengine/adaptor.go
  • relay/channel/volcengine/tts.go
  • relay/channel/xai/adaptor.go
  • relay/channel/xai/dto.go
  • relay/channel/xai/text.go
  • relay/channel/xunfei/adaptor.go
  • relay/channel/xunfei/dto.go
  • relay/channel/xunfei/relay-xunfei.go
  • relay/channel/zhipu/adaptor.go
  • relay/channel/zhipu/dto.go
  • relay/channel/zhipu/relay-zhipu.go
  • relay/channel/zhipu_4v/adaptor.go
  • relay/channel/zhipu_4v/dto.go
  • relay/channel/zhipu_4v/image.go
  • relay/channel/zhipu_4v/relay-zhipu_v4.go
  • relay/chat_completions_via_responses.go
  • relay/chat_completions_via_responses_test.go
  • relay/claude_handler.go
  • relay/common/override.go
  • relay/common/override_test.go
  • relay/common/relay_info.go
  • relay/common/relay_info_gemini_settings_test.go
  • relay/common/relay_info_test.go
  • relay/common/request_conversion.go
  • relay/common/stream_status.go
  • relay/common/tool_usage.go
  • relay/common/tool_usage_test.go
  • relay/common_handler/rerank.go
  • relay/compatible_handler.go
  • relay/embedding_handler.go
  • relay/gemini_handler.go
  • relay/helper/billing_expr_request.go
  • relay/helper/billing_expr_request_test.go
  • relay/helper/common.go
  • relay/helper/model_mapped.go
  • relay/helper/openai_image_request_test.go
  • relay/helper/price.go
  • relay/helper/price_test.go
  • relay/helper/valid_request.go
  • relay/image_handler.go
  • relay/param_override_error.go
  • relay/rerank_handler.go
  • relay/responses_handler.go
  • relay/websocket.go
  • relaykit/dto/alpha_search_request.go
  • relaykit/dto/audio.go
  • relaykit/dto/billing_usage.go
  • relaykit/dto/billing_usage_test.go
  • relaykit/dto/channel_settings.go
  • relaykit/dto/channel_settings_test.go
  • relaykit/dto/claude.go
  • relaykit/dto/embedding.go
  • relaykit/dto/error.go
  • relaykit/dto/gemini.go
  • relaykit/dto/gemini_generation_config_test.go
  • relaykit/dto/gemini_isstream_test.go
  • relaykit/dto/gemini_response_test.go
  • relaykit/dto/notify.go
  • relaykit/dto/openai_compaction.go
  • relaykit/dto/openai_image.go
  • relaykit/dto/openai_request.go
  • relaykit/dto/openai_request_zero_value_test.go
  • relaykit/dto/openai_response.go
  • relaykit/dto/openai_responses_compaction_request.go
  • relaykit/dto/openai_video.go
  • relaykit/dto/playground.go
  • relaykit/dto/pricing.go
  • relaykit/dto/ratio_sync.go
  • relaykit/dto/realtime.go
  • relaykit/dto/request_common.go
  • relaykit/dto/rerank.go
  • relaykit/dto/sensitive.go
  • relaykit/dto/user_settings.go
  • relaykit/dto/values.go
  • relaykit/go.mod
  • relaykit/reasonmap/reasonmap.go
  • relaykit/relayconvert/boundary_test.go
  • relaykit/relayconvert/claude_default_max_tokens_test.go
  • relaykit/relayconvert/convmeta/format.go
  • relaykit/relayconvert/convmeta/meta.go
  • relaykit/relayconvert/convmeta/meta_test.go
  • relaykit/relayconvert/convmeta/options.go
  • relaykit/relayconvert/golden_test.go
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_resp.go
  • relaykit/relayconvert/internal/gemini_chat/to_oai_chat_req.go
  • relaykit/relayconvert/internal/gemini_chat/to_oai_chat_resp.go
  • relaykit/relayconvert/internal/jsonutil/stringify.go
  • relaykit/relayconvert/internal/media/media.go
  • relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go
  • relaykit/relayconvert/internal/oai_chat/to_claude_messages_resp.go
  • relaykit/relayconvert/internal/oai_chat/to_claude_messages_resp_test.go
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_resp.go
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_resp_test.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_resp.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_resp_test.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_stream_resp.go
  • relaykit/relayconvert/internal/oai_responses/req_helpers.go
  • relaykit/relayconvert/internal/oai_responses/to_claude_messages_req.go
  • relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req.go
  • relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req_preprocess.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go
  • relaykit/relayconvert/internal/shared/claude/cache.go
  • relaykit/relayconvert/internal/shared/claude/errors.go
  • relaykit/relayconvert/internal/shared/claude/tool_choice.go
  • relaykit/relayconvert/internal/shared/gemini/request.go
  • relaykit/relayconvert/internal/shared/gemini/schema.go
  • relaykit/relayconvert/kitutil/json.go
  • relaykit/relayconvert/kitutil/log.go
  • relaykit/relayconvert/kitutil/log_test.go
  • relaykit/relayconvert/kitutil/mask.go
  • relaykit/relayconvert/kitutil/value.go
  • relaykit/relayconvert/media.go
  • relaykit/relayconvert/reasoning/suffix.go
  • relaykit/relayconvert/request_compat.go
  • relaykit/relayconvert/request_registry.go
  • relaykit/relayconvert/request_registry_test.go
  • relaykit/relayconvert/response_compat.go
  • relaykit/relayconvert/response_registry.go
  • relaykit/relayconvert/response_registry_test.go
  • relaykit/relayconvert/safety_settings_test.go
  • relaykit/relayconvert/testdata/golden/request/claude_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/request/claude_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/request/claude_to_openai_responses.golden.json
  • relaykit/relayconvert/testdata/golden/request/gemini_to_claude.golden.json
  • relaykit/relayconvert/testdata/golden/request/gemini_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/request/gemini_to_openai_responses.golden.json
  • relaykit/relayconvert/testdata/golden/request/openai_responses_to_claude.golden.json
  • relaykit/relayconvert/testdata/golden/request/openai_responses_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/request/openai_responses_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/request/openai_to_claude.golden.json
  • relaykit/relayconvert/testdata/golden/request/openai_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/request/openai_to_openai_responses.golden.json
  • relaykit/relayconvert/testdata/golden/response/claude_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/response/claude_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/response/claude_to_openai_responses.golden.json
  • relaykit/relayconvert/testdata/golden/response/gemini_to_claude.golden.json
  • relaykit/relayconvert/testdata/golden/response/gemini_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/response/gemini_to_openai_responses.golden.json
  • relaykit/relayconvert/testdata/golden/response/openai_responses_to_claude.golden.json
  • relaykit/relayconvert/testdata/golden/response/openai_responses_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/response/openai_responses_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/response/openai_to_claude.golden.json
  • relaykit/relayconvert/testdata/golden/response/openai_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/response/openai_to_openai_responses.golden.json
  • relaykit/relayconvert/testdata/golden/stream/claude_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/stream/claude_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/stream/claude_to_openai_responses.golden.json
  • relaykit/relayconvert/testdata/golden/stream/gemini_to_claude.golden.json
  • relaykit/relayconvert/testdata/golden/stream/gemini_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/stream/gemini_to_openai_responses.golden.json
  • relaykit/relayconvert/testdata/golden/stream/openai_responses_to_claude.golden.json
  • relaykit/relayconvert/testdata/golden/stream/openai_responses_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/stream/openai_responses_to_openai.golden.json
  • relaykit/relayconvert/testdata/golden/stream/openai_to_claude.golden.json
  • relaykit/relayconvert/testdata/golden/stream/openai_to_gemini.golden.json
  • relaykit/relayconvert/testdata/golden/stream/openai_to_openai_responses.golden.json
  • relaykit/relayconvert/text_converter_registry.go
  • relaykit/relayconvert/text_converter_registry_test.go
  • relaykit/types/channel_error.go
  • relaykit/types/endpoint_type.go
  • relaykit/types/error.go
  • relaykit/types/file_data.go
  • relaykit/types/file_source.go
  • relaykit/types/price_data.go
  • relaykit/types/relay_format.go
  • relaykit/types/request_meta.go
  • relaykit/types/rw_map.go
  • relaykit/types/set.go
  • router/relay-router.go
  • service/billing.go
  • service/billing_session.go
  • service/billing_usage.go
  • service/channel.go
  • service/channel_affinity.go
  • service/channel_affinity_usage_cache_test.go
  • service/convert.go
  • service/convert_test.go
  • service/error.go
  • service/error_test.go
  • service/file_decoder.go
  • service/file_service.go
  • service/log_info_generate.go
  • service/openai_chat_responses_compat.go
  • service/openai_chat_responses_mode.go
  • service/quota.go
  • service/quota_saturation_test.go
  • service/relayconvert/internal/matcher/regex.go
  • service/relayconvert/internal/meta/relay_info.go
  • service/relayconvert/internal/oai_chat/to_oai_responses_policy.go
  • service/relayconvert/request_compat.go
  • service/request_converter.go
  • service/sensitive.go
  • service/task_billing.go
  • service/task_billing_test.go
  • service/task_polling.go
  • service/task_polling_test.go
  • service/text_quota.go
  • service/text_quota_test.go
  • service/tiered_settle.go
  • service/tiered_settle_test.go
  • service/token_counter.go
  • service/usage_helpr.go
  • service/user_notify.go
  • service/violation_fee.go
  • service/webhook.go
  • setting/model_setting/claude.go
  • setting/model_setting/claude_test.go
  • setting/model_setting/gemini.go
  • setting/model_setting/gemini_test.go
  • setting/operation_setting/status_code_ranges.go
  • setting/payment_waffo.go
  • setting/ratio_setting/cache_ratio.go
  • setting/ratio_setting/group_ratio.go
  • setting/ratio_setting/model_ratio.go
  • setting/reasoning/suffix.go

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/relaykit

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.

Calcium-Ion and others added 4 commits July 21, 2026 16:46
…ct negative default_max_tokens

The Claude Messages API requires max_tokens (omitting it is a 400
"Field required"), but with a nil Options.Claude.DefaultMaxTokens hook
the converters silently emitted a request the upstream is guaranteed to
reject. Both OpenAI Chat and Responses → Claude conversions now return
sharedclaude.ErrMissingMaxTokens when no path (client value, default
hook, thinking-adapter floor) supplied one. Unreachable in the host,
which always configures the hook.

Host side, claude.default_max_tokens now rejects negative values at the
option API before persisting — they would wrap into huge unsigned values
during conversion. Zero stays allowed: the current API treats
max_tokens: 0 as cache pre-warming.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Calcium-Ion Calcium-Ion changed the title refactor:relaykit refactor: extract protocol conversion layer into standalone relaykit module Jul 26, 2026
@Calcium-Ion
Calcium-Ion merged commit 86ac0f7 into main Jul 27, 2026
1 check passed
agogo233 pushed a commit to agogo233/new-api that referenced this pull request Jul 27, 2026
…module (QuantumNous#6369)

* test(relayconvert): add golden snapshot matrix and relaykit boundary guard

Phase 0 of the relaykit extraction plan: pin byte-level output of every
registered (from,to) request/response/stream conversion route, and
forbid kit-bound packages from growing host-only imports.

* wip(relayconvert): drop gin.Context from converter signatures; add convmeta draft

Phase 1 in progress: relayconvert now takes context.Context; host media
resolver adapts gin.Context back at the service boundary.

* refactor(relayconvert): decouple converters from RelayInfo, gin, and settings

Phase 1 of the relaykit extraction plan:
- converters now depend on convmeta.Meta (implemented by RelayInfo) instead
  of *relaycommon.RelayInfo; ClaudeConvertInfo and the format guesser move
  to convmeta with aliases left behind
- host settings reach converters via a convmeta.Options snapshot built in
  RelayInfo.ConvOptions; no more model_setting/reasoning global reads inside
  the conversion layer
- effort-suffix helpers move to service/relayconvert/reasoning (old package
  forwards); chat-to-responses upgrade policy moves to service (host routing
  logic, not conversion)
- golden conversion matrix unchanged

* test(relayconvert): tighten boundary — kit packages now free of gin/setting imports

* refactor(dto): drop gin and logger dependencies

Phase 2 (part 1): dto.Request.IsStream now takes *http.Request instead of
*gin.Context (Gemini's impl reads query/path off the std request); dto's
three logger calls become common.SysError. Boundary test allowlist is now
empty — kit-bound packages import no gin/setting/logger/model.

* refactor(kit): extract dependency-free kitutil; dto/types/relayconvert stop importing common

Phase 2 of the relaykit extraction plan:
- new service/relayconvert/kitutil holds the pure helpers the kit needs
  (JSON wrappers, pointer/string/uuid/timestamp utils, MaskSensitiveInfo,
  pluggable LogInfo/LogError hooks, Debug flag)
- dto, types, and all relayconvert packages now use kitutil; their only
  remaining internal deps are dto/types/constant
- common keeps every original symbol (MaskSensitiveInfo delegates to
  kitutil) so host code is untouched; main.go routes kit logging into
  common.SysLog/SysError and mirrors DebugEnabled
- golden conversion matrix unchanged

* refactor(kit): move EndpointType/FinishReason to types; OpenRouter dialect via Options

Kit packages (dto/types/relayconvert/reasonmap) no longer import constant:
- EndpointType and finish-reason values live in types; constant re-exports
- the OpenRouter special-case in claude->openai request conversion reads
  Options.OpenRouterDialect, set by the host from the channel type;
  InitChannelMeta invalidates the cached snapshot on channel switch

* refactor: extract relaykit submodule (dto/types/relayconvert/reasonmap)

Phase 3 of the relaykit extraction plan:
- new go module github.com/QuantumNous/new-api/relaykit containing dto
  (minus task family), types, relayconvert (with convmeta/kitutil/reasoning),
  and reasonmap; host consumes it via require + replace, go.work for dev
- task-family dto (task/suno/midjourney/video) stays in the host dto
  package; dual-consumer host files alias it as taskdto
- relaykit builds and tests standalone (GOWORK=off): no host imports,
  no gin, no DB, no settings
- golden conversion matrix unchanged

* build(docker): copy relaykit/go.mod before go mod download

The local-replace submodule's go.mod must exist inside the build context
for the main module graph to resolve.

* fix: address relaykit extraction regressions

* fix: address relaykit review regressions

* docs: document Meta nil receiver contract

* fix(relaykit): fail OpenAI→Claude conversion without max_tokens; reject negative default_max_tokens

The Claude Messages API requires max_tokens (omitting it is a 400
"Field required"), but with a nil Options.Claude.DefaultMaxTokens hook
the converters silently emitted a request the upstream is guaranteed to
reject. Both OpenAI Chat and Responses → Claude conversions now return
sharedclaude.ErrMissingMaxTokens when no path (client value, default
hook, thinking-adapter floor) supplied one. Unreachable in the host,
which always configures the hook.

Host side, claude.default_max_tokens now rejects negative values at the
option API before persisting — they would wrap into huge unsigned values
during conversion. Zero stays allowed: the current API treats
max_tokens: 0 as cache pre-warming.

* fix: make Gemini safety settings read path race-free
IQZZ020501 pushed a commit to IQZZ020501/new-api that referenced this pull request Jul 30, 2026
…module (QuantumNous#6369)

* test(relayconvert): add golden snapshot matrix and relaykit boundary guard

Phase 0 of the relaykit extraction plan: pin byte-level output of every
registered (from,to) request/response/stream conversion route, and
forbid kit-bound packages from growing host-only imports.

* wip(relayconvert): drop gin.Context from converter signatures; add convmeta draft

Phase 1 in progress: relayconvert now takes context.Context; host media
resolver adapts gin.Context back at the service boundary.

* refactor(relayconvert): decouple converters from RelayInfo, gin, and settings

Phase 1 of the relaykit extraction plan:
- converters now depend on convmeta.Meta (implemented by RelayInfo) instead
  of *relaycommon.RelayInfo; ClaudeConvertInfo and the format guesser move
  to convmeta with aliases left behind
- host settings reach converters via a convmeta.Options snapshot built in
  RelayInfo.ConvOptions; no more model_setting/reasoning global reads inside
  the conversion layer
- effort-suffix helpers move to service/relayconvert/reasoning (old package
  forwards); chat-to-responses upgrade policy moves to service (host routing
  logic, not conversion)
- golden conversion matrix unchanged

* test(relayconvert): tighten boundary — kit packages now free of gin/setting imports

* refactor(dto): drop gin and logger dependencies

Phase 2 (part 1): dto.Request.IsStream now takes *http.Request instead of
*gin.Context (Gemini's impl reads query/path off the std request); dto's
three logger calls become common.SysError. Boundary test allowlist is now
empty — kit-bound packages import no gin/setting/logger/model.

* refactor(kit): extract dependency-free kitutil; dto/types/relayconvert stop importing common

Phase 2 of the relaykit extraction plan:
- new service/relayconvert/kitutil holds the pure helpers the kit needs
  (JSON wrappers, pointer/string/uuid/timestamp utils, MaskSensitiveInfo,
  pluggable LogInfo/LogError hooks, Debug flag)
- dto, types, and all relayconvert packages now use kitutil; their only
  remaining internal deps are dto/types/constant
- common keeps every original symbol (MaskSensitiveInfo delegates to
  kitutil) so host code is untouched; main.go routes kit logging into
  common.SysLog/SysError and mirrors DebugEnabled
- golden conversion matrix unchanged

* refactor(kit): move EndpointType/FinishReason to types; OpenRouter dialect via Options

Kit packages (dto/types/relayconvert/reasonmap) no longer import constant:
- EndpointType and finish-reason values live in types; constant re-exports
- the OpenRouter special-case in claude->openai request conversion reads
  Options.OpenRouterDialect, set by the host from the channel type;
  InitChannelMeta invalidates the cached snapshot on channel switch

* refactor: extract relaykit submodule (dto/types/relayconvert/reasonmap)

Phase 3 of the relaykit extraction plan:
- new go module github.com/QuantumNous/new-api/relaykit containing dto
  (minus task family), types, relayconvert (with convmeta/kitutil/reasoning),
  and reasonmap; host consumes it via require + replace, go.work for dev
- task-family dto (task/suno/midjourney/video) stays in the host dto
  package; dual-consumer host files alias it as taskdto
- relaykit builds and tests standalone (GOWORK=off): no host imports,
  no gin, no DB, no settings
- golden conversion matrix unchanged

* build(docker): copy relaykit/go.mod before go mod download

The local-replace submodule's go.mod must exist inside the build context
for the main module graph to resolve.

* fix: address relaykit extraction regressions

* fix: address relaykit review regressions

* docs: document Meta nil receiver contract

* fix(relaykit): fail OpenAI→Claude conversion without max_tokens; reject negative default_max_tokens

The Claude Messages API requires max_tokens (omitting it is a 400
"Field required"), but with a nil Options.Claude.DefaultMaxTokens hook
the converters silently emitted a request the upstream is guaranteed to
reject. Both OpenAI Chat and Responses → Claude conversions now return
sharedclaude.ErrMissingMaxTokens when no path (client value, default
hook, thinking-adapter floor) supplied one. Unreachable in the host,
which always configures the hook.

Host side, claude.default_max_tokens now rejects negative values at the
option API before persisting — they would wrap into huge unsigned values
during conversion. Zero stays allowed: the current API treats
max_tokens: 0 as cache pre-warming.

* fix: make Gemini safety settings read path race-free
Luckylos pushed a commit to Luckylos/new-api that referenced this pull request Aug 1, 2026
upstream extracted relaykit (QuantumNous#6369), moving NewAPIError/ErrorCode to
relaykit/types and AdvancedCustomConfig to relaykit/dto. Update the
test imports to match; behavior unchanged.
dreamlx added a commit to dreamlx/new-api that referenced this pull request Aug 3, 2026
Sync upstream/main (rc.11 → rc.23, 46 commits) into main. Key upstream
changes: QuantumNous#6369 relaykit module extraction (dto/types/service/relayconvert
moved to relaykit/ submodule with require+replace), QuantumNous#6518/QuantumNous#6570 tiered
retry billing, QuantumNous#6558 stream status exposed to log owners, QuantumNous#6562 DeepSeek
responses API, QuantumNous#6590 auto group, QuantumNous#6545 zstd decompression, per-channel
HTTP transport controls.

Conflicts resolved:
- constant/api_type.go, constant/channel.go: keep LH channel numbering
  (OspreyAI=58/HappyHorse=59/Seedance=60/AdvancedCustom=61 via reserved
  placeholders), append upstream Sub2API=62/NewAPI=63 before Dummy.
- relay/common/relay_info_test.go: union imports (LH common2/constant/gin
  + upstream convmeta/relaykit-types/assert), keep both test sets.

LH-only packages (ospreyai/happyhorse/seedance/task variants) had stale
host dto/types imports after relaykit extraction — migrated to
relaykit/dto and relaykit/types (task-family TaskError kept on host dto
via taskdto alias, matching upstream doubao pattern).

Co-Authored-By: Claude <noreply@anthropic.com>
Luckylos pushed a commit to Luckylos/new-api that referenced this pull request Aug 14, 2026
upstream extracted relaykit (QuantumNous#6369), moving NewAPIError/ErrorCode to
relaykit/types and AdvancedCustomConfig to relaykit/dto. Update the
test imports to match; behavior unchanged.
refeiner pushed a commit to wuqiang44444444/new-api that referenced this pull request Aug 17, 2026
…module (QuantumNous#6369)

* test(relayconvert): add golden snapshot matrix and relaykit boundary guard

Phase 0 of the relaykit extraction plan: pin byte-level output of every
registered (from,to) request/response/stream conversion route, and
forbid kit-bound packages from growing host-only imports.

* wip(relayconvert): drop gin.Context from converter signatures; add convmeta draft

Phase 1 in progress: relayconvert now takes context.Context; host media
resolver adapts gin.Context back at the service boundary.

* refactor(relayconvert): decouple converters from RelayInfo, gin, and settings

Phase 1 of the relaykit extraction plan:
- converters now depend on convmeta.Meta (implemented by RelayInfo) instead
  of *relaycommon.RelayInfo; ClaudeConvertInfo and the format guesser move
  to convmeta with aliases left behind
- host settings reach converters via a convmeta.Options snapshot built in
  RelayInfo.ConvOptions; no more model_setting/reasoning global reads inside
  the conversion layer
- effort-suffix helpers move to service/relayconvert/reasoning (old package
  forwards); chat-to-responses upgrade policy moves to service (host routing
  logic, not conversion)
- golden conversion matrix unchanged

* test(relayconvert): tighten boundary — kit packages now free of gin/setting imports

* refactor(dto): drop gin and logger dependencies

Phase 2 (part 1): dto.Request.IsStream now takes *http.Request instead of
*gin.Context (Gemini's impl reads query/path off the std request); dto's
three logger calls become common.SysError. Boundary test allowlist is now
empty — kit-bound packages import no gin/setting/logger/model.

* refactor(kit): extract dependency-free kitutil; dto/types/relayconvert stop importing common

Phase 2 of the relaykit extraction plan:
- new service/relayconvert/kitutil holds the pure helpers the kit needs
  (JSON wrappers, pointer/string/uuid/timestamp utils, MaskSensitiveInfo,
  pluggable LogInfo/LogError hooks, Debug flag)
- dto, types, and all relayconvert packages now use kitutil; their only
  remaining internal deps are dto/types/constant
- common keeps every original symbol (MaskSensitiveInfo delegates to
  kitutil) so host code is untouched; main.go routes kit logging into
  common.SysLog/SysError and mirrors DebugEnabled
- golden conversion matrix unchanged

* refactor(kit): move EndpointType/FinishReason to types; OpenRouter dialect via Options

Kit packages (dto/types/relayconvert/reasonmap) no longer import constant:
- EndpointType and finish-reason values live in types; constant re-exports
- the OpenRouter special-case in claude->openai request conversion reads
  Options.OpenRouterDialect, set by the host from the channel type;
  InitChannelMeta invalidates the cached snapshot on channel switch

* refactor: extract relaykit submodule (dto/types/relayconvert/reasonmap)

Phase 3 of the relaykit extraction plan:
- new go module github.com/QuantumNous/new-api/relaykit containing dto
  (minus task family), types, relayconvert (with convmeta/kitutil/reasoning),
  and reasonmap; host consumes it via require + replace, go.work for dev
- task-family dto (task/suno/midjourney/video) stays in the host dto
  package; dual-consumer host files alias it as taskdto
- relaykit builds and tests standalone (GOWORK=off): no host imports,
  no gin, no DB, no settings
- golden conversion matrix unchanged

* build(docker): copy relaykit/go.mod before go mod download

The local-replace submodule's go.mod must exist inside the build context
for the main module graph to resolve.

* fix: address relaykit extraction regressions

* fix: address relaykit review regressions

* docs: document Meta nil receiver contract

* fix(relaykit): fail OpenAI→Claude conversion without max_tokens; reject negative default_max_tokens

The Claude Messages API requires max_tokens (omitting it is a 400
"Field required"), but with a nil Options.Claude.DefaultMaxTokens hook
the converters silently emitted a request the upstream is guaranteed to
reject. Both OpenAI Chat and Responses → Claude conversions now return
sharedclaude.ErrMissingMaxTokens when no path (client value, default
hook, thinking-adapter floor) supplied one. Unreachable in the host,
which always configures the hook.

Host side, claude.default_max_tokens now rejects negative values at the
option API before persisting — they would wrap into huge unsigned values
during conversion. Zero stays allowed: the current API treats
max_tokens: 0 as cache pre-warming.

* fix: make Gemini safety settings read path race-free
330079598 pushed a commit to 330079598/new-api that referenced this pull request Aug 19, 2026
…module (QuantumNous#6369)

* test(relayconvert): add golden snapshot matrix and relaykit boundary guard

Phase 0 of the relaykit extraction plan: pin byte-level output of every
registered (from,to) request/response/stream conversion route, and
forbid kit-bound packages from growing host-only imports.

* wip(relayconvert): drop gin.Context from converter signatures; add convmeta draft

Phase 1 in progress: relayconvert now takes context.Context; host media
resolver adapts gin.Context back at the service boundary.

* refactor(relayconvert): decouple converters from RelayInfo, gin, and settings

Phase 1 of the relaykit extraction plan:
- converters now depend on convmeta.Meta (implemented by RelayInfo) instead
  of *relaycommon.RelayInfo; ClaudeConvertInfo and the format guesser move
  to convmeta with aliases left behind
- host settings reach converters via a convmeta.Options snapshot built in
  RelayInfo.ConvOptions; no more model_setting/reasoning global reads inside
  the conversion layer
- effort-suffix helpers move to service/relayconvert/reasoning (old package
  forwards); chat-to-responses upgrade policy moves to service (host routing
  logic, not conversion)
- golden conversion matrix unchanged

* test(relayconvert): tighten boundary — kit packages now free of gin/setting imports

* refactor(dto): drop gin and logger dependencies

Phase 2 (part 1): dto.Request.IsStream now takes *http.Request instead of
*gin.Context (Gemini's impl reads query/path off the std request); dto's
three logger calls become common.SysError. Boundary test allowlist is now
empty — kit-bound packages import no gin/setting/logger/model.

* refactor(kit): extract dependency-free kitutil; dto/types/relayconvert stop importing common

Phase 2 of the relaykit extraction plan:
- new service/relayconvert/kitutil holds the pure helpers the kit needs
  (JSON wrappers, pointer/string/uuid/timestamp utils, MaskSensitiveInfo,
  pluggable LogInfo/LogError hooks, Debug flag)
- dto, types, and all relayconvert packages now use kitutil; their only
  remaining internal deps are dto/types/constant
- common keeps every original symbol (MaskSensitiveInfo delegates to
  kitutil) so host code is untouched; main.go routes kit logging into
  common.SysLog/SysError and mirrors DebugEnabled
- golden conversion matrix unchanged

* refactor(kit): move EndpointType/FinishReason to types; OpenRouter dialect via Options

Kit packages (dto/types/relayconvert/reasonmap) no longer import constant:
- EndpointType and finish-reason values live in types; constant re-exports
- the OpenRouter special-case in claude->openai request conversion reads
  Options.OpenRouterDialect, set by the host from the channel type;
  InitChannelMeta invalidates the cached snapshot on channel switch

* refactor: extract relaykit submodule (dto/types/relayconvert/reasonmap)

Phase 3 of the relaykit extraction plan:
- new go module github.com/QuantumNous/new-api/relaykit containing dto
  (minus task family), types, relayconvert (with convmeta/kitutil/reasoning),
  and reasonmap; host consumes it via require + replace, go.work for dev
- task-family dto (task/suno/midjourney/video) stays in the host dto
  package; dual-consumer host files alias it as taskdto
- relaykit builds and tests standalone (GOWORK=off): no host imports,
  no gin, no DB, no settings
- golden conversion matrix unchanged

* build(docker): copy relaykit/go.mod before go mod download

The local-replace submodule's go.mod must exist inside the build context
for the main module graph to resolve.

* fix: address relaykit extraction regressions

* fix: address relaykit review regressions

* docs: document Meta nil receiver contract

* fix(relaykit): fail OpenAI→Claude conversion without max_tokens; reject negative default_max_tokens

The Claude Messages API requires max_tokens (omitting it is a 400
"Field required"), but with a nil Options.Claude.DefaultMaxTokens hook
the converters silently emitted a request the upstream is guaranteed to
reject. Both OpenAI Chat and Responses → Claude conversions now return
sharedclaude.ErrMissingMaxTokens when no path (client value, default
hook, thinking-adapter floor) supplied one. Unreachable in the host,
which always configures the hook.

Host side, claude.default_max_tokens now rejects negative values at the
option API before persisting — they would wrap into huge unsigned values
during conversion. Zero stays allowed: the current API treats
max_tokens: 0 as cache pre-warming.

* fix: make Gemini safety settings read path race-free
@Calcium-Ion
Calcium-Ion deleted the refactor/relaykit branch August 29, 2026 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant