diff --git a/backend/internal/handler/ops_error_logger.go b/backend/internal/handler/ops_error_logger.go index 0f70abcdd0..74bec3135f 100644 --- a/backend/internal/handler/ops_error_logger.go +++ b/backend/internal/handler/ops_error_logger.go @@ -1766,7 +1766,7 @@ func sanitizeOpsSSEDataForPersistence(body []byte) string { out.Grow(len(body)) for frameIndex, frame := range frames { if frameIndex > 0 { - out.WriteString("\n\n") + _, _ = out.WriteString("\n\n") } _, payload := parseOpsSSEFrameEnvelope(frame) trimmedPayload := bytes.TrimSpace(payload) @@ -1779,7 +1779,7 @@ func sanitizeOpsSSEDataForPersistence(body []byte) string { replacement = `{"payload_truncated":true}` } if replacement == "" { - out.Write(frame) + _, _ = out.Write(frame) continue } wroteData := false @@ -1794,9 +1794,9 @@ func sanitizeOpsSSEDataForPersistence(body []byte) string { wroteData = true } if emittedLine { - out.WriteByte('\n') + _ = out.WriteByte('\n') } - out.Write(line) + _, _ = out.Write(line) emittedLine = true } } diff --git a/backend/internal/service/openai_account_runtime_block_fastpath.go b/backend/internal/service/openai_account_runtime_block_fastpath.go index 54c73d1a0e..a25d78c697 100644 --- a/backend/internal/service/openai_account_runtime_block_fastpath.go +++ b/backend/internal/service/openai_account_runtime_block_fastpath.go @@ -18,7 +18,6 @@ const ( openAIOAuth429MaxAccountAttempts = 3 openAIStopSchedulingBridgeCooldown = 2 * time.Minute openAIOAuth429StormWindow = 10 * time.Second - openAIOAuth429StormThreshold = 20 openAIOAuth429StormMaxAccountSwitches = 1 ) @@ -426,17 +425,6 @@ func (s *OpenAIGatewayService) recordOpenAIOAuth429() { s.openaiOAuth429WindowCount.Add(1) } -func (s *OpenAIGatewayService) isOpenAIOAuth429Storm() bool { - if s == nil { - return false - } - windowStart := s.openaiOAuth429WindowStartUnixNano.Load() - if windowStart == 0 || time.Since(time.Unix(0, windowStart)) >= openAIOAuth429StormWindow { - return false - } - return s.openaiOAuth429WindowCount.Load() >= openAIOAuth429StormThreshold -} - func (s *OpenAIGatewayService) ShouldStopOpenAIOAuth429Failover(account *Account, statusCode int, failedSwitches int, state *OpenAIOAuth429FailoverState) bool { if failedSwitches < openAIOAuth429StormMaxAccountSwitches { return false diff --git a/backend/internal/service/openai_codex_tool_names_test.go b/backend/internal/service/openai_codex_tool_names_test.go index d8981dce98..d6cb9eafa1 100644 --- a/backend/internal/service/openai_codex_tool_names_test.go +++ b/backend/internal/service/openai_codex_tool_names_test.go @@ -30,11 +30,31 @@ func TestAliasOpenAIOAuthReservedToolNames_RewritesDeclarationsAndReferences(t * require.NoError(t, err) require.True(t, changed) require.Equal(t, "python", reverse[codexPythonToolAlias]) - require.Equal(t, codexPythonToolAlias, reqBody["tools"].([]any)[0].(map[string]any)["name"]) - require.Equal(t, codexPythonToolAlias, reqBody["tool_choice"].(map[string]any)["name"]) - require.Equal(t, codexPythonToolAlias, reqBody["input"].([]any)[0].(map[string]any)["name"]) - nested := reqBody["input"].([]any)[1].(map[string]any)["tools"].([]any)[0].(map[string]any)["function"].(map[string]any) - require.Equal(t, codexPythonToolAlias, nested["name"]) + tools, ok := reqBody["tools"].([]any) + require.True(t, ok) + require.NotEmpty(t, tools) + firstTool, ok := tools[0].(map[string]any) + require.True(t, ok) + require.Equal(t, codexPythonToolAlias, firstTool["name"]) + toolChoice, ok := reqBody["tool_choice"].(map[string]any) + require.True(t, ok) + require.Equal(t, codexPythonToolAlias, toolChoice["name"]) + input, ok := reqBody["input"].([]any) + require.True(t, ok) + require.NotEmpty(t, input) + firstInput, ok := input[0].(map[string]any) + require.True(t, ok) + require.Equal(t, codexPythonToolAlias, firstInput["name"]) + secondInput, ok := input[1].(map[string]any) + require.True(t, ok) + nestedTools, ok := secondInput["tools"].([]any) + require.True(t, ok) + require.NotEmpty(t, nestedTools) + nestedTool, ok := nestedTools[0].(map[string]any) + require.True(t, ok) + nestedFn, ok := nestedTool["function"].(map[string]any) + require.True(t, ok) + require.Equal(t, codexPythonToolAlias, nestedFn["name"]) } func TestAliasOpenAIOAuthReservedToolNames_CollisionDoesNotMutate(t *testing.T) { @@ -63,7 +83,12 @@ func TestApplyCodexOAuthTransform_ReservedPythonNameIsOAuthOnly(t *testing.T) { result := applyCodexOAuthTransform(reqBody, true, false) require.NoError(t, result.Error) require.Equal(t, "PYTHON", result.ToolNameReverse[codexPythonToolAlias]) - require.Equal(t, codexPythonToolAlias, reqBody["tools"].([]any)[0].(map[string]any)["name"]) + tools, ok := reqBody["tools"].([]any) + require.True(t, ok) + require.NotEmpty(t, tools) + tool, ok := tools[0].(map[string]any) + require.True(t, ok) + require.Equal(t, codexPythonToolAlias, tool["name"]) apiKeyBody := []byte(`{"type":"response.create","tools":[{"type":"function","name":"python"}]}`) normalized, changed, err := normalizeOpenAIResponsesWebSocketCompatibilityBody(apiKeyBody, &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}) @@ -158,9 +183,21 @@ func TestAliasOpenAIOAuthReservedToolNames_PromptCompatibilityRunsFirst(t *testi require.NoError(t, err) result := applyCodexOAuthTransform(reqBody, true, false) require.NoError(t, result.Error) - require.Equal(t, codexPythonToolAlias, reqBody["input"].([]any)[0].(map[string]any)["name"]) - require.Equal(t, codexPythonToolAlias, reqBody["tools"].([]any)[0].(map[string]any)["name"]) - require.Equal(t, codexPythonToolAlias, reqBody["tool_choice"].(map[string]any)["name"]) + input, ok := reqBody["input"].([]any) + require.True(t, ok) + require.NotEmpty(t, input) + firstInput, ok := input[0].(map[string]any) + require.True(t, ok) + require.Equal(t, codexPythonToolAlias, firstInput["name"]) + tools, ok := reqBody["tools"].([]any) + require.True(t, ok) + require.NotEmpty(t, tools) + tool, ok := tools[0].(map[string]any) + require.True(t, ok) + require.Equal(t, codexPythonToolAlias, tool["name"]) + toolChoice, ok := reqBody["tool_choice"].(map[string]any) + require.True(t, ok) + require.Equal(t, codexPythonToolAlias, toolChoice["name"]) encoded, err := json.Marshal(reqBody) require.NoError(t, err) require.Equal(t, "900719925474099312345", gjson.GetBytes(encoded, "sequence").Raw) diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index c929e29090..bd24f29c66 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -115,10 +115,6 @@ func normalizeCodexCallIDForItemType(itemType, id string) string { return compactCodexCallIDForItemType(itemType, candidate) } -func compactCodexCallID(id string) string { - return compactCodexCallIDForItemType("function_call", id) -} - func compactCodexCallIDForItemType(itemType, id string) string { prefix := openAIResponsesToolCallIDPrefix(itemType) + "_" digest := sha256.Sum256([]byte("sub2api:codex-call-id:v1:" + id)) diff --git a/backend/internal/service/openai_cyber_transcript.go b/backend/internal/service/openai_cyber_transcript.go index a276a4315b..1314d54c51 100644 --- a/backend/internal/service/openai_cyber_transcript.go +++ b/backend/internal/service/openai_cyber_transcript.go @@ -38,11 +38,9 @@ func deriveOpenAICyberTranscriptBlockKeys(apiKeyID int64, body []byte) openAICyb if !v.Exists() || (v.Type == gjson.String && strings.TrimSpace(v.String()) == "") { continue } - canonical := v.Raw + canonical := normalizeCompatSeedJSON(json.RawMessage(v.Raw)) if v.Type == gjson.String { canonical = v.String() - } else { - canonical = normalizeCompatSeedJSON(json.RawMessage(v.Raw)) } _, _ = h.Write([]byte("|")) _, _ = h.Write([]byte(field)) @@ -63,10 +61,11 @@ func deriveOpenAICyberTranscriptBlockKeys(apiKeyID int64, body []byte) openAICyb hasModelGeneratedItem := false sequence.ForEach(func(_, item gjson.Result) bool { canonical := item.Raw - if item.Type == gjson.String { + switch item.Type { + case gjson.String: encoded, _ := json.Marshal(item.String()) canonical = string(encoded) - } else if item.Type == gjson.JSON { + case gjson.JSON: canonical = normalizeCompatSeedJSON(json.RawMessage(item.Raw)) } if strings.TrimSpace(canonical) == "" { diff --git a/backend/internal/service/openai_gateway_grok_model_input.go b/backend/internal/service/openai_gateway_grok_model_input.go index ae6af5b498..6ad74dda42 100644 --- a/backend/internal/service/openai_gateway_grok_model_input.go +++ b/backend/internal/service/openai_gateway_grok_model_input.go @@ -371,7 +371,7 @@ func collapseGrokAssistantOutputText(content any) (string, bool) { if !ok { return "", false } - text.WriteString(value) + _, _ = text.WriteString(value) } if strings.TrimSpace(text.String()) == "" { return "", false diff --git a/backend/internal/service/openai_gateway_passthrough.go b/backend/internal/service/openai_gateway_passthrough.go index 8f44e8bac0..6aad83b268 100644 --- a/backend/internal/service/openai_gateway_passthrough.go +++ b/backend/internal/service/openai_gateway_passthrough.go @@ -427,8 +427,6 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough( c, account, requestedModel, body, http.StatusBadRequest, signal.message, signal.payload, compactModelFallbackRetried, ); retry { body = retryBody - upstreamPassthroughModel = fallbackModel - compactModelFallbackRetried = true SetOpsUpstreamModel(c, fallbackModel) return s.forwardOpenAIPassthrough(ctx, c, account, body, canonicalImageIntentBody, requestedModel, true, reasoningEffort, reqStream, startTime) } @@ -448,8 +446,6 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough( c, account, requestedModel, body, http.StatusBadRequest, signal.message, signal.payload, compactModelFallbackRetried, ); retry { body = retryBody - upstreamPassthroughModel = fallbackModel - compactModelFallbackRetried = true SetOpsUpstreamModel(c, fallbackModel) return s.forwardOpenAIPassthrough(ctx, c, account, body, canonicalImageIntentBody, requestedModel, true, reasoningEffort, reqStream, startTime) } @@ -1513,7 +1509,7 @@ func (s *OpenAIGatewayService) handleOpenAIStreamTerminalAccountSideEffects( } fallthrough case http.StatusUnauthorized, http.StatusTooManyRequests, 529: - var ctx context.Context = context.Background() + ctx := context.Background() if c != nil && c.Request != nil { ctx = c.Request.Context() } diff --git a/backend/internal/service/openai_gateway_response_flush_test.go b/backend/internal/service/openai_gateway_response_flush_test.go index 2b07992c32..16483567ad 100644 --- a/backend/internal/service/openai_gateway_response_flush_test.go +++ b/backend/internal/service/openai_gateway_response_flush_test.go @@ -459,7 +459,7 @@ func TestOpenAIResponseFlush_BareErrorTimeoutSynthesizesFailed(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reader, writer := io.Pipe() - defer writer.Close() + defer func() { _ = writer.Close() }() recorder := newOpenAIResponseFlushRecorder() resultCh, errCh := runOpenAIResponseFlushTestAsync(recorder, reader, tt.cfg) diff --git a/backend/internal/service/openai_responses_ingress_compat.go b/backend/internal/service/openai_responses_ingress_compat.go index 13c6168c7a..c9ae11c0ae 100644 --- a/backend/internal/service/openai_responses_ingress_compat.go +++ b/backend/internal/service/openai_responses_ingress_compat.go @@ -37,7 +37,6 @@ func normalizeOpenAIResponsesLegacyIngress(body []byte) ([]byte, bool, error) { request["input"] = legacy.input applyLegacyResponsesTopLevelFields(request, legacy) delete(request, "previous_response_id") - changed = true } } // messages is not a Responses field. When native input is present but the diff --git a/backend/internal/service/openai_responses_tool_schema.go b/backend/internal/service/openai_responses_tool_schema.go index b4d4e261ca..08c9ecc2d6 100644 --- a/backend/internal/service/openai_responses_tool_schema.go +++ b/backend/internal/service/openai_responses_tool_schema.go @@ -311,12 +311,18 @@ func (p *openAIResponsesToolSchemaParser) parseObject( } } +var ( + openAIResponsesJSONOneOfKey = []byte(`"oneOf"`) + openAIResponsesJSONAnyOfKey = []byte(`"anyOf"`) + openAIResponsesJSONEscapeNeedle = []byte{'\\'} +) + func openAIMissingRootObjectUnionTypeEdit(raw []byte, absoluteStart int) (openAIResponsesToolSchemaEdit, bool) { if len(raw) > openAIResponsesObjectUnionMaxSize { return openAIResponsesToolSchemaEdit{}, false } - if !bytes.Contains(raw, []byte(`"oneOf"`)) && !bytes.Contains(raw, []byte(`"anyOf"`)) && - !bytes.Contains(raw, []byte{'\\'}) { + if !bytes.Contains(raw, openAIResponsesJSONOneOfKey) && !bytes.Contains(raw, openAIResponsesJSONAnyOfKey) && + !bytes.Contains(raw, openAIResponsesJSONEscapeNeedle) { return openAIResponsesToolSchemaEdit{}, false } var schema map[string]json.RawMessage diff --git a/backend/internal/service/openai_ws_v2/passthrough_relay_internal_test.go b/backend/internal/service/openai_ws_v2/passthrough_relay_internal_test.go index efbc9881e0..547af3fc62 100644 --- a/backend/internal/service/openai_ws_v2/passthrough_relay_internal_test.go +++ b/backend/internal/service/openai_ws_v2/passthrough_relay_internal_test.go @@ -405,7 +405,9 @@ func TestObserveUpstreamMessageBareErrorClearsTurnStateAndFinalizesUsageOnce(t * require.Equal(t, Usage{}, state.turnUsage) require.Nil(t, state.activeTurn) require.Empty(t, state.turnTimingByID) - require.True(t, state.consumePendingTurnStartedAt().IsZero()) + // The error is bound to the active turn id, so settlement must not consume a + // later pending start that belongs to the next turn. + require.Equal(t, now.Add(-500*time.Millisecond), state.consumePendingTurnStartedAt()) // Re-observing and settling a later terminal without usage must not re-add the prior turn. observeUpstreamMessage(state, []byte(`{"type":"error","error":{"message":"again"}}`), now, func() time.Time { return now }, nil)