Merge pull request #2450 from wucm667/codex/issue-2431-responses-api-support
feat: 支持后台配置 OpenAI Responses API 路由
This commit is contained in:
commit
e365aae450
@ -294,7 +294,7 @@ func (h *OpenAIGatewayHandler) ChatCompletions(c *gin.Context) {
|
|||||||
|
|
||||||
// resolveRawCCUpstreamEndpoint returns the actual upstream endpoint for
|
// resolveRawCCUpstreamEndpoint returns the actual upstream endpoint for
|
||||||
// OpenAI Chat Completions requests. For APIKey accounts whose upstream
|
// OpenAI Chat Completions requests. For APIKey accounts whose upstream
|
||||||
// has been probed to not support the Responses API, the request is
|
// is forced or probed to not support the Responses API, the request is
|
||||||
// forwarded directly to /v1/chat/completions — not through the default
|
// forwarded directly to /v1/chat/completions — not through the default
|
||||||
// CC→Responses conversion path.
|
// CC→Responses conversion path.
|
||||||
func resolveRawCCUpstreamEndpoint(c *gin.Context, account *service.Account) string {
|
func resolveRawCCUpstreamEndpoint(c *gin.Context, account *service.Account) string {
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
// pensieve/short-term/maxims/preserve-existing-runtime-behavior-when-replacing-logic-in-stateful-systems)
|
// pensieve/short-term/maxims/preserve-existing-runtime-behavior-when-replacing-logic-in-stateful-systems)
|
||||||
package openai_compat
|
package openai_compat
|
||||||
|
|
||||||
// AccountResponsesSupport 描述账号上游对 OpenAI Responses API 的支持状态。
|
// AccountResponsesSupport 描述账号上游对 OpenAI Responses API 的有效支持状态。
|
||||||
//
|
//
|
||||||
// 仅用于 platform=openai + type=apikey 的账号;其他账号类型不应调用本包判定。
|
// 仅用于 platform=openai + type=apikey 的账号;其他账号类型不应调用本包判定。
|
||||||
type AccountResponsesSupport int
|
type AccountResponsesSupport int
|
||||||
@ -35,11 +35,43 @@ const (
|
|||||||
ResponsesSupportNo
|
ResponsesSupportNo
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExtraKeyResponsesSupported 是 accounts.extra JSON 中存储探测结果的键名。
|
// ResponsesSupportMode 描述账号级 Responses API 路由覆盖模式。
|
||||||
|
type ResponsesSupportMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ResponsesSupportModeAuto 表示跟随自动探测结果。
|
||||||
|
ResponsesSupportModeAuto ResponsesSupportMode = "auto"
|
||||||
|
|
||||||
|
// ResponsesSupportModeForceResponses 强制使用 /v1/responses。
|
||||||
|
ResponsesSupportModeForceResponses ResponsesSupportMode = "force_responses"
|
||||||
|
|
||||||
|
// ResponsesSupportModeForceChatCompletions 强制使用 /v1/chat/completions。
|
||||||
|
ResponsesSupportModeForceChatCompletions ResponsesSupportMode = "force_chat_completions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtraKeyResponsesMode 是 accounts.extra JSON 中存储手动覆盖模式的键名。
|
||||||
|
// 值类型为 string:auto=跟随探测,force_responses=强制 Responses,
|
||||||
|
// force_chat_completions=强制 Chat Completions。
|
||||||
|
const ExtraKeyResponsesMode = "openai_responses_mode"
|
||||||
|
|
||||||
|
// ExtraKeyResponsesSupported 是 accounts.extra JSON 中存储自动探测结果的键名。
|
||||||
// 值类型为 bool:true=支持、false=不支持、键缺失=未探测。
|
// 值类型为 bool:true=支持、false=不支持、键缺失=未探测。
|
||||||
const ExtraKeyResponsesSupported = "openai_responses_supported"
|
const ExtraKeyResponsesSupported = "openai_responses_supported"
|
||||||
|
|
||||||
// ResolveResponsesSupport 从账号的 extra map 中读取探测标记。
|
// NormalizeResponsesSupportMode 归一化账号级 Responses API 路由覆盖模式。
|
||||||
|
// 缺失或非法值按 auto 处理,以保持存量行为。
|
||||||
|
func NormalizeResponsesSupportMode(mode string) ResponsesSupportMode {
|
||||||
|
switch ResponsesSupportMode(mode) {
|
||||||
|
case ResponsesSupportModeForceResponses:
|
||||||
|
return ResponsesSupportModeForceResponses
|
||||||
|
case ResponsesSupportModeForceChatCompletions:
|
||||||
|
return ResponsesSupportModeForceChatCompletions
|
||||||
|
default:
|
||||||
|
return ResponsesSupportModeAuto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveResponsesSupport 从账号的 extra map 中读取手动覆盖模式与探测标记。
|
||||||
//
|
//
|
||||||
// 标记缺失或类型不匹配时返回 ResponsesSupportUnknown——调用方应按
|
// 标记缺失或类型不匹配时返回 ResponsesSupportUnknown——调用方应按
|
||||||
// "未探测=保留旧行为=走 Responses" 处理(参见 ShouldUseResponsesAPI)。
|
// "未探测=保留旧行为=走 Responses" 处理(参见 ShouldUseResponsesAPI)。
|
||||||
@ -47,6 +79,14 @@ func ResolveResponsesSupport(extra map[string]any) AccountResponsesSupport {
|
|||||||
if extra == nil {
|
if extra == nil {
|
||||||
return ResponsesSupportUnknown
|
return ResponsesSupportUnknown
|
||||||
}
|
}
|
||||||
|
if mode, ok := extra[ExtraKeyResponsesMode].(string); ok {
|
||||||
|
switch NormalizeResponsesSupportMode(mode) {
|
||||||
|
case ResponsesSupportModeForceResponses:
|
||||||
|
return ResponsesSupportYes
|
||||||
|
case ResponsesSupportModeForceChatCompletions:
|
||||||
|
return ResponsesSupportNo
|
||||||
|
}
|
||||||
|
}
|
||||||
v, ok := extra[ExtraKeyResponsesSupported]
|
v, ok := extra[ExtraKeyResponsesSupported]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ResponsesSupportUnknown
|
return ResponsesSupportUnknown
|
||||||
|
|||||||
@ -16,6 +16,12 @@ func TestResolveResponsesSupport(t *testing.T) {
|
|||||||
{"value wrong type string", map[string]any{ExtraKeyResponsesSupported: "true"}, ResponsesSupportUnknown},
|
{"value wrong type string", map[string]any{ExtraKeyResponsesSupported: "true"}, ResponsesSupportUnknown},
|
||||||
{"value wrong type number", map[string]any{ExtraKeyResponsesSupported: 1}, ResponsesSupportUnknown},
|
{"value wrong type number", map[string]any{ExtraKeyResponsesSupported: 1}, ResponsesSupportUnknown},
|
||||||
{"value nil", map[string]any{ExtraKeyResponsesSupported: nil}, ResponsesSupportUnknown},
|
{"value nil", map[string]any{ExtraKeyResponsesSupported: nil}, ResponsesSupportUnknown},
|
||||||
|
{"force responses", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceResponses)}, ResponsesSupportYes},
|
||||||
|
{"force chat completions", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceChatCompletions)}, ResponsesSupportNo},
|
||||||
|
{"auto follows probe", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeAuto), ExtraKeyResponsesSupported: false}, ResponsesSupportNo},
|
||||||
|
{"invalid mode follows probe", map[string]any{ExtraKeyResponsesMode: "bogus", ExtraKeyResponsesSupported: true}, ResponsesSupportYes},
|
||||||
|
{"force responses overrides probe false", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceResponses), ExtraKeyResponsesSupported: false}, ResponsesSupportYes},
|
||||||
|
{"force chat completions overrides probe true", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceChatCompletions), ExtraKeyResponsesSupported: true}, ResponsesSupportNo},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
@ -42,6 +48,10 @@ func TestShouldUseResponsesAPI(t *testing.T) {
|
|||||||
// 已探测:标记决定
|
// 已探测:标记决定
|
||||||
{"explicitly supported", map[string]any{ExtraKeyResponsesSupported: true}, true},
|
{"explicitly supported", map[string]any{ExtraKeyResponsesSupported: true}, true},
|
||||||
{"explicitly unsupported", map[string]any{ExtraKeyResponsesSupported: false}, false},
|
{"explicitly unsupported", map[string]any{ExtraKeyResponsesSupported: false}, false},
|
||||||
|
|
||||||
|
// 手动覆盖:覆盖自动探测结果
|
||||||
|
{"force responses overrides unsupported probe", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceResponses), ExtraKeyResponsesSupported: false}, true},
|
||||||
|
{"force chat completions overrides supported probe", map[string]any{ExtraKeyResponsesMode: string(ResponsesSupportModeForceChatCompletions), ExtraKeyResponsesSupported: true}, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
@ -53,3 +63,26 @@ func TestShouldUseResponsesAPI(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeResponsesSupportMode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode string
|
||||||
|
want ResponsesSupportMode
|
||||||
|
}{
|
||||||
|
{"empty", "", ResponsesSupportModeAuto},
|
||||||
|
{"auto", "auto", ResponsesSupportModeAuto},
|
||||||
|
{"force responses", "force_responses", ResponsesSupportModeForceResponses},
|
||||||
|
{"force chat completions", "force_chat_completions", ResponsesSupportModeForceChatCompletions},
|
||||||
|
{"invalid", "enabled", ResponsesSupportModeAuto},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := NormalizeResponsesSupportMode(tc.mode)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("NormalizeResponsesSupportMode(%q) = %q, want %q", tc.mode, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -12,3 +12,14 @@ func TestShouldEnqueueSchedulerOutboxForExtraUpdates_CompactCapabilityKeysAreRel
|
|||||||
t.Fatalf("expected compact capability updates to enqueue scheduler outbox")
|
t.Fatalf("expected compact capability updates to enqueue scheduler outbox")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldEnqueueSchedulerOutboxForExtraUpdates_OpenAIResponsesCapabilityKeysAreRelevant(t *testing.T) {
|
||||||
|
updates := map[string]any{
|
||||||
|
"openai_responses_mode": "force_chat_completions",
|
||||||
|
"openai_responses_supported": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldEnqueueSchedulerOutboxForExtraUpdates(updates) {
|
||||||
|
t.Fatalf("expected responses capability updates to enqueue scheduler outbox")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -546,6 +546,8 @@ func filterSchedulerExtra(extra map[string]any) map[string]any {
|
|||||||
"responses_websockets_v2_enabled",
|
"responses_websockets_v2_enabled",
|
||||||
"openai_ws_enabled",
|
"openai_ws_enabled",
|
||||||
"openai_ws_force_http",
|
"openai_ws_force_http",
|
||||||
|
"openai_responses_mode",
|
||||||
|
"openai_responses_supported",
|
||||||
}
|
}
|
||||||
filtered := make(map[string]any)
|
filtered := make(map[string]any)
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
|
|||||||
@ -18,6 +18,8 @@ func TestBuildSchedulerMetadataAccount_KeepsOpenAIWSFlags(t *testing.T) {
|
|||||||
"openai_oauth_responses_websockets_v2_enabled": true,
|
"openai_oauth_responses_websockets_v2_enabled": true,
|
||||||
"openai_oauth_responses_websockets_v2_mode": service.OpenAIWSIngressModePassthrough,
|
"openai_oauth_responses_websockets_v2_mode": service.OpenAIWSIngressModePassthrough,
|
||||||
"openai_ws_force_http": true,
|
"openai_ws_force_http": true,
|
||||||
|
"openai_responses_mode": "force_chat_completions",
|
||||||
|
"openai_responses_supported": false,
|
||||||
"mixed_scheduling": true,
|
"mixed_scheduling": true,
|
||||||
"unused_large_field": "drop-me",
|
"unused_large_field": "drop-me",
|
||||||
},
|
},
|
||||||
@ -28,6 +30,8 @@ func TestBuildSchedulerMetadataAccount_KeepsOpenAIWSFlags(t *testing.T) {
|
|||||||
require.Equal(t, true, got.Extra["openai_oauth_responses_websockets_v2_enabled"])
|
require.Equal(t, true, got.Extra["openai_oauth_responses_websockets_v2_enabled"])
|
||||||
require.Equal(t, service.OpenAIWSIngressModePassthrough, got.Extra["openai_oauth_responses_websockets_v2_mode"])
|
require.Equal(t, service.OpenAIWSIngressModePassthrough, got.Extra["openai_oauth_responses_websockets_v2_mode"])
|
||||||
require.Equal(t, true, got.Extra["openai_ws_force_http"])
|
require.Equal(t, true, got.Extra["openai_ws_force_http"])
|
||||||
|
require.Equal(t, "force_chat_completions", got.Extra["openai_responses_mode"])
|
||||||
|
require.Equal(t, false, got.Extra["openai_responses_supported"])
|
||||||
require.Equal(t, true, got.Extra["mixed_scheduling"])
|
require.Equal(t, true, got.Extra["mixed_scheduling"])
|
||||||
require.Nil(t, got.Extra["unused_large_field"])
|
require.Nil(t, got.Extra["unused_large_field"])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,10 +48,10 @@ var cursorResponsesUnsupportedFields = []string{
|
|||||||
// 正确的,但 sub2api 接入 DeepSeek/Kimi/GLM 等第三方 OpenAI 兼容上游后假设破裂:
|
// 正确的,但 sub2api 接入 DeepSeek/Kimi/GLM 等第三方 OpenAI 兼容上游后假设破裂:
|
||||||
// 这些上游普遍只支持 /v1/chat/completions,无 /v1/responses 端点。
|
// 这些上游普遍只支持 /v1/chat/completions,无 /v1/responses 端点。
|
||||||
//
|
//
|
||||||
// 当前路由策略(基于账号探测标记,详见 openai_compat.ShouldUseResponsesAPI):
|
// 当前路由策略(基于账号覆盖模式/探测标记,详见 openai_compat.ShouldUseResponsesAPI):
|
||||||
// - APIKey 账号 + 探测确认不支持 Responses → 走 forwardAsRawChatCompletions
|
// - APIKey 账号 + 强制或探测确认不支持 Responses → 走 forwardAsRawChatCompletions
|
||||||
// 直转上游 /v1/chat/completions,不做协议转换
|
// 直转上游 /v1/chat/completions,不做协议转换
|
||||||
// - 其他所有情况(OAuth、APIKey 探测确认支持、未探测)→ 走原有 CC→Responses
|
// - 其他所有情况(OAuth、APIKey 强制/探测确认支持、未探测)→ 走原有 CC→Responses
|
||||||
// 转换路径(保留旧行为,存量未探测账号零兼容破坏)
|
// 转换路径(保留旧行为,存量未探测账号零兼容破坏)
|
||||||
func (s *OpenAIGatewayService) ForwardAsChatCompletions(
|
func (s *OpenAIGatewayService) ForwardAsChatCompletions(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@ -61,8 +61,8 @@ func (s *OpenAIGatewayService) ForwardAsChatCompletions(
|
|||||||
promptCacheKey string,
|
promptCacheKey string,
|
||||||
defaultMappedModel string,
|
defaultMappedModel string,
|
||||||
) (*OpenAIForwardResult, error) {
|
) (*OpenAIForwardResult, error) {
|
||||||
// 入口分流:APIKey 账号 + 已探测且确认上游不支持 Responses,走 CC 直转。
|
// 入口分流:APIKey 账号 + 强制或已探测确认上游不支持 Responses,走 CC 直转。
|
||||||
// 标记缺失(未探测)按"现状即证据"原则继续走下方原 Responses 转换路径。
|
// 自动模式下标记缺失(未探测)按"现状即证据"原则继续走下方原 Responses 转换路径。
|
||||||
if account.Type == AccountTypeAPIKey && !openai_compat.ShouldUseResponsesAPI(account.Extra) {
|
if account.Type == AccountTypeAPIKey && !openai_compat.ShouldUseResponsesAPI(account.Extra) {
|
||||||
return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel)
|
return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1409,6 +1409,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- OpenAI APIKey Responses API support mode -->
|
||||||
|
<div
|
||||||
|
v-if="account?.platform === 'openai' && account?.type === 'apikey'"
|
||||||
|
class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="input-label mb-0">{{ t('admin.accounts.openai.responsesMode') }}</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.accounts.openai.responsesModeDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-56">
|
||||||
|
<Select
|
||||||
|
v-model="openAIResponsesMode"
|
||||||
|
:options="openAIResponsesModeOptions"
|
||||||
|
data-testid="openai-responses-mode-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:bg-dark-700 dark:text-gray-300">
|
||||||
|
<span class="font-medium">{{ t(openAIResponsesStatusKey) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Anthropic API Key 自动透传开关 -->
|
<!-- Anthropic API Key 自动透传开关 -->
|
||||||
<div
|
<div
|
||||||
v-if="account?.platform === 'anthropic' && account?.type === 'apikey'"
|
v-if="account?.platform === 'anthropic' && account?.type === 'apikey'"
|
||||||
@ -2193,7 +2218,7 @@ import { useAppStore } from '@/stores/app'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import { useQuotaNotifyState } from '@/composables/useQuotaNotifyState'
|
import { useQuotaNotifyState } from '@/composables/useQuotaNotifyState'
|
||||||
import type { Account, Proxy, AdminGroup, CheckMixedChannelResponse, OpenAICompactMode } from '@/types'
|
import type { Account, Proxy, AdminGroup, CheckMixedChannelResponse, OpenAICompactMode, OpenAIResponsesMode } from '@/types'
|
||||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||||
import Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
@ -2344,6 +2369,7 @@ const customBaseUrl = ref('')
|
|||||||
// OpenAI 自动透传开关(OAuth/API Key)
|
// OpenAI 自动透传开关(OAuth/API Key)
|
||||||
const openaiPassthroughEnabled = ref(false)
|
const openaiPassthroughEnabled = ref(false)
|
||||||
const openAICompactMode = ref<OpenAICompactMode>('auto')
|
const openAICompactMode = ref<OpenAICompactMode>('auto')
|
||||||
|
const openAIResponsesMode = ref<OpenAIResponsesMode>('auto')
|
||||||
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)
|
||||||
@ -2445,9 +2471,36 @@ const openAICompactModeOptions = computed(() => [
|
|||||||
{ value: 'force_on', label: t('admin.accounts.openai.compactModeForceOn') },
|
{ value: 'force_on', label: t('admin.accounts.openai.compactModeForceOn') },
|
||||||
{ value: 'force_off', label: t('admin.accounts.openai.compactModeForceOff') }
|
{ value: 'force_off', label: t('admin.accounts.openai.compactModeForceOff') }
|
||||||
])
|
])
|
||||||
|
const openAIResponsesModeOptions = computed(() => [
|
||||||
|
{ value: 'auto', label: t('admin.accounts.openai.responsesModeAuto') },
|
||||||
|
{ value: 'force_responses', label: t('admin.accounts.openai.responsesModeForceResponses') },
|
||||||
|
{ value: 'force_chat_completions', label: t('admin.accounts.openai.responsesModeForceChatCompletions') }
|
||||||
|
])
|
||||||
|
const normalizeOpenAIResponsesMode = (mode: unknown): OpenAIResponsesMode => {
|
||||||
|
if (mode === 'force_responses' || mode === 'force_chat_completions') {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
return 'auto'
|
||||||
|
}
|
||||||
const isOpenAIModelRestrictionDisabled = computed(() =>
|
const isOpenAIModelRestrictionDisabled = computed(() =>
|
||||||
props.account?.platform === 'openai' && openaiPassthroughEnabled.value
|
props.account?.platform === 'openai' && openaiPassthroughEnabled.value
|
||||||
)
|
)
|
||||||
|
const openAIResponsesStatusKey = computed(() => {
|
||||||
|
if (openAIResponsesMode.value === 'force_responses') {
|
||||||
|
return 'admin.accounts.openai.responsesStatusForcedResponses'
|
||||||
|
}
|
||||||
|
if (openAIResponsesMode.value === 'force_chat_completions') {
|
||||||
|
return 'admin.accounts.openai.responsesStatusForcedChatCompletions'
|
||||||
|
}
|
||||||
|
const extra = props.account?.extra as Record<string, unknown> | undefined
|
||||||
|
if (extra?.openai_responses_supported === true) {
|
||||||
|
return 'admin.accounts.openai.responsesStatusAutoSupported'
|
||||||
|
}
|
||||||
|
if (extra?.openai_responses_supported === false) {
|
||||||
|
return 'admin.accounts.openai.responsesStatusAutoUnsupported'
|
||||||
|
}
|
||||||
|
return 'admin.accounts.openai.responsesStatusAutoUnknown'
|
||||||
|
})
|
||||||
const openAICompactStatusKey = computed(() => {
|
const openAICompactStatusKey = computed(() => {
|
||||||
const extra = props.account?.extra as Record<string, unknown> | undefined
|
const extra = props.account?.extra as Record<string, unknown> | undefined
|
||||||
if (!props.account || props.account.platform !== 'openai') return ''
|
if (!props.account || props.account.platform !== 'openai') return ''
|
||||||
@ -2594,6 +2647,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
|||||||
// Load OpenAI passthrough toggle (OpenAI OAuth/API Key)
|
// Load OpenAI passthrough toggle (OpenAI OAuth/API Key)
|
||||||
openaiPassthroughEnabled.value = false
|
openaiPassthroughEnabled.value = false
|
||||||
openAICompactMode.value = 'auto'
|
openAICompactMode.value = 'auto'
|
||||||
|
openAIResponsesMode.value = 'auto'
|
||||||
openAICompactModelMappings.value = []
|
openAICompactModelMappings.value = []
|
||||||
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiOAuthResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
openaiAPIKeyResponsesWebSocketV2Mode.value = OPENAI_WS_MODE_OFF
|
||||||
@ -2604,6 +2658,9 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
|||||||
if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'apikey')) {
|
if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'apikey')) {
|
||||||
openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true
|
openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true
|
||||||
openAICompactMode.value = (extra?.openai_compact_mode as OpenAICompactMode) || 'auto'
|
openAICompactMode.value = (extra?.openai_compact_mode as OpenAICompactMode) || 'auto'
|
||||||
|
if (newAccount.type === 'apikey') {
|
||||||
|
openAIResponsesMode.value = normalizeOpenAIResponsesMode(extra?.openai_responses_mode)
|
||||||
|
}
|
||||||
const codexImageGenerationBridgeValue = typeof extra?.codex_image_generation_bridge === 'boolean'
|
const codexImageGenerationBridgeValue = typeof extra?.codex_image_generation_bridge === 'boolean'
|
||||||
? extra.codex_image_generation_bridge
|
? extra.codex_image_generation_bridge
|
||||||
: extra?.codex_image_generation_bridge_enabled
|
: extra?.codex_image_generation_bridge_enabled
|
||||||
@ -3767,6 +3824,13 @@ const handleSubmit = async () => {
|
|||||||
} else {
|
} else {
|
||||||
newExtra.openai_compact_mode = openAICompactMode.value
|
newExtra.openai_compact_mode = openAICompactMode.value
|
||||||
}
|
}
|
||||||
|
if (props.account.type === 'apikey') {
|
||||||
|
if (openAIResponsesMode.value === 'auto') {
|
||||||
|
delete newExtra.openai_responses_mode
|
||||||
|
} else {
|
||||||
|
newExtra.openai_responses_mode = openAIResponsesMode.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delete newExtra.codex_image_generation_bridge_enabled
|
delete newExtra.codex_image_generation_bridge_enabled
|
||||||
if (codexImageGenerationBridgeMode.value === 'inherit') {
|
if (codexImageGenerationBridgeMode.value === 'inherit') {
|
||||||
|
|||||||
@ -217,6 +217,48 @@ describe('EditAccountModal', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('submits OpenAI APIKey Responses support override mode', async () => {
|
||||||
|
const account = buildAccount()
|
||||||
|
account.extra = {
|
||||||
|
openai_responses_mode: 'force_chat_completions',
|
||||||
|
openai_responses_supported: false
|
||||||
|
}
|
||||||
|
updateAccountMock.mockReset()
|
||||||
|
checkMixedChannelRiskMock.mockReset()
|
||||||
|
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
||||||
|
updateAccountMock.mockResolvedValue(account)
|
||||||
|
|
||||||
|
const wrapper = mountModal(account)
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="openai-responses-mode-select"]').setValue('force_responses')
|
||||||
|
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||||
|
|
||||||
|
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_responses_mode).toBe('force_responses')
|
||||||
|
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_responses_supported).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears OpenAI APIKey Responses override when set back to auto', async () => {
|
||||||
|
const account = buildAccount()
|
||||||
|
account.extra = {
|
||||||
|
openai_responses_mode: 'force_chat_completions',
|
||||||
|
openai_responses_supported: true
|
||||||
|
}
|
||||||
|
updateAccountMock.mockReset()
|
||||||
|
checkMixedChannelRiskMock.mockReset()
|
||||||
|
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
||||||
|
updateAccountMock.mockResolvedValue(account)
|
||||||
|
|
||||||
|
const wrapper = mountModal(account)
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="openai-responses-mode-select"]').setValue('auto')
|
||||||
|
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||||
|
|
||||||
|
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(updateAccountMock.mock.calls[0]?.[1]?.extra).not.toHaveProperty('openai_responses_mode')
|
||||||
|
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_responses_supported).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('submits account-level Codex image generation bridge override', async () => {
|
it('submits account-level Codex image generation bridge override', async () => {
|
||||||
const account = buildAccount()
|
const account = buildAccount()
|
||||||
account.extra = {
|
account.extra = {
|
||||||
|
|||||||
@ -3167,6 +3167,17 @@ export default {
|
|||||||
'Only applies to OpenAI API Key. This account can use OpenAI WebSocket Mode only when enabled.',
|
'Only applies to OpenAI API Key. This account can use OpenAI WebSocket Mode only when enabled.',
|
||||||
responsesWebsocketsV2PassthroughHint:
|
responsesWebsocketsV2PassthroughHint:
|
||||||
'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.',
|
'Automatic passthrough is currently enabled: it only affects HTTP passthrough and does not disable WS mode.',
|
||||||
|
responsesMode: 'Responses API support',
|
||||||
|
responsesModeDesc:
|
||||||
|
'Only applies to OpenAI API Key accounts. Auto follows probe results; force modes override probing.',
|
||||||
|
responsesModeAuto: 'Auto',
|
||||||
|
responsesModeForceResponses: 'Force Responses',
|
||||||
|
responsesModeForceChatCompletions: 'Force Chat Completions',
|
||||||
|
responsesStatusAutoSupported: 'Auto probe: Responses',
|
||||||
|
responsesStatusAutoUnsupported: 'Auto probe: Chat Completions',
|
||||||
|
responsesStatusAutoUnknown: 'Auto probe: unknown',
|
||||||
|
responsesStatusForcedResponses: 'Forced Responses',
|
||||||
|
responsesStatusForcedChatCompletions: 'Forced Chat Completions',
|
||||||
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.',
|
||||||
|
|||||||
@ -3313,6 +3313,17 @@ export default {
|
|||||||
apiKeyResponsesWebsocketsV2Desc:
|
apiKeyResponsesWebsocketsV2Desc:
|
||||||
'仅对 OpenAI API Key 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。',
|
'仅对 OpenAI API Key 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。',
|
||||||
responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。',
|
responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。',
|
||||||
|
responsesMode: 'Responses API 支持',
|
||||||
|
responsesModeDesc:
|
||||||
|
'仅对 OpenAI API Key 生效。自动跟随探测结果,强制模式会覆盖自动探测。',
|
||||||
|
responsesModeAuto: '自动',
|
||||||
|
responsesModeForceResponses: '强制 Responses',
|
||||||
|
responsesModeForceChatCompletions: '强制 Chat Completions',
|
||||||
|
responsesStatusAutoSupported: '自动探测:Responses',
|
||||||
|
responsesStatusAutoUnsupported: '自动探测:Chat Completions',
|
||||||
|
responsesStatusAutoUnknown: '自动探测:未探测',
|
||||||
|
responsesStatusForcedResponses: '已强制 Responses',
|
||||||
|
responsesStatusForcedChatCompletions: '已强制 Chat Completions',
|
||||||
codexCLIOnly: '仅允许 Codex 官方客户端',
|
codexCLIOnly: '仅允许 Codex 官方客户端',
|
||||||
codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。',
|
codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。',
|
||||||
codexImageGenerationBridge: 'Codex 图片生成桥接',
|
codexImageGenerationBridge: 'Codex 图片生成桥接',
|
||||||
|
|||||||
@ -970,6 +970,7 @@ export interface CodexUsageSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type OpenAICompactMode = 'auto' | 'force_on' | 'force_off'
|
export type OpenAICompactMode = 'auto' | 'force_on' | 'force_off'
|
||||||
|
export type OpenAIResponsesMode = 'auto' | 'force_responses' | 'force_chat_completions'
|
||||||
|
|
||||||
export interface OpenAICompactState {
|
export interface OpenAICompactState {
|
||||||
openai_compact_mode?: OpenAICompactMode
|
openai_compact_mode?: OpenAICompactMode
|
||||||
@ -979,6 +980,11 @@ export interface OpenAICompactState {
|
|||||||
openai_compact_last_error?: string
|
openai_compact_last_error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenAIResponsesState {
|
||||||
|
openai_responses_mode?: OpenAIResponsesMode
|
||||||
|
openai_responses_supported?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateAccountRequest {
|
export interface CreateAccountRequest {
|
||||||
name: string
|
name: string
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user