diff --git a/.golangci.yml b/.golangci.yml index 1b151e6..514a43c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,10 +4,13 @@ run: tests: true linters: - default: none + default: standard enable: + - errcheck - govet - ineffassign + - staticcheck + - unused settings: dupl: threshold: 100 diff --git a/cmd/ds2api-tests/main.go b/cmd/ds2api-tests/main.go index cc66c91..e2becd3 100644 --- a/cmd/ds2api-tests/main.go +++ b/cmd/ds2api-tests/main.go @@ -30,8 +30,8 @@ func main() { opts.Timeout = time.Duration(timeoutSeconds) * time.Second if err := testsuite.Run(context.Background(), opts); err != nil { - fmt.Fprintln(os.Stderr, err.Error()) + _, _ = fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } - fmt.Fprintln(os.Stdout, "testsuite completed successfully") + _, _ = fmt.Fprintln(os.Stdout, "testsuite completed successfully") } diff --git a/internal/adapter/claude/handler_messages.go b/internal/adapter/claude/handler_messages.go index 5bfa839..526d316 100644 --- a/internal/adapter/claude/handler_messages.go +++ b/internal/adapter/claude/handler_messages.go @@ -64,7 +64,7 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, store C rec := httptest.NewRecorder() h.OpenAI.ChatCompletions(rec, proxyReq) res := rec.Result() - defer res.Body.Close() + defer func() { _ = res.Body.Close() }() body, _ := io.ReadAll(res.Body) for k, vv := range res.Header { for _, v := range vv { @@ -94,7 +94,7 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, store C rec := httptest.NewRecorder() h.OpenAI.ChatCompletions(rec, proxyReq) res := rec.Result() - defer res.Body.Close() + defer func() { _ = res.Body.Close() }() body, _ := io.ReadAll(res.Body) if res.StatusCode < 200 || res.StatusCode >= 300 { for k, vv := range res.Header { @@ -124,7 +124,7 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, store C } func (h *Handler) handleClaudeStreamRealtime(w http.ResponseWriter, r *http.Request, resp *http.Response, model string, messages []any, thinkingEnabled, searchEnabled bool, toolNames []string) { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) writeClaudeError(w, http.StatusInternalServerError, string(body)) diff --git a/internal/adapter/claude/handler_utils.go b/internal/adapter/claude/handler_utils.go index c9283bf..5c53958 100644 --- a/internal/adapter/claude/handler_utils.go +++ b/internal/adapter/claude/handler_utils.go @@ -101,6 +101,7 @@ func buildClaudeToolPrompt(tools []any) string { toolcall.BuildToolCallInstructions(names) } +//nolint:unused // retained for compatibility with pending Claude tool-result prompt flow. func formatClaudeToolResultForPrompt(block map[string]any) string { if block == nil { return "" diff --git a/internal/adapter/claude/handler_utils_sanitize.go b/internal/adapter/claude/handler_utils_sanitize.go index 10980cb..95f2065 100644 --- a/internal/adapter/claude/handler_utils_sanitize.go +++ b/internal/adapter/claude/handler_utils_sanitize.go @@ -96,6 +96,7 @@ func looksLikeBase64Payload(v string) bool { return true } +//nolint:unused // helper kept for compatibility with upcoming sanitize pipeline. func marshalCompactJSON(v any) string { b, err := json.Marshal(v) if err != nil { diff --git a/internal/adapter/claude/standard_request.go b/internal/adapter/claude/standard_request.go index 488bdf6..7b16c96 100644 --- a/internal/adapter/claude/standard_request.go +++ b/internal/adapter/claude/standard_request.go @@ -18,7 +18,7 @@ func normalizeClaudeRequest(store ConfigReader, req map[string]any) (claudeNorma model, _ := req["model"].(string) messagesRaw, _ := req["messages"].([]any) if strings.TrimSpace(model) == "" || len(messagesRaw) == 0 { - return claudeNormalizedRequest{}, fmt.Errorf("Request must include 'model' and 'messages'.") + return claudeNormalizedRequest{}, fmt.Errorf("request must include 'model' and 'messages'") } if _, ok := req["max_tokens"]; !ok { req["max_tokens"] = 8192 diff --git a/internal/adapter/gemini/convert_passthrough.go b/internal/adapter/gemini/convert_passthrough.go index 05cd6cd..ba943a9 100644 --- a/internal/adapter/gemini/convert_passthrough.go +++ b/internal/adapter/gemini/convert_passthrough.go @@ -5,6 +5,7 @@ import ( "strings" ) +//nolint:unused // compatibility hook for native Gemini request normalization path. func collectGeminiPassThrough(req map[string]any) map[string]any { cfg, _ := req["generationConfig"].(map[string]any) if len(cfg) == 0 { diff --git a/internal/adapter/gemini/convert_request.go b/internal/adapter/gemini/convert_request.go index 2eca687..34eb2a2 100644 --- a/internal/adapter/gemini/convert_request.go +++ b/internal/adapter/gemini/convert_request.go @@ -9,6 +9,7 @@ import ( "ds2api/internal/util" ) +//nolint:unused // kept for native Gemini adapter route compatibility. func normalizeGeminiRequest(store ConfigReader, routeModel string, req map[string]any, stream bool) (util.StandardRequest, error) { requestedModel := strings.TrimSpace(routeModel) if requestedModel == "" { @@ -17,13 +18,13 @@ func normalizeGeminiRequest(store ConfigReader, routeModel string, req map[strin resolvedModel, ok := config.ResolveModel(store, requestedModel) if !ok { - return util.StandardRequest{}, fmt.Errorf("Model '%s' is not available.", requestedModel) + return util.StandardRequest{}, fmt.Errorf("model %q is not available", requestedModel) } thinkingEnabled, searchEnabled, _ := config.GetModelConfig(resolvedModel) messagesRaw := geminiMessagesFromRequest(req) if len(messagesRaw) == 0 { - return util.StandardRequest{}, fmt.Errorf("Request must include non-empty contents.") + return util.StandardRequest{}, fmt.Errorf("request must include non-empty contents") } toolsRaw := convertGeminiTools(req["tools"]) diff --git a/internal/adapter/gemini/convert_tools.go b/internal/adapter/gemini/convert_tools.go index 4611f85..3df3a7b 100644 --- a/internal/adapter/gemini/convert_tools.go +++ b/internal/adapter/gemini/convert_tools.go @@ -2,6 +2,7 @@ package gemini import "strings" +//nolint:unused // kept for native Gemini adapter route compatibility. func convertGeminiTools(raw any) []any { tools, _ := raw.([]any) if len(tools) == 0 { diff --git a/internal/adapter/gemini/handler_generate.go b/internal/adapter/gemini/handler_generate.go index c265f36..b03b3ea 100644 --- a/internal/adapter/gemini/handler_generate.go +++ b/internal/adapter/gemini/handler_generate.go @@ -58,7 +58,7 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, stream rec := httptest.NewRecorder() h.OpenAI.ChatCompletions(rec, proxyReq) res := rec.Result() - defer res.Body.Close() + defer func() { _ = res.Body.Close() }() body, _ := io.ReadAll(res.Body) for k, vv := range res.Header { for _, v := range vv { @@ -88,7 +88,7 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, stream rec := httptest.NewRecorder() h.OpenAI.ChatCompletions(rec, proxyReq) res := rec.Result() - defer res.Body.Close() + defer func() { _ = res.Body.Close() }() body, _ := io.ReadAll(res.Body) if res.StatusCode < 200 || res.StatusCode >= 300 { for k, vv := range res.Header { @@ -132,8 +132,9 @@ func writeGeminiErrorFromOpenAI(w http.ResponseWriter, status int, raw []byte) { writeGeminiError(w, status, message) } +//nolint:unused // retained for native Gemini non-stream handling path. func (h *Handler) handleNonStreamGenerateContent(w http.ResponseWriter, resp *http.Response, model, finalPrompt string, thinkingEnabled bool, toolNames []string) { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) writeGeminiError(w, resp.StatusCode, strings.TrimSpace(string(body))) @@ -152,6 +153,7 @@ func (h *Handler) handleNonStreamGenerateContent(w http.ResponseWriter, resp *ht )) } +//nolint:unused // retained for native Gemini non-stream handling path. func buildGeminiGenerateContentResponse(model, finalPrompt, finalThinking, finalText string, toolNames []string, outputTokens int) map[string]any { parts := buildGeminiPartsFromFinal(finalText, finalThinking, toolNames) usage := buildGeminiUsage(finalPrompt, finalThinking, finalText, outputTokens) @@ -171,6 +173,7 @@ func buildGeminiGenerateContentResponse(model, finalPrompt, finalThinking, final } } +//nolint:unused // retained for native Gemini non-stream handling path. func buildGeminiUsage(finalPrompt, finalThinking, finalText string, outputTokens int) map[string]any { promptTokens := util.EstimateTokens(finalPrompt) reasoningTokens := util.EstimateTokens(finalThinking) @@ -186,6 +189,7 @@ func buildGeminiUsage(finalPrompt, finalThinking, finalText string, outputTokens } } +//nolint:unused // retained for native Gemini non-stream handling path. func buildGeminiPartsFromFinal(finalText, finalThinking string, toolNames []string) []map[string]any { detected := toolcall.ParseToolCalls(finalText, toolNames) if len(detected) == 0 && finalThinking != "" { diff --git a/internal/adapter/gemini/handler_routes.go b/internal/adapter/gemini/handler_routes.go index 28338a6..fb573da 100644 --- a/internal/adapter/gemini/handler_routes.go +++ b/internal/adapter/gemini/handler_routes.go @@ -17,6 +17,7 @@ type Handler struct { OpenAI OpenAIChatRunner } +//nolint:unused // used by native Gemini stream/non-stream runtime helpers. func (h *Handler) compatStripReferenceMarkers() bool { if h == nil || h.Store == nil { return true diff --git a/internal/adapter/gemini/handler_stream_runtime.go b/internal/adapter/gemini/handler_stream_runtime.go index 2cbe348..e7c9b87 100644 --- a/internal/adapter/gemini/handler_stream_runtime.go +++ b/internal/adapter/gemini/handler_stream_runtime.go @@ -12,8 +12,9 @@ import ( streamengine "ds2api/internal/stream" ) +//nolint:unused // retained for native Gemini stream handling path. func (h *Handler) handleStreamGenerateContent(w http.ResponseWriter, r *http.Request, resp *http.Response, model, finalPrompt string, thinkingEnabled, searchEnabled bool, toolNames []string) { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) writeGeminiError(w, resp.StatusCode, strings.TrimSpace(string(body))) @@ -49,6 +50,7 @@ func (h *Handler) handleStreamGenerateContent(w http.ResponseWriter, r *http.Req }) } +//nolint:unused // retained for native Gemini stream handling path. type geminiStreamRuntime struct { w http.ResponseWriter rc *http.ResponseController @@ -68,6 +70,7 @@ type geminiStreamRuntime struct { outputTokens int } +//nolint:unused // retained for native Gemini stream handling path. func newGeminiStreamRuntime( w http.ResponseWriter, rc *http.ResponseController, @@ -93,6 +96,7 @@ func newGeminiStreamRuntime( } } +//nolint:unused // retained for native Gemini stream handling path. func (s *geminiStreamRuntime) sendChunk(payload map[string]any) { b, _ := json.Marshal(payload) _, _ = s.w.Write([]byte("data: ")) @@ -103,6 +107,7 @@ func (s *geminiStreamRuntime) sendChunk(payload map[string]any) { } } +//nolint:unused // retained for native Gemini stream handling path. func (s *geminiStreamRuntime) onParsed(parsed sse.LineResult) streamengine.ParsedDecision { if !parsed.Parsed { return streamengine.ParsedDecision{} @@ -158,6 +163,7 @@ func (s *geminiStreamRuntime) onParsed(parsed sse.LineResult) streamengine.Parse return streamengine.ParsedDecision{ContentSeen: contentSeen} } +//nolint:unused // retained for native Gemini stream handling path. func (s *geminiStreamRuntime) finalize() { finalThinking := s.thinking.String() finalText := cleanVisibleOutput(s.text.String(), s.stripReferenceMarkers) diff --git a/internal/adapter/gemini/handler_test.go b/internal/adapter/gemini/handler_test.go index 20eed21..b7aea1b 100644 --- a/internal/adapter/gemini/handler_test.go +++ b/internal/adapter/gemini/handler_test.go @@ -42,19 +42,23 @@ func (m testGeminiAuth) Determine(_ *http.Request) (*auth.RequestAuth, error) { func (testGeminiAuth) Release(_ *auth.RequestAuth) {} +//nolint:unused // reserved test double for native Gemini DS-call path coverage. type testGeminiDS struct { resp *http.Response err error } +//nolint:unused // reserved test double for native Gemini DS-call path coverage. func (m testGeminiDS) CreateSession(_ context.Context, _ *auth.RequestAuth, _ int) (string, error) { return "session-id", nil } +//nolint:unused // reserved test double for native Gemini DS-call path coverage. func (m testGeminiDS) GetPow(_ context.Context, _ *auth.RequestAuth, _ int) (string, error) { return "pow", nil } +//nolint:unused // reserved test double for native Gemini DS-call path coverage. func (m testGeminiDS) CallCompletion(_ context.Context, _ *auth.RequestAuth, _ map[string]any, _ string, _ int) (*http.Response, error) { if m.err != nil { return nil, m.err @@ -100,6 +104,7 @@ func (s geminiOpenAISuccessStub) ChatCompletions(w http.ResponseWriter, _ *http. _, _ = w.Write([]byte(out)) } +//nolint:unused // helper retained for native Gemini stream fixture tests. func makeGeminiUpstreamResponse(lines ...string) *http.Response { body := strings.Join(lines, "\n") if !strings.HasSuffix(body, "\n") { diff --git a/internal/adapter/gemini/output_clean.go b/internal/adapter/gemini/output_clean.go index 56b2545..4dff321 100644 --- a/internal/adapter/gemini/output_clean.go +++ b/internal/adapter/gemini/output_clean.go @@ -2,6 +2,7 @@ package gemini import textclean "ds2api/internal/textclean" +//nolint:unused // retained for native Gemini output post-processing path. func cleanVisibleOutput(text string, stripReferenceMarkers bool) string { if text == "" { return text diff --git a/internal/adapter/openai/handler_chat.go b/internal/adapter/openai/handler_chat.go index 9c2924f..95337b6 100644 --- a/internal/adapter/openai/handler_chat.go +++ b/internal/adapter/openai/handler_chat.go @@ -116,7 +116,7 @@ func (h *Handler) autoDeleteRemoteSession(ctx context.Context, a *auth.RequestAu func (h *Handler) handleNonStream(w http.ResponseWriter, ctx context.Context, resp *http.Response, completionID, model, finalPrompt string, thinkingEnabled bool, toolNames []string) { if resp.StatusCode != http.StatusOK { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() body, _ := io.ReadAll(resp.Body) writeOpenAIError(w, resp.StatusCode, string(body)) return @@ -143,7 +143,7 @@ func (h *Handler) handleNonStream(w http.ResponseWriter, ctx context.Context, re } func (h *Handler) handleStream(w http.ResponseWriter, r *http.Request, resp *http.Response, completionID, model, finalPrompt string, thinkingEnabled, searchEnabled bool, toolNames []string) { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) writeOpenAIError(w, resp.StatusCode, string(body)) diff --git a/internal/adapter/openai/handler_chat_auto_delete_test.go b/internal/adapter/openai/handler_chat_auto_delete_test.go index 0196db0..d8faa5b 100644 --- a/internal/adapter/openai/handler_chat_auto_delete_test.go +++ b/internal/adapter/openai/handler_chat_auto_delete_test.go @@ -107,7 +107,7 @@ type autoDeleteCtxDSStub struct { } func (m *autoDeleteCtxDSStub) DeleteSessionForToken(ctx context.Context, token string, sessionID string) (*deepseek.DeleteSessionResult, error) { - return m.autoDeleteModeDSStub.DeleteSessionForTokenCtx(ctx, token, sessionID) + return m.DeleteSessionForTokenCtx(ctx, token, sessionID) } func (m *autoDeleteCtxDSStub) DeleteAllSessionsForToken(_ context.Context, _ string) error { diff --git a/internal/adapter/openai/responses_handler.go b/internal/adapter/openai/responses_handler.go index 8b4677e..7cb7ec3 100644 --- a/internal/adapter/openai/responses_handler.go +++ b/internal/adapter/openai/responses_handler.go @@ -107,7 +107,7 @@ func (h *Handler) Responses(w http.ResponseWriter, r *http.Request) { } func (h *Handler) handleResponsesNonStream(w http.ResponseWriter, resp *http.Response, owner, responseID, model, finalPrompt string, thinkingEnabled bool, toolNames []string, toolChoice util.ToolChoicePolicy, traceID string) { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) writeOpenAIError(w, resp.StatusCode, strings.TrimSpace(string(body))) @@ -143,7 +143,7 @@ func (h *Handler) handleResponsesNonStream(w http.ResponseWriter, resp *http.Res } func (h *Handler) handleResponsesStream(w http.ResponseWriter, r *http.Request, resp *http.Response, owner, responseID, model, finalPrompt string, thinkingEnabled, searchEnabled bool, toolNames []string, toolChoice util.ToolChoicePolicy, traceID string) { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) writeOpenAIError(w, resp.StatusCode, strings.TrimSpace(string(body))) diff --git a/internal/adapter/openai/standard_request.go b/internal/adapter/openai/standard_request.go index af382cb..e9904f7 100644 --- a/internal/adapter/openai/standard_request.go +++ b/internal/adapter/openai/standard_request.go @@ -12,11 +12,11 @@ func normalizeOpenAIChatRequest(store ConfigReader, req map[string]any, traceID model, _ := req["model"].(string) messagesRaw, _ := req["messages"].([]any) if strings.TrimSpace(model) == "" || len(messagesRaw) == 0 { - return util.StandardRequest{}, fmt.Errorf("Request must include 'model' and 'messages'.") + return util.StandardRequest{}, fmt.Errorf("request must include 'model' and 'messages'") } resolvedModel, ok := config.ResolveModel(store, model) if !ok { - return util.StandardRequest{}, fmt.Errorf("Model '%s' is not available.", model) + return util.StandardRequest{}, fmt.Errorf("model %q is not available", model) } thinkingEnabled, searchEnabled, _ := config.GetModelConfig(resolvedModel) responseModel := strings.TrimSpace(model) @@ -48,11 +48,11 @@ func normalizeOpenAIResponsesRequest(store ConfigReader, req map[string]any, tra model, _ := req["model"].(string) model = strings.TrimSpace(model) if model == "" { - return util.StandardRequest{}, fmt.Errorf("Request must include 'model'.") + return util.StandardRequest{}, fmt.Errorf("request must include 'model'") } resolvedModel, ok := config.ResolveModel(store, model) if !ok { - return util.StandardRequest{}, fmt.Errorf("Model '%s' is not available.", model) + return util.StandardRequest{}, fmt.Errorf("model %q is not available", model) } thinkingEnabled, searchEnabled, _ := config.GetModelConfig(resolvedModel) @@ -68,7 +68,7 @@ func normalizeOpenAIResponsesRequest(store ConfigReader, req map[string]any, tra messagesRaw = msgs } if len(messagesRaw) == 0 { - return util.StandardRequest{}, fmt.Errorf("Request must include 'input' or 'messages'.") + return util.StandardRequest{}, fmt.Errorf("request must include 'input' or 'messages'") } toolPolicy, err := parseToolChoicePolicy(req["tool_choice"], req["tools"]) if err != nil { @@ -152,7 +152,7 @@ func parseToolChoicePolicy(toolChoiceRaw any, toolsRaw any) (util.ToolChoicePoli case "required": policy.Mode = util.ToolChoiceRequired default: - return util.ToolChoicePolicy{}, fmt.Errorf("Unsupported tool_choice: %q", v) + return util.ToolChoicePolicy{}, fmt.Errorf("unsupported tool_choice: %q", v) } case map[string]any: allowedOverride, hasAllowedOverride, err := parseAllowedToolNames(v["allowed_tools"]) @@ -198,7 +198,7 @@ func parseToolChoicePolicy(toolChoiceRaw any, toolsRaw any) (util.ToolChoicePoli policy.ForcedName = name policy.Allowed = namesToSet([]string{name}) default: - return util.ToolChoicePolicy{}, fmt.Errorf("Unsupported tool_choice.type: %q", typ) + return util.ToolChoicePolicy{}, fmt.Errorf("unsupported tool_choice.type: %q", typ) } default: return util.ToolChoicePolicy{}, fmt.Errorf("tool_choice must be a string or object") @@ -206,7 +206,7 @@ func parseToolChoicePolicy(toolChoiceRaw any, toolsRaw any) (util.ToolChoicePoli if policy.Mode == util.ToolChoiceRequired || policy.Mode == util.ToolChoiceForced { if len(declaredNames) == 0 { - return util.ToolChoicePolicy{}, fmt.Errorf("tool_choice=%s requires non-empty tools.", policy.Mode) + return util.ToolChoicePolicy{}, fmt.Errorf("tool_choice=%s requires non-empty tools", policy.Mode) } } if policy.Mode == util.ToolChoiceForced { diff --git a/internal/adapter/openai/tool_sieve_xml.go b/internal/adapter/openai/tool_sieve_xml.go index ef6d921..bcafe08 100644 --- a/internal/adapter/openai/tool_sieve_xml.go +++ b/internal/adapter/openai/tool_sieve_xml.go @@ -8,6 +8,7 @@ import ( // --- XML tool call support for the streaming sieve --- +//nolint:unused // kept as explicit tag inventory for future XML sieve refinements. var xmlToolCallClosingTags = []string{"", "", "", "", "", "", // Agent-style XML tags (Roo Code, Cline, etc.) "", "", "", ""} @@ -33,6 +34,8 @@ var xmlToolCallTagPairs = []struct{ open, close string }{ } // xmlToolCallBlockPattern matches a complete XML tool call block (wrapper or standalone). +// +//nolint:unused // reserved for future fast-path XML block detection. var xmlToolCallBlockPattern = regexp.MustCompile(`(?is)(\s*(?:.*?)\s*|\s*(?:.*?)\s*|]*>(?:.*?)|]*>(?:.*?)|(?:.*?)|(?:.*?)|(?:.*?)|(?:.*?))`) // xmlToolTagsToDetect is the set of XML tag prefixes used by findToolSegmentStart. diff --git a/internal/admin/handler_accounts_testing.go b/internal/admin/handler_accounts_testing.go index 8bfa376..dcf00dc 100644 --- a/internal/admin/handler_accounts_testing.go +++ b/internal/admin/handler_accounts_testing.go @@ -165,7 +165,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me return result } if resp.StatusCode != http.StatusOK { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() result["message"] = fmt.Sprintf("请求失败: HTTP %d", resp.StatusCode) return result } @@ -218,7 +218,7 @@ func (h *Handler) testAPI(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"success": false, "error": err.Error()}) return } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() body, _ := io.ReadAll(resp.Body) if resp.StatusCode == http.StatusOK { var parsed any diff --git a/internal/admin/handler_config_write.go b/internal/admin/handler_config_write.go index bfc6296..c371e13 100644 --- a/internal/admin/handler_config_write.go +++ b/internal/admin/handler_config_write.go @@ -85,7 +85,7 @@ func (h *Handler) addKey(w http.ResponseWriter, r *http.Request) { err := h.Store.Update(func(c *config.Config) error { for _, k := range c.Keys { if k == key { - return fmt.Errorf("Key 已存在") + return fmt.Errorf("key 已存在") } } c.Keys = append(c.Keys, key) @@ -109,7 +109,7 @@ func (h *Handler) deleteKey(w http.ResponseWriter, r *http.Request) { } } if idx < 0 { - return fmt.Errorf("Key 不存在") + return fmt.Errorf("key 不存在") } c.Keys = append(c.Keys[:idx], c.Keys[idx+1:]...) return nil diff --git a/internal/admin/handler_vercel.go b/internal/admin/handler_vercel.go index 5f66a68..e0734ed 100644 --- a/internal/admin/handler_vercel.go +++ b/internal/admin/handler_vercel.go @@ -301,7 +301,7 @@ func vercelRequest(ctx context.Context, client *http.Client, method, endpoint st if err != nil { return nil, 0, err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) parsed := map[string]any{} _ = json.Unmarshal(b, &parsed) diff --git a/internal/admin/handler_version.go b/internal/admin/handler_version.go index 1c38dfe..2d2ef53 100644 --- a/internal/admin/handler_version.go +++ b/internal/admin/handler_version.go @@ -43,7 +43,7 @@ func (h *Handler) getVersion(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, resp) return } - defer r.Body.Close() + defer func() { _ = r.Body.Close() }() if r.StatusCode < 200 || r.StatusCode >= 300 { resp["check_error"] = "github api status: " + r.Status writeJSON(w, http.StatusOK, resp) diff --git a/internal/auth/auth_edge_test.go b/internal/auth/auth_edge_test.go index 929b753..0dad649 100644 --- a/internal/auth/auth_edge_test.go +++ b/internal/auth/auth_edge_test.go @@ -130,9 +130,7 @@ func TestMarkTokenInvalidNotConfigToken(t *testing.T) { a := &RequestAuth{UseConfigToken: false, DeepSeekToken: "direct", resolver: r} r.MarkTokenInvalid(a) // Should not panic, token should be unchanged for non-config - if a.DeepSeekToken != "" { - // Actually it does clear it; that's fine - let's check behavior - } + _ = a.DeepSeekToken // Actual behavior may clear it; this test only asserts no panic. } func TestMarkTokenInvalidEmptyAccountID(t *testing.T) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 2cc0d3d..f3e8703 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -58,8 +58,7 @@ func TestLoadStorePreservesFileBackedTokensForRuntime(t *testing.T) { if err != nil { t.Fatalf("create temp config: %v", err) } - defer tmp.Close() - + defer func() { _ = tmp.Close() }() if _, err := tmp.WriteString(`{ "accounts":[{"email":"u@example.com","password":"p","token":"persisted-token"}] }`); err != nil { @@ -355,7 +354,7 @@ func TestAccountTestStatusIsRuntimeOnlyAndNotPersisted(t *testing.T) { if err != nil { t.Fatalf("create temp config: %v", err) } - defer tmp.Close() + defer func() { _ = tmp.Close() }() if _, err := tmp.WriteString(`{ "accounts":[{"email":"u@example.com","password":"p","test_status":"ok"}] }`); err != nil { diff --git a/internal/deepseek/client_continue_test.go b/internal/deepseek/client_continue_test.go index fa0b843..4758ab0 100644 --- a/internal/deepseek/client_continue_test.go +++ b/internal/deepseek/client_continue_test.go @@ -54,8 +54,7 @@ func TestCallContinuePropagatesPowHeaderToFallbackRequest(t *testing.T) { if err != nil { t.Fatalf("callContinue returned error: %v", err) } - defer resp.Body.Close() - + defer func() { _ = resp.Body.Close() }() if seenPow != "pow-response-abc" { t.Fatalf("continue request pow header=%q want=%q", seenPow, "pow-response-abc") } @@ -105,8 +104,7 @@ func TestCallCompletionAutoContinueThreadsPowHeader(t *testing.T) { if err != nil { t.Fatalf("CallCompletion returned error: %v", err) } - defer resp.Body.Close() - + defer func() { _ = resp.Body.Close() }() out, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("read auto-continued body failed: %v", err) diff --git a/internal/deepseek/client_http_helpers.go b/internal/deepseek/client_http_helpers.go index 05de224..10b5006 100644 --- a/internal/deepseek/client_http_helpers.go +++ b/internal/deepseek/client_http_helpers.go @@ -19,7 +19,7 @@ func readResponseBody(resp *http.Response) ([]byte, error) { if err != nil { return nil, err } - defer gz.Close() + defer func() { _ = gz.Close() }() reader = gz case "br": reader = brotli.NewReader(resp.Body) diff --git a/internal/deepseek/client_http_json.go b/internal/deepseek/client_http_json.go index 9de0e57..4ddc239 100644 --- a/internal/deepseek/client_http_json.go +++ b/internal/deepseek/client_http_json.go @@ -49,7 +49,7 @@ func (c *Client) postJSONWithStatus(ctx context.Context, doer trans.Doer, url st return nil, 0, err } } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() payloadBytes, err := readResponseBody(resp) if err != nil { return nil, resp.StatusCode, err @@ -86,7 +86,7 @@ func (c *Client) getJSONWithStatus(ctx context.Context, doer trans.Doer, url str return nil, 0, err } } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() payloadBytes, err := readResponseBody(resp) if err != nil { return nil, resp.StatusCode, err diff --git a/internal/rawsample/visible_text.go b/internal/rawsample/visible_text.go index 4746590..1896dd6 100644 --- a/internal/rawsample/visible_text.go +++ b/internal/rawsample/visible_text.go @@ -5,6 +5,7 @@ import ( "strings" ) +//nolint:unused // retained for raw-sample processing entrypoints. func extractProcessedVisibleText(raw []byte, kind, contentType string) string { if len(raw) == 0 { return "" @@ -22,6 +23,7 @@ func extractProcessedVisibleText(raw []byte, kind, contentType string) string { return parseOpenAIStreamText(string(raw)) } +//nolint:unused // retained for raw-sample processing entrypoints. func parseOpenAIStreamText(raw string) string { if strings.TrimSpace(raw) == "" { return "" @@ -54,6 +56,7 @@ func parseOpenAIStreamText(raw string) string { return out.String() } +//nolint:unused // retained for raw-sample processing entrypoints. func parseOpenAIJSONText(raw string) string { if strings.TrimSpace(raw) == "" { return "" @@ -65,6 +68,7 @@ func parseOpenAIJSONText(raw string) string { return extractOpenAIVisibleTextValue(decoded) } +//nolint:unused // retained for raw-sample processing entrypoints. func extractOpenAIVisibleTextValue(v any) string { switch x := v.(type) { case nil: diff --git a/internal/sse/consumer.go b/internal/sse/consumer.go index 49ddd63..141bd93 100644 --- a/internal/sse/consumer.go +++ b/internal/sse/consumer.go @@ -24,7 +24,7 @@ type CollectResult struct { // The caller is responsible for closing resp.Body unless closeBody is true. func CollectStream(resp *http.Response, thinkingEnabled bool, closeBody bool) CollectResult { if closeBody { - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() } text := strings.Builder{} thinking := strings.Builder{} diff --git a/internal/sse/parser.go b/internal/sse/parser.go index c8ba685..eee46f9 100644 --- a/internal/sse/parser.go +++ b/internal/sse/parser.go @@ -57,9 +57,7 @@ func isFragmentStatusPath(path string) bool { if mid == "" { return false } - if strings.HasPrefix(mid, "-") { - mid = mid[1:] - } + mid = strings.TrimPrefix(mid, "-") if mid == "" { return false } @@ -310,11 +308,12 @@ func extractContentRecursive(items []any, defaultType string) ([]ContentPart, bo } typeName, _ := x["type"].(string) typeName = strings.ToUpper(typeName) - if typeName == "THINK" || typeName == "THINKING" { + switch typeName { + case "THINK", "THINKING": parts = append(parts, ContentPart{Text: ct, Type: "thinking"}) - } else if typeName == "RESPONSE" { + case "RESPONSE": parts = append(parts, ContentPart{Text: ct, Type: "text"}) - } else { + default: parts = append(parts, ContentPart{Text: ct, Type: partType}) } case string: diff --git a/internal/sse/raw_stream_token_replay_test.go b/internal/sse/raw_stream_token_replay_test.go index 3ba0c29..ddf0d7c 100644 --- a/internal/sse/raw_stream_token_replay_test.go +++ b/internal/sse/raw_stream_token_replay_test.go @@ -3,6 +3,7 @@ package sse import ( "bufio" "encoding/json" + "errors" "os" "path/filepath" "strconv" @@ -32,7 +33,10 @@ func TestRawStreamSamplesTokenReplay(t *testing.T) { if err != nil { t.Fatalf("read sample: %v", err) } - parsedTokens, expectedTokens := replayAndCollectTokens(string(raw)) + parsedTokens, expectedTokens, err := replayAndCollectTokens(string(raw)) + if err != nil { + t.Fatalf("replay token collection failed: %v", err) + } if expectedTokens <= 0 { t.Fatalf("expected positive token usage from raw stream, got %d", expectedTokens) } @@ -47,9 +51,10 @@ func TestRawStreamSamplesTokenReplay(t *testing.T) { } } -func replayAndCollectTokens(raw string) (parsedTokens int, expectedTokens int) { +func replayAndCollectTokens(raw string) (parsedTokens int, expectedTokens int, err error) { currentType := "thinking" scanner := bufio.NewScanner(strings.NewReader(raw)) + scanner.Buffer(make([]byte, 0, 64*1024), 2*1024*1024) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if !strings.HasPrefix(line, "data:") { @@ -72,7 +77,13 @@ func replayAndCollectTokens(raw string) (parsedTokens int, expectedTokens int) { parsedTokens = res.OutputTokens } } - return parsedTokens, expectedTokens + if scanErr := scanner.Err(); scanErr != nil { + if errors.Is(scanErr, bufio.ErrTooLong) { + return 0, 0, errors.New("raw stream line exceeds 2MiB scanner limit") + } + return 0, 0, scanErr + } + return parsedTokens, expectedTokens, nil } func rawAccumulatedTokenUsage(v any) int { diff --git a/internal/sse/stream_edge_test.go b/internal/sse/stream_edge_test.go index 927b023..5de3a58 100644 --- a/internal/sse/stream_edge_test.go +++ b/internal/sse/stream_edge_test.go @@ -103,7 +103,7 @@ func TestStartParsedLinePumpContextCancellation(t *testing.T) { // Cancel context - this will cause the pump to exit on next send cancel() // Close the pipe to unblock scanner.Scan() - pw.Close() + _ = pw.Close() // Drain remaining results for range results { diff --git a/internal/testsuite/edge_cases.go b/internal/testsuite/edge_cases.go index 50bc8ac..1cdf72e 100644 --- a/internal/testsuite/edge_cases.go +++ b/internal/testsuite/edge_cases.go @@ -170,7 +170,7 @@ func (r *Runner) caseToolcallStreamMixed(ctx context.Context, cc *caseContext) e cc.assert("tool_calls_delta_present", hasTool, "tool_calls delta missing") cc.assert("no_raw_tool_json_leak", !rawLeak, "raw tool_calls leaked") cc.assert("done_terminated", done, "expected [DONE]") - if !(hasTool && hasText) { + if !hasTool || !hasText { r.warnings = append(r.warnings, "toolcall mixed stream did not produce both text and tool_calls in this run (model-side behavior dependent)") } return nil diff --git a/internal/testsuite/edge_cases_abort.go b/internal/testsuite/edge_cases_abort.go index 2cc1fc1..a6895ad 100644 --- a/internal/testsuite/edge_cases_abort.go +++ b/internal/testsuite/edge_cases_abort.go @@ -58,7 +58,7 @@ func (cc *caseContext) abortStreamRequest(ctx context.Context, spec requestSpec) }) return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() buf := make([]byte, 512) _, _ = resp.Body.Read(buf) _ = resp.Body.Close() diff --git a/internal/testsuite/runner_env.go b/internal/testsuite/runner_env.go index 24eb297..7d4fa11 100644 --- a/internal/testsuite/runner_env.go +++ b/internal/testsuite/runner_env.go @@ -66,7 +66,7 @@ func (r *Runner) pruneOldRuns() error { if err := os.RemoveAll(dirPath); err != nil { errs = append(errs, fmt.Sprintf("remove %s: %v", name, err)) } else { - fmt.Fprintf(os.Stdout, "pruned old test run: %s\n", name) + _, _ = fmt.Fprintf(os.Stdout, "pruned old test run: %s\n", name) } } @@ -82,7 +82,7 @@ func (r *Runner) runPreflight(ctx context.Context) error { if err != nil { return err } - defer f.Close() + defer func() { _ = f.Close() }() for _, step := range steps { if _, err := fmt.Fprintf(f, "\n$ %s\n", strings.Join(step, " ")); err != nil { return err @@ -218,7 +218,7 @@ func (r *Runner) ping(path string) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("status=%d", resp.StatusCode) } diff --git a/internal/testsuite/runner_http.go b/internal/testsuite/runner_http.go index d98c60a..1942bc0 100644 --- a/internal/testsuite/runner_http.go +++ b/internal/testsuite/runner_http.go @@ -115,7 +115,7 @@ func (cc *caseContext) requestOnce(ctx context.Context, spec requestSpec, attemp cc.mu.Unlock() return nil, err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() body, _ := io.ReadAll(resp.Body) cc.mu.Lock() @@ -131,7 +131,7 @@ func (cc *caseContext) requestOnce(ctx context.Context, spec requestSpec, attemp }) if spec.Stream { - cc.streamRaw.WriteString(fmt.Sprintf("### trace=%s url=%s\n", traceID, fullURL)) + _, _ = fmt.Fprintf(&cc.streamRaw, "### trace=%s url=%s\n", traceID, fullURL) cc.streamRaw.Write(body) cc.streamRaw.WriteString("\n\n") } diff --git a/internal/testsuite/runner_utils.go b/internal/testsuite/runner_utils.go index c4879c6..d86710a 100644 --- a/internal/testsuite/runner_utils.go +++ b/internal/testsuite/runner_utils.go @@ -126,7 +126,7 @@ func findFreePort() (int, error) { if err != nil { return 0, err } - defer ln.Close() + defer func() { _ = ln.Close() }() addr, ok := ln.Addr().(*net.TCPAddr) if !ok { return 0, errors.New("failed to detect tcp port") diff --git a/internal/toolcall/toolcalls_candidates.go b/internal/toolcall/toolcalls_candidates.go index 43c88d0..1035e98 100644 --- a/internal/toolcall/toolcalls_candidates.go +++ b/internal/toolcall/toolcalls_candidates.go @@ -8,6 +8,8 @@ import ( var toolCallPattern = regexp.MustCompile(`\{\s*["']tool_calls["']\s*:\s*\[(.*?)\]\s*\}`) var fencedJSONPattern = regexp.MustCompile("(?s)```(?:json)?\\s*(.*?)\\s*```") var fencedCodeBlockPattern = regexp.MustCompile("(?s)```[\\s\\S]*?```") + +//nolint:unused // retained for future markup tool-call heuristics. var markupToolSyntaxPattern = regexp.MustCompile(`(?i)<(?:(?:[a-z0-9_:-]+:)?(?:tool_call|function_call|invoke)\b|(?:[a-z0-9_:-]+:)?function_calls\b|(?:[a-z0-9_:-]+:)?tool_use\b)`) func buildToolCallCandidates(text string) []string { @@ -190,6 +192,7 @@ func shouldSkipToolCallParsingForCodeFenceExample(text string) bool { return !looksLikeToolCallSyntax(stripped) } +//nolint:unused // retained for future markup tool-call heuristics. func looksLikeMarkupToolSyntax(text string) bool { return markupToolSyntaxPattern.MatchString(text) } diff --git a/internal/toolcall/toolcalls_json_repair.go b/internal/toolcall/toolcalls_json_repair.go index d5c2ce3..884d26f 100644 --- a/internal/toolcall/toolcalls_json_repair.go +++ b/internal/toolcall/toolcalls_json_repair.go @@ -27,7 +27,7 @@ func repairInvalidJSONBackslashes(s string) string { isHex := true for j := 1; j <= 4; j++ { r := runes[i+1+j] - if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) { + if (r < '0' || r > '9') && (r < 'a' || r > 'f') && (r < 'A' || r > 'F') { isHex = false break } diff --git a/internal/toolcall/toolcalls_name_match.go b/internal/toolcall/toolcalls_name_match.go index afa85a0..1d06bfe 100644 --- a/internal/toolcall/toolcalls_name_match.go +++ b/internal/toolcall/toolcalls_name_match.go @@ -5,8 +5,10 @@ import ( "strings" ) +//nolint:unused // retained for policy-level tool-name matching compatibility. var toolNameLoosePattern = regexp.MustCompile(`[^a-z0-9]+`) +//nolint:unused // retained for policy-level tool-name matching compatibility. func resolveAllowedToolNameWithLooseMatch(name string, allowed map[string]struct{}, allowedCanonical map[string]string) string { if _, ok := allowed[name]; ok { return name diff --git a/internal/toolcall/toolcalls_parse.go b/internal/toolcall/toolcalls_parse.go index b5c5714..8f0a289 100644 --- a/internal/toolcall/toolcalls_parse.go +++ b/internal/toolcall/toolcalls_parse.go @@ -164,6 +164,7 @@ func filterToolCallsDetailed(parsed []ParsedToolCall, availableToolNames []strin return out, nil } +//nolint:unused // retained for policy-level tool-name matching compatibility. func resolveAllowedToolName(name string, allowed map[string]struct{}, allowedCanonical map[string]string) string { return resolveAllowedToolNameWithLooseMatch(name, allowed, allowedCanonical) } diff --git a/internal/toolcall/toolcalls_parse_item.go b/internal/toolcall/toolcalls_parse_item.go index 269fe3c..78b252c 100644 --- a/internal/toolcall/toolcalls_parse_item.go +++ b/internal/toolcall/toolcalls_parse_item.go @@ -7,7 +7,7 @@ func isLikelyJSONToolPayloadCandidate(candidate string) bool { if trimmed == "" { return false } - if !(strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[")) { + if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") { return false } lower := strings.ToLower(trimmed) diff --git a/scripts/lint.sh b/scripts/lint.sh index af07c27..cf8e14a 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -9,9 +9,30 @@ BOOTSTRAP_VERSION="${GOLANGCI_LINT_VERSION:-v2.11.4}" BOOTSTRAP_BIN="${ROOT_DIR}/.tmp/golangci-lint-${BOOTSTRAP_VERSION}" bootstrap_golangci_lint() { - local version_no_v archive_url tmp_dir + local version_no_v os arch artifact archive_url tmp_dir version_no_v="${BOOTSTRAP_VERSION#v}" - archive_url="https://github.com/golangci/golangci-lint/releases/download/${BOOTSTRAP_VERSION}/golangci-lint-${version_no_v}-linux-amd64.tar.gz" + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m | tr '[:upper:]' '[:lower:]')" + + case "$os" in + linux|darwin|windows) ;; + *) + echo "unsupported OS for bootstrap: ${os}" >&2 + return 1 + ;; + esac + + case "$arch" in + x86_64|amd64) arch="amd64" ;; + aarch64|arm64) arch="arm64" ;; + *) + echo "unsupported architecture for bootstrap: ${arch}" >&2 + return 1 + ;; + esac + + artifact="${os}-${arch}" + archive_url="https://github.com/golangci/golangci-lint/releases/download/${BOOTSTRAP_VERSION}/golangci-lint-${version_no_v}-${artifact}.tar.gz" mkdir -p "${ROOT_DIR}/.tmp" tmp_dir="$(mktemp -d)" @@ -19,7 +40,7 @@ bootstrap_golangci_lint() { curl -sSfL "${archive_url}" -o "${tmp_dir}/golangci-lint.tar.gz" tar -xzf "${tmp_dir}/golangci-lint.tar.gz" -C "${tmp_dir}" - cp "${tmp_dir}/golangci-lint-${version_no_v}-linux-amd64/golangci-lint" "${BOOTSTRAP_BIN}" + cp "${tmp_dir}/golangci-lint-${version_no_v}-${artifact}/golangci-lint" "${BOOTSTRAP_BIN}" chmod +x "${BOOTSTRAP_BIN}" echo "bootstrapped golangci-lint ${BOOTSTRAP_VERSION} to ${BOOTSTRAP_BIN}" >&2 @@ -34,6 +55,25 @@ run_lint() { fi } +is_compatibility_error() { + case "$1" in + *"command not found"*|\ + *"not recognized as an internal or external command"*|\ + *"No such file or directory"*|\ + *"unknown command \"fmt\""*|\ + *"unknown command \"run\""*|\ + *"unknown flag"*|\ + *"no such flag"*|\ + *"unsupported version of the configuration"*|\ + *"can't load config"*) + return 0 + ;; + *) + return 1 + ;; + esac +} + # v2 separates formatters from linters; enforce both in one entrypoint. if lint_output="$(run_lint "$LINT_BIN" 2>&1)"; then [[ -n "$lint_output" ]] && echo "$lint_output" @@ -46,9 +86,20 @@ if [[ -n "${GOLANGCI_LINT_BIN:-}" ]]; then exit 1 fi -echo "default golangci-lint is incompatible; bootstrapping ${BOOTSTRAP_VERSION}..." >&2 +if ! is_compatibility_error "$lint_output"; then + echo "$lint_output" >&2 + exit 1 +fi + +echo "default golangci-lint appears incompatible; bootstrapping ${BOOTSTRAP_VERSION}..." >&2 if [[ ! -x "${BOOTSTRAP_BIN}" ]]; then bootstrap_golangci_lint fi -run_lint "${BOOTSTRAP_BIN}" +if lint_output="$(run_lint "${BOOTSTRAP_BIN}" 2>&1)"; then + [[ -n "$lint_output" ]] && echo "$lint_output" + exit 0 +fi + +echo "$lint_output" >&2 +exit 1 diff --git a/tests/repair_json_tool.go b/tests/repair_json_tool.go index 7abf952..857caad 100644 --- a/tests/repair_json_tool.go +++ b/tests/repair_json_tool.go @@ -27,7 +27,7 @@ func repairInvalidJSONBackslashes(s string) string { isHex := true for j := 1; j <= 4; j++ { r := runes[i+1+j] - if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) { + if (r < '0' || r > '9') && (r < 'a' || r > 'f') && (r < 'A' || r > 'F') { isHex = false break }