فهرست منبع

fix: claude stream tool call index save to state (#404)

* fix: claude stream tool call index save to state

* fix: choice index need set to 0

* fix: ci lint
zijiren 1 ماه پیش
والد
کامیت
321d4e4518

+ 10 - 2
.golangci.yml

@@ -43,6 +43,7 @@ linters:
     - loggercheck
     - mirror
     - misspell
+    - modernize
     - musttag
     - nakedret
     - noctx
@@ -97,12 +98,21 @@ linters:
     errcheck:
       check-type-assertions: true
     forbidigo:
+      forbid:
+        - pattern: ^print(ln)?$
       analyze-types: true
     prealloc:
       for-loops: true
     staticcheck:
       dot-import-whitelist: []
       http-status-code-whitelist: []
+    modernize:
+      disable:
+        - forvar
+        - omitzero
+    perfsprint:
+      # use modernize stringsbuilder
+      concat-loop: false
     usestdlibvars:
       time-date-month: true
       time-month: true
@@ -136,8 +146,6 @@ formatters:
   settings:
     gofmt:
       rewrite-rules:
-        - pattern: "interface{}"
-          replacement: "any"
         - pattern: "a[b:len(a)]"
           replacement: "a[b:]"
     gofumpt:

+ 2 - 5
core/controller/mcp/groupmcp-server.go

@@ -1,6 +1,7 @@
 package controller
 
 import (
+	"maps"
 	"net/http"
 	"net/url"
 
@@ -167,13 +168,9 @@ func handleGroupProxyStreamable(c *gin.Context, config *model.GroupMCPProxyConfi
 		return
 	}
 
-	headers := make(map[string]string)
+	headers := maps.Clone(config.Headers)
 	backendQuery := backendURL.Query()
 
-	for k, v := range config.Headers {
-		headers[k] = v
-	}
-
 	for k, v := range config.Querys {
 		backendQuery.Set(k, v)
 	}

+ 3 - 6
core/controller/mcp/publicmcp-server.go

@@ -2,6 +2,7 @@ package controller
 
 import (
 	"fmt"
+	"maps"
 	"net/http"
 	"net/url"
 
@@ -192,9 +193,7 @@ func prepareProxyConfig(
 		}
 	}
 
-	for k, v := range publicMcp.ProxyConfig.Headers {
-		headers[k] = v
-	}
+	maps.Copy(headers, publicMcp.ProxyConfig.Headers)
 
 	url.RawQuery = backendQuery.Encode()
 
@@ -398,9 +397,7 @@ func handlePublicProxyStreamable(
 		return
 	}
 
-	for k, v := range config.Headers {
-		headers[k] = v
-	}
+	maps.Copy(headers, config.Headers)
 
 	for k, v := range config.Querys {
 		backendQuery.Set(k, v)

+ 1 - 0
core/model/channel.go

@@ -536,6 +536,7 @@ func DeleteChannelsByIDs(ids []int) (err error) {
 	defer func() {
 		if err == nil {
 			_ = InitModelConfigAndChannelCache()
+
 			for _, id := range ids {
 				_ = monitor.ClearChannelAllModelErrors(context.Background(), id)
 			}

+ 1 - 4
core/relay/adaptor/ali/stt-realtime.go

@@ -223,10 +223,7 @@ func STTDoResponse(
 		case "task-started":
 			chunkSize := 3 * 1024
 			for i := 0; i < len(audioData); i += chunkSize {
-				end := i + chunkSize
-				if end > len(audioData) {
-					end = len(audioData)
-				}
+				end := min(i+chunkSize, len(audioData))
 
 				chunk := audioData[i:end]
 

+ 3 - 1
core/relay/adaptor/anthropic/main.go

@@ -242,6 +242,8 @@ func StreamHandler(
 		writed bool
 	)
 
+	streamState := NewStreamState()
+
 	for scanner.Scan() {
 		data := scanner.Bytes()
 		if !render.IsValidSSEData(data) {
@@ -253,7 +255,7 @@ func StreamHandler(
 			break
 		}
 
-		response, err := StreamResponse2OpenAI(m, data)
+		response, err := streamState.StreamResponse2OpenAI(m, data)
 		if err != nil {
 			if writed {
 				log.Errorf("response error: %+v", err)

+ 49 - 13
core/relay/adaptor/anthropic/openai.go

@@ -268,11 +268,7 @@ func batchPatchImage2Base64(ctx context.Context, imageTasks []*relaymodel.Claude
 			continue
 		}
 
-		wg.Add(1)
-
-		go func() {
-			defer wg.Done()
-
+		wg.Go(func() {
 			_ = sem.Acquire(ctx, 1)
 			defer sem.Release(1)
 
@@ -291,7 +287,7 @@ func batchPatchImage2Base64(ctx context.Context, imageTasks []*relaymodel.Claude
 			task.Source.URL = ""
 			task.Source.MediaType = mimeType
 			task.Source.Data = data
-		}()
+		})
 	}
 
 	wg.Wait()
@@ -303,8 +299,42 @@ func batchPatchImage2Base64(ctx context.Context, imageTasks []*relaymodel.Claude
 	return nil
 }
 
-// https://docs.anthropic.com/claude/reference/messages-streaming
-func StreamResponse2OpenAI(
+// StreamState maintains state during streaming response conversion
+type StreamState struct {
+	// claudeIndexToToolCallIndex maps Claude's content block index to OpenAI tool call index
+	// Claude's index includes all content blocks (text, thinking, tool_use), but OpenAI only counts tool calls
+	claudeIndexToToolCallIndex map[int]int
+	// nextToolCallIndex tracks the next tool call index to assign (0-based)
+	nextToolCallIndex int
+}
+
+func NewStreamState() *StreamState {
+	return &StreamState{
+		claudeIndexToToolCallIndex: make(map[int]int),
+		nextToolCallIndex:          0,
+	}
+}
+
+// getToolCallIndex returns the OpenAI tool call index for a given Claude content block index
+// If this is the first time seeing this Claude index for a tool call, assigns a new tool call index
+func (s *StreamState) getToolCallIndex(claudeIndex int, isNewToolCall bool) int {
+	if idx, exists := s.claudeIndexToToolCallIndex[claudeIndex]; exists {
+		return idx
+	}
+
+	if isNewToolCall {
+		toolCallIndex := s.nextToolCallIndex
+		s.claudeIndexToToolCallIndex[claudeIndex] = toolCallIndex
+		s.nextToolCallIndex++
+		return toolCallIndex
+	}
+
+	// This shouldn't happen in normal flow, but return a safe default
+	return 0
+}
+
+// StreamResponse2OpenAI converts Claude streaming response to OpenAI format
+func (s *StreamState) StreamResponse2OpenAI(
 	meta *meta.Meta,
 	respData []byte,
 ) (*relaymodel.ChatCompletionsStreamResponse, adaptor.Error) {
@@ -340,8 +370,9 @@ func StreamResponse2OpenAI(
 		if claudeResponse.ContentBlock != nil {
 			content = claudeResponse.ContentBlock.Text
 			if claudeResponse.ContentBlock.Type == toolUseType {
+				toolCallIndex := s.getToolCallIndex(claudeResponse.Index, true)
 				tools = append(tools, relaymodel.ToolCall{
-					Index: claudeResponse.Index,
+					Index: toolCallIndex,
 					ID:    claudeResponse.ContentBlock.ID,
 					Type:  "function",
 					Function: relaymodel.Function{
@@ -354,8 +385,9 @@ func StreamResponse2OpenAI(
 		if claudeResponse.Delta != nil {
 			switch claudeResponse.Delta.Type {
 			case "input_json_delta":
+				toolCallIndex := s.getToolCallIndex(claudeResponse.Index, false)
 				tools = append(tools, relaymodel.ToolCall{
-					Index: claudeResponse.Index,
+					Index: toolCallIndex,
 					Type:  "function",
 					Function: relaymodel.Function{
 						Arguments: claudeResponse.Delta.PartialJSON,
@@ -393,6 +425,7 @@ func StreamResponse2OpenAI(
 			ToolCalls:        tools,
 			Role:             "assistant",
 		},
+		Index:        0,
 		FinishReason: stopReasonClaude2OpenAI(stopReason),
 	}
 
@@ -445,8 +478,9 @@ func Response2OpenAI(
 		case toolUseType:
 			args, _ := sonic.MarshalString(v.Input)
 			tools = append(tools, relaymodel.ToolCall{
-				ID:   v.ID,
-				Type: "function",
+				Index: len(tools),
+				ID:    v.ID,
+				Type:  "function",
 				Function: relaymodel.Function{
 					Name:      v.Name,
 					Arguments: args,
@@ -514,6 +548,8 @@ func OpenAIStreamHandler(
 		writed bool
 	)
 
+	streamState := NewStreamState()
+
 	for scanner.Scan() {
 		data := scanner.Bytes()
 		if !render.IsValidSSEData(data) {
@@ -525,7 +561,7 @@ func OpenAIStreamHandler(
 			break
 		}
 
-		response, err := StreamResponse2OpenAI(m, data)
+		response, err := streamState.StreamResponse2OpenAI(m, data)
 		if err != nil {
 			if writed {
 				log.Errorf("response error: %+v", err)

+ 3 - 1
core/relay/adaptor/aws/claude/claude.go

@@ -79,6 +79,8 @@ func StreamHandler(meta *meta.Meta, c *gin.Context) (model.Usage, adaptor.Error)
 		writed bool
 	)
 
+	streamState := anthropic.NewStreamState()
+
 	log := common.GetLogger(c)
 
 	for event := range stream.Events() {
@@ -86,7 +88,7 @@ func StreamHandler(meta *meta.Meta, c *gin.Context) (model.Usage, adaptor.Error)
 		case *types.ResponseStreamMemberChunk:
 			data := v.Value.Bytes
 
-			response, err := anthropic.StreamResponse2OpenAI(meta, v.Value.Bytes)
+			response, err := streamState.StreamResponse2OpenAI(meta, v.Value.Bytes)
 			if err != nil {
 				if writed {
 					log.Errorf("response error: %+v", err)

+ 3 - 1
core/relay/adaptor/aws/claude/openai.go

@@ -90,12 +90,14 @@ func OpenaiStreamHandler(meta *meta.Meta, c *gin.Context) (model.Usage, adaptor.
 		writed bool
 	)
 
+	streamState := anthropic.NewStreamState()
+
 	log := common.GetLogger(c)
 
 	for event := range stream.Events() {
 		switch v := event.(type) {
 		case *types.ResponseStreamMemberChunk:
-			response, err := anthropic.StreamResponse2OpenAI(meta, v.Value.Bytes)
+			response, err := streamState.StreamResponse2OpenAI(meta, v.Value.Bytes)
 			if err != nil {
 				if writed {
 					log.Errorf("response error: %+v", err)

+ 2 - 2
core/relay/adaptor/cohere/main.go

@@ -52,8 +52,8 @@ func ConvertRequest(textRequest *relaymodel.GeneralOpenAIRequest) *Request {
 		cohereRequest.Model = "command-r"
 	}
 
-	if strings.HasSuffix(cohereRequest.Model, "-internet") {
-		cohereRequest.Model = strings.TrimSuffix(cohereRequest.Model, "-internet")
+	if before, ok := strings.CutSuffix(cohereRequest.Model, "-internet"); ok {
+		cohereRequest.Model = before
 		cohereRequest.Connectors = append(cohereRequest.Connectors, WebSearchConnector)
 	}
 

+ 2 - 6
core/relay/adaptor/gemini/main.go

@@ -380,11 +380,7 @@ func processImageTasks(ctx context.Context, imageTasks []*Part) error {
 			continue
 		}
 
-		wg.Add(1)
-
-		go func() {
-			defer wg.Done()
-
+		wg.Go(func() {
 			_ = sem.Acquire(ctx, 1)
 			defer sem.Release(1)
 
@@ -401,7 +397,7 @@ func processImageTasks(ctx context.Context, imageTasks []*Part) error {
 
 			task.InlineData.MimeType = mimeType
 			task.InlineData.Data = data
-		}()
+		})
 	}
 
 	wg.Wait()

+ 2 - 3
core/relay/controller/dohelper.go

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"maps"
 	"net/http"
 	"strings"
 	"sync"
@@ -228,9 +229,7 @@ func setupRequestHeader(
 	req *http.Request,
 	header http.Header,
 ) adaptor.Error {
-	for key, value := range header {
-		req.Header[key] = value
-	}
+	maps.Copy(req.Header, header)
 
 	if err := a.SetupRequestHeader(meta, store, c, req); err != nil {
 		return relaymodel.WrapperErrorWithMessage(

+ 2 - 4
core/relay/plugin/cache/cache.go

@@ -7,6 +7,7 @@ import (
 	"encoding/hex"
 	"errors"
 	"fmt"
+	"maps"
 	"net/http"
 	"strconv"
 	"sync"
@@ -388,10 +389,7 @@ func (c *Cache) DoResponse(
 		}
 
 		// Convert http.Header to map[string][]string for JSON serialization
-		headerMap := make(map[string][]string)
-		for k, v := range rw.Header() {
-			headerMap[k] = v
-		}
+		headerMap := maps.Clone(rw.Header())
 
 		// Store in cache
 		item := Item{

+ 1 - 4
core/relay/plugin/thinksplit/splitter/splitter.go

@@ -159,10 +159,7 @@ func (s *Splitter) processSeekTail() ([]byte, []byte) {
 			if data[i] == tail[j] {
 				j++
 				if j == tailLen {
-					end := i - tailLen + 1
-					if end < 0 {
-						end = 0
-					}
+					end := max(i-tailLen+1, 0)
 
 					result := data[:end]
 					remaining := data[i+1:]

+ 1 - 1
core/relay/plugin/web-search/search.go

@@ -491,7 +491,7 @@ func (p *WebSearch) generateSearchContexts(
 // parseSearchContexts extracts search queries from LLM response
 func (p *WebSearch) parseSearchContexts(defaultLanguage, content string) []engine.SearchQuery {
 	var searchContexts []engine.SearchQuery
-	for _, line := range strings.Split(content, "\n") {
+	for line := range strings.SplitSeq(content, "\n") {
 		line = strings.TrimSpace(line)
 		if line == "" {
 			continue

+ 5 - 4
core/task/task.go

@@ -6,6 +6,7 @@ import (
 	"encoding/hex"
 	"fmt"
 	"slices"
+	"strings"
 	"time"
 
 	"github.com/bytedance/sonic"
@@ -209,18 +210,18 @@ func formatGroupUsageAlerts(alerts []model.GroupUsageAlertItem) string {
 		return ""
 	}
 
-	var result string
+	var result strings.Builder
 	for _, alert := range alerts {
-		result += fmt.Sprintf(
+		result.WriteString(fmt.Sprintf(
 			"GroupID: %s | 3-Day Avg: %.4f | Today: %.4f | Ratio: %.2fx\n",
 			alert.GroupID,
 			alert.ThreeDayAvgAmount,
 			alert.TodayAmount,
 			alert.Ratio,
-		)
+		))
 	}
 
-	return result
+	return result.String()
 }
 
 // CleanLogTask 清理日志任务

+ 29 - 26
mcp-servers/hosted/12306/parsers.go

@@ -3,6 +3,7 @@ package train12306
 import (
 	"fmt"
 	"regexp"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -178,9 +179,12 @@ func (s *Server) formatTicketsInfo(ticketsInfo []TicketInfo) string {
 		return "没有查询到相关车次信息"
 	}
 
-	result := "车次 | 出发站 -> 到达站 | 出发时间 -> 到达时间 | 历时\n"
+	var result strings.Builder
+	result.WriteString("车次 | 出发站 -> 到达站 | 出发时间 -> 到达时间 | 历时\n")
+
 	for _, ticketInfo := range ticketsInfo {
-		infoStr := fmt.Sprintf(
+		var infoStr strings.Builder
+		infoStr.WriteString(fmt.Sprintf(
 			"%s(实际车次train_no: %s) %s(telecode: %s) -> %s(telecode: %s) %s -> %s 历时:%s",
 			ticketInfo.StartTrainCode,
 			ticketInfo.TrainNo,
@@ -191,17 +195,19 @@ func (s *Server) formatTicketsInfo(ticketsInfo []TicketInfo) string {
 			ticketInfo.StartTime,
 			ticketInfo.ArriveTime,
 			ticketInfo.Lishi,
-		)
+		))
 
 		for _, price := range ticketInfo.Prices {
 			ticketStatus := s.formatTicketStatus(price.Num)
-			infoStr += fmt.Sprintf("\n- %s: %s %.1f元", price.SeatName, ticketStatus, price.Price)
+			infoStr.WriteString(
+				fmt.Sprintf("\n- %s: %s %.1f元", price.SeatName, ticketStatus, price.Price),
+			)
 		}
 
-		result += infoStr + "\n"
+		result.WriteString(infoStr.String() + "\n")
 	}
 
-	return result
+	return result.String()
 }
 
 // filterTicketsInfo filters and sorts ticket information
@@ -271,13 +277,7 @@ func (s *Server) matchesTrainFilter(ticketInfo TicketInfo, filter string) bool {
 
 // containsFlag checks if the flag list contains a specific flag
 func (s *Server) containsFlag(flags []string, flag string) bool {
-	for _, f := range flags {
-		if f == flag {
-			return true
-		}
-	}
-
-	return false
+	return slices.Contains(flags, flag)
 }
 
 // sortTicketsInfo sorts ticket information
@@ -855,40 +855,43 @@ func (s *Server) compareInterlineDuration(a, b InterlineInfo) int {
 
 // formatInterlinesInfo formats interline information for display
 func (s *Server) formatInterlinesInfo(interlinesInfo []InterlineInfo) string {
-	result := "出发时间 -> 到达时间 | 出发车站 -> 中转车站 -> 到达车站 | 换乘标志 |换乘等待时间| 总历时\n\n"
+	var result strings.Builder
+	result.WriteString("出发时间 -> 到达时间 | 出发车站 -> 中转车站 -> 到达车站 | 换乘标志 |换乘等待时间| 总历时\n\n")
 
 	for _, interlineInfo := range interlinesInfo {
-		result += fmt.Sprintf(
+		result.WriteString(fmt.Sprintf(
 			"%s %s -> %s %s | ",
 			interlineInfo.StartDate,
 			interlineInfo.StartTime,
 			interlineInfo.ArriveDate,
 			interlineInfo.ArriveTime,
-		)
-		result += fmt.Sprintf(
+		))
+		result.WriteString(fmt.Sprintf(
 			"%s -> %s -> %s | ",
 			interlineInfo.FromStationName,
 			interlineInfo.MiddleStationName,
 			interlineInfo.EndStationName,
-		)
+		))
 
 		switch {
 		case interlineInfo.SameStation:
-			result += "同站换乘"
+			result.WriteString("同站换乘")
 		case interlineInfo.SameTrain:
-			result += "同车换乘"
+			result.WriteString("同车换乘")
 		default:
-			result += "换站换乘"
+			result.WriteString("换站换乘")
 		}
 
-		result += fmt.Sprintf(" | %s | %s\n\n", interlineInfo.WaitTime, interlineInfo.Lishi)
-		result += "\t" + strings.ReplaceAll(
+		result.WriteString(
+			fmt.Sprintf(" | %s | %s\n\n", interlineInfo.WaitTime, interlineInfo.Lishi),
+		)
+		result.WriteString("\t" + strings.ReplaceAll(
 			s.formatTicketsInfo(interlineInfo.TicketList),
 			"\n",
 			"\n\t",
-		)
-		result += "\n"
+		))
+		result.WriteString("\n")
 	}
 
-	return result
+	return result.String()
 }

+ 2 - 2
mcp-servers/hosted/12306/tools.go

@@ -103,7 +103,7 @@ func (s *Server) addGetStationCodeOfCitysTool() {
 			}
 
 			result := make(map[string]any)
-			for _, city := range strings.Split(citys, "|") {
+			for city := range strings.SplitSeq(citys, "|") {
 				if station, exists := s.cityStationCodes[city]; exists {
 					result[city] = station
 				} else {
@@ -147,7 +147,7 @@ func (s *Server) addGetStationCodeByNamesTool() {
 			}
 
 			result := make(map[string]any)
-			for _, stationName := range strings.Split(stationNames, "|") {
+			for stationName := range strings.SplitSeq(stationNames, "|") {
 				cleanName := strings.TrimSuffix(stationName, "站")
 
 				if station, exists := s.nameStations[cleanName]; exists {

+ 2 - 5
mcp-servers/hosted/firecrawl/server.go

@@ -132,11 +132,8 @@ func (s *Server) withRetry(
 				strings.Contains(err.Error(), "429")
 
 			if isRateLimit && attempt < s.config.MaxAttempts {
-				delay := time.Duration(float64(s.config.InitialDelay) *
-					float64(int(1)<<(attempt-1)) * s.config.BackoffFactor)
-				if delay > s.config.MaxDelay {
-					delay = s.config.MaxDelay
-				}
+				delay := min(time.Duration(float64(s.config.InitialDelay)*
+					float64(int(1)<<(attempt-1))*s.config.BackoffFactor), s.config.MaxDelay)
 
 				select {
 				case <-time.After(delay):

+ 3 - 4
mcp-servers/hosted/hefeng-weather/server.go

@@ -7,6 +7,7 @@ import (
 	"io"
 	"net/http"
 	"net/url"
+	"slices"
 	"strconv"
 	"strings"
 	"time"
@@ -154,10 +155,8 @@ func ListTools(ctx context.Context) ([]mcp.Tool, error) {
 // validateDays validates the days parameter
 func validateDays(days string) error {
 	validDays := []string{"now", "24h", "72h", "168h", "3d", "7d", "10d", "15d", "30d"}
-	for _, validDay := range validDays {
-		if days == validDay {
-			return nil
-		}
+	if slices.Contains(validDays, days) {
+		return nil
 	}
 
 	return fmt.Errorf("无效的预报天数: %s,有效值为: %s", days, strings.Join(validDays, ", "))

+ 2 - 7
mcp-servers/hosted/howtocook/server.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"math"
 	"math/rand"
+	"slices"
 	"strings"
 
 	"github.com/bytedance/sonic"
@@ -780,13 +781,7 @@ func (s *Server) processRecipeIngredients(recipe Recipe, ingredientMap map[strin
 			existingItem.RecipeCount++
 
 			// Add recipe name if not already present
-			found := false
-			for _, recipeName := range existingItem.Recipes {
-				if recipeName == recipe.Name {
-					found = true
-					break
-				}
-			}
+			found := slices.Contains(existingItem.Recipes, recipe.Name)
 
 			if !found {
 				existingItem.Recipes = append(existingItem.Recipes, recipe.Name)

+ 2 - 6
mcp-servers/hosted/jina-tools/jina.go

@@ -5,6 +5,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"maps"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -135,12 +136,7 @@ func ListTools(ctx context.Context) ([]mcp.Tool, error) {
 
 // createHeaders creates HTTP headers with optional API key
 func (s *JinaServer) createHeaders(baseHeaders map[string]string) map[string]string {
-	headers := make(map[string]string)
-
-	// Copy base headers
-	for k, v := range baseHeaders {
-		headers[k] = v
-	}
+	headers := maps.Clone(baseHeaders)
 
 	// Add authorization if API key is available
 	if s.apiKey != "" {

+ 1 - 2
mcp-servers/hosted/nothon/utils.go

@@ -29,8 +29,7 @@ func ParseEnabledTools(enabledToolsStr string) map[string]bool {
 		return enabledToolsSet
 	}
 
-	tools := strings.Split(enabledToolsStr, ",")
-	for _, tool := range tools {
+	for tool := range strings.SplitSeq(enabledToolsStr, ",") {
 		tool = strings.TrimSpace(tool)
 		if tool != "" {
 			enabledToolsSet[tool] = true

+ 2 - 4
mcp-servers/hosted/web-search/server.go

@@ -50,10 +50,8 @@ var configTemplates = map[string]mcpservers.ConfigTemplate{
 		Description: "Default search engine to use (google, bing, arxiv)",
 		Validator: func(value string) error {
 			validEngines := []string{"google", "bing", "arxiv", "searchxng", "bingcn"}
-			for _, e := range validEngines {
-				if value == e {
-					return nil
-				}
+			if slices.Contains(validEngines, value) {
+				return nil
 			}
 			return fmt.Errorf(
 				"invalid engine: %s, must be one of: %s",

+ 10 - 8
openapi-mcp/convert/convert.go

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"maps"
 	"net/http"
 	"net/url"
 	"strings"
@@ -451,7 +452,9 @@ func (c *Converter) generateResponseDescription(responses openapi3.Responses) st
 		}
 
 		response := responseRef.Value
-		desc := fmt.Sprintf("- status: %s, description: %s", code, *response.Description)
+
+		var desc strings.Builder
+		desc.WriteString(fmt.Sprintf("- status: %s, description: %s", code, *response.Description))
 
 		rawSchema, ok := response.Extensions["schema"].(map[string]any)
 		if ok && len(rawSchema) > 0 {
@@ -474,7 +477,7 @@ func (c *Converter) generateResponseDescription(responses openapi3.Responses) st
 				continue
 			}
 
-			desc += fmt.Sprintf(", schema: %s", str)
+			desc.WriteString(fmt.Sprintf(", schema: %s", str))
 		}
 
 		if len(response.Content) > 0 {
@@ -490,12 +493,14 @@ func (c *Converter) generateResponseDescription(responses openapi3.Responses) st
 						continue
 					}
 
-					desc += fmt.Sprintf(", content type: %s, schema: %s", contentType, str)
+					desc.WriteString(
+						fmt.Sprintf(", content type: %s, schema: %s", contentType, str),
+					)
 				}
 			}
 		}
 
-		responseDescriptions = append(responseDescriptions, desc)
+		responseDescriptions = append(responseDescriptions, desc.String())
 	}
 
 	return strings.Join(responseDescriptions, "\n\n")
@@ -779,10 +784,7 @@ func (c *Converter) processSchemaProperty(
 
 		visited[refKey] = true
 		// Create a copy of the visited map to avoid cross-contamination between different branches
-		visitedCopy := make(map[string]bool)
-		for k, v := range visited {
-			visitedCopy[k] = v
-		}
+		visitedCopy := maps.Clone(visited)
 
 		visited = visitedCopy
 	}