Merge pull request #7649 from Yukinoshita-Yukino/fix/codex-image-bridge-openai-only

fix(openai): only inject the Codex image generation bridge for OpenAI accounts
This commit is contained in:
Wesley Liddick
2026-10-12 14:38:00 +08:00
committed by GitHub
2 changed files with 113 additions and 0 deletions
@@ -613,7 +613,14 @@ func (s *OpenAIGatewayService) ResolveChannelMappingAndRestrict(ctx context.Cont
return s.channelService.ResolveChannelMappingAndRestrict(ctx, groupID, model)
}
// isCodexImageGenerationBridgeEnabled 决定是否为 Codex 客户端自动注入 hosted
// image_generation 工具。该工具是 OpenAI Responses 专属能力,Kimi / DeepSeek /
// MiniMax 等经 OpenAI 网关转发的原生 Responses 上游不识别,注入后会被 400 拒绝,
// 因此非 OpenAI 平台账号一律不桥接,后续再按账号 → 渠道 → 全局的优先级取开关。
func (s *OpenAIGatewayService) isCodexImageGenerationBridgeEnabled(ctx context.Context, account *Account, apiKey *APIKey) bool {
if account == nil || account.Platform != PlatformOpenAI {
return false
}
if override := account.CodexImageGenerationBridgeOverride(); override != nil {
return *override
}
@@ -424,6 +424,76 @@ func TestOpenAIGatewayServiceForward_CodexBridgePreservesExistingToolChoice(t *t
require.Equal(t, "image_generation", gjson.GetBytes(upstream.lastBody, "tool_choice.type").String())
}
// 国产原生 Responses 账号(Kimi / DeepSeek / MiniMax)在桥接由渠道级开关或全局开关
// 单独打开时,发往上游的请求体都不能带 hosted image_generation 工具 / tool_choice / 桥接指令。
func TestOpenAIGatewayServiceForward_CodexBridgeSkipsNativeCNResponsesAccounts(t *testing.T) {
gin.SetMode(gin.TestMode)
groupID := int64(4242)
triggers := []struct {
name string
global bool
channel *Channel
}{
{
name: "channel_openai_true_global_off",
global: false,
channel: &Channel{ID: 1, Status: StatusActive, FeaturesConfig: map[string]any{
featureKeyCodexImageGenerationBridge: map[string]any{PlatformOpenAI: true},
}},
},
{
name: "global_on_no_channel",
global: true,
},
}
for _, platform := range []string{PlatformKimi, PlatformDeepseek, PlatformMiniMax} {
for _, trig := range triggers {
t.Run(platform+"/"+trig.name, func(t *testing.T) {
upstream := &httpUpstreamRecorder{
resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"id":"resp_cn_codex","model":"cn-model","usage":{"input_tokens":1,"output_tokens":1}}`)),
},
}
svc := newOpenAIImageGenerationControlTestService(upstream)
svc.cfg.Gateway.CodexImageGenerationBridgeEnabled = trig.global
if trig.channel != nil {
svc.channelService = newOpenAIImageGenerationControlChannelService(groupID, trig.channel)
}
c, _ := newOpenAIImageGenerationControlTestContext(true, "codex_cli_rs/0.144.1")
account := &Account{
ID: 6100,
Name: platform + "-native-responses",
Platform: platform,
Type: AccountTypeAPIKey,
Status: StatusActive,
Schedulable: true,
Concurrency: 1,
Credentials: map[string]any{
"api_key": "sk-cn-test",
"api_protocol": APIProtocolResponses,
},
}
// 前置条件:账号确实走原生 Responses 转发,而不是 Chat Completions 转换链。
require.True(t, account.UsesNativeCNResponses())
require.False(t, shouldForwardOpenAIResponsesViaRawChatCompletions(account))
result, err := svc.Forward(context.Background(), c, account, []byte(`{"model":"cn-model","input":"hello","stream":false,"tools":[{"type":"function","name":"shell","parameters":{"type":"object"}}]}`))
require.NoError(t, err)
require.NotNil(t, result)
require.NotNil(t, upstream.lastReq)
require.Falsef(t, gjson.GetBytes(upstream.lastBody, `tools.#(type=="image_generation")`).Exists(), "image_generation tool injected into %s upstream body", platform)
require.Falsef(t, gjson.GetBytes(upstream.lastBody, "tool_choice").Exists(), "tool_choice injected into %s upstream body", platform)
require.NotContains(t, gjson.GetBytes(upstream.lastBody, "instructions").String(), codexImageGenerationBridgeMarker)
require.Equal(t, "shell", gjson.GetBytes(upstream.lastBody, "tools.0.name").String())
})
}
}
}
func TestOpenAIGatewayServiceForward_CodexBridgeSkipsCompactRequests(t *testing.T) {
gin.SetMode(gin.TestMode)
@@ -524,6 +594,42 @@ func TestOpenAIGatewayService_CodexImageGenerationBridgeOverridePrecedence(t *te
},
want: false,
},
{
name: "kimi account ignores channel openai true",
global: false,
channel: &Channel{ID: 1, Status: StatusActive, FeaturesConfig: map[string]any{
featureKeyCodexImageGenerationBridge: map[string]any{PlatformOpenAI: true},
}},
account: &Account{Platform: PlatformKimi, Type: AccountTypeAPIKey},
want: false,
},
{
name: "kimi account ignores enabled global",
global: true,
account: &Account{Platform: PlatformKimi, Type: AccountTypeAPIKey},
want: false,
},
{
name: "deepseek account ignores enabled global",
global: true,
account: &Account{Platform: PlatformDeepseek, Type: AccountTypeAPIKey},
want: false,
},
{
name: "minimax account ignores channel openai true",
global: false,
channel: &Channel{ID: 1, Status: StatusActive, FeaturesConfig: map[string]any{
featureKeyCodexImageGenerationBridge: map[string]any{PlatformOpenAI: true},
}},
account: &Account{Platform: PlatformMiniMax, Type: AccountTypeAPIKey},
want: false,
},
{
name: "nil account never bridges",
global: true,
account: nil,
want: false,
},
}
for _, tt := range tests {