Merge pull request #2834 from DaydreamCoding/pr/openai-codex-cli-allow-claude-code
feat(openai): codex_cli_only 新增放行 Claude Code Codex 插件的机制
This commit is contained in:
commit
433f8dcd13
@ -256,6 +256,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
|||||||
RewriteMessageCacheControl: settings.RewriteMessageCacheControl,
|
RewriteMessageCacheControl: settings.RewriteMessageCacheControl,
|
||||||
AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion,
|
AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion,
|
||||||
OpenAICodexUserAgent: settings.OpenAICodexUserAgent,
|
OpenAICodexUserAgent: settings.OpenAICodexUserAgent,
|
||||||
|
OpenAIAllowClaudeCodeCodexPlugin: settings.OpenAIAllowClaudeCodeCodexPlugin,
|
||||||
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
|
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
|
||||||
PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource,
|
PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource,
|
||||||
PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource,
|
PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource,
|
||||||
@ -584,6 +585,7 @@ type UpdateSettingsRequest struct {
|
|||||||
RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
|
RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
|
||||||
AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
|
AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
|
||||||
OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
|
OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
|
||||||
|
OpenAIAllowClaudeCodeCodexPlugin *bool `json:"openai_allow_claude_code_codex_plugin"`
|
||||||
|
|
||||||
// Payment visible method routing
|
// Payment visible method routing
|
||||||
PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
|
PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
|
||||||
@ -1655,6 +1657,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return previousSettings.OpenAICodexUserAgent
|
return previousSettings.OpenAICodexUserAgent
|
||||||
}(),
|
}(),
|
||||||
|
OpenAIAllowClaudeCodeCodexPlugin: func() bool {
|
||||||
|
if req.OpenAIAllowClaudeCodeCodexPlugin != nil {
|
||||||
|
return *req.OpenAIAllowClaudeCodeCodexPlugin
|
||||||
|
}
|
||||||
|
return previousSettings.OpenAIAllowClaudeCodeCodexPlugin
|
||||||
|
}(),
|
||||||
PaymentVisibleMethodAlipaySource: func() string {
|
PaymentVisibleMethodAlipaySource: func() string {
|
||||||
if req.PaymentVisibleMethodAlipaySource != nil {
|
if req.PaymentVisibleMethodAlipaySource != nil {
|
||||||
return strings.TrimSpace(*req.PaymentVisibleMethodAlipaySource)
|
return strings.TrimSpace(*req.PaymentVisibleMethodAlipaySource)
|
||||||
@ -2031,6 +2039,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
|
RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
|
||||||
AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
|
AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
|
||||||
OpenAICodexUserAgent: updatedSettings.OpenAICodexUserAgent,
|
OpenAICodexUserAgent: updatedSettings.OpenAICodexUserAgent,
|
||||||
|
OpenAIAllowClaudeCodeCodexPlugin: updatedSettings.OpenAIAllowClaudeCodeCodexPlugin,
|
||||||
PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
|
PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
|
||||||
PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
|
PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
|
||||||
PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled,
|
PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled,
|
||||||
@ -2500,6 +2509,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
|||||||
if before.OpenAICodexUserAgent != after.OpenAICodexUserAgent {
|
if before.OpenAICodexUserAgent != after.OpenAICodexUserAgent {
|
||||||
changed = append(changed, "openai_codex_user_agent")
|
changed = append(changed, "openai_codex_user_agent")
|
||||||
}
|
}
|
||||||
|
if before.OpenAIAllowClaudeCodeCodexPlugin != after.OpenAIAllowClaudeCodeCodexPlugin {
|
||||||
|
changed = append(changed, "openai_allow_claude_code_codex_plugin")
|
||||||
|
}
|
||||||
if before.PaymentVisibleMethodAlipaySource != after.PaymentVisibleMethodAlipaySource {
|
if before.PaymentVisibleMethodAlipaySource != after.PaymentVisibleMethodAlipaySource {
|
||||||
changed = append(changed, "payment_visible_method_alipay_source")
|
changed = append(changed, "payment_visible_method_alipay_source")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -185,6 +185,7 @@ type SystemSettings struct {
|
|||||||
RewriteMessageCacheControl bool `json:"rewrite_message_cache_control"`
|
RewriteMessageCacheControl bool `json:"rewrite_message_cache_control"`
|
||||||
AntigravityUserAgentVersion string `json:"antigravity_user_agent_version"`
|
AntigravityUserAgentVersion string `json:"antigravity_user_agent_version"`
|
||||||
OpenAICodexUserAgent string `json:"openai_codex_user_agent"`
|
OpenAICodexUserAgent string `json:"openai_codex_user_agent"`
|
||||||
|
OpenAIAllowClaudeCodeCodexPlugin bool `json:"openai_allow_claude_code_codex_plugin"`
|
||||||
|
|
||||||
// Web Search Emulation
|
// Web Search Emulation
|
||||||
WebSearchEmulationEnabled bool `json:"web_search_emulation_enabled"`
|
WebSearchEmulationEnabled bool `json:"web_search_emulation_enabled"`
|
||||||
|
|||||||
78
backend/internal/pkg/openai/allowed_client.go
Normal file
78
backend/internal/pkg/openai/allowed_client.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package openai
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// 命名预设 ID。账号侧 codex_cli_only_allowed_clients 只能引用这些预设键,
|
||||||
|
// 具体匹配规则固化在下方 registry 中,配置只能「选择启用哪些预设」、不能自定义规则,
|
||||||
|
// 以防该白名单退化为可任意放宽的后门。
|
||||||
|
const (
|
||||||
|
// AllowedClientClaudeCode 对应 Claude Code CLI 的 codex 插件。
|
||||||
|
AllowedClientClaudeCode = "claude_code"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllowedClientEntry 描述一个被额外放行的非官方 Codex 客户端签名。
|
||||||
|
// Originator 必须精确等值匹配(归一化后)。
|
||||||
|
// UAContains 为必填字段:列表为空,或列表中存在任何空白 marker,均视为非法配置,
|
||||||
|
// 整体安全失败(return false);每一项都必须出现在 User-Agent 中。
|
||||||
|
// 这确保双因子匹配不会因缺失 UA 声明而退化为仅凭可伪造的 originator 单因子放行。
|
||||||
|
type AllowedClientEntry struct {
|
||||||
|
Originator string
|
||||||
|
UAContains []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowedClientRegistry 固化各命名预设的签名规则。
|
||||||
|
//
|
||||||
|
// Claude Code codex 插件签名来源:插件以 clientInfo.name="Claude Code" 完成 app-server
|
||||||
|
// initialize 握手,codex 据此把 originator 设为 "Claude Code",User-Agent 前缀同样为
|
||||||
|
// "Claude Code/"(两者同源)。若上游 Claude Code 插件更改 clientInfo.name,此处需同步更新。
|
||||||
|
var allowedClientRegistry = map[string]AllowedClientEntry{
|
||||||
|
AllowedClientClaudeCode: {
|
||||||
|
Originator: "Claude Code",
|
||||||
|
UAContains: []string{"Claude Code/"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAllowedClientMatch 判断请求头是否命中给定的额外客户端签名。
|
||||||
|
// originator 必须精确等值(归一化后);UAContains 中每一项都必须出现在 UA 中。
|
||||||
|
// UAContains 为必填:列表为空或含任何空白 marker 均视为非法配置,整体安全失败。
|
||||||
|
func IsAllowedClientMatch(userAgent, originator string, entry AllowedClientEntry) bool {
|
||||||
|
wantOriginator := normalizeCodexClientHeader(entry.Originator)
|
||||||
|
if wantOriginator == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if normalizeCodexClientHeader(originator) != wantOriginator {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 预设必须声明 UA 特征:否则将退化为仅凭可伪造的 originator 单因子匹配。
|
||||||
|
if len(entry.UAContains) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ua := normalizeCodexClientHeader(userAgent)
|
||||||
|
for _, marker := range entry.UAContains {
|
||||||
|
normalizedMarker := normalizeCodexClientHeader(marker)
|
||||||
|
if normalizedMarker == "" {
|
||||||
|
// 空白 marker 让该项失去校验能力,会让双因子退化为仅 originator
|
||||||
|
// 单因子;视为非法配置,安全失败。
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.Contains(ua, normalizedMarker) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchAllowedClients 判断请求头是否命中 clientIDs 引用的任一预设签名。
|
||||||
|
// 未知预设 ID 会被忽略;空列表恒不放行(默认拒绝)。
|
||||||
|
func MatchAllowedClients(userAgent, originator string, clientIDs []string) bool {
|
||||||
|
for _, id := range clientIDs {
|
||||||
|
entry, ok := allowedClientRegistry[normalizeCodexClientHeader(id)]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if IsAllowedClientMatch(userAgent, originator, entry) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
95
backend/internal/pkg/openai/allowed_client_test.go
Normal file
95
backend/internal/pkg/openai/allowed_client_test.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package openai
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// 真实的 Claude Code codex 插件请求头:originator 与 UA 前缀同源于 clientInfo.name="Claude Code"。
|
||||||
|
const (
|
||||||
|
testClaudeCodeOriginator = "Claude Code"
|
||||||
|
testClaudeCodeUserAgent = "Claude Code/0.5.0 (Macos 15.5; arm64) iTerm2.app (Claude Code; 1.0.4)"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsAllowedClientMatch(t *testing.T) {
|
||||||
|
entry := AllowedClientEntry{Originator: "Claude Code", UAContains: []string{"Claude Code/"}}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ua string
|
||||||
|
originator string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "真实签名命中", ua: testClaudeCodeUserAgent, originator: testClaudeCodeOriginator, want: true},
|
||||||
|
{name: "大小写不敏感", ua: "claude code/0.5.0 (macos)", originator: "claude code", want: true},
|
||||||
|
{name: "originator 两侧空白被裁剪", ua: testClaudeCodeUserAgent, originator: " Claude Code ", want: true},
|
||||||
|
{name: "originator 非精确(带后缀)不命中", ua: testClaudeCodeUserAgent, originator: "Claude Code Extra", want: false},
|
||||||
|
{name: "originator 为空不命中", ua: testClaudeCodeUserAgent, originator: "", want: false},
|
||||||
|
{name: "originator 是官方 codex 不命中", ua: testClaudeCodeUserAgent, originator: "codex_cli_rs", want: false},
|
||||||
|
{name: "UA 缺少 Claude Code/ 标记不命中", ua: "curl/8.0", originator: testClaudeCodeOriginator, want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := IsAllowedClientMatch(tt.ua, tt.originator, entry); got != tt.want {
|
||||||
|
t.Fatalf("IsAllowedClientMatch(%q, %q) = %v, want %v", tt.ua, tt.originator, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsAllowedClientMatch_EmptyOriginatorEntryNeverMatches(t *testing.T) {
|
||||||
|
// registry 条目若没有配置 Originator,绝不放行,避免成为宽松后门。
|
||||||
|
entry := AllowedClientEntry{Originator: "", UAContains: []string{"Claude Code/"}}
|
||||||
|
if IsAllowedClientMatch(testClaudeCodeUserAgent, "", entry) {
|
||||||
|
t.Fatal("空 Originator 的条目不应匹配任何请求")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsAllowedClientMatch_EmptyUAContainsNeverMatches(t *testing.T) {
|
||||||
|
// 预设必须声明 UA 特征,否则退化为仅凭可伪造的 originator 单因子匹配,绝不放行。
|
||||||
|
entry := AllowedClientEntry{Originator: "Claude Code", UAContains: nil}
|
||||||
|
if IsAllowedClientMatch(testClaudeCodeUserAgent, testClaudeCodeOriginator, entry) {
|
||||||
|
t.Fatal("未声明 UA 特征的预设不应匹配,避免退化为单因子 originator 匹配")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsAllowedClientMatch_WhitespaceUAMarkerNeverMatches(t *testing.T) {
|
||||||
|
// 全空白 marker 归一化后为空,若被跳过则退化为仅 originator 单因子;
|
||||||
|
// 任何空白 marker 视为非法预设配置,必须安全失败。
|
||||||
|
entry := AllowedClientEntry{Originator: "Claude Code", UAContains: []string{" "}}
|
||||||
|
if IsAllowedClientMatch(testClaudeCodeUserAgent, testClaudeCodeOriginator, entry) {
|
||||||
|
t.Fatal("UAContains 含全空白 marker 不应匹配,避免退化为单因子 originator 匹配")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsAllowedClientMatch_MixedEmptyUAMarkerNeverMatches(t *testing.T) {
|
||||||
|
// 即便 UAContains 含一个真实 marker,只要其中混入任何空白 marker 也视为非法配置;
|
||||||
|
// 防止维护者只为对齐凑数而插入空字符串。
|
||||||
|
entry := AllowedClientEntry{Originator: "Claude Code", UAContains: []string{"", "Claude Code/"}}
|
||||||
|
if IsAllowedClientMatch(testClaudeCodeUserAgent, testClaudeCodeOriginator, entry) {
|
||||||
|
t.Fatal("UAContains 混入空白 marker 不应匹配")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchAllowedClients(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ua string
|
||||||
|
originator string
|
||||||
|
clientIDs []string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "claude_code 预设命中真实签名", ua: testClaudeCodeUserAgent, originator: testClaudeCodeOriginator, clientIDs: []string{AllowedClientClaudeCode}, want: true},
|
||||||
|
{name: "claude_code 预设 + 伪造 originator 不命中", ua: testClaudeCodeUserAgent, originator: "my_client", clientIDs: []string{AllowedClientClaudeCode}, want: false},
|
||||||
|
{name: "空列表不放行", ua: testClaudeCodeUserAgent, originator: testClaudeCodeOriginator, clientIDs: nil, want: false},
|
||||||
|
{name: "未知预设 ID 不放行", ua: testClaudeCodeUserAgent, originator: testClaudeCodeOriginator, clientIDs: []string{"unknown_client"}, want: false},
|
||||||
|
{name: "ID 大小写/空白容错", ua: testClaudeCodeUserAgent, originator: testClaudeCodeOriginator, clientIDs: []string{" Claude_Code "}, want: true},
|
||||||
|
{name: "多预设任一命中即放行", ua: testClaudeCodeUserAgent, originator: testClaudeCodeOriginator, clientIDs: []string{"unknown_client", AllowedClientClaudeCode}, want: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := MatchAllowedClients(tt.ua, tt.originator, tt.clientIDs); got != tt.want {
|
||||||
|
t.Fatalf("MatchAllowedClients(%q, %q, %v) = %v, want %v", tt.ua, tt.originator, tt.clientIDs, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -843,6 +843,7 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
"payment_visible_method_wxpay_enabled": false,
|
"payment_visible_method_wxpay_enabled": false,
|
||||||
"openai_advanced_scheduler_enabled": true,
|
"openai_advanced_scheduler_enabled": true,
|
||||||
"openai_codex_user_agent": "",
|
"openai_codex_user_agent": "",
|
||||||
|
"openai_allow_claude_code_codex_plugin": false,
|
||||||
"openai_fast_policy_settings": {
|
"openai_fast_policy_settings": {
|
||||||
"rules": []
|
"rules": []
|
||||||
},
|
},
|
||||||
@ -1079,6 +1080,7 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
"payment_visible_method_wxpay_enabled": false,
|
"payment_visible_method_wxpay_enabled": false,
|
||||||
"openai_advanced_scheduler_enabled": false,
|
"openai_advanced_scheduler_enabled": false,
|
||||||
"openai_codex_user_agent": "",
|
"openai_codex_user_agent": "",
|
||||||
|
"openai_allow_claude_code_codex_plugin": false,
|
||||||
"openai_fast_policy_settings": {
|
"openai_fast_policy_settings": {
|
||||||
"rules": []
|
"rules": []
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1525,6 +1525,38 @@ func (a *Account) IsCodexCLIOnlyEnabled() bool {
|
|||||||
return ok && enabled
|
return ok && enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCodexCLIOnlyAllowedClients 返回 codex_cli_only 之上额外放行的命名客户端预设 ID 列表。
|
||||||
|
// 仅 OpenAI OAuth 账号生效;缺失或类型不符时返回空。预设 ID 的具体匹配规则由
|
||||||
|
// openai 包的 registry 固化,配置只能引用预设键、不能自定义规则。
|
||||||
|
func (a *Account) GetCodexCLIOnlyAllowedClients() []string {
|
||||||
|
if a == nil || !a.IsOpenAIOAuth() || a.Extra == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw, ok := a.Extra["codex_cli_only_allowed_clients"]
|
||||||
|
if !ok || raw == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case []string:
|
||||||
|
result := make([]string, 0, len(v))
|
||||||
|
for _, s := range v {
|
||||||
|
if strings.TrimSpace(s) != "" {
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
case []any:
|
||||||
|
result := make([]string, 0, len(v))
|
||||||
|
for _, item := range v {
|
||||||
|
if s, ok := item.(string); ok && strings.TrimSpace(s) != "" {
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// WindowCostSchedulability 窗口费用调度状态
|
// WindowCostSchedulability 窗口费用调度状态
|
||||||
type WindowCostSchedulability int
|
type WindowCostSchedulability int
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccount_GetCodexCLIOnlyAllowedClients(t *testing.T) {
|
||||||
|
t.Run("OAuth 账号读取 []any 字符串列表", func(t *testing.T) {
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{"codex_cli_only_allowed_clients": []any{"claude_code"}},
|
||||||
|
}
|
||||||
|
require.Equal(t, []string{"claude_code"}, account.GetCodexCLIOnlyAllowedClients())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OAuth 账号读取 []string 列表", func(t *testing.T) {
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{"codex_cli_only_allowed_clients": []string{"claude_code"}},
|
||||||
|
}
|
||||||
|
require.Equal(t, []string{"claude_code"}, account.GetCodexCLIOnlyAllowedClients())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("[]string 跳过空白元素", func(t *testing.T) {
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{"codex_cli_only_allowed_clients": []string{"claude_code", "", " "}},
|
||||||
|
}
|
||||||
|
require.Equal(t, []string{"claude_code"}, account.GetCodexCLIOnlyAllowedClients())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("跳过非字符串与空白元素", func(t *testing.T) {
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{"codex_cli_only_allowed_clients": []any{"claude_code", 123, "", " "}},
|
||||||
|
}
|
||||||
|
require.Equal(t, []string{"claude_code"}, account.GetCodexCLIOnlyAllowedClients())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("非 OAuth 账号返回空", func(t *testing.T) {
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeAPIKey,
|
||||||
|
Extra: map[string]any{"codex_cli_only_allowed_clients": []any{"claude_code"}},
|
||||||
|
}
|
||||||
|
require.Empty(t, account.GetCodexCLIOnlyAllowedClients())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Extra 为空返回空", func(t *testing.T) {
|
||||||
|
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}
|
||||||
|
require.Empty(t, account.GetCodexCLIOnlyAllowedClients())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("字段缺失返回空", func(t *testing.T) {
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{},
|
||||||
|
}
|
||||||
|
require.Empty(t, account.GetCodexCLIOnlyAllowedClients())
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -431,6 +431,9 @@ const (
|
|||||||
// 当客户端 UA 被识别为浏览器(Chrome/Firefox/Safari/Edge 等)时,转发给 OpenAI 上游前会替换为此值,
|
// 当客户端 UA 被识别为浏览器(Chrome/Firefox/Safari/Edge 等)时,转发给 OpenAI 上游前会替换为此值,
|
||||||
// 用于避免 Cloudflare 对浏览器型 UA 的质询拦截。
|
// 用于避免 Cloudflare 对浏览器型 UA 的质询拦截。
|
||||||
SettingKeyOpenAICodexUserAgent = "openai_codex_user_agent"
|
SettingKeyOpenAICodexUserAgent = "openai_codex_user_agent"
|
||||||
|
// SettingKeyOpenAIAllowClaudeCodeCodexPlugin 全局开关:是否额外放行 Claude Code 的 Codex 插件(默认 false)。
|
||||||
|
// 仅在账号 codex_cli_only 开启时生效;开启后无需逐账号配置 codex_cli_only_allowed_clients。
|
||||||
|
SettingKeyOpenAIAllowClaudeCodeCodexPlugin = "openai_allow_claude_code_codex_plugin"
|
||||||
|
|
||||||
// 余额不足提醒
|
// 余额不足提醒
|
||||||
SettingKeyBalanceLowNotifyEnabled = "balance_low_notify_enabled" // 全局开关
|
SettingKeyBalanceLowNotifyEnabled = "balance_low_notify_enabled" // 全局开关
|
||||||
|
|||||||
@ -13,6 +13,10 @@ const (
|
|||||||
CodexClientRestrictionReasonMatchedUA = "official_client_user_agent_matched"
|
CodexClientRestrictionReasonMatchedUA = "official_client_user_agent_matched"
|
||||||
// CodexClientRestrictionReasonMatchedOriginator 表示请求命中官方客户端 originator 白名单。
|
// CodexClientRestrictionReasonMatchedOriginator 表示请求命中官方客户端 originator 白名单。
|
||||||
CodexClientRestrictionReasonMatchedOriginator = "official_client_originator_matched"
|
CodexClientRestrictionReasonMatchedOriginator = "official_client_originator_matched"
|
||||||
|
// CodexClientRestrictionReasonMatchedAllowedClient 表示请求命中账号级额外放行的命名客户端预设。
|
||||||
|
CodexClientRestrictionReasonMatchedAllowedClient = "allowed_client_matched"
|
||||||
|
// CodexClientRestrictionReasonMatchedGlobalAllowedClient 表示请求命中全局额外放行的命名客户端预设。
|
||||||
|
CodexClientRestrictionReasonMatchedGlobalAllowedClient = "global_allowed_client_matched"
|
||||||
// CodexClientRestrictionReasonNotMatchedUA 表示请求未命中官方客户端 UA 白名单。
|
// CodexClientRestrictionReasonNotMatchedUA 表示请求未命中官方客户端 UA 白名单。
|
||||||
CodexClientRestrictionReasonNotMatchedUA = "official_client_user_agent_not_matched"
|
CodexClientRestrictionReasonNotMatchedUA = "official_client_user_agent_not_matched"
|
||||||
// CodexClientRestrictionReasonForceCodexCLI 表示通过 ForceCodexCLI 配置兜底放行。
|
// CodexClientRestrictionReasonForceCodexCLI 表示通过 ForceCodexCLI 配置兜底放行。
|
||||||
@ -28,7 +32,7 @@ type CodexClientRestrictionDetectionResult struct {
|
|||||||
|
|
||||||
// CodexClientRestrictionDetector 定义 codex_cli_only 统一检测入口。
|
// CodexClientRestrictionDetector 定义 codex_cli_only 统一检测入口。
|
||||||
type CodexClientRestrictionDetector interface {
|
type CodexClientRestrictionDetector interface {
|
||||||
Detect(c *gin.Context, account *Account) CodexClientRestrictionDetectionResult
|
Detect(c *gin.Context, account *Account, globalAllowedClients []string) CodexClientRestrictionDetectionResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenAICodexClientRestrictionDetector 为 OpenAI OAuth codex_cli_only 的默认实现。
|
// OpenAICodexClientRestrictionDetector 为 OpenAI OAuth codex_cli_only 的默认实现。
|
||||||
@ -40,7 +44,7 @@ func NewOpenAICodexClientRestrictionDetector(cfg *config.Config) *OpenAICodexCli
|
|||||||
return &OpenAICodexClientRestrictionDetector{cfg: cfg}
|
return &OpenAICodexClientRestrictionDetector{cfg: cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *OpenAICodexClientRestrictionDetector) Detect(c *gin.Context, account *Account) CodexClientRestrictionDetectionResult {
|
func (d *OpenAICodexClientRestrictionDetector) Detect(c *gin.Context, account *Account, globalAllowedClients []string) CodexClientRestrictionDetectionResult {
|
||||||
if account == nil || !account.IsCodexCLIOnlyEnabled() {
|
if account == nil || !account.IsCodexCLIOnlyEnabled() {
|
||||||
return CodexClientRestrictionDetectionResult{
|
return CodexClientRestrictionDetectionResult{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
@ -78,6 +82,26 @@ func (d *OpenAICodexClientRestrictionDetector) Detect(c *gin.Context, account *A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 官方客户端白名单未命中时,先尝试账号级额外放行的命名客户端预设(如 Claude Code codex 插件)。
|
||||||
|
if allowed := account.GetCodexCLIOnlyAllowedClients(); len(allowed) > 0 &&
|
||||||
|
openai.MatchAllowedClients(userAgent, originator, allowed) {
|
||||||
|
return CodexClientRestrictionDetectionResult{
|
||||||
|
Enabled: true,
|
||||||
|
Matched: true,
|
||||||
|
Reason: CodexClientRestrictionReasonMatchedAllowedClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再尝试由更高作用域(全局设置)注入的额外放行客户端列表。
|
||||||
|
if len(globalAllowedClients) > 0 &&
|
||||||
|
openai.MatchAllowedClients(userAgent, originator, globalAllowedClients) {
|
||||||
|
return CodexClientRestrictionDetectionResult{
|
||||||
|
Enabled: true,
|
||||||
|
Matched: true,
|
||||||
|
Reason: CodexClientRestrictionReasonMatchedGlobalAllowedClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return CodexClientRestrictionDetectionResult{
|
return CodexClientRestrictionDetectionResult{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Matched: false,
|
Matched: false,
|
||||||
|
|||||||
@ -30,7 +30,7 @@ func TestOpenAICodexClientRestrictionDetector_Detect(t *testing.T) {
|
|||||||
detector := NewOpenAICodexClientRestrictionDetector(nil)
|
detector := NewOpenAICodexClientRestrictionDetector(nil)
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: map[string]any{}}
|
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: map[string]any{}}
|
||||||
|
|
||||||
result := detector.Detect(newCodexDetectorTestContext("curl/8.0", ""), account)
|
result := detector.Detect(newCodexDetectorTestContext("curl/8.0", ""), account, nil)
|
||||||
require.False(t, result.Enabled)
|
require.False(t, result.Enabled)
|
||||||
require.False(t, result.Matched)
|
require.False(t, result.Matched)
|
||||||
require.Equal(t, CodexClientRestrictionReasonDisabled, result.Reason)
|
require.Equal(t, CodexClientRestrictionReasonDisabled, result.Reason)
|
||||||
@ -44,7 +44,7 @@ func TestOpenAICodexClientRestrictionDetector_Detect(t *testing.T) {
|
|||||||
Extra: map[string]any{"codex_cli_only": true},
|
Extra: map[string]any{"codex_cli_only": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := detector.Detect(newCodexDetectorTestContext("codex_cli_rs/0.99.0", ""), account)
|
result := detector.Detect(newCodexDetectorTestContext("codex_cli_rs/0.99.0", ""), account, nil)
|
||||||
require.True(t, result.Enabled)
|
require.True(t, result.Enabled)
|
||||||
require.True(t, result.Matched)
|
require.True(t, result.Matched)
|
||||||
require.Equal(t, CodexClientRestrictionReasonMatchedUA, result.Reason)
|
require.Equal(t, CodexClientRestrictionReasonMatchedUA, result.Reason)
|
||||||
@ -58,7 +58,7 @@ func TestOpenAICodexClientRestrictionDetector_Detect(t *testing.T) {
|
|||||||
Extra: map[string]any{"codex_cli_only": true},
|
Extra: map[string]any{"codex_cli_only": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := detector.Detect(newCodexDetectorTestContext("codex_vscode/1.0.0", ""), account)
|
result := detector.Detect(newCodexDetectorTestContext("codex_vscode/1.0.0", ""), account, nil)
|
||||||
require.True(t, result.Enabled)
|
require.True(t, result.Enabled)
|
||||||
require.True(t, result.Matched)
|
require.True(t, result.Matched)
|
||||||
require.Equal(t, CodexClientRestrictionReasonMatchedUA, result.Reason)
|
require.Equal(t, CodexClientRestrictionReasonMatchedUA, result.Reason)
|
||||||
@ -72,7 +72,7 @@ func TestOpenAICodexClientRestrictionDetector_Detect(t *testing.T) {
|
|||||||
Extra: map[string]any{"codex_cli_only": true},
|
Extra: map[string]any{"codex_cli_only": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := detector.Detect(newCodexDetectorTestContext("codex_app/2.1.0", ""), account)
|
result := detector.Detect(newCodexDetectorTestContext("codex_app/2.1.0", ""), account, nil)
|
||||||
require.True(t, result.Enabled)
|
require.True(t, result.Enabled)
|
||||||
require.True(t, result.Matched)
|
require.True(t, result.Matched)
|
||||||
require.Equal(t, CodexClientRestrictionReasonMatchedUA, result.Reason)
|
require.Equal(t, CodexClientRestrictionReasonMatchedUA, result.Reason)
|
||||||
@ -86,7 +86,7 @@ func TestOpenAICodexClientRestrictionDetector_Detect(t *testing.T) {
|
|||||||
Extra: map[string]any{"codex_cli_only": true},
|
Extra: map[string]any{"codex_cli_only": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := detector.Detect(newCodexDetectorTestContext("curl/8.0", "codex_chatgpt_desktop"), account)
|
result := detector.Detect(newCodexDetectorTestContext("curl/8.0", "codex_chatgpt_desktop"), account, nil)
|
||||||
require.True(t, result.Enabled)
|
require.True(t, result.Enabled)
|
||||||
require.True(t, result.Matched)
|
require.True(t, result.Matched)
|
||||||
require.Equal(t, CodexClientRestrictionReasonMatchedOriginator, result.Reason)
|
require.Equal(t, CodexClientRestrictionReasonMatchedOriginator, result.Reason)
|
||||||
@ -100,7 +100,7 @@ func TestOpenAICodexClientRestrictionDetector_Detect(t *testing.T) {
|
|||||||
Extra: map[string]any{"codex_cli_only": true},
|
Extra: map[string]any{"codex_cli_only": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := detector.Detect(newCodexDetectorTestContext("curl/8.0", "my_client"), account)
|
result := detector.Detect(newCodexDetectorTestContext("curl/8.0", "my_client"), account, nil)
|
||||||
require.True(t, result.Enabled)
|
require.True(t, result.Enabled)
|
||||||
require.False(t, result.Matched)
|
require.False(t, result.Matched)
|
||||||
require.Equal(t, CodexClientRestrictionReasonNotMatchedUA, result.Reason)
|
require.Equal(t, CodexClientRestrictionReasonNotMatchedUA, result.Reason)
|
||||||
@ -116,9 +116,146 @@ func TestOpenAICodexClientRestrictionDetector_Detect(t *testing.T) {
|
|||||||
Extra: map[string]any{"codex_cli_only": true},
|
Extra: map[string]any{"codex_cli_only": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := detector.Detect(newCodexDetectorTestContext("curl/8.0", "my_client"), account)
|
result := detector.Detect(newCodexDetectorTestContext("curl/8.0", "my_client"), account, nil)
|
||||||
require.True(t, result.Enabled)
|
require.True(t, result.Enabled)
|
||||||
require.True(t, result.Matched)
|
require.True(t, result.Matched)
|
||||||
require.Equal(t, CodexClientRestrictionReasonForceCodexCLI, result.Reason)
|
require.Equal(t, CodexClientRestrictionReasonForceCodexCLI, result.Reason)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenAICodexClientRestrictionDetector_Detect_AllowedClients(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
const (
|
||||||
|
claudeCodeUA = "Claude Code/0.5.0 (Macos 15.5; arm64) iTerm2.app (Claude Code; 1.0.4)"
|
||||||
|
claudeCodeOriginator = "Claude Code"
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("配置 claude_code 白名单且命中真实签名时放行", func(t *testing.T) {
|
||||||
|
detector := NewOpenAICodexClientRestrictionDetector(nil)
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{
|
||||||
|
"codex_cli_only": true,
|
||||||
|
"codex_cli_only_allowed_clients": []any{"claude_code"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := detector.Detect(newCodexDetectorTestContext(claudeCodeUA, claudeCodeOriginator), account, nil)
|
||||||
|
require.True(t, result.Enabled)
|
||||||
|
require.True(t, result.Matched)
|
||||||
|
require.Equal(t, CodexClientRestrictionReasonMatchedAllowedClient, result.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("配置白名单但伪造 originator 仍拒绝", func(t *testing.T) {
|
||||||
|
detector := NewOpenAICodexClientRestrictionDetector(nil)
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{
|
||||||
|
"codex_cli_only": true,
|
||||||
|
"codex_cli_only_allowed_clients": []any{"claude_code"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := detector.Detect(newCodexDetectorTestContext(claudeCodeUA, "my_client"), account, nil)
|
||||||
|
require.True(t, result.Enabled)
|
||||||
|
require.False(t, result.Matched)
|
||||||
|
require.Equal(t, CodexClientRestrictionReasonNotMatchedUA, result.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("未配置白名单时 Claude Code 签名仍拒绝", func(t *testing.T) {
|
||||||
|
detector := NewOpenAICodexClientRestrictionDetector(nil)
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{"codex_cli_only": true},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := detector.Detect(newCodexDetectorTestContext(claudeCodeUA, claudeCodeOriginator), account, nil)
|
||||||
|
require.True(t, result.Enabled)
|
||||||
|
require.False(t, result.Matched)
|
||||||
|
require.Equal(t, CodexClientRestrictionReasonNotMatchedUA, result.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("未开启 codex_cli_only 时白名单不参与,直接绕过", func(t *testing.T) {
|
||||||
|
detector := NewOpenAICodexClientRestrictionDetector(nil)
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{"codex_cli_only_allowed_clients": []any{"claude_code"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := detector.Detect(newCodexDetectorTestContext(claudeCodeUA, claudeCodeOriginator), account, nil)
|
||||||
|
require.False(t, result.Enabled)
|
||||||
|
require.False(t, result.Matched)
|
||||||
|
require.Equal(t, CodexClientRestrictionReasonDisabled, result.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("全局列表含 claude_code + 命中签名 → 放行(global)", func(t *testing.T) {
|
||||||
|
detector := NewOpenAICodexClientRestrictionDetector(nil)
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{"codex_cli_only": true},
|
||||||
|
}
|
||||||
|
result := detector.Detect(
|
||||||
|
newCodexDetectorTestContext("Claude Code/0.5.0 (Macos 15.5; arm64) iTerm2.app (Claude Code; 1.0.4)", "Claude Code"),
|
||||||
|
account,
|
||||||
|
[]string{"claude_code"},
|
||||||
|
)
|
||||||
|
require.True(t, result.Enabled)
|
||||||
|
require.True(t, result.Matched)
|
||||||
|
require.Equal(t, CodexClientRestrictionReasonMatchedGlobalAllowedClient, result.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("全局列表含 claude_code + 非签名 → 403", func(t *testing.T) {
|
||||||
|
detector := NewOpenAICodexClientRestrictionDetector(nil)
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{"codex_cli_only": true},
|
||||||
|
}
|
||||||
|
result := detector.Detect(newCodexDetectorTestContext("curl/8.0", "my_client"), account, []string{"claude_code"})
|
||||||
|
require.True(t, result.Enabled)
|
||||||
|
require.False(t, result.Matched)
|
||||||
|
require.Equal(t, CodexClientRestrictionReasonNotMatchedUA, result.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("全局列表为空 + 账号未配 → 403", func(t *testing.T) {
|
||||||
|
detector := NewOpenAICodexClientRestrictionDetector(nil)
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{"codex_cli_only": true},
|
||||||
|
}
|
||||||
|
result := detector.Detect(
|
||||||
|
newCodexDetectorTestContext("Claude Code/0.5.0 (Macos) (Claude Code; 1.0.4)", "Claude Code"),
|
||||||
|
account,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.True(t, result.Enabled)
|
||||||
|
require.False(t, result.Matched)
|
||||||
|
require.Equal(t, CodexClientRestrictionReasonNotMatchedUA, result.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("账号白名单优先于全局列表(reason=account)", func(t *testing.T) {
|
||||||
|
detector := NewOpenAICodexClientRestrictionDetector(nil)
|
||||||
|
account := &Account{
|
||||||
|
Platform: PlatformOpenAI,
|
||||||
|
Type: AccountTypeOAuth,
|
||||||
|
Extra: map[string]any{
|
||||||
|
"codex_cli_only": true,
|
||||||
|
"codex_cli_only_allowed_clients": []any{"claude_code"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := detector.Detect(
|
||||||
|
newCodexDetectorTestContext("Claude Code/0.5.0 (Macos) (Claude Code; 1.0.4)", "Claude Code"),
|
||||||
|
account,
|
||||||
|
[]string{"claude_code"},
|
||||||
|
)
|
||||||
|
require.True(t, result.Matched)
|
||||||
|
require.Equal(t, CodexClientRestrictionReasonMatchedAllowedClient, result.Reason)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -901,7 +901,17 @@ func SnapshotOpenAICompatibilityFallbackMetrics() OpenAICompatibilityFallbackMet
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *OpenAIGatewayService) detectCodexClientRestriction(c *gin.Context, account *Account) CodexClientRestrictionDetectionResult {
|
func (s *OpenAIGatewayService) detectCodexClientRestriction(c *gin.Context, account *Account) CodexClientRestrictionDetectionResult {
|
||||||
return s.getCodexClientRestrictionDetector().Detect(c, account)
|
var globalAllowedClients []string
|
||||||
|
if account != nil && account.IsCodexCLIOnlyEnabled() && s != nil && s.settingService != nil {
|
||||||
|
ctx := context.Background()
|
||||||
|
if c != nil && c.Request != nil {
|
||||||
|
ctx = c.Request.Context()
|
||||||
|
}
|
||||||
|
if s.settingService.IsOpenAIAllowClaudeCodeCodexPluginEnabled(ctx) {
|
||||||
|
globalAllowedClients = []string{openai.AllowedClientClaudeCode}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.getCodexClientRestrictionDetector().Detect(c, account, globalAllowedClients)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAPIKeyIDFromContext(c *gin.Context) int64 {
|
func getAPIKeyIDFromContext(c *gin.Context) int64 {
|
||||||
@ -959,6 +969,7 @@ func logCodexCLIOnlyDetection(ctx context.Context, c *gin.Context, account *Acco
|
|||||||
}
|
}
|
||||||
log := logger.FromContext(ctx).With(fields...)
|
log := logger.FromContext(ctx).With(fields...)
|
||||||
if result.Matched {
|
if result.Matched {
|
||||||
|
log.Info("OpenAI codex_cli_only 放行请求")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Warn("OpenAI codex_cli_only 拒绝非官方客户端请求")
|
log.Warn("OpenAI codex_cli_only 拒绝非官方客户端请求")
|
||||||
|
|||||||
@ -18,7 +18,7 @@ type stubCodexRestrictionDetector struct {
|
|||||||
result CodexClientRestrictionDetectionResult
|
result CodexClientRestrictionDetectionResult
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubCodexRestrictionDetector) Detect(_ *gin.Context, _ *Account) CodexClientRestrictionDetectionResult {
|
func (s *stubCodexRestrictionDetector) Detect(_ *gin.Context, _ *Account, _ []string) CodexClientRestrictionDetectionResult {
|
||||||
return s.result
|
return s.result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ func TestOpenAIGatewayService_GetCodexClientRestrictionDetector(t *testing.T) {
|
|||||||
c.Request.Header.Set("User-Agent", "curl/8.0")
|
c.Request.Header.Set("User-Agent", "curl/8.0")
|
||||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: map[string]any{"codex_cli_only": true}}
|
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: map[string]any{"codex_cli_only": true}}
|
||||||
|
|
||||||
result := got.Detect(c, account)
|
result := got.Detect(c, account, nil)
|
||||||
require.True(t, result.Enabled)
|
require.True(t, result.Enabled)
|
||||||
require.True(t, result.Matched)
|
require.True(t, result.Matched)
|
||||||
require.Equal(t, CodexClientRestrictionReasonForceCodexCLI, result.Reason)
|
require.Equal(t, CodexClientRestrictionReasonForceCodexCLI, result.Reason)
|
||||||
|
|||||||
@ -141,6 +141,17 @@ const openAICodexUserAgentCacheTTL = 60 * time.Second
|
|||||||
const openAICodexUserAgentErrorTTL = 5 * time.Second
|
const openAICodexUserAgentErrorTTL = 5 * time.Second
|
||||||
const openAICodexUserAgentDBTimeout = 5 * time.Second
|
const openAICodexUserAgentDBTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
// cachedOpenAIAllowCodexPlugin Codex 插件放行开关缓存(进程内缓存,60s TTL)。
|
||||||
|
// IsOpenAIAllowClaudeCodeCodexPluginEnabled 在每个 codex_cli_only 账号的网关请求热路径上被调用,避免每次访问 DB。
|
||||||
|
type cachedOpenAIAllowCodexPlugin struct {
|
||||||
|
value bool
|
||||||
|
expiresAt int64 // unix nano
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAIAllowCodexPluginCacheTTL = 60 * time.Second
|
||||||
|
const openAIAllowCodexPluginErrorTTL = 5 * time.Second
|
||||||
|
const openAIAllowCodexPluginDBTimeout = 5 * time.Second
|
||||||
|
|
||||||
// DefaultSubscriptionGroupReader validates group references used by default subscriptions.
|
// DefaultSubscriptionGroupReader validates group references used by default subscriptions.
|
||||||
type DefaultSubscriptionGroupReader interface {
|
type DefaultSubscriptionGroupReader interface {
|
||||||
GetByID(ctx context.Context, id int64) (*Group, error)
|
GetByID(ctx context.Context, id int64) (*Group, error)
|
||||||
@ -152,17 +163,19 @@ type WebSearchManagerBuilder func(cfg *WebSearchEmulationConfig, proxyURLs map[i
|
|||||||
|
|
||||||
// SettingService 系统设置服务
|
// SettingService 系统设置服务
|
||||||
type SettingService struct {
|
type SettingService struct {
|
||||||
settingRepo SettingRepository
|
settingRepo SettingRepository
|
||||||
defaultSubGroupReader DefaultSubscriptionGroupReader
|
defaultSubGroupReader DefaultSubscriptionGroupReader
|
||||||
proxyRepo ProxyRepository // for resolving websearch provider proxy URLs
|
proxyRepo ProxyRepository // for resolving websearch provider proxy URLs
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
onUpdate func() // Callback when settings are updated (for cache invalidation)
|
onUpdate func() // Callback when settings are updated (for cache invalidation)
|
||||||
version string // Application version
|
version string // Application version
|
||||||
webSearchManagerBuilder WebSearchManagerBuilder
|
webSearchManagerBuilder WebSearchManagerBuilder
|
||||||
antigravityUAVersionCache atomic.Value // *cachedAntigravityUserAgentVersion
|
antigravityUAVersionCache atomic.Value // *cachedAntigravityUserAgentVersion
|
||||||
antigravityUAVersionSF singleflight.Group
|
antigravityUAVersionSF singleflight.Group
|
||||||
openAICodexUACache atomic.Value // *cachedOpenAICodexUserAgent
|
openAICodexUACache atomic.Value // *cachedOpenAICodexUserAgent
|
||||||
openAICodexUASF singleflight.Group
|
openAICodexUASF singleflight.Group
|
||||||
|
openAIAllowCodexPluginCache atomic.Value // *cachedOpenAIAllowCodexPlugin
|
||||||
|
openAIAllowCodexPluginSF singleflight.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultPlatformQuotaSetting 单 platform 三档限额(nil = 沿用上层;0 = 显式禁用;>0 = 上限)
|
// DefaultPlatformQuotaSetting 单 platform 三档限额(nil = 沿用上层;0 = 显式禁用;>0 = 上限)
|
||||||
@ -1015,6 +1028,54 @@ func (s *SettingService) GetOpenAICodexUserAgent(ctx context.Context) string {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsOpenAIAllowClaudeCodeCodexPluginEnabled 全局开关:是否额外放行 Claude Code 的 Codex 插件(默认关闭)。
|
||||||
|
// 仅在调用方已确认账号 codex_cli_only 开启时读取,避免对非受限账号产生无谓查询。
|
||||||
|
// 使用进程内 atomic.Value 缓存(60s TTL),避免在每个网关请求热路径上访问 DB。
|
||||||
|
func (s *SettingService) IsOpenAIAllowClaudeCodeCodexPluginEnabled(ctx context.Context) bool {
|
||||||
|
if cached, ok := s.openAIAllowCodexPluginCache.Load().(*cachedOpenAIAllowCodexPlugin); ok && cached != nil {
|
||||||
|
if time.Now().UnixNano() < cached.expiresAt {
|
||||||
|
return cached.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, _, _ := s.openAIAllowCodexPluginSF.Do("openai_allow_codex_plugin_enabled", func() (any, error) {
|
||||||
|
if cached, ok := s.openAIAllowCodexPluginCache.Load().(*cachedOpenAIAllowCodexPlugin); ok && cached != nil {
|
||||||
|
if time.Now().UnixNano() < cached.expiresAt {
|
||||||
|
return cached.value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), openAIAllowCodexPluginDBTimeout)
|
||||||
|
defer cancel()
|
||||||
|
value, err := s.settingRepo.GetValue(dbCtx, SettingKeyOpenAIAllowClaudeCodeCodexPlugin)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrSettingNotFound) {
|
||||||
|
// 设置不存在 → 默认关闭,正常 TTL 缓存
|
||||||
|
s.openAIAllowCodexPluginCache.Store(&cachedOpenAIAllowCodexPlugin{
|
||||||
|
value: false,
|
||||||
|
expiresAt: time.Now().Add(openAIAllowCodexPluginCacheTTL).UnixNano(),
|
||||||
|
})
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
slog.Warn("failed to get openai_allow_claude_code_codex_plugin setting", "error", err)
|
||||||
|
// DB 错误 → 安全默认关闭,短 TTL 快速重试
|
||||||
|
s.openAIAllowCodexPluginCache.Store(&cachedOpenAIAllowCodexPlugin{
|
||||||
|
value: false,
|
||||||
|
expiresAt: time.Now().Add(openAIAllowCodexPluginErrorTTL).UnixNano(),
|
||||||
|
})
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
enabled := value == "true"
|
||||||
|
s.openAIAllowCodexPluginCache.Store(&cachedOpenAIAllowCodexPlugin{
|
||||||
|
value: enabled,
|
||||||
|
expiresAt: time.Now().Add(openAIAllowCodexPluginCacheTTL).UnixNano(),
|
||||||
|
})
|
||||||
|
return enabled, nil
|
||||||
|
})
|
||||||
|
if val, ok := result.(bool); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// SetOnUpdateCallback sets a callback function to be called when settings are updated
|
// SetOnUpdateCallback sets a callback function to be called when settings are updated
|
||||||
// This is used for cache invalidation (e.g., HTML cache in frontend server)
|
// This is used for cache invalidation (e.g., HTML cache in frontend server)
|
||||||
func (s *SettingService) SetOnUpdateCallback(callback func()) {
|
func (s *SettingService) SetOnUpdateCallback(callback func()) {
|
||||||
@ -1816,6 +1877,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
|||||||
updates[SettingKeyRewriteMessageCacheControl] = strconv.FormatBool(settings.RewriteMessageCacheControl)
|
updates[SettingKeyRewriteMessageCacheControl] = strconv.FormatBool(settings.RewriteMessageCacheControl)
|
||||||
updates[SettingKeyAntigravityUserAgentVersion] = antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion)
|
updates[SettingKeyAntigravityUserAgentVersion] = antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion)
|
||||||
updates[SettingKeyOpenAICodexUserAgent] = strings.TrimSpace(settings.OpenAICodexUserAgent)
|
updates[SettingKeyOpenAICodexUserAgent] = strings.TrimSpace(settings.OpenAICodexUserAgent)
|
||||||
|
updates[SettingKeyOpenAIAllowClaudeCodeCodexPlugin] = strconv.FormatBool(settings.OpenAIAllowClaudeCodeCodexPlugin)
|
||||||
updates[SettingPaymentVisibleMethodAlipaySource] = settings.PaymentVisibleMethodAlipaySource
|
updates[SettingPaymentVisibleMethodAlipaySource] = settings.PaymentVisibleMethodAlipaySource
|
||||||
updates[SettingPaymentVisibleMethodWxpaySource] = settings.PaymentVisibleMethodWxpaySource
|
updates[SettingPaymentVisibleMethodWxpaySource] = settings.PaymentVisibleMethodWxpaySource
|
||||||
updates[SettingPaymentVisibleMethodAlipayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodAlipayEnabled)
|
updates[SettingPaymentVisibleMethodAlipayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodAlipayEnabled)
|
||||||
@ -1968,6 +2030,11 @@ func (s *SettingService) refreshCachedSettings(settings *SystemSettings) {
|
|||||||
if s.cfg != nil {
|
if s.cfg != nil {
|
||||||
s.cfg.SetTrustForwardedIPForAPIKeyACL(settings.APIKeyACLTrustForwardedIP)
|
s.cfg.SetTrustForwardedIPForAPIKeyACL(settings.APIKeyACLTrustForwardedIP)
|
||||||
}
|
}
|
||||||
|
s.openAIAllowCodexPluginSF.Forget("openai_allow_codex_plugin_enabled")
|
||||||
|
s.openAIAllowCodexPluginCache.Store(&cachedOpenAIAllowCodexPlugin{
|
||||||
|
value: settings.OpenAIAllowClaudeCodeCodexPlugin,
|
||||||
|
expiresAt: time.Now().Add(openAIAllowCodexPluginCacheTTL).UnixNano(),
|
||||||
|
})
|
||||||
if s.onUpdate != nil {
|
if s.onUpdate != nil {
|
||||||
s.onUpdate() // Invalidate cache after settings update
|
s.onUpdate() // Invalidate cache after settings update
|
||||||
}
|
}
|
||||||
@ -3233,6 +3300,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|||||||
}
|
}
|
||||||
result.AntigravityUserAgentVersion = antigravity.NormalizeUserAgentVersion(settings[SettingKeyAntigravityUserAgentVersion])
|
result.AntigravityUserAgentVersion = antigravity.NormalizeUserAgentVersion(settings[SettingKeyAntigravityUserAgentVersion])
|
||||||
result.OpenAICodexUserAgent = strings.TrimSpace(settings[SettingKeyOpenAICodexUserAgent])
|
result.OpenAICodexUserAgent = strings.TrimSpace(settings[SettingKeyOpenAICodexUserAgent])
|
||||||
|
result.OpenAIAllowClaudeCodeCodexPlugin = settings[SettingKeyOpenAIAllowClaudeCodeCodexPlugin] == "true"
|
||||||
|
|
||||||
// Web search emulation: quick enabled check from the JSON config
|
// Web search emulation: quick enabled check from the JSON config
|
||||||
if raw := settings[SettingKeyWebSearchEmulationConfig]; raw != "" {
|
if raw := settings[SettingKeyWebSearchEmulationConfig]; raw != "" {
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type allowClaudeCodeSettingRepoStub struct{ values map[string]string }
|
||||||
|
|
||||||
|
func (s *allowClaudeCodeSettingRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
func (s *allowClaudeCodeSettingRepoStub) GetValue(ctx context.Context, key string) (string, error) {
|
||||||
|
if v, ok := s.values[key]; ok {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
return "", ErrSettingNotFound
|
||||||
|
}
|
||||||
|
func (s *allowClaudeCodeSettingRepoStub) Set(ctx context.Context, key, value string) error {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
func (s *allowClaudeCodeSettingRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
func (s *allowClaudeCodeSettingRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
func (s *allowClaudeCodeSettingRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
func (s *allowClaudeCodeSettingRepoStub) Delete(ctx context.Context, key string) error {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSettingService_IsOpenAIAllowClaudeCodeCodexPluginEnabled(t *testing.T) {
|
||||||
|
t.Run("默认关闭(设置缺失)", func(t *testing.T) {
|
||||||
|
svc := NewSettingService(&allowClaudeCodeSettingRepoStub{values: map[string]string{}}, &config.Config{})
|
||||||
|
require.False(t, svc.IsOpenAIAllowClaudeCodeCodexPluginEnabled(context.Background()))
|
||||||
|
})
|
||||||
|
t.Run("值为 true 时开启", func(t *testing.T) {
|
||||||
|
svc := NewSettingService(&allowClaudeCodeSettingRepoStub{values: map[string]string{
|
||||||
|
SettingKeyOpenAIAllowClaudeCodeCodexPlugin: "true",
|
||||||
|
}}, &config.Config{})
|
||||||
|
require.True(t, svc.IsOpenAIAllowClaudeCodeCodexPluginEnabled(context.Background()))
|
||||||
|
})
|
||||||
|
t.Run("值非 true 时关闭", func(t *testing.T) {
|
||||||
|
svc := NewSettingService(&allowClaudeCodeSettingRepoStub{values: map[string]string{
|
||||||
|
SettingKeyOpenAIAllowClaudeCodeCodexPlugin: "false",
|
||||||
|
}}, &config.Config{})
|
||||||
|
require.False(t, svc.IsOpenAIAllowClaudeCodeCodexPluginEnabled(context.Background()))
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -195,6 +195,7 @@ type SystemSettings struct {
|
|||||||
RewriteMessageCacheControl bool // 是否改写 messages[*].content[*].cache_control(默认 false)
|
RewriteMessageCacheControl bool // 是否改写 messages[*].content[*].cache_control(默认 false)
|
||||||
AntigravityUserAgentVersion string // Antigravity 上游 User-Agent 版本号;空值使用配置/默认值
|
AntigravityUserAgentVersion string // Antigravity 上游 User-Agent 版本号;空值使用配置/默认值
|
||||||
OpenAICodexUserAgent string // OpenAI Codex 上游完整 User-Agent;空值使用内置默认
|
OpenAICodexUserAgent string // OpenAI Codex 上游完整 User-Agent;空值使用内置默认
|
||||||
|
OpenAIAllowClaudeCodeCodexPlugin bool // 全局开关:是否额外放行 Claude Code 的 Codex 插件(默认 false)
|
||||||
|
|
||||||
// Web Search Emulation
|
// Web Search Emulation
|
||||||
WebSearchEmulationEnabled bool // 是否启用 web search 模拟
|
WebSearchEmulationEnabled bool // 是否启用 web search 模拟
|
||||||
|
|||||||
@ -560,6 +560,7 @@ export interface SystemSettings {
|
|||||||
rewrite_message_cache_control: boolean;
|
rewrite_message_cache_control: boolean;
|
||||||
antigravity_user_agent_version: string;
|
antigravity_user_agent_version: string;
|
||||||
openai_codex_user_agent: string;
|
openai_codex_user_agent: string;
|
||||||
|
openai_allow_claude_code_codex_plugin: boolean;
|
||||||
web_search_emulation_enabled?: boolean;
|
web_search_emulation_enabled?: boolean;
|
||||||
|
|
||||||
// Payment configuration
|
// Payment configuration
|
||||||
@ -792,6 +793,7 @@ export interface UpdateSettingsRequest {
|
|||||||
rewrite_message_cache_control?: boolean;
|
rewrite_message_cache_control?: boolean;
|
||||||
antigravity_user_agent_version?: string;
|
antigravity_user_agent_version?: string;
|
||||||
openai_codex_user_agent?: string;
|
openai_codex_user_agent?: string;
|
||||||
|
openai_allow_claude_code_codex_plugin?: boolean;
|
||||||
// Payment configuration
|
// Payment configuration
|
||||||
payment_enabled?: boolean;
|
payment_enabled?: boolean;
|
||||||
risk_control_enabled?: boolean;
|
risk_control_enabled?: boolean;
|
||||||
|
|||||||
@ -742,6 +742,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- OpenAI OAuth: 额外放行 Claude Code 的 Codex 插件 -->
|
||||||
|
<div v-if="allOpenAIOAuth" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
id="bulk-edit-openai-codex-allow-claude-code-label"
|
||||||
|
class="input-label mb-0"
|
||||||
|
for="bulk-edit-openai-codex-allow-claude-code-enabled"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCode') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="enableCodexCLIOnlyAllowClaudeCode"
|
||||||
|
id="bulk-edit-openai-codex-allow-claude-code-enabled"
|
||||||
|
type="checkbox"
|
||||||
|
aria-controls="bulk-edit-openai-codex-allow-claude-code"
|
||||||
|
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="bulk-edit-openai-codex-allow-claude-code"
|
||||||
|
:class="!enableCodexCLIOnlyAllowClaudeCode && 'pointer-events-none opacity-50'"
|
||||||
|
>
|
||||||
|
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCodeDesc') }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
id="bulk-edit-openai-codex-allow-claude-code-toggle"
|
||||||
|
type="button"
|
||||||
|
:class="[
|
||||||
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
|
@click="codexCLIOnlyAllowClaudeCodeEnabled = !codexCLIOnlyAllowClaudeCodeEnabled"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- OpenAI API Key WS mode -->
|
<!-- OpenAI API Key WS mode -->
|
||||||
<div v-if="allOpenAIAPIKey" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
<div v-if="allOpenAIAPIKey" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
@ -1219,6 +1263,7 @@ const enableOpenAIPassthrough = ref(false)
|
|||||||
const enableOpenAIWSMode = ref(false)
|
const enableOpenAIWSMode = ref(false)
|
||||||
const enableOpenAIAPIKeyWSMode = ref(false)
|
const enableOpenAIAPIKeyWSMode = ref(false)
|
||||||
const enableCodexCLIOnly = ref(false)
|
const enableCodexCLIOnly = ref(false)
|
||||||
|
const enableCodexCLIOnlyAllowClaudeCode = ref(false)
|
||||||
const enableOpenAICompactMode = ref(false)
|
const enableOpenAICompactMode = ref(false)
|
||||||
const enableOpenAICompactModelMapping = ref(false)
|
const enableOpenAICompactModelMapping = ref(false)
|
||||||
const enableRpmLimit = ref(false)
|
const enableRpmLimit = ref(false)
|
||||||
@ -1246,6 +1291,7 @@ const openaiPassthroughEnabled = ref(false)
|
|||||||
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||||
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||||
const codexCLIOnlyEnabled = ref(false)
|
const codexCLIOnlyEnabled = ref(false)
|
||||||
|
const codexCLIOnlyAllowClaudeCodeEnabled = ref(false)
|
||||||
const openAICompactMode = ref<OpenAICompactMode>('auto')
|
const openAICompactMode = ref<OpenAICompactMode>('auto')
|
||||||
const openAICompactModelMappings = ref<ModelMapping[]>([])
|
const openAICompactModelMappings = ref<ModelMapping[]>([])
|
||||||
const rpmLimitEnabled = ref(false)
|
const rpmLimitEnabled = ref(false)
|
||||||
@ -1496,6 +1542,11 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
|||||||
extra.codex_cli_only = codexCLIOnlyEnabled.value
|
extra.codex_cli_only = codexCLIOnlyEnabled.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableCodexCLIOnlyAllowClaudeCode.value) {
|
||||||
|
const extra = ensureExtra()
|
||||||
|
extra.codex_cli_only_allowed_clients = codexCLIOnlyAllowClaudeCodeEnabled.value ? ['claude_code'] : []
|
||||||
|
}
|
||||||
|
|
||||||
if (enableOpenAICompactMode.value) {
|
if (enableOpenAICompactMode.value) {
|
||||||
const extra = ensureExtra()
|
const extra = ensureExtra()
|
||||||
extra.openai_compact_mode = openAICompactMode.value
|
extra.openai_compact_mode = openAICompactMode.value
|
||||||
@ -1602,6 +1653,7 @@ const handleSubmit = async () => {
|
|||||||
enableOpenAIWSMode.value ||
|
enableOpenAIWSMode.value ||
|
||||||
enableOpenAIAPIKeyWSMode.value ||
|
enableOpenAIAPIKeyWSMode.value ||
|
||||||
enableCodexCLIOnly.value ||
|
enableCodexCLIOnly.value ||
|
||||||
|
enableCodexCLIOnlyAllowClaudeCode.value ||
|
||||||
enableOpenAICompactMode.value ||
|
enableOpenAICompactMode.value ||
|
||||||
enableOpenAICompactModelMapping.value ||
|
enableOpenAICompactModelMapping.value ||
|
||||||
enableRpmLimit.value ||
|
enableRpmLimit.value ||
|
||||||
@ -1704,6 +1756,7 @@ watch(
|
|||||||
enableOpenAIWSMode.value = false
|
enableOpenAIWSMode.value = false
|
||||||
enableOpenAIAPIKeyWSMode.value = false
|
enableOpenAIAPIKeyWSMode.value = false
|
||||||
enableCodexCLIOnly.value = false
|
enableCodexCLIOnly.value = false
|
||||||
|
enableCodexCLIOnlyAllowClaudeCode.value = false
|
||||||
enableOpenAICompactMode.value = false
|
enableOpenAICompactMode.value = false
|
||||||
enableOpenAICompactModelMapping.value = false
|
enableOpenAICompactModelMapping.value = false
|
||||||
enableRpmLimit.value = false
|
enableRpmLimit.value = false
|
||||||
@ -1727,6 +1780,7 @@ watch(
|
|||||||
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
codexCLIOnlyEnabled.value = false
|
codexCLIOnlyEnabled.value = false
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled.value = false
|
||||||
openAICompactMode.value = 'auto'
|
openAICompactMode.value = 'auto'
|
||||||
openAICompactModelMappings.value = []
|
openAICompactModelMappings.value = []
|
||||||
rpmLimitEnabled.value = false
|
rpmLimitEnabled.value = false
|
||||||
|
|||||||
@ -2635,6 +2635,32 @@
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="codexCLIOnlyEnabled"
|
||||||
|
class="mt-4 flex items-center justify-between border-l-2 border-gray-200 pl-4 dark:border-dark-600"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCode') }}</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCodeDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="codexCLIOnlyAllowClaudeCodeEnabled = !codexCLIOnlyAllowClaudeCodeEnabled"
|
||||||
|
:class="[
|
||||||
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OpenAI Compact 能力配置 -->
|
<!-- OpenAI Compact 能力配置 -->
|
||||||
@ -3383,6 +3409,7 @@ const openAIEndpointCapabilities = ref<OpenAIEndpointCapability[]>(['chat_comple
|
|||||||
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||||
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||||
const codexCLIOnlyEnabled = ref(false)
|
const codexCLIOnlyEnabled = ref(false)
|
||||||
|
const codexCLIOnlyAllowClaudeCodeEnabled = ref(false)
|
||||||
const anthropicPassthroughEnabled = ref(false)
|
const anthropicPassthroughEnabled = ref(false)
|
||||||
const webSearchEmulationMode = ref('default')
|
const webSearchEmulationMode = ref('default')
|
||||||
const webSearchGlobalEnabled = ref(false)
|
const webSearchGlobalEnabled = ref(false)
|
||||||
@ -3807,6 +3834,7 @@ watch(
|
|||||||
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
codexCLIOnlyEnabled.value = false
|
codexCLIOnlyEnabled.value = false
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled.value = false
|
||||||
}
|
}
|
||||||
if (newPlatform !== 'anthropic') {
|
if (newPlatform !== 'anthropic') {
|
||||||
anthropicPassthroughEnabled.value = false
|
anthropicPassthroughEnabled.value = false
|
||||||
@ -3827,6 +3855,7 @@ watch(
|
|||||||
([category, platform]) => {
|
([category, platform]) => {
|
||||||
if (platform === 'openai' && category !== 'oauth-based') {
|
if (platform === 'openai' && category !== 'oauth-based') {
|
||||||
codexCLIOnlyEnabled.value = false
|
codexCLIOnlyEnabled.value = false
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled.value = false
|
||||||
}
|
}
|
||||||
if (platform !== 'anthropic' || category !== 'apikey') {
|
if (platform !== 'anthropic' || category !== 'apikey') {
|
||||||
anthropicPassthroughEnabled.value = false
|
anthropicPassthroughEnabled.value = false
|
||||||
@ -4207,6 +4236,7 @@ const resetForm = () => {
|
|||||||
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
codexCLIOnlyEnabled.value = false
|
codexCLIOnlyEnabled.value = false
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled.value = false
|
||||||
anthropicPassthroughEnabled.value = false
|
anthropicPassthroughEnabled.value = false
|
||||||
webSearchEmulationMode.value = 'default'
|
webSearchEmulationMode.value = 'default'
|
||||||
// Reset quota control state
|
// Reset quota control state
|
||||||
@ -4285,6 +4315,15 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
|
|||||||
} else {
|
} else {
|
||||||
delete extra.codex_cli_only
|
delete extra.codex_cli_only
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
accountCategory.value === 'oauth-based' &&
|
||||||
|
codexCLIOnlyEnabled.value &&
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled.value
|
||||||
|
) {
|
||||||
|
extra.codex_cli_only_allowed_clients = ['claude_code']
|
||||||
|
} else {
|
||||||
|
delete extra.codex_cli_only_allowed_clients
|
||||||
|
}
|
||||||
if (openAICompactMode.value !== 'auto') {
|
if (openAICompactMode.value !== 'auto') {
|
||||||
extra.openai_compact_mode = openAICompactMode.value
|
extra.openai_compact_mode = openAICompactMode.value
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1673,6 +1673,32 @@
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="codexCLIOnlyEnabled"
|
||||||
|
class="mt-4 flex items-center justify-between border-l-2 border-gray-200 pl-4 dark:border-dark-600"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label class="input-label mb-0">{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCode') }}</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.accounts.openai.codexCLIOnlyAllowClaudeCodeDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="codexCLIOnlyAllowClaudeCodeEnabled = !codexCLIOnlyAllowClaudeCodeEnabled"
|
||||||
|
:class="[
|
||||||
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -2476,6 +2502,7 @@ const openAIEndpointCapabilities = ref<OpenAIEndpointCapability[]>(['chat_comple
|
|||||||
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||||
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||||
const codexCLIOnlyEnabled = ref(false)
|
const codexCLIOnlyEnabled = ref(false)
|
||||||
|
const codexCLIOnlyAllowClaudeCodeEnabled = ref(false)
|
||||||
type CodexImageGenerationBridgeMode = 'inherit' | 'enabled' | 'disabled'
|
type CodexImageGenerationBridgeMode = 'inherit' | 'enabled' | 'disabled'
|
||||||
const codexImageGenerationBridgeMode = ref<CodexImageGenerationBridgeMode>('inherit')
|
const codexImageGenerationBridgeMode = ref<CodexImageGenerationBridgeMode>('inherit')
|
||||||
const anthropicPassthroughEnabled = ref(false)
|
const anthropicPassthroughEnabled = ref(false)
|
||||||
@ -2848,6 +2875,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
|||||||
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
codexCLIOnlyEnabled.value = false
|
codexCLIOnlyEnabled.value = false
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled.value = false
|
||||||
codexImageGenerationBridgeMode.value = 'inherit'
|
codexImageGenerationBridgeMode.value = 'inherit'
|
||||||
anthropicPassthroughEnabled.value = false
|
anthropicPassthroughEnabled.value = false
|
||||||
webSearchEmulationMode.value = 'default'
|
webSearchEmulationMode.value = 'default'
|
||||||
@ -2885,6 +2913,9 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
|||||||
})
|
})
|
||||||
if (newAccount.type === 'oauth') {
|
if (newAccount.type === 'oauth') {
|
||||||
codexCLIOnlyEnabled.value = extra?.codex_cli_only === true
|
codexCLIOnlyEnabled.value = extra?.codex_cli_only === true
|
||||||
|
codexCLIOnlyAllowClaudeCodeEnabled.value =
|
||||||
|
Array.isArray(extra?.codex_cli_only_allowed_clients) &&
|
||||||
|
(extra.codex_cli_only_allowed_clients as unknown[]).includes('claude_code')
|
||||||
}
|
}
|
||||||
const credentials = newAccount.credentials as Record<string, unknown> | undefined
|
const credentials = newAccount.credentials as Record<string, unknown> | undefined
|
||||||
const compactMappings = credentials?.compact_model_mapping as Record<string, string> | undefined
|
const compactMappings = credentials?.compact_model_mapping as Record<string, string> | undefined
|
||||||
@ -4004,6 +4035,12 @@ const handleSubmit = async () => {
|
|||||||
} else {
|
} else {
|
||||||
delete newExtra.codex_cli_only
|
delete newExtra.codex_cli_only
|
||||||
}
|
}
|
||||||
|
// 仅当 codex_cli_only 开启且子开关开启时写入 Claude Code 插件白名单,否则清除避免孤立字段
|
||||||
|
if (codexCLIOnlyEnabled.value && codexCLIOnlyAllowClaudeCodeEnabled.value) {
|
||||||
|
newExtra.codex_cli_only_allowed_clients = ['claude_code']
|
||||||
|
} else {
|
||||||
|
delete newExtra.codex_cli_only_allowed_clients
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePayload.extra = newExtra
|
updatePayload.extra = newExtra
|
||||||
|
|||||||
@ -197,6 +197,25 @@ describe('BulkEditAccountModal', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('OpenAI OAuth 批量编辑应提交 codex_cli_only_allowed_clients 字段', async () => {
|
||||||
|
const wrapper = mountModal({
|
||||||
|
selectedPlatforms: ['openai'],
|
||||||
|
selectedTypes: ['oauth']
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.get('#bulk-edit-openai-codex-allow-claude-code-enabled').setValue(true)
|
||||||
|
await wrapper.get('#bulk-edit-openai-codex-allow-claude-code-toggle').trigger('click')
|
||||||
|
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1)
|
||||||
|
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
|
||||||
|
extra: {
|
||||||
|
codex_cli_only_allowed_clients: ['claude_code']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('OpenAI API Key 批量编辑应提交 API Key 专属 WS mode 字段', async () => {
|
it('OpenAI API Key 批量编辑应提交 API Key 专属 WS mode 字段', async () => {
|
||||||
const wrapper = mountModal({
|
const wrapper = mountModal({
|
||||||
selectedPlatforms: ['openai'],
|
selectedPlatforms: ['openai'],
|
||||||
|
|||||||
@ -3372,6 +3372,9 @@ export default {
|
|||||||
codexCLIOnly: 'Codex official clients only',
|
codexCLIOnly: 'Codex official clients only',
|
||||||
codexCLIOnlyDesc:
|
codexCLIOnlyDesc:
|
||||||
'Only applies to OpenAI OAuth. When enabled, only Codex official client families are allowed; when disabled, the gateway bypasses this restriction and keeps existing behavior.',
|
'Only applies to OpenAI OAuth. When enabled, only Codex official client families are allowed; when disabled, the gateway bypasses this restriction and keeps existing behavior.',
|
||||||
|
codexCLIOnlyAllowClaudeCode: "Also allow Claude Code's Codex plugin",
|
||||||
|
codexCLIOnlyAllowClaudeCodeDesc:
|
||||||
|
'Only takes effect when the switch above is on. Additionally allows requests from the Claude Code Codex plugin (exact match on originator=Claude Code) without weakening blocking of other non-official clients.',
|
||||||
codexImageGenerationBridge: 'Codex image-generation bridge',
|
codexImageGenerationBridge: 'Codex image-generation bridge',
|
||||||
codexImageGenerationBridgeDesc:
|
codexImageGenerationBridgeDesc:
|
||||||
'Account policy takes precedence over channel and global settings. Only controls whether Codex requests through the /responses text endpoint receive the image_generation tool; standalone image-generation endpoints are unaffected.',
|
'Account policy takes precedence over channel and global settings. Only controls whether Codex requests through the /responses text endpoint receive the image_generation tool; standalone image-generation endpoints are unaffected.',
|
||||||
@ -5611,6 +5614,9 @@ export default {
|
|||||||
openaiCodexUserAgent: 'OpenAI Codex UA',
|
openaiCodexUserAgent: 'OpenAI Codex UA',
|
||||||
openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)',
|
openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)',
|
||||||
openaiCodexUserAgentHint: 'Used to bypass Cloudflare browser-UA challenges on the OpenAI upstream. Only applies when the client User-Agent is detected as a browser (Mozilla/...). Leave empty to use the built-in default.',
|
openaiCodexUserAgentHint: 'Used to bypass Cloudflare browser-UA challenges on the OpenAI upstream. Only applies when the client User-Agent is detected as a browser (Mozilla/...). Leave empty to use the built-in default.',
|
||||||
|
openaiAllowClaudeCodeCodexPlugin: "Allow using the Codex plugin in Claude Code",
|
||||||
|
openaiAllowClaudeCodeCodexPluginDesc:
|
||||||
|
"Global switch; only affects OpenAI OAuth accounts that have 'Codex official clients only' enabled. When on, all such accounts additionally allow requests from the Claude Code Codex plugin (exact match on originator=Claude Code) without per-account config; upstream requests remain pass-through.",
|
||||||
},
|
},
|
||||||
webSearchEmulation: {
|
webSearchEmulation: {
|
||||||
title: 'Web Search Emulation',
|
title: 'Web Search Emulation',
|
||||||
|
|||||||
@ -3516,6 +3516,8 @@ export default {
|
|||||||
responsesStatusForcedChatCompletions: '已强制 Chat Completions',
|
responsesStatusForcedChatCompletions: '已强制 Chat Completions',
|
||||||
codexCLIOnly: '仅允许 Codex 官方客户端',
|
codexCLIOnly: '仅允许 Codex 官方客户端',
|
||||||
codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。',
|
codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。',
|
||||||
|
codexCLIOnlyAllowClaudeCode: '额外放行 Claude Code 的 Codex 插件',
|
||||||
|
codexCLIOnlyAllowClaudeCodeDesc: '仅在上方开关开启时生效。额外放行通过 Claude Code 的 Codex 插件发起的请求(精确匹配 originator=Claude Code),不影响对其他非官方客户端的拦截。',
|
||||||
codexImageGenerationBridge: 'Codex 图片生成桥接',
|
codexImageGenerationBridge: 'Codex 图片生成桥接',
|
||||||
codexImageGenerationBridgeDesc:
|
codexImageGenerationBridgeDesc:
|
||||||
'账号级策略优先于渠道和全局配置。仅控制 Codex 走 /responses 文本端点时是否注入 image_generation 工具;不影响独立图片生成接口。',
|
'账号级策略优先于渠道和全局配置。仅控制 Codex 走 /responses 文本端点时是否注入 image_generation 工具;不影响独立图片生成接口。',
|
||||||
@ -5766,6 +5768,9 @@ export default {
|
|||||||
openaiCodexUserAgent: 'OpenAI Codex UA',
|
openaiCodexUserAgent: 'OpenAI Codex UA',
|
||||||
openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)',
|
openaiCodexUserAgentPlaceholder: 'codex-tui/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.125.0)',
|
||||||
openaiCodexUserAgentHint: '用于规避 OpenAI 上游 Cloudflare 对浏览器 UA 的访问质询。仅在检测到客户端 User-Agent 为浏览器(Mozilla/...)时生效,其他客户端原样透传。留空使用内置默认值。',
|
openaiCodexUserAgentHint: '用于规避 OpenAI 上游 Cloudflare 对浏览器 UA 的访问质询。仅在检测到客户端 User-Agent 为浏览器(Mozilla/...)时生效,其他客户端原样透传。留空使用内置默认值。',
|
||||||
|
openaiAllowClaudeCodeCodexPlugin: '允许在 Claude Code 中使用 Codex 插件',
|
||||||
|
openaiAllowClaudeCodeCodexPluginDesc:
|
||||||
|
'全局开关,仅对已开启「仅允许 Codex 官方客户端」的 OpenAI OAuth 账号生效。开启后,所有此类账号都额外放行通过 Claude Code 的 Codex 插件发起的请求(精确匹配 originator=Claude Code),无需逐账号配置;上游请求仍保持透传。',
|
||||||
},
|
},
|
||||||
webSearchEmulation: {
|
webSearchEmulation: {
|
||||||
title: 'Web Search 模拟',
|
title: 'Web Search 模拟',
|
||||||
|
|||||||
@ -3948,6 +3948,19 @@
|
|||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 是否允许在 Claude Code 中使用 Codex 插件(全局开关) -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="pr-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t("admin.settings.gatewayForwarding.openaiAllowClaudeCodeCodexPlugin") }}
|
||||||
|
</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t("admin.settings.gatewayForwarding.openaiAllowClaudeCodeCodexPluginDesc") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle v-model="form.openai_allow_claude_code_codex_plugin" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Web Search Emulation -->
|
<!-- Web Search Emulation -->
|
||||||
@ -7162,6 +7175,7 @@ const form = reactive<SettingsForm>({
|
|||||||
rewrite_message_cache_control: false,
|
rewrite_message_cache_control: false,
|
||||||
antigravity_user_agent_version: "",
|
antigravity_user_agent_version: "",
|
||||||
openai_codex_user_agent: "",
|
openai_codex_user_agent: "",
|
||||||
|
openai_allow_claude_code_codex_plugin: false,
|
||||||
// 余额、订阅到期与账号限额通知
|
// 余额、订阅到期与账号限额通知
|
||||||
balance_low_notify_enabled: false,
|
balance_low_notify_enabled: false,
|
||||||
balance_low_notify_threshold: 0,
|
balance_low_notify_threshold: 0,
|
||||||
@ -8267,6 +8281,7 @@ async function saveSettings() {
|
|||||||
form.antigravity_user_agent_version?.trim() || "",
|
form.antigravity_user_agent_version?.trim() || "",
|
||||||
openai_codex_user_agent:
|
openai_codex_user_agent:
|
||||||
form.openai_codex_user_agent?.trim() || "",
|
form.openai_codex_user_agent?.trim() || "",
|
||||||
|
openai_allow_claude_code_codex_plugin: form.openai_allow_claude_code_codex_plugin,
|
||||||
// Payment configuration
|
// Payment configuration
|
||||||
payment_enabled: form.payment_enabled,
|
payment_enabled: form.payment_enabled,
|
||||||
risk_control_enabled: form.risk_control_enabled,
|
risk_control_enabled: form.risk_control_enabled,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user