From ed7ef863478ae46557f24a344b2eccfac22e153c Mon Sep 17 00:00:00 2001 From: weak-fox <827367480@qq.com> Date: Fri, 15 May 2026 10:41:57 +0800 Subject: [PATCH 01/17] test: add capacity retry regressions --- ...nai_gateway_service_codex_cli_only_test.go | 58 +++++++++++++++++++ .../service/openai_gateway_service_test.go | 41 +++++++++++++ 2 files changed, 99 insertions(+) diff --git a/backend/internal/service/openai_gateway_service_codex_cli_only_test.go b/backend/internal/service/openai_gateway_service_codex_cli_only_test.go index fe58e92f..951860cd 100644 --- a/backend/internal/service/openai_gateway_service_codex_cli_only_test.go +++ b/backend/internal/service/openai_gateway_service_codex_cli_only_test.go @@ -218,6 +218,12 @@ func TestIsOpenAITransientProcessingError(t *testing.T) { nil, )) + require.True(t, isOpenAITransientProcessingError( + http.StatusBadRequest, + "Selected model is at capacity. Please try a different model.", + []byte(`{"error":{"message":"Selected model is at capacity. Please try a different model.","type":"invalid_request_error"}}`), + )) + require.True(t, isOpenAITransientProcessingError( http.StatusBadRequest, "", @@ -332,3 +338,55 @@ func TestOpenAIGatewayService_Forward_TransientProcessingErrorTriggersFailover(t require.Contains(t, string(failoverErr.ResponseBody), "An error occurred while processing your request") require.False(t, c.Writer.Written(), "service 层应返回 failover 错误给上层换号,而不是直接向客户端写响应") } + +func TestOpenAIGatewayService_Forward_ModelCapacityErrorTriggersFailoverAndSameAccountRetry(t *testing.T) { + gin.SetMode(gin.TestMode) + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(nil)) + c.Request.Header.Set("User-Agent", "codex_cli_rs/0.1.0") + c.Request.Header.Set("Content-Type", "application/json") + + upstream := &httpUpstreamRecorder{ + resp: &http.Response{ + StatusCode: http.StatusBadRequest, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + "x-request-id": []string{"rid-capacity-400"}, + }, + Body: io.NopCloser(strings.NewReader(`{"error":{"message":"Selected model is at capacity. Please try a different model.","type":"invalid_request_error"}}`)), + }, + } + svc := &OpenAIGatewayService{ + cfg: &config.Config{ + Gateway: config.GatewayConfig{ForceCodexCLI: false}, + }, + httpUpstream: upstream, + } + account := &Account{ + ID: 1001, + Name: "codex max套餐", + Platform: PlatformOpenAI, + Type: AccountTypeAPIKey, + Concurrency: 1, + Credentials: map[string]any{ + "api_key": "sk-test", + "pool_mode": true, + }, + Status: StatusActive, + Schedulable: true, + RateMultiplier: f64p(1), + } + body := []byte(`{"model":"gpt-5.4","stream":false,"input":[{"type":"text","text":"hello"}]}`) + + _, err := svc.Forward(context.Background(), c, account, body) + require.Error(t, err) + + var failoverErr *UpstreamFailoverError + require.ErrorAs(t, err, &failoverErr) + require.Equal(t, http.StatusBadRequest, failoverErr.StatusCode) + require.True(t, failoverErr.RetryableOnSameAccount) + require.Contains(t, string(failoverErr.ResponseBody), "Selected model is at capacity") + require.False(t, c.Writer.Written(), "service 层应返回 failover 错误给上层重试/换号,而不是直接向客户端写响应") +} diff --git a/backend/internal/service/openai_gateway_service_test.go b/backend/internal/service/openai_gateway_service_test.go index 84a2fe71..15fe85ad 100644 --- a/backend/internal/service/openai_gateway_service_test.go +++ b/backend/internal/service/openai_gateway_service_test.go @@ -1116,6 +1116,47 @@ func TestOpenAIStreamingResponseFailedBeforeOutputReturnsFailover(t *testing.T) require.Empty(t, rec.Body.String()) } +func TestOpenAIStreamingResponseFailedBeforeOutputCapacityErrorReturnsFailover(t *testing.T) { + gin.SetMode(gin.TestMode) + cfg := &config.Config{ + Gateway: config.GatewayConfig{ + StreamDataIntervalTimeout: 0, + StreamKeepaliveInterval: 0, + MaxLineSize: defaultMaxLineSize, + }, + } + svc := &OpenAIGatewayService{cfg: cfg} + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodPost, "/", nil) + + resp := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(strings.Join([]string{ + "event: response.created", + `data: {"type":"response.created","response":{"id":"resp_1"}}`, + "", + "event: response.in_progress", + `data: {"type":"response.in_progress","response":{"id":"resp_1"}}`, + "", + "event: response.failed", + `data: {"type":"response.failed","error":{"message":"Selected model is at capacity. Please try a different model.","type":"invalid_request_error"}}`, + "", + }, "\n"))), + Header: http.Header{"X-Request-Id": []string{"rid-capacity-failed"}}, + } + + _, err := svc.handleStreamingResponse(c.Request.Context(), resp, c, &Account{ID: 1, Platform: PlatformOpenAI, Name: "acc"}, time.Now(), "model", "model") + require.Error(t, err) + var failoverErr *UpstreamFailoverError + require.ErrorAs(t, err, &failoverErr) + require.Equal(t, http.StatusBadGateway, failoverErr.StatusCode) + require.Contains(t, string(failoverErr.ResponseBody), "Selected model is at capacity") + require.False(t, c.Writer.Written()) + require.Empty(t, rec.Body.String()) +} + func TestOpenAIStreamingPreambleOnlyMissingTerminalReturnsFailover(t *testing.T) { gin.SetMode(gin.TestMode) cfg := &config.Config{ From 9f07741c139e05463983b236faf9a2b269af0fe4 Mon Sep 17 00:00:00 2001 From: weak-fox <827367480@qq.com> Date: Fri, 15 May 2026 10:43:29 +0800 Subject: [PATCH 02/17] fix: retry model capacity transient errors --- backend/internal/service/openai_gateway_service.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index e12b208e..6cda65c0 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -1113,6 +1113,9 @@ func isOpenAITransientProcessingError(upstreamStatusCode int, upstreamMsg string if strings.Contains(lower, "an error occurred while processing your request") { return true } + if strings.Contains(lower, "selected model is at capacity") { + return true + } return strings.Contains(lower, "you can retry your request") && strings.Contains(lower, "help.openai.com") && strings.Contains(lower, "request id") @@ -3400,6 +3403,9 @@ func openAIStreamDataStartsClientOutput(data, eventType string) bool { } func openAIStreamFailedEventShouldFailover(payload []byte, message string) bool { + if isOpenAITransientProcessingError(http.StatusBadRequest, message, payload) { + return true + } code := strings.ToLower(strings.TrimSpace(gjson.GetBytes(payload, "response.error.code").String())) if code == "" { code = strings.ToLower(strings.TrimSpace(gjson.GetBytes(payload, "error.code").String())) From 6e66edbb095f91b13e329f48e06d655e9e454940 Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 15 May 2026 20:58:47 +0800 Subject: [PATCH 03/17] chore: update sponsors --- README.md | 19 ++++++++++--------- README_CN.md | 18 +++++++++--------- README_JA.md | 19 ++++++++++--------- assets/partners/logos/pptoken.png | Bin 0 -> 71017 bytes 4 files changed, 29 insertions(+), 27 deletions(-) create mode 100644 assets/partners/logos/pptoken.png diff --git a/README.md b/README.md index bdb09d15..bf961f88 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,13 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot -PoixeAi -Thanks to Poixe Ai for sponsoring this project! Poixe AI provides reliable LLM API services. You can leverage the platform's API endpoints to seamlessly build AI-powered products. Additionally, you can become a vendor by providing AI API resources to the platform and earn revenue. Register through the exclusive sub2api referral link and receive a bonus of $5 USD on your first top-up. +CTok +Thanks to CTok.ai for sponsoring this project! CTok.ai is dedicated to building a one-stop AI programming tool service platform. We offer professional Claude Code packages and technical community services, with support for Google Gemini and OpenAI Codex. Through carefully designed plans and a professional tech community, we provide developers with reliable service guarantees and continuous technical support, making AI-assisted programming a true productivity tool. Click here to register! -CTok -Thanks to CTok.ai for sponsoring this project! CTok.ai is dedicated to building a one-stop AI programming tool service platform. We offer professional Claude Code packages and technical community services, with support for Google Gemini and OpenAI Codex. Through carefully designed plans and a professional tech community, we provide developers with reliable service guarantees and continuous technical support, making AI-assisted programming a true productivity tool. Click here to register! +AIGoCode +Thanks to AIGoCode for sponsoring this project! AIGoCode is an all-in-one platform that integrates Claude Code, Codex, and the latest Gemini models, providing you with stable, efficient, and highly cost-effective AI coding services. The platform offers flexible subscription plans, zero risk of account suspension, direct access with no VPN required, and lightning-fast responses. AIGoCode has prepared a special benefit for sub2api users: if you register via this link, you'll receive an extra 10% bonus credit on your first top-up! @@ -86,11 +86,6 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot Thanks to AICodeMirror for sponsoring this project! AICodeMirror provides official high-stability relay services for Claude Code / Codex / Gemini CLI, with enterprise-grade concurrency, fast invoicing, and 24/7 dedicated technical support. Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original price, with extra discounts on top-ups! AICodeMirror offers special benefits for sub2api users: register via this link to enjoy 20% off your first top-up, and enterprise customers can get up to 25% off! - -AIGoCode -Thanks to AIGoCode for sponsoring this project! AIGoCode is an all-in-one platform that integrates Claude Code, Codex, and the latest Gemini models, providing you with stable, efficient, and highly cost-effective AI coding services. The platform offers flexible subscription plans, zero risk of account suspension, direct access with no VPN required, and lightning-fast responses. AIGoCode has prepared a special benefit for sub2api users: if you register via this link, you'll receive an extra 10% bonus credit on your first top-up! - - bmoplus Huge thanks to BmoPlus for sponsoring this project! BmoPlus is a highly reliable AI account provider built strictly for heavy AI users and developers. They offer rock-solid, ready-to-use accounts and official top-up services for ChatGPT Plus / ChatGPT Pro (Full Warranty) / Claude Pro / Super Grok / Gemini Pro. By registering and ordering through BmoPlus - Premium AI Accounts & Top-ups, users can unlock the mind-blowing rate of 10% of the official GPT subscription price (90% OFF) @@ -108,6 +103,12 @@ Enterprise-grade high concurrency is also supported, with a dedicated management Register now via this link to receive $3 in trial credits. User top-ups start as low as 60% off, and referring friends earns both parties rewards — referral bonuses up to $150. + +pptoken +Thanks to PPToken.org for sponsoring this project! PPToken.org specializes in GPT model API relay services, supporting Codex, Claude Code, OpenAI-compatible clients, and Gemini CLI integration. Top-ups are 1:1 (¥1 = $1 credit); GPT models start at 0.16x rate multiplier, with overall cost at roughly 2.2% of official pricing and first-token latency around 1 second — ideal for developers seeking low-cost, high-speed access to GPT model capabilities. Technical support: 24/7 real human responses (no bots), @tech in the group chat and get a reply within 10 minutes. Sponsor benefit: the first 200 users who register via the exclusive registration link and enter promo code `SUB2API` can claim free Codex / Claude Code trial credits — no minimum spend, no card required. + + + ## Ecosystem diff --git a/README_CN.md b/README_CN.md index e13f86de..001c5681 100644 --- a/README_CN.md +++ b/README_CN.md @@ -61,13 +61,13 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的 -PoixeAI -感谢 Poixe AI 赞助了本项目!Poixe AI 提供可靠的 AI 模型接口服务,您可以使用平台提供的 LLM API 接口轻松构建 AI 产品,同时也可以成为供应商,为平台提供大模型资源以赚取收益。通过 此链接 专属链接注册,充值额外赠送 $5 美金 +CTok +感谢 CTok.ai 赞助了本项目!CTok.ai 致力于打造一站式 AI 编程工具服务平台。我们提供 Claude Code 专业套餐及技术社群服务,同时支持 Google Gemini 和 OpenAI Codex。通过精心设计的套餐方案和专业的技术社群,为开发者提供稳定的服务保障和持续的技术支持,让 AI 辅助编程真正成为开发者的生产力工具。点击这里注册! -CTok -感谢 CTok.ai 赞助了本项目!CTok.ai 致力于打造一站式 AI 编程工具服务平台。我们提供 Claude Code 专业套餐及技术社群服务,同时支持 Google Gemini 和 OpenAI Codex。通过精心设计的套餐方案和专业的技术社群,为开发者提供稳定的服务保障和持续的技术支持,让 AI 辅助编程真正成为开发者的生产力工具。点击这里注册! +AIGoCode +感谢 AIGoCode 赞助了本项目!AIGoCode 是一站式集成 Claude Code、Codex 以及最新 Gemini 模型的综合平台,为您提供稳定、高效、高性价比的 AI 编程服务。平台提供灵活的订阅方案,零封号风险,免 VPN 直连,响应极速。AIGoCode 为 sub2api 用户准备了专属福利:通过此链接注册,首次充值可额外获得 10% 赠送额度! @@ -85,11 +85,6 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的 感谢 AICodeMirror 赞助了本项目!AICodeMirror 提供 Claude Code / Codex / Gemini CLI 官方高稳定性中转服务,企业级并发、快速开票、7×24 小时专属技术支持。Claude Code / Codex / Gemini 官方通道低至原价 38% / 2% / 9%,充值更享额外折扣!AICodeMirror 为 sub2api 用户提供专属福利:通过此链接注册,首次充值立享 8 折优惠,企业客户最高可享 75 折! - -AIGoCode -感谢 AIGoCode 赞助了本项目!AIGoCode 是一站式集成 Claude Code、Codex 以及最新 Gemini 模型的综合平台,为您提供稳定、高效、高性价比的 AI 编程服务。平台提供灵活的订阅方案,零封号风险,免 VPN 直连,响应极速。AIGoCode 为 sub2api 用户准备了专属福利:通过此链接注册,首次充值可额外获得 10% 赠送额度! - - bmoplus 感谢 BmoPlus 赞助了本项目!BmoPlus 是一家专为AI订阅重度用户打造的可靠 AI 账号代充服务商,提供稳定的 ChatGPT Plus / ChatGPT Pro(全程质保) / Claude Pro / Super Grok / Gemini Pro 的官方代充&成品账号。 通过BmoPlus AI成品号专卖/代充注册下单的用户,可享GPT 官网订阅一折 的震撼价格! @@ -107,6 +102,11 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的 现在通过 此链接 注册即送 $3 试用额度,用户充值低至 6 折,邀请好友双向赠送,邀请奖励可达 $150。 + +pptoken +感谢 PPToken.org 赞助本项目! PPToken.org 主打 GPT 系列模型 API 中转服务,支持 Codex、Claude Code、OpenAI 兼容客户端及 Gemini CLI 等工具接入。充值 1:1,1 元=1 美元额度;GPT 模型最低 0.16 倍倍率,综合成本约为官方价格的 0.22 折,最快首字 Token 约 1 秒,适合开发者低成本、高响应速度接入 GPT 模型能力。技术支持: 7×24 小时真人响应(不是机器人),群内@技术,10 分钟内有回复 。赞助商福利:前 200 名用户通过 [专属注册链接] 注册,输入优惠码 `SUB2API`,可领取 Codex / Claude Code 免费试用额度,无门槛、不绑卡。 + + ## 生态项目 diff --git a/README_JA.md b/README_JA.md index 73331a07..94ed3d01 100644 --- a/README_JA.md +++ b/README_JA.md @@ -61,13 +61,13 @@ Sub2API は、AI 製品のサブスクリプションから API クォータを -PoixeAi -Poixe AI のご支援に感謝します!Poixe AI は信頼性の高い LLM API サービスを提供しています。プラットフォームの API エンドポイントを活用して、AI 搭載プロダクトをシームレスに構築できます。また、ベンダーとして AI API リソースをプラットフォームに提供し、収益を得ることも可能です。専用の sub2api 紹介リンクから登録すると、初回チャージ時に $5 USD のボーナスがもらえます。 +CTok +CTok.ai のご支援に感謝します!CTok.ai はワンストップ AI プログラミングツールサービスプラットフォームの構築に取り組んでいます。Claude Code の専用プランと技術コミュニティサービスを提供し、Google Gemini や OpenAI Codex もサポートしています。丁寧に設計されたプランと専門的な技術コミュニティを通じて、開発者に安定したサービス保証と継続的な技術サポートを提供し、AI アシスト プログラミングを真の生産性向上ツールにします。こちらから登録! -CTok -CTok.ai のご支援に感謝します!CTok.ai はワンストップ AI プログラミングツールサービスプラットフォームの構築に取り組んでいます。Claude Code の専用プランと技術コミュニティサービスを提供し、Google Gemini や OpenAI Codex もサポートしています。丁寧に設計されたプランと専門的な技術コミュニティを通じて、開発者に安定したサービス保証と継続的な技術サポートを提供し、AI アシスト プログラミングを真の生産性向上ツールにします。こちらから登録! +AIGoCode +AIGoCode のご支援に感謝します!AIGoCode は Claude Code、Codex、最新の Gemini モデルを統合したオールインワンプラットフォームで、安定的かつ効率的でコストパフォーマンスに優れた AI コーディングサービスを提供します。柔軟なサブスクリプションプラン、アカウント停止リスクゼロ、VPN 不要の直接アクセス、超高速レスポンスが特長です。AIGoCode は sub2api ユーザー向けに特別特典を用意しています:こちらのリンクから登録すると、初回チャージ時に 10% のボーナスクレジットを追加プレゼント! @@ -85,11 +85,6 @@ Sub2API は、AI 製品のサブスクリプションから API クォータを AICodeMirror のご支援に感謝します!AICodeMirror は Claude Code / Codex / Gemini CLI の公式高安定性リレーサービスを提供しており、エンタープライズグレードの同時実行、迅速な請求書発行、24時間年中無休の専属テクニカルサポートを備えています。Claude Code / Codex / Gemini の公式チャネルを定価の 38% / 2% / 9% で利用可能、チャージ時にはさらに追加割引!AICodeMirror は sub2api ユーザー向けに特別特典を提供中:こちらのリンクから登録すると、初回チャージが 20% オフ、法人のお客様は最大 25% オフ! - -AIGoCode -AIGoCode のご支援に感謝します!AIGoCode は Claude Code、Codex、最新の Gemini モデルを統合したオールインワンプラットフォームで、安定的かつ効率的でコストパフォーマンスに優れた AI コーディングサービスを提供します。柔軟なサブスクリプションプラン、アカウント停止リスクゼロ、VPN 不要の直接アクセス、超高速レスポンスが特長です。AIGoCode は sub2api ユーザー向けに特別特典を用意しています:こちらのリンクから登録すると、初回チャージ時に 10% のボーナスクレジットを追加プレゼント! - - bmoplus 本プロジェクトにご支援いただいた BmoPlus に感謝いたします!BmoPlusは、AIサブスクリプションのヘビーユーザー向けに特化した信頼性の高いAIアカウントサービスプロバイダーであり、安定した ChatGPT Plus / ChatGPT Pro (完全保証) / Claude Pro / Super Grok / Gemini Pro の公式代行チャージおよび即納アカウントを提供しています。こちらのBmoPlus AIアカウント専門店/代行チャージ経由でご登録・ご注文いただいたユーザー様は、GPTを 公式サイト価格の約1割(90% OFF) という驚異的な価格でご利用いただけます! @@ -107,6 +102,12 @@ Sub2API は、AI 製品のサブスクリプションから API クォータを こちらのリンクから登録すると、$3 のトライアルクレジットがもらえます。チャージは最大40%オフ、友達紹介で双方にボーナス付与 — 紹介報酬は最大 $150。 + +pptoken +PPToken.org のご支援に感謝します!PPToken.org は GPT シリーズモデルの API 中継サービスを専門としており、Codex、Claude Code、OpenAI 互換クライアント、Gemini CLI などのツール接続をサポートしています。チャージは 1:1(1元=1ドル分のクレジット)、GPT モデルは最低 0.16 倍のレート倍率で、総合コストは公式価格の約 2.2% 、最速ファーストトークンは約1秒 — 開発者が低コスト・高速レスポンスで GPT モデル機能にアクセスするのに最適です。テクニカルサポート:24時間365日リアルな人間が対応(ボットではありません)、グループ内で @技術 すれば 10 分以内に返信。スポンサー特典:先着 200 名のユーザーが専用登録リンクから登録し、プロモコード `SUB2API` を入力すると、Codex / Claude Code の無料トライアルクレジットを獲得できます — 最低利用額なし、カード登録不要。 + + + ## エコシステム diff --git a/assets/partners/logos/pptoken.png b/assets/partners/logos/pptoken.png new file mode 100644 index 0000000000000000000000000000000000000000..c199e6e96d4b4821d3f43ef32b10f0bcb11245b3 GIT binary patch literal 71017 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%0A; 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

qEzPMkuwg8Q` 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%0DBRy*+L=<@0HvFIu+xD%C2^3dls=j zuEGuitQknZmw}hf>PXgFM>J_^pI+hCl?4)$mD@&9$ZPdK@TT∓$G=ccD=y>W9Ci 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 bED-(=c0V23gVIeX00000NkvXXu0mjfsk`6R literal 0 HcmV?d00001 From 348a487739cf5d0b7e4b44f76003a38adba7a89c Mon Sep 17 00:00:00 2001 From: yetone Date: Fri, 15 May 2026 23:29:56 +0800 Subject: [PATCH 04/17] fix(codex-transform): preserve underscore when rewriting call_* tool-call ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `fixCallIDPrefix` builds malformed ids when the input has the standard OpenAI `call_` prefix: input: call_YYen1qxDejd2myJwcTCf7Nyp output: fcYYen1qxDejd2myJwcTCf7Nyp ← no underscore between 'fc' and the nanoid ChatGPT's codex backend then rejects the replayed item with: 400 Invalid 'input[N].id': 'fcYYen1qxDejd2myJwcTCf7Nyp'. Expected an ID that contains letters, numbers, underscores, or dashes, but this value contained additional characters. Sub2api wraps that into 502 to the client. Clients using the OpenAI SDK on the OAuth/codex path see every multi-hop turn (after the first tool call) fail because the item_reference rewritten this way gets sent on every subsequent hop. The other two branches of the same function correctly emit `fc_` (line 1029: pass-through when already `fc*`; line 1035 fallback: `fc_" + id`). Only the `call_` → `fc_` rewrite was missing the underscore — looks like a copy-paste slip during the original commit. Fix: change `"fc"` to `"fc_"` on the call_ branch. One character. Repro: client (OpenAI SDK) sends a function_call_output whose call_id is `call_` (default OpenAI format). The sub2api request body also contains an item_reference whose id mirrors the call_id (also `call_`). On the codex OAuth path, this rewrite fires for the item_reference's id, producing the malformed value. Affects: `platform=openai type=oauth` accounts whose clients use the official OpenAI SDK / Responses API conventions (id prefix `call_`). API-key accounts and bridge-mode requests are untouched. --- backend/internal/service/openai_codex_transform.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index a3b69dee..5b5f2bc3 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -1030,7 +1030,7 @@ func filterCodexInputWithOptions(input []any, opts codexInputFilterOptions) []an return id } if strings.HasPrefix(id, "call_") { - return "fc" + strings.TrimPrefix(id, "call_") + return "fc_" + strings.TrimPrefix(id, "call_") } return "fc_" + id } From b0c7723393a43ffca8277f597f375e148ba3ff2f Mon Sep 17 00:00:00 2001 From: yetone Date: Sat, 16 May 2026 00:50:35 +0800 Subject: [PATCH 05/17] fix(admin/settings): make tab shell readable in dark mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vue's scoped-CSS compiler was dropping the `:global(.dark) .settings-tabs-shell` rules in the production build, so the tab strip kept its light-mode white background and the inactive tab labels (text-gray-300) showed at ~1.6:1 contrast — effectively unreadable. Hoist the three dark-mode overrides into an unscoped ` + + From 0393bd7c82da1ca385af0744e9e19c1490c93bc0 Mon Sep 17 00:00:00 2001 From: name <136912576+is7Qin@users.noreply.github.com> Date: Sat, 16 May 2026 02:37:55 +0800 Subject: [PATCH 06/17] Fix OpenAI compat usage parsing --- backend/internal/pkg/apicompat/types.go | 31 ++++++++++ .../service/openai_compat_model_test.go | 57 +++++++++++++++++++ .../service/openai_gateway_service.go | 49 +++++++++++----- .../service/openai_gateway_service_test.go | 19 +++++++ .../internal/service/openai_ws_forwarder.go | 12 +--- ..._ws_forwarder_hotpath_optimization_test.go | 8 +++ 6 files changed, 152 insertions(+), 24 deletions(-) diff --git a/backend/internal/pkg/apicompat/types.go b/backend/internal/pkg/apicompat/types.go index f9cd5a1c..df75ce50 100644 --- a/backend/internal/pkg/apicompat/types.go +++ b/backend/internal/pkg/apicompat/types.go @@ -306,6 +306,37 @@ type ResponsesUsage struct { OutputTokensDetails *ResponsesOutputTokensDetails `json:"output_tokens_details,omitempty"` } +func (u *ResponsesUsage) UnmarshalJSON(data []byte) error { + type responsesUsageAlias ResponsesUsage + var aux struct { + responsesUsageAlias + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + PromptTokensDetails *ResponsesInputTokensDetails `json:"prompt_tokens_details,omitempty"` + CompletionTokensDetails *ResponsesOutputTokensDetails `json:"completion_tokens_details,omitempty"` + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + *u = ResponsesUsage(aux.responsesUsageAlias) + if u.InputTokens == 0 && aux.PromptTokens != 0 { + u.InputTokens = aux.PromptTokens + } + if u.OutputTokens == 0 && aux.CompletionTokens != 0 { + u.OutputTokens = aux.CompletionTokens + } + if u.InputTokensDetails == nil && aux.PromptTokensDetails != nil { + u.InputTokensDetails = aux.PromptTokensDetails + } + if u.OutputTokensDetails == nil && aux.CompletionTokensDetails != nil { + u.OutputTokensDetails = aux.CompletionTokensDetails + } + if u.TotalTokens == 0 && (u.InputTokens != 0 || u.OutputTokens != 0) { + u.TotalTokens = u.InputTokens + u.OutputTokens + } + return nil +} + // ResponsesInputTokensDetails breaks down input token usage. type ResponsesInputTokensDetails struct { CachedTokens int `json:"cached_tokens,omitempty"` diff --git a/backend/internal/service/openai_compat_model_test.go b/backend/internal/service/openai_compat_model_test.go index e222b093..6aa4ef09 100644 --- a/backend/internal/service/openai_compat_model_test.go +++ b/backend/internal/service/openai_compat_model_test.go @@ -183,6 +183,63 @@ func TestForwardAsAnthropic_NormalizesRoutingAndEffortForGpt54XHigh(t *testing.T t.Logf("response body: %s", rec.Body.String()) } +func TestForwardAsAnthropic_MappedClaudeModelAcceptsChatUsageShape(t *testing.T) { + t.Parallel() + gin.SetMode(gin.TestMode) + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + body := []byte(`{"model":"claude-opus-4-7","max_tokens":16,"messages":[{"role":"user","content":"compact this"}],"stream":true}`) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + upstreamBody := strings.Join([]string{ + `data: {"type":"response.created","response":{"id":"resp_compact","model":"gpt-5.5","status":"in_progress","output":[]}}`, + "", + `data: {"type":"response.output_text.delta","delta":"ok"}`, + "", + `data: {"type":"response.completed","response":{"id":"resp_compact","object":"response","model":"gpt-5.5","status":"completed","output":[{"type":"message","id":"msg_1","role":"assistant","status":"completed","content":[{"type":"output_text","text":"ok"}]}],"usage":{"prompt_tokens":31,"completion_tokens":9,"total_tokens":40,"prompt_tokens_details":{"cached_tokens":11}}}}`, + "", + "data: [DONE]", + "", + }, "\n") + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_compact_usage"}}, + Body: io.NopCloser(strings.NewReader(upstreamBody)), + }} + + svc := &OpenAIGatewayService{ + httpUpstream: upstream, + cfg: &config.Config{Security: config.SecurityConfig{URLAllowlist: config.URLAllowlistConfig{Enabled: false}}}, + } + account := &Account{ + ID: 1, + Name: "openai-apikey", + Platform: PlatformOpenAI, + Type: AccountTypeAPIKey, + Concurrency: 1, + Credentials: map[string]any{ + "api_key": "sk-test", + "base_url": "https://api.openai.com/v1", + "model_mapping": map[string]any{ + "gpt-5.5": "gpt-5.5", + }, + }, + } + + result, err := svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "gpt-5.5") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, "claude-opus-4-7", result.Model) + require.Equal(t, "gpt-5.5", result.BillingModel) + require.Equal(t, "gpt-5.5", result.UpstreamModel) + require.Equal(t, 31, result.Usage.InputTokens) + require.Equal(t, 9, result.Usage.OutputTokens) + require.Equal(t, 11, result.Usage.CacheReadInputTokens) + require.Equal(t, "gpt-5.5", gjson.GetBytes(upstream.lastBody, "model").String()) +} + func TestForwardAsAnthropic_InjectsPromptCacheKeyForAPIKeyMessagesDispatch(t *testing.T) { t.Parallel() gin.SetMode(gin.TestMode) diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index e12b208e..3a2cb0c3 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -4639,28 +4639,47 @@ func (s *OpenAIGatewayService) parseSSEUsageBytes(data []byte, usage *OpenAIUsag return } - usage.InputTokens = int(gjson.GetBytes(data, "response.usage.input_tokens").Int()) - usage.OutputTokens = int(gjson.GetBytes(data, "response.usage.output_tokens").Int()) - usage.CacheReadInputTokens = int(gjson.GetBytes(data, "response.usage.input_tokens_details.cached_tokens").Int()) - usage.ImageOutputTokens = int(gjson.GetBytes(data, "response.usage.output_tokens_details.image_tokens").Int()) + if parsedUsage, ok := extractOpenAIUsageFromJSONBytes(data); ok { + *usage = parsedUsage + } } func extractOpenAIUsageFromJSONBytes(body []byte) (OpenAIUsage, bool) { if len(body) == 0 || !gjson.ValidBytes(body) { return OpenAIUsage{}, false } - values := gjson.GetManyBytes( - body, - "usage.input_tokens", - "usage.output_tokens", - "usage.input_tokens_details.cached_tokens", - "usage.output_tokens_details.image_tokens", - ) + if usage, ok := openAIUsageFromGJSON(gjson.GetBytes(body, "usage")); ok { + return usage, true + } + return openAIUsageFromGJSON(gjson.GetBytes(body, "response.usage")) +} + +func openAIUsageFromGJSON(value gjson.Result) (OpenAIUsage, bool) { + if !value.Exists() || !value.IsObject() { + return OpenAIUsage{}, false + } + inputTokens := value.Get("input_tokens").Int() + if inputTokens == 0 { + inputTokens = value.Get("prompt_tokens").Int() + } + outputTokens := value.Get("output_tokens").Int() + if outputTokens == 0 { + outputTokens = value.Get("completion_tokens").Int() + } + cacheReadTokens := value.Get("input_tokens_details.cached_tokens").Int() + if cacheReadTokens == 0 { + cacheReadTokens = value.Get("prompt_tokens_details.cached_tokens").Int() + } + imageOutputTokens := value.Get("output_tokens_details.image_tokens").Int() + if imageOutputTokens == 0 { + imageOutputTokens = value.Get("completion_tokens_details.image_tokens").Int() + } return OpenAIUsage{ - InputTokens: int(values[0].Int()), - OutputTokens: int(values[1].Int()), - CacheReadInputTokens: int(values[2].Int()), - ImageOutputTokens: int(values[3].Int()), + InputTokens: int(inputTokens), + OutputTokens: int(outputTokens), + CacheCreationInputTokens: int(value.Get("cache_creation_input_tokens").Int()), + CacheReadInputTokens: int(cacheReadTokens), + ImageOutputTokens: int(imageOutputTokens), }, true } diff --git a/backend/internal/service/openai_gateway_service_test.go b/backend/internal/service/openai_gateway_service_test.go index 84a2fe71..708a7146 100644 --- a/backend/internal/service/openai_gateway_service_test.go +++ b/backend/internal/service/openai_gateway_service_test.go @@ -2174,6 +2174,25 @@ func TestParseSSEUsage_SelectiveParsing(t *testing.T) { require.Equal(t, 13, usage.InputTokens) require.Equal(t, 15, usage.OutputTokens) require.Equal(t, 4, usage.CacheReadInputTokens) + + svc.parseSSEUsage(`{"type":"response.completed","response":{"usage":{"prompt_tokens":21,"completion_tokens":8,"prompt_tokens_details":{"cached_tokens":6}}}}`, usage) + require.Equal(t, 21, usage.InputTokens) + require.Equal(t, 8, usage.OutputTokens) + require.Equal(t, 6, usage.CacheReadInputTokens) +} + +func TestExtractOpenAIUsageFromJSONBytes_AcceptsResponseAndChatUsageShapes(t *testing.T) { + usage, ok := extractOpenAIUsageFromJSONBytes([]byte(`{"id":"resp_1","usage":{"input_tokens":3,"output_tokens":5,"input_tokens_details":{"cached_tokens":2}}}`)) + require.True(t, ok) + require.Equal(t, 3, usage.InputTokens) + require.Equal(t, 5, usage.OutputTokens) + require.Equal(t, 2, usage.CacheReadInputTokens) + + usage, ok = extractOpenAIUsageFromJSONBytes([]byte(`{"type":"response.completed","response":{"usage":{"prompt_tokens":13,"completion_tokens":7,"prompt_tokens_details":{"cached_tokens":4}}}}`)) + require.True(t, ok) + require.Equal(t, 13, usage.InputTokens) + require.Equal(t, 7, usage.OutputTokens) + require.Equal(t, 4, usage.CacheReadInputTokens) } func TestExtractCodexFinalResponse_SampleReplay(t *testing.T) { diff --git a/backend/internal/service/openai_ws_forwarder.go b/backend/internal/service/openai_ws_forwarder.go index 77cf7d95..192ff90a 100644 --- a/backend/internal/service/openai_ws_forwarder.go +++ b/backend/internal/service/openai_ws_forwarder.go @@ -399,15 +399,9 @@ func parseOpenAIWSResponseUsageFromCompletedEvent(message []byte, usage *OpenAIU if usage == nil || len(message) == 0 { return } - values := gjson.GetManyBytes( - message, - "response.usage.input_tokens", - "response.usage.output_tokens", - "response.usage.input_tokens_details.cached_tokens", - ) - usage.InputTokens = int(values[0].Int()) - usage.OutputTokens = int(values[1].Int()) - usage.CacheReadInputTokens = int(values[2].Int()) + if parsedUsage, ok := extractOpenAIUsageFromJSONBytes(message); ok { + *usage = parsedUsage + } } func parseOpenAIWSErrorEventFields(message []byte) (code string, errType string, errMessage string) { diff --git a/backend/internal/service/openai_ws_forwarder_hotpath_optimization_test.go b/backend/internal/service/openai_ws_forwarder_hotpath_optimization_test.go index 76167603..0350bde9 100644 --- a/backend/internal/service/openai_ws_forwarder_hotpath_optimization_test.go +++ b/backend/internal/service/openai_ws_forwarder_hotpath_optimization_test.go @@ -29,6 +29,14 @@ func TestParseOpenAIWSResponseUsageFromCompletedEvent(t *testing.T) { require.Equal(t, 11, usage.InputTokens) require.Equal(t, 7, usage.OutputTokens) require.Equal(t, 3, usage.CacheReadInputTokens) + + parseOpenAIWSResponseUsageFromCompletedEvent( + []byte(`{"type":"response.completed","response":{"usage":{"prompt_tokens":19,"completion_tokens":5,"prompt_tokens_details":{"cached_tokens":4}}}}`), + usage, + ) + require.Equal(t, 19, usage.InputTokens) + require.Equal(t, 5, usage.OutputTokens) + require.Equal(t, 4, usage.CacheReadInputTokens) } func TestOpenAIWSErrorEventHelpers_ConsistentWithWrapper(t *testing.T) { From 44995404ef409772f6779efe4ba1f6b77c07a58f Mon Sep 17 00:00:00 2001 From: wucm667 Date: Sun, 17 May 2026 11:19:47 +0800 Subject: [PATCH 07/17] fix(docker): pin frontend builder pnpm to v9 `corepack prepare pnpm@latest` now resolves to pnpm 11, which promotes ERR_PNPM_IGNORED_BUILDS to a hard error and breaks the frontend stage of `docker build`. Pin pnpm to v9 to match the CI workflow (pnpm/action-setup version: 9) and keep image builds reproducible. Fixes #2442 --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7befb464..d556008b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,8 +20,8 @@ FROM ${NODE_IMAGE} AS frontend-builder WORKDIR /app/frontend -# Install pnpm -RUN corepack enable && corepack prepare pnpm@latest --activate +# Install pnpm (pinned to v9 to match CI and keep builds reproducible) +RUN corepack enable && corepack prepare pnpm@9 --activate # Install dependencies first (better caching) COPY frontend/package.json frontend/pnpm-lock.yaml ./ From cc5328c4917cfb7a0b221ac5636f546bca43b565 Mon Sep 17 00:00:00 2001 From: lyen1688 Date: Sun, 17 May 2026 15:33:34 +0800 Subject: [PATCH 08/17] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20OpenAI=20Responses?= =?UTF-8?q?=20SSE=20=E7=BB=88=E6=AD=A2=E4=BA=8B=E4=BB=B6=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/openai_compat_model_test.go | 190 ++++++++++++++++++ .../openai_gateway_chat_completions.go | 37 +++- .../openai_gateway_chat_completions_test.go | 114 +++++++++++ .../service/openai_gateway_messages.go | 49 ++++- .../service/openai_gateway_service.go | 70 +++++++ .../service/openai_gateway_service_test.go | 26 +++ 6 files changed, 474 insertions(+), 12 deletions(-) diff --git a/backend/internal/service/openai_compat_model_test.go b/backend/internal/service/openai_compat_model_test.go index e222b093..0ba2a63f 100644 --- a/backend/internal/service/openai_compat_model_test.go +++ b/backend/internal/service/openai_compat_model_test.go @@ -1360,6 +1360,135 @@ func TestForwardAsAnthropic_TerminalUsageWithoutUpstreamCloseReturns(t *testing. } } +func TestForwardAsAnthropic_EventNamedTerminalWithoutUpstreamCloseReturns(t *testing.T) { + gin.SetMode(gin.TestMode) + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Writer = &openAICompatFailingWriter{ResponseWriter: c.Writer, failAfter: 0} + body := []byte(`{"model":"gpt-5.4","max_tokens":16,"messages":[{"role":"user","content":"hello"}],"stream":true}`) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + upstreamBody := []byte(strings.Join([]string{ + `event: response.completed`, + `data: {"response":{"id":"resp_1","object":"response","model":"gpt-5.4","status":"completed","output":[{"type":"message","id":"msg_1","role":"assistant","status":"completed","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":15,"output_tokens":6,"total_tokens":21,"input_tokens_details":{"cached_tokens":5}}}}`, + ``, + ``, + }, "\n")) + upstreamStream := newOpenAICompatBlockingReadCloser(upstreamBody) + defer func() { + require.NoError(t, upstreamStream.Close()) + }() + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_messages_event_named_terminal"}}, + Body: upstreamStream, + }} + + svc := &OpenAIGatewayService{httpUpstream: upstream} + account := &Account{ + ID: 1, + Name: "openai-oauth", + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Concurrency: 1, + Credentials: map[string]any{ + "access_token": "oauth-token", + "chatgpt_account_id": "chatgpt-acc", + }, + } + + type forwardResult struct { + result *OpenAIForwardResult + err error + } + resultCh := make(chan forwardResult, 1) + go func() { + result, err := svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "gpt-5.1") + resultCh <- forwardResult{result: result, err: err} + }() + + select { + case got := <-resultCh: + require.NoError(t, got.err) + require.NotNil(t, got.result) + require.Equal(t, 15, got.result.Usage.InputTokens) + require.Equal(t, 6, got.result.Usage.OutputTokens) + require.Equal(t, 5, got.result.Usage.CacheReadInputTokens) + case <-time.After(time.Second): + require.Fail(t, "ForwardAsAnthropic should use SSE event names when data payloads omit type") + } +} + +func TestForwardAsAnthropic_EventNamedTerminalWithKeepaliveReturns(t *testing.T) { + gin.SetMode(gin.TestMode) + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Writer = &openAICompatFailingWriter{ResponseWriter: c.Writer, failAfter: 0} + body := []byte(`{"model":"gpt-5.4","max_tokens":16,"messages":[{"role":"user","content":"hello"}],"stream":true}`) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + upstreamBody := []byte(strings.Join([]string{ + `: upstream ping`, + ``, + `event: response.completed`, + `data: {"response":{"id":"resp_1","object":"response","model":"gpt-5.4","status":"completed","output":[{"type":"message","id":"msg_1","role":"assistant","status":"completed","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":15,"output_tokens":6,"total_tokens":21,"input_tokens_details":{"cached_tokens":5}}}}`, + ``, + ``, + }, "\n")) + upstreamStream := newOpenAICompatBlockingReadCloser(upstreamBody) + defer func() { + require.NoError(t, upstreamStream.Close()) + }() + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_messages_event_named_keepalive"}}, + Body: upstreamStream, + }} + + svc := &OpenAIGatewayService{ + cfg: &config.Config{Gateway: config.GatewayConfig{ + StreamKeepaliveInterval: 5, + }}, + httpUpstream: upstream, + } + account := &Account{ + ID: 1, + Name: "openai-oauth", + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Concurrency: 1, + Credentials: map[string]any{ + "access_token": "oauth-token", + "chatgpt_account_id": "chatgpt-acc", + }, + } + + type forwardResult struct { + result *OpenAIForwardResult + err error + } + resultCh := make(chan forwardResult, 1) + go func() { + result, err := svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "gpt-5.1") + resultCh <- forwardResult{result: result, err: err} + }() + + select { + case got := <-resultCh: + require.NoError(t, got.err) + require.NotNil(t, got.result) + require.Equal(t, 15, got.result.Usage.InputTokens) + require.Equal(t, 6, got.result.Usage.OutputTokens) + require.Equal(t, 5, got.result.Usage.CacheReadInputTokens) + case <-time.After(time.Second): + require.Fail(t, "ForwardAsAnthropic keepalive path should use SSE event names when data payloads omit type") + } +} + func TestForwardAsAnthropic_BufferedTerminalWithoutUpstreamCloseReturns(t *testing.T) { gin.SetMode(gin.TestMode) @@ -1416,6 +1545,67 @@ func TestForwardAsAnthropic_BufferedTerminalWithoutUpstreamCloseReturns(t *testi } } +func TestForwardAsAnthropic_BufferedEventNamedTerminalWithoutUpstreamCloseReturns(t *testing.T) { + gin.SetMode(gin.TestMode) + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + body := []byte(`{"model":"gpt-5.4","max_tokens":16,"messages":[{"role":"user","content":"hello"}],"stream":false}`) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + upstreamBody := []byte(strings.Join([]string{ + `event: response.completed`, + `data: {"response":{"id":"resp_1","object":"response","model":"gpt-5.4","status":"completed","output":[{"type":"message","id":"msg_1","role":"assistant","status":"completed","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":15,"output_tokens":6,"total_tokens":21,"input_tokens_details":{"cached_tokens":5}}}}`, + ``, + ``, + }, "\n")) + upstreamStream := newOpenAICompatBlockingReadCloser(upstreamBody) + defer func() { + require.NoError(t, upstreamStream.Close()) + }() + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_messages_buffered_event_named"}}, + Body: upstreamStream, + }} + + svc := &OpenAIGatewayService{httpUpstream: upstream} + account := &Account{ + ID: 1, + Name: "openai-oauth", + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Concurrency: 1, + Credentials: map[string]any{ + "access_token": "oauth-token", + "chatgpt_account_id": "chatgpt-acc", + }, + } + + type forwardResult struct { + result *OpenAIForwardResult + err error + } + resultCh := make(chan forwardResult, 1) + go func() { + result, err := svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "gpt-5.1") + resultCh <- forwardResult{result: result, err: err} + }() + + select { + case got := <-resultCh: + require.NoError(t, got.err) + require.NotNil(t, got.result) + require.Equal(t, 15, got.result.Usage.InputTokens) + require.Equal(t, 6, got.result.Usage.OutputTokens) + require.Equal(t, 5, got.result.Usage.CacheReadInputTokens) + require.Contains(t, rec.Body.String(), `"stop_reason":"end_turn"`) + case <-time.After(time.Second): + require.Fail(t, "ForwardAsAnthropic buffered response should use SSE event names when data payloads omit type") + } +} + func TestForwardAsAnthropic_DoneSentinelWithoutTerminalReturnsError(t *testing.T) { gin.SetMode(gin.TestMode) diff --git a/backend/internal/service/openai_gateway_chat_completions.go b/backend/internal/service/openai_gateway_chat_completions.go index 84d85c74..5b3c0e6f 100644 --- a/backend/internal/service/openai_gateway_chat_completions.go +++ b/backend/internal/service/openai_gateway_chat_completions.go @@ -554,6 +554,13 @@ func (s *OpenAIGatewayService) handleChatStreamingResponse( missingTerminalErr := func() (*OpenAIForwardResult, error) { return resultWithUsage(), fmt.Errorf("stream usage incomplete: missing terminal event") } + processFrame := func(frame openAICompatSSEFrame) bool { + payload := openAICompatPayloadWithEventType(frame.Data, frame.EventType) + if strings.TrimSpace(payload) == "[DONE]" { + return false + } + return processDataLine(payload) + } // Determine keepalive interval keepaliveInterval := time.Duration(0) @@ -563,16 +570,17 @@ func (s *OpenAIGatewayService) handleChatStreamingResponse( // No keepalive: fast synchronous path if streamInterval <= 0 && keepaliveInterval <= 0 { + var parser openAICompatSSEFrameParser for scanner.Scan() { line := scanner.Text() - payload, ok := extractOpenAISSEDataLine(line) + frame, ok := parser.AddLine(line) if !ok { continue } - if strings.TrimSpace(payload) == "[DONE]" { + if strings.TrimSpace(frame.Data) == "[DONE]" { return missingTerminalErr() } - if processDataLine(payload) { + if processFrame(frame) { return finalizeStream() } } @@ -580,6 +588,14 @@ func (s *OpenAIGatewayService) handleChatStreamingResponse( handleScanErr(err) return resultWithUsage(), fmt.Errorf("stream usage incomplete: %w", err) } + if frame, ok := parser.Finish(); ok { + if strings.TrimSpace(frame.Data) == "[DONE]" { + return missingTerminalErr() + } + if processFrame(frame) { + return finalizeStream() + } + } return missingTerminalErr() } @@ -624,11 +640,20 @@ func (s *OpenAIGatewayService) handleChatStreamingResponse( keepaliveCh = keepaliveTicker.C } lastDataAt := time.Now() + var parser openAICompatSSEFrameParser for { select { case ev, ok := <-events: if !ok { + if frame, ok := parser.Finish(); ok { + if strings.TrimSpace(frame.Data) == "[DONE]" { + return missingTerminalErr() + } + if processFrame(frame) { + return finalizeStream() + } + } return missingTerminalErr() } if ev.err != nil { @@ -637,14 +662,14 @@ func (s *OpenAIGatewayService) handleChatStreamingResponse( } lastDataAt = time.Now() line := ev.line - payload, ok := extractOpenAISSEDataLine(line) + frame, ok := parser.AddLine(line) if !ok { continue } - if strings.TrimSpace(payload) == "[DONE]" { + if strings.TrimSpace(frame.Data) == "[DONE]" { return missingTerminalErr() } - if processDataLine(payload) { + if processFrame(frame) { return finalizeStream() } diff --git a/backend/internal/service/openai_gateway_chat_completions_test.go b/backend/internal/service/openai_gateway_chat_completions_test.go index b0d1fa31..a26091a3 100644 --- a/backend/internal/service/openai_gateway_chat_completions_test.go +++ b/backend/internal/service/openai_gateway_chat_completions_test.go @@ -236,6 +236,120 @@ func TestForwardAsChatCompletions_TerminalUsageWithoutUpstreamCloseReturns(t *te } } +func TestForwardAsChatCompletions_EventNamedTerminalWithoutUpstreamCloseReturns(t *testing.T) { + gin.SetMode(gin.TestMode) + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + body := []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"hello"}],"stream":true}`) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + upstreamBody := []byte(strings.Join([]string{ + `event: response.created`, + `data: {"response":{"id":"resp_1","model":"gpt-5.4","status":"in_progress","output":[]}}`, + ``, + `event: response.output_text.delta`, + `data: {"delta":"ok"}`, + ``, + `event: response.completed`, + `data: {"response":{"id":"resp_1","object":"response","model":"gpt-5.4","status":"completed","output":[{"type":"message","id":"msg_1","role":"assistant","status":"completed","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":17,"output_tokens":8,"total_tokens":25,"input_tokens_details":{"cached_tokens":6}}}}`, + ``, + ``, + }, "\n")) + upstreamStream := newOpenAICompatBlockingReadCloser(upstreamBody) + defer func() { + require.NoError(t, upstreamStream.Close()) + }() + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_chat_event_named_terminal"}}, + Body: upstreamStream, + }} + + svc := &OpenAIGatewayService{httpUpstream: upstream} + account := &Account{ + ID: 1, + Name: "openai-oauth", + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Concurrency: 1, + Credentials: map[string]any{ + "access_token": "oauth-token", + "chatgpt_account_id": "chatgpt-acc", + }, + } + + type forwardResult struct { + result *OpenAIForwardResult + err error + } + resultCh := make(chan forwardResult, 1) + go func() { + result, err := svc.ForwardAsChatCompletions(context.Background(), c, account, body, "", "gpt-5.1") + resultCh <- forwardResult{result: result, err: err} + }() + + select { + case got := <-resultCh: + require.NoError(t, got.err) + require.NotNil(t, got.result) + require.Equal(t, 17, got.result.Usage.InputTokens) + require.Equal(t, 8, got.result.Usage.OutputTokens) + require.Equal(t, 6, got.result.Usage.CacheReadInputTokens) + require.Contains(t, rec.Body.String(), `"content":"ok"`) + case <-time.After(time.Second): + require.Fail(t, "ForwardAsChatCompletions should use SSE event names when data payloads omit type") + } +} + +func TestForwardAsChatCompletions_EventTypeDoesNotLeakAcrossFrames(t *testing.T) { + gin.SetMode(gin.TestMode) + + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + body := []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"hello"}],"stream":true}`) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + upstreamBody := strings.Join([]string{ + `event: response.created`, + `data: {"response":{"id":"resp_1","model":"gpt-5.4","status":"in_progress","output":[]}}`, + ``, + `data: {"type":"response.output_text.delta","delta":"ok"}`, + ``, + `event: response.completed`, + `data: {"response":{"id":"resp_1","object":"response","model":"gpt-5.4","status":"completed","output":[{"type":"message","id":"msg_1","role":"assistant","status":"completed","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":17,"output_tokens":8,"total_tokens":25,"input_tokens_details":{"cached_tokens":6}}}}`, + ``, + `data: [DONE]`, + ``, + }, "\n") + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/event-stream"}, "x-request-id": []string{"rid_chat_event_boundary"}}, + Body: io.NopCloser(strings.NewReader(upstreamBody)), + }} + + svc := &OpenAIGatewayService{httpUpstream: upstream} + account := &Account{ + ID: 1, + Name: "openai-oauth", + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Concurrency: 1, + Credentials: map[string]any{ + "access_token": "oauth-token", + "chatgpt_account_id": "chatgpt-acc", + }, + } + + result, err := svc.ForwardAsChatCompletions(context.Background(), c, account, body, "", "gpt-5.1") + require.NoError(t, err) + require.NotNil(t, result) + require.Contains(t, rec.Body.String(), `"content":"ok"`) + require.Contains(t, rec.Body.String(), `data: [DONE]`) +} + func TestForwardAsChatCompletions_BufferedTerminalWithoutUpstreamCloseReturns(t *testing.T) { gin.SetMode(gin.TestMode) diff --git a/backend/internal/service/openai_gateway_messages.go b/backend/internal/service/openai_gateway_messages.go index aefa8fd2..6d74f7dd 100644 --- a/backend/internal/service/openai_gateway_messages.go +++ b/backend/internal/service/openai_gateway_messages.go @@ -560,10 +560,24 @@ func (s *OpenAIGatewayService) readOpenAICompatBufferedTerminal( }() defer close(done) + var parser openAICompatSSEFrameParser for { select { case ev, ok := <-events: if !ok { + if frame, ok := parser.Finish(); ok { + payload := openAICompatPayloadWithEventType(frame.Data, frame.EventType) + var event apicompat.ResponsesStreamEvent + if err := json.Unmarshal([]byte(payload), &event); err == nil { + acc.ProcessEvent(&event) + if isOpenAICompatResponsesTerminalEvent(event.Type) && event.Response != nil { + if event.Response.Usage != nil { + usage = copyOpenAIUsageFromResponsesUsage(event.Response.Usage) + } + return event.Response, usage, acc, nil + } + } + } return nil, usage, acc, nil } resetTimeout() @@ -580,10 +594,11 @@ func (s *OpenAIGatewayService) readOpenAICompatBufferedTerminal( if isOpenAICompatDoneSentinelLine(ev.line) { return nil, usage, acc, nil } - payload, ok := extractOpenAISSEDataLine(ev.line) - if !ok || payload == "" { + frame, ok := parser.AddLine(ev.line) + if !ok { continue } + payload := openAICompatPayloadWithEventType(frame.Data, frame.EventType) var event apicompat.ResponsesStreamEvent if err := json.Unmarshal([]byte(payload), &event); err != nil { @@ -772,6 +787,10 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse( missingTerminalErr := func() (*OpenAIForwardResult, error) { return resultWithUsage(), fmt.Errorf("stream usage incomplete: missing terminal event") } + processFrame := func(frame openAICompatSSEFrame) bool { + payload := openAICompatPayloadWithEventType(frame.Data, frame.EventType) + return processDataLine(payload) + } // ── Determine keepalive interval ── keepaliveInterval := time.Duration(0) @@ -781,16 +800,17 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse( // ── No keepalive: fast synchronous path (no goroutine overhead) ── if streamInterval <= 0 && keepaliveInterval <= 0 { + var parser openAICompatSSEFrameParser for scanner.Scan() { line := scanner.Text() if isOpenAICompatDoneSentinelLine(line) { return missingTerminalErr() } - payload, ok := extractOpenAISSEDataLine(line) + frame, ok := parser.AddLine(line) if !ok { continue } - if processDataLine(payload) { + if processFrame(frame) { return finalizeStream() } } @@ -798,6 +818,14 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse( handleScanErr(err) return resultWithUsage(), fmt.Errorf("stream usage incomplete: %w", err) } + if frame, ok := parser.Finish(); ok { + if strings.TrimSpace(frame.Data) == "[DONE]" { + return missingTerminalErr() + } + if processFrame(frame) { + return finalizeStream() + } + } return missingTerminalErr() } @@ -842,12 +870,21 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse( keepaliveCh = keepaliveTicker.C } lastDataAt := time.Now() + var parser openAICompatSSEFrameParser for { select { case ev, ok := <-events: if !ok { // Upstream closed + if frame, ok := parser.Finish(); ok { + if strings.TrimSpace(frame.Data) == "[DONE]" { + return missingTerminalErr() + } + if processFrame(frame) { + return finalizeStream() + } + } return missingTerminalErr() } if ev.err != nil { @@ -859,11 +896,11 @@ func (s *OpenAIGatewayService) handleAnthropicStreamingResponse( if isOpenAICompatDoneSentinelLine(line) { return missingTerminalErr() } - payload, ok := extractOpenAISSEDataLine(line) + frame, ok := parser.AddLine(line) if !ok { continue } - if processDataLine(payload) { + if processFrame(frame) { return finalizeStream() } diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index e12b208e..a2276353 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -4578,6 +4578,76 @@ func extractOpenAISSEDataLine(line string) (string, bool) { return line[start:], true } +func extractOpenAISSEEventLine(line string) (string, bool) { + if !strings.HasPrefix(line, "event:") { + return "", false + } + start := len("event:") + for start < len(line) { + if line[start] != ' ' && line[start] != ' ' { + break + } + start++ + } + return strings.TrimSpace(line[start:]), true +} + +type openAICompatSSEFrame struct { + EventType string + Data string +} + +type openAICompatSSEFrameParser struct { + eventType string + dataLines []string +} + +func (p *openAICompatSSEFrameParser) AddLine(line string) (openAICompatSSEFrame, bool) { + if line == "" { + return p.dispatch() + } + if strings.HasPrefix(line, ":") { + return openAICompatSSEFrame{}, false + } + if eventType, ok := extractOpenAISSEEventLine(line); ok { + p.eventType = eventType + return openAICompatSSEFrame{}, false + } + if data, ok := extractOpenAISSEDataLine(line); ok { + p.dataLines = append(p.dataLines, data) + } + return openAICompatSSEFrame{}, false +} + +func (p *openAICompatSSEFrameParser) Finish() (openAICompatSSEFrame, bool) { + return p.dispatch() +} + +func (p *openAICompatSSEFrameParser) dispatch() (openAICompatSSEFrame, bool) { + frame := openAICompatSSEFrame{ + EventType: p.eventType, + Data: strings.Join(p.dataLines, "\n"), + } + p.eventType = "" + p.dataLines = nil + return frame, frame.Data != "" +} + +func openAICompatPayloadWithEventType(payload, eventType string) string { + eventType = strings.TrimSpace(eventType) + if eventType == "" || strings.TrimSpace(payload) == "" || strings.TrimSpace(payload) == "[DONE]" { + return payload + } + if gjson.Get(payload, "type").Exists() { + return payload + } + patched, err := sjson.Set(payload, "type", eventType) + if err != nil { + return payload + } + return patched +} + func (s *OpenAIGatewayService) replaceModelInSSELine(line, fromModel, toModel string) string { data, ok := extractOpenAISSEDataLine(line) if !ok { diff --git a/backend/internal/service/openai_gateway_service_test.go b/backend/internal/service/openai_gateway_service_test.go index 84a2fe71..d636cf27 100644 --- a/backend/internal/service/openai_gateway_service_test.go +++ b/backend/internal/service/openai_gateway_service_test.go @@ -2293,3 +2293,29 @@ func TestHandleSSEToJSON_ResponseFailedReturnsProtocolError(t *testing.T) { require.Contains(t, rec.Body.String(), "upstream rejected request") require.Contains(t, rec.Header().Get("Content-Type"), "application/json") } + +func TestOpenAICompatSSEFrameParserResetsEventTypeAtFrameBoundary(t *testing.T) { + var parser openAICompatSSEFrameParser + + frame, ok := parser.AddLine("event: response.created") + require.False(t, ok) + require.Empty(t, frame) + + frame, ok = parser.AddLine(`data: {"response":{"id":"resp_1"}}`) + require.False(t, ok) + require.Empty(t, frame) + + frame, ok = parser.AddLine("") + require.True(t, ok) + require.Equal(t, "response.created", frame.EventType) + require.JSONEq(t, `{"response":{"id":"resp_1"}}`, frame.Data) + + frame, ok = parser.AddLine(`data: {"delta":"ok"}`) + require.False(t, ok) + require.Empty(t, frame.EventType) + + frame, ok = parser.AddLine("") + require.True(t, ok) + require.Empty(t, frame.EventType) + require.JSONEq(t, `{"delta":"ok"}`, frame.Data) +} From 1b03240515a465f878f421cd04f00550ed70e0ac Mon Sep 17 00:00:00 2001 From: Yuhao Jiang Date: Sun, 17 May 2026 14:58:42 -0500 Subject: [PATCH 09/17] =?UTF-8?q?fix(payment):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E5=AE=9D=E5=AE=98=E6=96=B9=E6=89=AB=E7=A0=81?= =?UTF-8?q?=E4=BA=8C=E7=BB=B4=E7=A0=81=E7=94=9F=E6=88=90=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支付宝官方服务商在 precreate(当面付)不可用回退到 page.pay 时, 错误地把网页跳转 URL 当作可扫码二维码内容返回。前端用 QRCode 库 把这段 URL 渲染成二维码后,支付宝 APP 无法识别(扫到的只是个 HTTP URL,不是支付二维码),用户必须点"重新打开支付页面"跳转到支付宝 收银台才能扫到真正可用的二维码。 - 后端 alipay.go:createPagePayTrade 不再把 PayURL 塞给 QRCode; createDesktopTrade 在 paymentMode == "redirect" 时跳过 precreate 直接走 page.pay,避免没开通"当面付"的商户走一次无用的 API 调用 - 前端管理端 PaymentProviderDialog:让支付宝官方实例可在"支付模式" 中选择"跳转",开启后始终在新标签页打开支付宝收银台 - ProviderCard 的 modeLabel 增加 redirect 分支 - 补充 TestCreateTradeRedirectModeSkipsPrecreate 单元测试 Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/internal/payment/provider/alipay.go | 25 ++++++-- .../internal/payment/provider/alipay_test.go | 59 ++++++++++++++++++- .../payment/PaymentProviderDialog.vue | 50 ++++++++++++++-- .../src/components/payment/ProviderCard.vue | 3 +- .../src/components/payment/providerConfig.ts | 5 ++ 5 files changed, 130 insertions(+), 12 deletions(-) diff --git a/backend/internal/payment/provider/alipay.go b/backend/internal/payment/provider/alipay.go index 1234b568..c4c6e634 100644 --- a/backend/internal/payment/provider/alipay.go +++ b/backend/internal/payment/provider/alipay.go @@ -105,10 +105,16 @@ func (a *Alipay) MerchantIdentityMetadata() map[string]string { // CreatePayment creates an Alipay payment using the following routing: // - Mobile (H5): alipay.trade.wap.pay — browser redirect into Alipay. -// - Desktop: prefer alipay.trade.precreate to get a scan payload directly. -// - Desktop fallback: if precreate is unavailable for the merchant, fall back -// to alipay.trade.page.pay and expose both pay_url and qr_code so the -// frontend can render a QR while still allowing direct page open. +// - Desktop, default: prefer alipay.trade.precreate (FACE_TO_FACE_PAYMENT) to +// get a scannable QR payload. If precreate is unavailable for the merchant, +// fall back to alipay.trade.page.pay and expose pay_url only — the frontend +// opens the Alipay checkout in a new tab. +// - Desktop, paymentMode == "redirect": skip precreate and go straight to +// alipay.trade.page.pay so the frontend always opens the Alipay checkout +// in a new tab. Use this when the merchant has not enabled FACE_TO_FACE_PAYMENT. +// +// Note: alipay.trade.page.pay returns a checkout page URL, not a scannable +// payment QR. Never expose it via the QRCode field. func (a *Alipay) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) { client, err := a.getClient() if err != nil { @@ -150,6 +156,13 @@ func (a *Alipay) createWapTrade(client *alipay.Client, req payment.CreatePayment } func (a *Alipay) createDesktopTrade(ctx context.Context, client *alipay.Client, req payment.CreatePaymentRequest, notifyURL, returnURL string) (*payment.CreatePaymentResponse, error) { + // Explicit redirect mode: merchant opted into "always open the Alipay + // checkout page in a new tab" via the provider instance's payment_mode. + // Skip precreate to avoid a wasted API call. + if strings.EqualFold(strings.TrimSpace(a.config["paymentMode"]), "redirect") { + return a.createPagePayTrade(client, req, notifyURL, returnURL) + } + resp, precreateErr := a.createPrecreateTrade(ctx, client, req, notifyURL) if precreateErr == nil { return resp, nil @@ -204,10 +217,12 @@ func (a *Alipay) createPagePayTrade(client *alipay.Client, req payment.CreatePay if err != nil { return nil, fmt.Errorf("alipay TradePagePay: %w", err) } + // Only PayURL is exposed: alipay.trade.page.pay returns a checkout page URL + // that must be opened in a browser, not a scannable payment QR. Setting it + // as QRCode would let the frontend render an unscannable image. return &payment.CreatePaymentResponse{ TradeNo: req.OrderID, PayURL: payURL.String(), - QRCode: payURL.String(), }, nil } diff --git a/backend/internal/payment/provider/alipay_test.go b/backend/internal/payment/provider/alipay_test.go index fdc8eec1..9f8aec53 100644 --- a/backend/internal/payment/provider/alipay_test.go +++ b/backend/internal/payment/provider/alipay_test.go @@ -189,8 +189,63 @@ func TestCreateTradeUsesPagePayForDesktop(t *testing.T) { if resp.PayURL == "" { t.Fatal("expected pay_url for desktop page pay") } - if resp.QRCode != resp.PayURL { - t.Fatalf("qr_code = %q, want same as pay_url %q", resp.QRCode, resp.PayURL) + // page.pay returns a checkout page URL, not a scannable QR payload — + // it must never be exposed via QRCode (the frontend would render an + // unscannable image from it). + if resp.QRCode != "" { + t.Fatalf("qr_code = %q, want empty for page pay", resp.QRCode) + } +} + +// When the provider instance is configured with paymentMode == "redirect", +// the desktop flow must skip precreate and go straight to page.pay. +func TestCreateTradeRedirectModeSkipsPrecreate(t *testing.T) { + origPreCreate := alipayTradePreCreate + origPagePay := alipayTradePagePay + t.Cleanup(func() { + alipayTradePreCreate = origPreCreate + alipayTradePagePay = origPagePay + }) + + preCreateCalls := 0 + pagePayCalls := 0 + alipayTradePreCreate = func(ctx context.Context, client *alipay.Client, param alipay.TradePreCreate) (*alipay.TradePreCreateRsp, error) { + preCreateCalls++ + return &alipay.TradePreCreateRsp{ + Error: alipay.Error{Code: alipay.CodeSuccess}, + QRCode: "https://qr.alipay.example.com/precreate-token", + }, nil + } + alipayTradePagePay = func(client *alipay.Client, param alipay.TradePagePay) (*url.URL, error) { + pagePayCalls++ + if param.ProductCode != alipayProductCodePagePay { + t.Fatalf("product_code = %q, want %q", param.ProductCode, alipayProductCodePagePay) + } + return url.Parse("https://openapi.alipay.com/gateway.do?page-pay") + } + + provider := &Alipay{ + config: map[string]string{"paymentMode": "redirect"}, + } + resp, err := provider.createDesktopTrade(context.Background(), &alipay.Client{}, payment.CreatePaymentRequest{ + OrderID: "sub2_103", + Amount: "12.00", + Subject: "Balance recharge", + }, "https://merchant.example.com/api/v1/payment/webhook/alipay", "https://merchant.example.com/payment/result") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if preCreateCalls != 0 { + t.Fatalf("precreate calls = %d, want 0 (redirect mode must skip precreate)", preCreateCalls) + } + if pagePayCalls != 1 { + t.Fatalf("page pay calls = %d, want 1", pagePayCalls) + } + if resp.PayURL == "" { + t.Fatal("expected pay_url for redirect mode") + } + if resp.QRCode != "" { + t.Fatalf("qr_code = %q, want empty for redirect mode", resp.QRCode) } } diff --git a/frontend/src/components/payment/PaymentProviderDialog.vue b/frontend/src/components/payment/PaymentProviderDialog.vue index 86304cf6..b6085ed0 100644 --- a/frontend/src/components/payment/PaymentProviderDialog.vue +++ b/frontend/src/components/payment/PaymentProviderDialog.vue @@ -34,7 +34,7 @@ -

+
{{ t('admin.settings.payment.paymentMode') }}