Merge remote-tracking branch 'upstream/main' into fix/openai-responses-compatibility

# Conflicts:
#	backend/internal/handler/ops_error_logger.go
This commit is contained in:
IanShaw
2026-08-20 00:31:12 -07:00
36 changed files with 1582 additions and 107 deletions
+5 -1
View File
@@ -131,7 +131,11 @@ func classifyNoAccountErrorFromGin(
if c != nil && c.Request != nil {
ctx = c.Request.Context()
}
return classifyNoAccountError(ctx, diag, apiKey, routingModel, displayModel, platform)
classification := classifyNoAccountError(ctx, diag, apiKey, routingModel, displayModel, platform)
if classification.ModelNotFound {
service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalModelConfiguration)
}
return classification
}
func classifyOpenAICompatibleNoAccountErrorFromGin(
@@ -128,6 +128,8 @@ func TestClassifyNoAccountError_ModelNotSupported_Returns404(t *testing.T) {
require.Equal(t, service.PlatformOpenAI, fd.calls[0].Platform)
require.NotNil(t, fd.calls[0].GroupID)
require.Equal(t, int64(42), *fd.calls[0].GroupID)
require.True(t, service.HasOpsClientBusinessLimited(c))
require.Equal(t, service.OpsClientBusinessLimitedReasonLocalModelConfiguration, service.OpsClientBusinessLimitedReason(c))
}
func TestClassifyOpenAICompatibleNoAccountError_GrokUsesGrokPlatform(t *testing.T) {
@@ -149,6 +151,8 @@ func TestClassifyOpenAICompatibleNoAccountError_GrokUsesGrokPlatform(t *testing.
require.True(t, cls.ModelNotFound)
require.Len(t, fd.calls, 1)
require.Equal(t, service.PlatformGrok, fd.calls[0].Platform)
require.True(t, service.HasOpsClientBusinessLimited(c))
require.Equal(t, service.OpsClientBusinessLimitedReasonLocalModelConfiguration, service.OpsClientBusinessLimitedReason(c))
logErr := openAICompatibleSelectionErrorForLog(
fmt.Errorf("no available OpenAI accounts supporting model: grok-4.5"),
@@ -157,6 +161,18 @@ func TestClassifyOpenAICompatibleNoAccountError_GrokUsesGrokPlatform(t *testing.
require.EqualError(t, logErr, "no available Grok accounts supporting model: grok-4.5")
}
func TestClassifyNoAccountError_PureClassifierDoesNotMarkGinContext(t *testing.T) {
c := newTestGinContextWithRequest()
fd := &fakeDiagnoser{resp: service.ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: false}}
apiKey := &service.APIKey{GroupID: ptrInt64(7)}
cls := classifyNoAccountError(c.Request.Context(), fd, apiKey, "gpt-5", "gpt-5", service.PlatformOpenAI)
require.True(t, cls.ModelNotFound)
require.False(t, service.HasOpsClientBusinessLimited(c))
require.Empty(t, service.OpsClientBusinessLimitedReason(c))
}
func TestClassifyNoAccountError_HasModelSupport_KeepsRoutingMessageGenerationToCaller(t *testing.T) {
c := newTestGinContextWithRequest()
fd := &fakeDiagnoser{resp: service.ModelAvailabilityDiagnosis{HasAccountsInPool: true, HasModelSupport: true}}
@@ -215,4 +231,6 @@ func TestClassifyNoAccountError_FromGin_NilContextStillSafe(t *testing.T) {
require.Equal(t, http.StatusNotFound, cls.Status, "even with a nil gin context the classifier must still run and yield a coherent response")
require.True(t, cls.ModelNotFound)
require.False(t, service.HasOpsClientBusinessLimited(nil))
require.Empty(t, service.OpsClientBusinessLimitedReason(nil))
}
+23 -5
View File
@@ -1186,6 +1186,7 @@ func OpsErrorLoggerMiddleware(ops *service.OpsService) gin.HandlerFunc {
entry.UpstreamStatusCode = &finalStatus
}
}
suppressOpsUpstreamAttributionForLocalModelConfiguration(c, entry)
if apiKey != nil {
entry.APIKeyID = &apiKey.ID
@@ -1531,6 +1532,19 @@ func applyOpsUpstreamFieldsFromContext(c *gin.Context, entry *service.OpsInsertE
}
}
func suppressOpsUpstreamAttributionForLocalModelConfiguration(c *gin.Context, entry *service.OpsInsertErrorLogInput) {
if entry == nil || !service.HasOpsClientBusinessLimited(c) || service.OpsClientBusinessLimitedReason(c) != service.OpsClientBusinessLimitedReasonLocalModelConfiguration {
return
}
entry.AccountID = nil
entry.UpstreamEndpoint = ""
entry.UpstreamModel = ""
entry.UpstreamStatusCode = nil
entry.UpstreamErrorMessage = nil
entry.UpstreamErrorDetail = nil
entry.UpstreamErrors = nil
}
func getContextLatencyMs(c *gin.Context, key string) *int64 {
if c == nil || strings.TrimSpace(key) == "" {
return nil
@@ -1990,23 +2004,27 @@ func classifyOpsErrorLog(c *gin.Context, errType, message, code string, status i
phase = classifyOpsPhase(errType, message, code)
routingCapacityLimited := isOpsRoutingCapacityLimited(c)
clientBusinessLimited := service.HasOpsClientBusinessLimited(c)
localModelConfiguration := clientBusinessLimited && service.OpsClientBusinessLimitedReason(c) == service.OpsClientBusinessLimitedReasonLocalModelConfiguration
upstreamError := hasOpsUpstreamErrorContext(c)
accountAuthFailure := hasOpsAccountAuthFailure(c)
if accountAuthFailure && !routingCapacityLimited {
if localModelConfiguration {
phase = "routing"
} else if accountAuthFailure && !routingCapacityLimited {
phase = "account_auth"
} else if upstreamError && !routingCapacityLimited {
phase = "upstream"
}
if clientBusinessLimited && !upstreamError && !routingCapacityLimited {
if clientBusinessLimited && !upstreamError && !routingCapacityLimited && !localModelConfiguration {
phase = "auth"
}
if routingCapacityLimited {
phase = "routing"
}
msg := strings.ToLower(message)
localClientAuthError := !upstreamError && phase == "auth" && isOpsClientAuthError(code, msg)
localBusinessLimited := !upstreamError && classifyOpsIsBusinessLimited(errType, phase, code, status, message, localClientAuthError)
isBusinessLimited = routingCapacityLimited || (clientBusinessLimited && !upstreamError) || localBusinessLimited
effectiveUpstreamError := upstreamError && !localModelConfiguration
localClientAuthError := !effectiveUpstreamError && phase == "auth" && isOpsClientAuthError(code, msg)
localBusinessLimited := !effectiveUpstreamError && classifyOpsIsBusinessLimited(errType, phase, code, status, message, localClientAuthError)
isBusinessLimited = localModelConfiguration || routingCapacityLimited || (clientBusinessLimited && !effectiveUpstreamError) || localBusinessLimited
errorOwner = classifyOpsErrorOwner(phase, message)
errorSource = classifyOpsErrorSource(phase, message)
return phase, isBusinessLimited, errorOwner, errorSource
@@ -754,6 +754,107 @@ func TestClassifyOpsRoutingCapacityMarkerExcludesMaskedSelectionFailureFromSLA(t
require.Equal(t, "gateway", errorSource)
}
func TestClassifyOpsLocalModelConfigurationRejection(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalModelConfiguration)
phase, isBusinessLimited, errorOwner, errorSource := classifyOpsErrorLog(
c,
"model_not_found",
"Model \"gpt-missing\" is not supported by any configured account in this group",
"",
http.StatusNotFound,
)
require.Equal(t, "routing", phase)
require.True(t, isBusinessLimited)
require.Equal(t, "platform", errorOwner)
require.Equal(t, "gateway", errorSource)
}
func TestClassifyOpsLocalModelConfigurationOverridesStaleUpstreamMarkers(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalModelConfiguration)
c.Set(service.OpsUpstreamStatusCodeKey, http.StatusUnauthorized)
c.Set(service.OpsUpstreamErrorsKey, []*service.OpsUpstreamErrorEvent{{
Stage: string(service.GatewayFailureStageAccountAuth),
UpstreamStatusCode: http.StatusUnauthorized,
}})
phase, limited, owner, source := classifyOpsErrorLog(c, "model_not_found", "unsupported configured model", "", http.StatusNotFound)
require.Equal(t, "routing", phase)
require.True(t, limited)
require.Equal(t, "platform", owner)
require.Equal(t, "gateway", source)
}
func TestClassifyOpsLocalModelConfigurationRequiresMarkerAndReason(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set(service.OpsClientBusinessLimitedReasonKey, service.OpsClientBusinessLimitedReasonLocalModelConfiguration)
c.Set(service.OpsUpstreamStatusCodeKey, http.StatusBadGateway)
phase, limited, owner, source := classifyOpsErrorLog(c, "upstream_error", "provider failed", "", http.StatusBadGateway)
require.Equal(t, "upstream", phase)
require.False(t, limited)
require.Equal(t, "provider", owner)
require.Equal(t, "upstream_http", source)
}
func TestOpsErrorLoggerMiddleware_LocalModelConfigurationFields(t *testing.T) {
setupOpsErrorLogTestQueue(t, 1)
gin.SetMode(gin.TestMode)
ops := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
router := gin.New()
router.Use(OpsErrorLoggerMiddleware(ops))
router.POST("/v1/chat/completions", func(c *gin.Context) {
service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalModelConfiguration)
c.Set(opsAccountIDKey, int64(99))
c.Set(opsUpstreamModelKey, "stale-upstream-model")
setActualUpstreamEndpoint(c, "/v1/chat/completions")
c.Set(service.OpsUpstreamStatusCodeKey, http.StatusUnauthorized)
c.Set(service.OpsUpstreamErrorMessageKey, "stale upstream error")
c.Set(service.OpsUpstreamErrorDetailKey, "stale upstream detail")
c.Set(service.OpsUpstreamErrorsKey, []*service.OpsUpstreamErrorEvent{{
Stage: string(service.GatewayFailureStageAccountAuth),
UpstreamStatusCode: http.StatusUnauthorized,
Message: "stale auth failure",
}})
c.JSON(http.StatusNotFound, gin.H{
"error": gin.H{
"type": "model_not_found",
"message": "Model \"gpt-missing\" is not supported by any configured account in this group",
},
})
})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusNotFound, w.Code)
require.JSONEq(t, `{"error":{"type":"model_not_found","message":"Model \"gpt-missing\" is not supported by any configured account in this group"}}`, w.Body.String())
job := <-opsErrorLogQueue
require.Equal(t, http.StatusNotFound, job.entry.StatusCode)
require.Equal(t, "routing", job.entry.ErrorPhase)
require.True(t, job.entry.IsBusinessLimited)
require.Equal(t, "platform", job.entry.ErrorOwner)
require.Equal(t, "gateway", job.entry.ErrorSource)
require.Nil(t, job.entry.AccountID)
require.Nil(t, job.entry.UpstreamStatusCode)
require.Nil(t, job.entry.UpstreamErrors)
require.Nil(t, job.entry.UpstreamErrorMessage)
require.Nil(t, job.entry.UpstreamErrorDetail)
require.Empty(t, job.entry.UpstreamModel)
require.Empty(t, job.entry.UpstreamEndpoint)
}
func TestClassifyOpsAuthClientErrorsExcludedFromSLA(t *testing.T) {
tests := []struct {
name string