mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #6411 from Wei-Shaw/fix/openai-ttft-setting-ui
fix(openai): move TTFT metric mode to admin settings
This commit is contained in:
@@ -288,6 +288,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
MaxClaudeCodeVersion: settings.MaxClaudeCodeVersion,
|
||||
AllowUngroupedKeyScheduling: settings.AllowUngroupedKeyScheduling,
|
||||
BackendModeEnabled: settings.BackendModeEnabled,
|
||||
OpenAITTFTMode: settings.OpenAITTFTMode,
|
||||
EnableFingerprintUnification: settings.EnableFingerprintUnification,
|
||||
EnableMetadataPassthrough: settings.EnableMetadataPassthrough,
|
||||
EnableCCHSigning: settings.EnableCCHSigning,
|
||||
|
||||
@@ -452,6 +452,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if before.EnableFingerprintUnification != after.EnableFingerprintUnification {
|
||||
changed = append(changed, "enable_fingerprint_unification")
|
||||
}
|
||||
if before.OpenAITTFTMode != after.OpenAITTFTMode {
|
||||
changed = append(changed, "openai_ttft_mode")
|
||||
}
|
||||
if before.EnableMetadataPassthrough != after.EnableMetadataPassthrough {
|
||||
changed = append(changed, "enable_metadata_passthrough")
|
||||
}
|
||||
|
||||
@@ -243,6 +243,7 @@ type UpdateSettingsRequest struct {
|
||||
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
||||
|
||||
// Gateway forwarding behavior
|
||||
OpenAITTFTMode *string `json:"openai_ttft_mode"`
|
||||
EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
|
||||
EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
|
||||
EnableCCHSigning *bool `json:"enable_cch_signing"`
|
||||
@@ -1683,6 +1684,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
return previousSettings.EnableFingerprintUnification
|
||||
}(),
|
||||
OpenAITTFTMode: func() string {
|
||||
if req.OpenAITTFTMode != nil {
|
||||
return *req.OpenAITTFTMode
|
||||
}
|
||||
return previousSettings.OpenAITTFTMode
|
||||
}(),
|
||||
EnableMetadataPassthrough: func() bool {
|
||||
if req.EnableMetadataPassthrough != nil {
|
||||
return *req.EnableMetadataPassthrough
|
||||
|
||||
@@ -201,6 +201,7 @@ type SystemSettings struct {
|
||||
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
||||
|
||||
// Gateway forwarding behavior
|
||||
OpenAITTFTMode string `json:"openai_ttft_mode"`
|
||||
EnableFingerprintUnification bool `json:"enable_fingerprint_unification"`
|
||||
EnableMetadataPassthrough bool `json:"enable_metadata_passthrough"`
|
||||
EnableCCHSigning bool `json:"enable_cch_signing"`
|
||||
|
||||
@@ -922,6 +922,7 @@ func TestAPIContracts(t *testing.T) {
|
||||
"payment_visible_method_wxpay_enabled": false,
|
||||
"openai_low_upstream_rate_priority_enabled": true,
|
||||
"openai_oauth_scheduling_rate_multiplier": 0.05,
|
||||
"openai_ttft_mode": "semantic",
|
||||
"openai_advanced_scheduler_enabled": true,
|
||||
"openai_advanced_scheduler_sticky_weighted_enabled": false,
|
||||
"openai_advanced_scheduler_subscription_priority_enabled": false,
|
||||
@@ -1236,6 +1237,7 @@ func TestAPIContracts(t *testing.T) {
|
||||
"payment_visible_method_wxpay_enabled": false,
|
||||
"openai_low_upstream_rate_priority_enabled": false,
|
||||
"openai_oauth_scheduling_rate_multiplier": 1,
|
||||
"openai_ttft_mode": "semantic",
|
||||
"openai_advanced_scheduler_enabled": false,
|
||||
"openai_advanced_scheduler_sticky_weighted_enabled": false,
|
||||
"openai_advanced_scheduler_subscription_priority_enabled": false,
|
||||
|
||||
@@ -620,6 +620,10 @@ const (
|
||||
SettingKeyBackendModeEnabled = "backend_mode_enabled"
|
||||
|
||||
// Gateway Forwarding Behavior
|
||||
// SettingKeyOpenAITTFTMode 控制 first_token_ms 的统计口径。
|
||||
SettingKeyOpenAITTFTMode = "openai_ttft_mode"
|
||||
OpenAITTFTModeSemantic = "semantic"
|
||||
OpenAITTFTModeVisible = "visible"
|
||||
// SettingKeyEnableFingerprintUnification 是否统一 OAuth 账号的 X-Stainless-* 指纹头(默认 true)
|
||||
SettingKeyEnableFingerprintUnification = "enable_fingerprint_unification"
|
||||
// SettingKeyEnableMetadataPassthrough 是否透传客户端原始 metadata.user_id(默认 false)
|
||||
|
||||
@@ -403,6 +403,16 @@ func assertOpenAINativeLargeOpenEventTimesOutWithoutLeak(t *testing.T, line stri
|
||||
}
|
||||
|
||||
func TestOpenAINativeFirstOutputEOFDispatchesTerminalEventWithoutBlankLine(t *testing.T) {
|
||||
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
|
||||
openAITTFTMode: OpenAITTFTModeVisible,
|
||||
expiresAt: time.Now().Add(time.Minute).UnixNano(),
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
|
||||
openAITTFTMode: OpenAITTFTModeSemantic,
|
||||
expiresAt: time.Now().Add(time.Minute).UnixNano(),
|
||||
})
|
||||
})
|
||||
cfg := &config.Config{Gateway: config.GatewayConfig{
|
||||
OpenAIFirstOutputTimeoutSeconds: 1,
|
||||
MaxLineSize: defaultMaxLineSize,
|
||||
|
||||
@@ -1231,6 +1231,47 @@ func openAIStreamDataStartsVisibleOutput(data, eventType string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// openAIStreamDataStartsSemanticTTFT 保留 900194fab 之前的 first_token_ms
|
||||
// 口径:跳过 Responses preamble 后,首个语义 SSE 事件即视为首 token。
|
||||
func openAIStreamDataStartsSemanticTTFT(data, eventType string) bool {
|
||||
trimmed := strings.TrimSpace(data)
|
||||
if trimmed == "" || trimmed == "[DONE]" {
|
||||
return false
|
||||
}
|
||||
eventType = strings.TrimSpace(eventType)
|
||||
if eventType == "" && gjson.Valid(trimmed) {
|
||||
eventType = strings.TrimSpace(gjson.Get(trimmed, "type").String())
|
||||
}
|
||||
switch eventType {
|
||||
case "response.failed":
|
||||
return false
|
||||
case "error":
|
||||
payload := []byte(trimmed)
|
||||
return !openAIStreamFailedEventShouldFailover(payload, extractOpenAISSEErrorMessage(payload))
|
||||
default:
|
||||
return !openAIStreamEventIsPreamble(eventType)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) openAITTFTMode(ctx context.Context) string {
|
||||
mode := OpenAITTFTModeSemantic
|
||||
if s != nil && s.settingService != nil {
|
||||
mode = s.settingService.GetOpenAITTFTMode(ctx)
|
||||
} else if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil {
|
||||
if cached.expiresAt == 0 || time.Now().UnixNano() < cached.expiresAt {
|
||||
mode = normalizeOpenAITTFTMode(cached.openAITTFTMode)
|
||||
}
|
||||
}
|
||||
return normalizeOpenAITTFTMode(mode)
|
||||
}
|
||||
|
||||
func openAIStreamDataStartsTTFT(data, eventType string, forceOutput bool, mode string) bool {
|
||||
if mode == OpenAITTFTModeVisible {
|
||||
return openAIStreamDataStartsVisibleOutput(data, eventType)
|
||||
}
|
||||
return forceOutput || openAIStreamDataStartsSemanticTTFT(data, eventType)
|
||||
}
|
||||
|
||||
// openAIStreamFailedEventErrorCode 提取流内 failed 事件的错误码(小写),
|
||||
// 兼容 response.failed 的嵌套形态与裸 error 形态。
|
||||
func openAIStreamFailedEventErrorCode(payload []byte) string {
|
||||
@@ -1806,6 +1847,7 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
|
||||
imageCounter := newOpenAIImageOutputCounter()
|
||||
var firstTokenMs *int
|
||||
responseID := ""
|
||||
ttftMode := s.openAITTFTMode(ctx)
|
||||
clientDisconnected := false
|
||||
sawDone := false
|
||||
sawTerminalEvent := false
|
||||
@@ -2096,7 +2138,7 @@ func (s *OpenAIGatewayService) handleStreamingResponsePassthrough(
|
||||
openAIResponsesCompletedEventIsEmpty(dataBytes, usage) {
|
||||
return resultWithUsage(), newOpenAIResponsesEmptyCompletedFailoverError(c, account, upstreamRequestID)
|
||||
}
|
||||
if firstTokenMs == nil && openAIStreamDataStartsVisibleOutput(trimmedData, eventType) {
|
||||
if firstTokenMs == nil && openAIStreamDataStartsTTFT(trimmedData, eventType, forceFlushFailedEvent, ttftMode) {
|
||||
ms := int(time.Since(startTime).Milliseconds())
|
||||
firstTokenMs = &ms
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ func (s *OpenAIGatewayService) handleStreamingResponseWithReasoning(ctx context.
|
||||
maxLineSize = s.cfg.Gateway.MaxLineSize
|
||||
}
|
||||
var firstTokenMs *int
|
||||
ttftMode := s.openAITTFTMode(ctx)
|
||||
firstOutputProgressObserved := false
|
||||
bufferedWriter := bufio.NewWriterSize(w, 4*1024)
|
||||
var firstOutputStage *openAIFirstOutputStage
|
||||
@@ -263,7 +264,7 @@ func (s *OpenAIGatewayService) handleStreamingResponseWithReasoning(ctx context.
|
||||
pendingSSEEventType := ""
|
||||
eventInProgress := false
|
||||
eventStartsClientOutput := false
|
||||
eventStartsVisibleOutput := false
|
||||
eventStartsTTFTOutput := false
|
||||
eventShouldFlush := false
|
||||
handlePendingWriteError := func(err error) {
|
||||
if firstOutputStage != nil && !firstOutputStage.closed {
|
||||
@@ -283,7 +284,7 @@ func (s *OpenAIGatewayService) handleStreamingResponseWithReasoning(ctx context.
|
||||
}
|
||||
completeGuardedEvent := func(queueDrained bool) {
|
||||
completedProgressEvent := eventStartsClientOutput
|
||||
completedVisibleEvent := eventStartsVisibleOutput
|
||||
completedTTFTEvent := eventStartsTTFTOutput
|
||||
shouldFlush := eventShouldFlush || (queueDrained && clientOutputStarted)
|
||||
eventInProgress = false
|
||||
if !clientDisconnected {
|
||||
@@ -305,12 +306,12 @@ func (s *OpenAIGatewayService) handleStreamingResponseWithReasoning(ctx context.
|
||||
firstOutputProgressObserved = true
|
||||
stopFirstOutputTimer()
|
||||
}
|
||||
if completedVisibleEvent && firstTokenMs == nil {
|
||||
if completedTTFTEvent && firstTokenMs == nil {
|
||||
ms := int(time.Since(startTime).Milliseconds())
|
||||
firstTokenMs = &ms
|
||||
}
|
||||
eventStartsClientOutput = false
|
||||
eventStartsVisibleOutput = false
|
||||
eventStartsTTFTOutput = false
|
||||
eventShouldFlush = false
|
||||
}
|
||||
sendErrorEvent := func(reason string) {
|
||||
@@ -662,9 +663,10 @@ func (s *OpenAIGatewayService) handleStreamingResponseWithReasoning(ctx context.
|
||||
}
|
||||
startsClientOutput := forceFlushFailedEvent || openAIStreamDataStartsClientOutput(data, eventType)
|
||||
startsVisibleOutput := openAIStreamDataStartsVisibleOutput(data, eventType)
|
||||
startsTTFTOutput := openAIStreamDataStartsTTFT(data, eventType, forceFlushFailedEvent, ttftMode)
|
||||
if stageFirstOutput {
|
||||
eventStartsClientOutput = eventStartsClientOutput || startsClientOutput
|
||||
eventStartsVisibleOutput = eventStartsVisibleOutput || startsVisibleOutput
|
||||
eventStartsTTFTOutput = eventStartsTTFTOutput || startsTTFTOutput
|
||||
if startsClientOutput {
|
||||
firstOutputScanGuard.Store(false)
|
||||
}
|
||||
@@ -703,7 +705,7 @@ func (s *OpenAIGatewayService) handleStreamingResponseWithReasoning(ctx context.
|
||||
}
|
||||
|
||||
// Record first token time
|
||||
if !guardFirstOutput && firstTokenMs == nil && startsVisibleOutput {
|
||||
if !guardFirstOutput && firstTokenMs == nil && startsTTFTOutput {
|
||||
ms := int(time.Since(startTime).Milliseconds())
|
||||
firstTokenMs = &ms
|
||||
stopFirstOutputTimer()
|
||||
@@ -720,7 +722,7 @@ func (s *OpenAIGatewayService) handleStreamingResponseWithReasoning(ctx context.
|
||||
terminalFailurePending = false
|
||||
eventInProgress = false
|
||||
eventStartsClientOutput = false
|
||||
eventStartsVisibleOutput = false
|
||||
eventStartsTTFTOutput = false
|
||||
eventShouldFlush = false
|
||||
return
|
||||
}
|
||||
@@ -728,7 +730,7 @@ func (s *OpenAIGatewayService) handleStreamingResponseWithReasoning(ctx context.
|
||||
terminalFailurePending = false
|
||||
eventInProgress = false
|
||||
eventStartsClientOutput = false
|
||||
eventStartsVisibleOutput = false
|
||||
eventStartsTTFTOutput = false
|
||||
eventShouldFlush = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestOpenAIResponsesTTFTStartsAtVisibleOutput(t *testing.T) {
|
||||
name = "passthrough"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result := runSyntheticVisibleTTFTStream(t, passthrough, 120*time.Millisecond, 0,
|
||||
result := runSyntheticVisibleTTFTStream(t, passthrough, 120*time.Millisecond, 0, OpenAITTFTModeVisible,
|
||||
`{"type":"response.output_text.delta","delta":"test output"}`)
|
||||
require.NotNil(t, result.firstTokenMs)
|
||||
require.GreaterOrEqual(t, *result.firstTokenMs, 100)
|
||||
@@ -63,7 +63,7 @@ func TestOpenAIResponsesTTFTStartsAtCompletedImage(t *testing.T) {
|
||||
name = "passthrough"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result := runSyntheticVisibleTTFTStream(t, passthrough, 120*time.Millisecond, 0,
|
||||
result := runSyntheticVisibleTTFTStream(t, passthrough, 120*time.Millisecond, 0, OpenAITTFTModeVisible,
|
||||
`{"type":"response.output_item.done","item":{"id":"item_test","type":"image_generation_call","result":"dGVzdA=="}}`)
|
||||
require.NotNil(t, result.firstTokenMs)
|
||||
require.GreaterOrEqual(t, *result.firstTokenMs, 100)
|
||||
@@ -105,9 +105,32 @@ func TestOpenAINativeMetadataDoesNotDisarmFirstOutputTimeout(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func runSyntheticVisibleTTFTStream(t *testing.T, passthrough bool, visibleDelay time.Duration, timeoutSeconds int, visibleEvent string) *openaiStreamingResult {
|
||||
func TestOpenAIResponsesTTFTDefaultsToSemanticOutput(t *testing.T) {
|
||||
for _, passthrough := range []bool{false, true} {
|
||||
name := "native"
|
||||
if passthrough {
|
||||
name = "passthrough"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result := runSyntheticVisibleTTFTStream(t, passthrough, 120*time.Millisecond, 0, "",
|
||||
`{"type":"response.output_text.delta","delta":"test output"}`)
|
||||
require.NotNil(t, result.firstTokenMs)
|
||||
require.Less(t, *result.firstTokenMs, 100)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runSyntheticVisibleTTFTStream(t *testing.T, passthrough bool, visibleDelay time.Duration, timeoutSeconds int, ttftMode string, visibleEvent string) *openaiStreamingResult {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
mode := ttftMode
|
||||
if mode == "" {
|
||||
mode = OpenAITTFTModeSemantic
|
||||
}
|
||||
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{openAITTFTMode: mode, expiresAt: time.Now().Add(time.Minute).UnixNano()})
|
||||
t.Cleanup(func() {
|
||||
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{openAITTFTMode: OpenAITTFTModeSemantic, expiresAt: time.Now().Add(time.Minute).UnixNano()})
|
||||
})
|
||||
svc := &OpenAIGatewayService{cfg: &config.Config{Gateway: config.GatewayConfig{
|
||||
MaxLineSize: defaultMaxLineSize,
|
||||
OpenAIFirstOutputTimeoutSeconds: timeoutSeconds,
|
||||
|
||||
@@ -53,6 +53,7 @@ const backendModeDBTimeout = 5 * time.Second
|
||||
|
||||
// cachedGatewayForwardingSettings 缓存网关转发行为设置(进程内缓存,60s TTL)
|
||||
type cachedGatewayForwardingSettings struct {
|
||||
openAITTFTMode string
|
||||
fingerprintUnification bool
|
||||
metadataPassthrough bool
|
||||
cchSigning bool
|
||||
@@ -736,6 +737,7 @@ func (s *SettingService) IsBackendModeEnabled(ctx context.Context) bool {
|
||||
}
|
||||
|
||||
type gatewayForwardingSettingsResult struct {
|
||||
openAITTFTMode string
|
||||
fp, mp, cch, claudeOAuthSystemPromptInjection, cacheTTL1h, rewriteMessageCacheControl bool
|
||||
clientDatelineNormalization bool
|
||||
claudeOAuthSystemPrompt, claudeOAuthSystemPromptBlocks string
|
||||
@@ -745,6 +747,7 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
||||
if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil {
|
||||
if time.Now().UnixNano() < cached.expiresAt {
|
||||
return gatewayForwardingSettingsResult{
|
||||
openAITTFTMode: cached.openAITTFTMode,
|
||||
fp: cached.fingerprintUnification,
|
||||
mp: cached.metadataPassthrough,
|
||||
cch: cached.cchSigning,
|
||||
@@ -761,6 +764,7 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
||||
if cached, ok := gatewayForwardingCache.Load().(*cachedGatewayForwardingSettings); ok && cached != nil {
|
||||
if time.Now().UnixNano() < cached.expiresAt {
|
||||
return gatewayForwardingSettingsResult{
|
||||
openAITTFTMode: cached.openAITTFTMode,
|
||||
fp: cached.fingerprintUnification,
|
||||
mp: cached.metadataPassthrough,
|
||||
cch: cached.cchSigning,
|
||||
@@ -776,6 +780,7 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
||||
dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout)
|
||||
defer cancel()
|
||||
values, err := s.settingRepo.GetMultiple(dbCtx, []string{
|
||||
SettingKeyOpenAITTFTMode,
|
||||
SettingKeyEnableFingerprintUnification,
|
||||
SettingKeyEnableMetadataPassthrough,
|
||||
SettingKeyEnableCCHSigning,
|
||||
@@ -789,6 +794,7 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
||||
if err != nil {
|
||||
slog.Warn("failed to get gateway forwarding settings", "error", err)
|
||||
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
|
||||
openAITTFTMode: OpenAITTFTModeSemantic,
|
||||
fingerprintUnification: true,
|
||||
metadataPassthrough: false,
|
||||
cchSigning: false,
|
||||
@@ -798,8 +804,9 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
||||
clientDatelineNormalization: true,
|
||||
expiresAt: time.Now().Add(gatewayForwardingErrorTTL).UnixNano(),
|
||||
})
|
||||
return gatewayForwardingSettingsResult{fp: true, claudeOAuthSystemPromptInjection: true, rewriteMessageCacheControl: s.defaultRewriteMessageCacheControl(), clientDatelineNormalization: true}, nil
|
||||
return gatewayForwardingSettingsResult{openAITTFTMode: OpenAITTFTModeSemantic, fp: true, claudeOAuthSystemPromptInjection: true, rewriteMessageCacheControl: s.defaultRewriteMessageCacheControl(), clientDatelineNormalization: true}, nil
|
||||
}
|
||||
ttftMode := normalizeOpenAITTFTMode(values[SettingKeyOpenAITTFTMode])
|
||||
fp := true
|
||||
if v, ok := values[SettingKeyEnableFingerprintUnification]; ok && v != "" {
|
||||
fp = v == "true"
|
||||
@@ -822,6 +829,7 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
||||
clientDatelineNormalization = v == "true"
|
||||
}
|
||||
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
|
||||
openAITTFTMode: ttftMode,
|
||||
fingerprintUnification: fp,
|
||||
metadataPassthrough: mp,
|
||||
cchSigning: cch,
|
||||
@@ -834,6 +842,7 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
||||
expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
|
||||
})
|
||||
return gatewayForwardingSettingsResult{
|
||||
openAITTFTMode: ttftMode,
|
||||
fp: fp,
|
||||
mp: mp,
|
||||
cch: cch,
|
||||
@@ -851,6 +860,11 @@ func (s *SettingService) getGatewayForwardingSettingsCached(ctx context.Context)
|
||||
return gatewayForwardingSettingsResult{fp: true, claudeOAuthSystemPromptInjection: true, clientDatelineNormalization: true}
|
||||
}
|
||||
|
||||
// GetOpenAITTFTMode 返回 Responses first_token_ms 的统计口径。
|
||||
func (s *SettingService) GetOpenAITTFTMode(ctx context.Context) string {
|
||||
return s.getGatewayForwardingSettingsCached(ctx).openAITTFTMode
|
||||
}
|
||||
|
||||
// GetGatewayForwardingSettings returns cached gateway forwarding settings.
|
||||
// Uses in-process atomic.Value cache with 60s TTL, zero-lock hot path.
|
||||
// Returns (fingerprintUnification, metadataPassthrough, cchSigning).
|
||||
|
||||
@@ -846,6 +846,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
|
||||
// Gateway forwarding behavior (defaults: fingerprint=true, metadata_passthrough=false,
|
||||
// cch_signing=false, claude_oauth_system_prompt_injection=true)
|
||||
result.OpenAITTFTMode = normalizeOpenAITTFTMode(settings[SettingKeyOpenAITTFTMode])
|
||||
if v, ok := settings[SettingKeyEnableFingerprintUnification]; ok && v != "" {
|
||||
result.EnableFingerprintUnification = v == "true"
|
||||
} else {
|
||||
@@ -979,6 +980,13 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeOpenAITTFTMode(mode string) string {
|
||||
if strings.EqualFold(strings.TrimSpace(mode), OpenAITTFTModeVisible) {
|
||||
return OpenAITTFTModeVisible
|
||||
}
|
||||
return OpenAITTFTModeSemantic
|
||||
}
|
||||
|
||||
func clampAffiliateRebateRate(value float64) float64 {
|
||||
if math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return AffiliateRebateRateDefault
|
||||
|
||||
@@ -460,6 +460,11 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
||||
updates[SettingKeyBackendModeEnabled] = strconv.FormatBool(settings.BackendModeEnabled)
|
||||
|
||||
// Gateway forwarding behavior
|
||||
mode := normalizeOpenAITTFTMode(settings.OpenAITTFTMode)
|
||||
if strings.TrimSpace(settings.OpenAITTFTMode) != "" && strings.ToLower(strings.TrimSpace(settings.OpenAITTFTMode)) != OpenAITTFTModeSemantic && strings.ToLower(strings.TrimSpace(settings.OpenAITTFTMode)) != OpenAITTFTModeVisible {
|
||||
return nil, fmt.Errorf("%s must be one of: %s/%s", SettingKeyOpenAITTFTMode, OpenAITTFTModeSemantic, OpenAITTFTModeVisible)
|
||||
}
|
||||
updates[SettingKeyOpenAITTFTMode] = mode
|
||||
updates[SettingKeyEnableFingerprintUnification] = strconv.FormatBool(settings.EnableFingerprintUnification)
|
||||
updates[SettingKeyEnableMetadataPassthrough] = strconv.FormatBool(settings.EnableMetadataPassthrough)
|
||||
updates[SettingKeyEnableCCHSigning] = strconv.FormatBool(settings.EnableCCHSigning)
|
||||
@@ -697,6 +702,7 @@ func (s *SettingService) refreshCachedSettings(settings *SystemSettings) {
|
||||
})
|
||||
gatewayForwardingSF.Forget("gateway_forwarding")
|
||||
gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
|
||||
openAITTFTMode: normalizeOpenAITTFTMode(settings.OpenAITTFTMode),
|
||||
fingerprintUnification: settings.EnableFingerprintUnification,
|
||||
metadataPassthrough: settings.EnableMetadataPassthrough,
|
||||
cchSigning: settings.EnableCCHSigning,
|
||||
|
||||
@@ -228,6 +228,7 @@ type SystemSettings struct {
|
||||
BackendModeEnabled bool
|
||||
|
||||
// Gateway forwarding behavior
|
||||
OpenAITTFTMode string // Responses first_token_ms 统计口径(默认 semantic)
|
||||
EnableFingerprintUnification bool // 是否统一 OAuth 账号的指纹头(默认 true)
|
||||
EnableMetadataPassthrough bool // 是否透传客户端原始 metadata(默认 false)
|
||||
EnableCCHSigning bool // 已废弃 no-op:新版 CLI 取消 cch 签名后网关不再注入/签名 cch,开关无效果
|
||||
|
||||
@@ -620,6 +620,7 @@ export interface SystemSettings {
|
||||
allow_ungrouped_key_scheduling: boolean;
|
||||
|
||||
// Gateway forwarding behavior
|
||||
openai_ttft_mode: string;
|
||||
enable_fingerprint_unification: boolean;
|
||||
enable_metadata_passthrough: boolean;
|
||||
enable_cch_signing: boolean;
|
||||
@@ -935,6 +936,7 @@ export interface UpdateSettingsRequest {
|
||||
min_claude_code_version?: string;
|
||||
max_claude_code_version?: string;
|
||||
allow_ungrouped_key_scheduling?: boolean;
|
||||
openai_ttft_mode?: string;
|
||||
enable_fingerprint_unification?: boolean;
|
||||
enable_metadata_passthrough?: boolean;
|
||||
enable_cch_signing?: boolean;
|
||||
|
||||
@@ -473,6 +473,10 @@ export default {
|
||||
grokBaseURLModeUSEast1: 'Regional API (us-east-1)',
|
||||
grokBaseURLModeUSWest2: 'Regional API (us-west-2)',
|
||||
grokBaseURLModeEUWest1: 'Regional API (eu-west-1)',
|
||||
openaiTTFTMode: 'OpenAI Responses first-token metric',
|
||||
openaiTTFTModeSemantic: 'Legacy-compatible (semantic event)',
|
||||
openaiTTFTModeVisible: 'Actual visible output',
|
||||
openaiTTFTModeHint: 'The default records first_token_ms at the first non-preamble semantic event. Actual visible output records it only when non-empty text, tool arguments, or image content arrives.',
|
||||
fingerprintUnification: 'Fingerprint Unification',
|
||||
fingerprintUnificationHint: 'Unify X-Stainless-* headers across users sharing the same OAuth account. Disabling passes through each client\'s original headers.',
|
||||
metadataPassthrough: 'Metadata Passthrough',
|
||||
|
||||
@@ -466,6 +466,10 @@ export default {
|
||||
grokBaseURLModeUSEast1: '区域 API(us-east-1)',
|
||||
grokBaseURLModeUSWest2: '区域 API(us-west-2)',
|
||||
grokBaseURLModeEUWest1: '区域 API(eu-west-1)',
|
||||
openaiTTFTMode: 'OpenAI Responses 首 token 统计口径',
|
||||
openaiTTFTModeSemantic: '历史兼容(语义事件)',
|
||||
openaiTTFTModeVisible: '真实可见输出',
|
||||
openaiTTFTModeHint: '默认使用历史兼容口径,首个非预置语义事件即记录 first_token_ms。选择真实可见输出后,仅在首个非空文本、工具参数或图片内容到达时记录。',
|
||||
fingerprintUnification: '指纹统一化',
|
||||
fingerprintUnificationHint: '统一共享同一 OAuth 账号的用户的 X-Stainless-* 请求头。关闭后透传客户端原始请求头。',
|
||||
metadataPassthrough: 'Metadata 透传',
|
||||
|
||||
@@ -5268,6 +5268,32 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI Responses 首 token 统计 -->
|
||||
<div class="border-b border-gray-100 pb-5 dark:border-dark-700 md:col-span-2">
|
||||
<label
|
||||
for="openai-ttft-mode"
|
||||
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.openaiTTFTMode") }}
|
||||
</label>
|
||||
<select
|
||||
id="openai-ttft-mode"
|
||||
v-model="form.openai_ttft_mode"
|
||||
class="input mt-2 w-full"
|
||||
data-testid="openai-ttft-mode"
|
||||
>
|
||||
<option value="semantic">
|
||||
{{ t("admin.settings.gatewayForwarding.openaiTTFTModeSemantic") }}
|
||||
</option>
|
||||
<option value="visible">
|
||||
{{ t("admin.settings.gatewayForwarding.openaiTTFTModeVisible") }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.gatewayForwarding.openaiTTFTModeHint") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Fingerprint Unification -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -9735,6 +9761,7 @@ const form = reactive<SettingsForm>({
|
||||
openai_advanced_scheduler_weight_previous_response: "",
|
||||
openai_advanced_scheduler_weight_session_sticky: "",
|
||||
// Gateway forwarding behavior
|
||||
openai_ttft_mode: "semantic",
|
||||
enable_fingerprint_unification: true,
|
||||
enable_metadata_passthrough: false,
|
||||
enable_cch_signing: false,
|
||||
@@ -11308,6 +11335,8 @@ async function saveSettings() {
|
||||
min_claude_code_version: form.min_claude_code_version,
|
||||
max_claude_code_version: form.max_claude_code_version,
|
||||
allow_ungrouped_key_scheduling: form.allow_ungrouped_key_scheduling,
|
||||
openai_ttft_mode:
|
||||
form.openai_ttft_mode === "visible" ? "visible" : "semantic",
|
||||
enable_fingerprint_unification: form.enable_fingerprint_unification,
|
||||
enable_metadata_passthrough: form.enable_metadata_passthrough,
|
||||
enable_cch_signing: form.enable_cch_signing,
|
||||
|
||||
@@ -464,6 +464,7 @@ const baseSettingsResponse = {
|
||||
min_claude_code_version: "",
|
||||
max_claude_code_version: "",
|
||||
allow_ungrouped_key_scheduling: false,
|
||||
openai_ttft_mode: "semantic",
|
||||
enable_fingerprint_unification: true,
|
||||
enable_metadata_passthrough: false,
|
||||
enable_cch_signing: false,
|
||||
@@ -1363,6 +1364,27 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
expect(payload.grok_cross_client_model_map_enabled).toBe(false);
|
||||
});
|
||||
|
||||
it("loads and saves the OpenAI Responses first-token metric mode", async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...baseSettingsResponse,
|
||||
openai_ttft_mode: "visible",
|
||||
});
|
||||
const wrapper = mountView();
|
||||
|
||||
await flushPromises();
|
||||
await openGatewayTab(wrapper);
|
||||
|
||||
const modeSelect = wrapper.get('[data-testid="openai-ttft-mode"]');
|
||||
expect((modeSelect.element as HTMLSelectElement).value).toBe("visible");
|
||||
|
||||
await modeSelect.setValue("semantic");
|
||||
await wrapper.find("form").trigger("submit.prevent");
|
||||
await flushPromises();
|
||||
|
||||
const payload = updateSettings.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expect(payload.openai_ttft_mode).toBe("semantic");
|
||||
});
|
||||
|
||||
it("loads fail-safe-off Ollama Cloud usage refresh settings and saves an explicit opt-in", async () => {
|
||||
const wrapper = mountView();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user