package proxy import ( "encoding/base64" "encoding/json" "regexp" "strings" "time" "github.com/google/uuid" ) // 模型映射(有序,长 key 优先匹配,避免 "claude-sonnet-4" 误匹配 "claude-sonnet-4.5") type modelMapping struct { key string value string } var modelMapOrdered = []modelMapping{ {"claude-sonnet-4-20250514", "claude-sonnet-4"}, {"claude-sonnet-4-5", "claude-sonnet-4.5"}, {"claude-sonnet-4.5", "claude-sonnet-4.5"}, {"claude-sonnet-4-6", "claude-sonnet-4.6"}, {"claude-sonnet-4.6", "claude-sonnet-4.6"}, {"claude-opus-4-7", "claude-opus-4-7"}, {"claude-opus-4.7", "claude-opus-4-7"}, {"claude-haiku-4-5", "claude-haiku-4.5"}, {"claude-haiku-4.5", "claude-haiku-4.5"}, {"claude-opus-4-5", "claude-opus-4.5"}, {"claude-opus-4.5", "claude-opus-4.5"}, {"claude-opus-4-6", "claude-opus-4.6"}, {"claude-opus-4.6", "claude-opus-4.6"}, {"claude-sonnet-4", "claude-sonnet-4"}, {"claude-3-5-sonnet", "claude-sonnet-4.5"}, {"claude-3-opus", "claude-sonnet-4.5"}, {"claude-3-sonnet", "claude-sonnet-4"}, {"claude-3-haiku", "claude-haiku-4.5"}, {"gpt-4-turbo", "claude-sonnet-4.5"}, {"gpt-4o", "claude-sonnet-4.5"}, {"gpt-4", "claude-sonnet-4.5"}, {"gpt-3.5-turbo", "claude-sonnet-4.5"}, } // Thinking 模式提示 const ThinkingModePrompt = `enabled 200000` const minimalFallbackUserContent = "." // ParseModelAndThinking 解析模型名称,返回实际模型和是否启用 thinking func ParseModelAndThinking(model string, thinkingSuffix string) (string, bool) { lower := strings.ToLower(model) thinking := false // 使用配置的后缀检查 suffixLower := strings.ToLower(thinkingSuffix) if strings.HasSuffix(lower, suffixLower) { thinking = true model = model[:len(model)-len(thinkingSuffix)] lower = strings.ToLower(model) } // 映射模型(有序匹配,长 key 优先) for _, m := range modelMapOrdered { if strings.Contains(lower, m.key) { return m.value, thinking } } // 如果已经是有效的 Kiro 模型,直接返回 if strings.HasPrefix(lower, "claude-") { return model, thinking } return "claude-sonnet-4.5", thinking } func MapModel(model string) string { mapped, _ := ParseModelAndThinking(model, "-thinking") return mapped } // ==================== Claude API 类型 ==================== type ClaudeRequest struct { Model string `json:"model"` Messages []ClaudeMessage `json:"messages"` MaxTokens int `json:"max_tokens"` Temperature float64 `json:"temperature,omitempty"` TopP float64 `json:"top_p,omitempty"` Stream bool `json:"stream,omitempty"` System interface{} `json:"system,omitempty"` // string or []SystemBlock Tools []ClaudeTool `json:"tools,omitempty"` ToolChoice interface{} `json:"tool_choice,omitempty"` } type ClaudeMessage struct { Role string `json:"role"` Content interface{} `json:"content"` // string or []ContentBlock } type ClaudeContentBlock struct { Type string `json:"type"` Text string `json:"text,omitempty"` Thinking string `json:"thinking,omitempty"` ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Input interface{} `json:"input,omitempty"` ToolUseID string `json:"tool_use_id,omitempty"` Content interface{} `json:"content,omitempty"` // for tool_result Source *ImageSource `json:"source,omitempty"` } type ImageSource struct { Type string `json:"type"` MediaType string `json:"media_type"` Data string `json:"data"` } type ClaudeTool struct { Name string `json:"name"` Description string `json:"description"` InputSchema interface{} `json:"input_schema"` } type ClaudeResponse struct { ID string `json:"id"` Type string `json:"type"` Role string `json:"role"` Content []ClaudeContentBlock `json:"content"` Model string `json:"model"` StopReason string `json:"stop_reason"` StopSequence *string `json:"stop_sequence"` Usage ClaudeUsage `json:"usage"` } type ClaudeUsage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` } // ==================== Claude -> Kiro 转换 ==================== const maxToolDescLen = 10237 func ClaudeToKiro(req *ClaudeRequest, thinking bool) *KiroPayload { modelID := MapModel(req.Model) origin := "AI_EDITOR" // 提取系统提示 systemPrompt := extractSystemPrompt(req.System) // 如果启用 thinking 模式,注入 thinking 提示 if thinking { systemPrompt = ThinkingModePrompt + "\n\n" + systemPrompt } // 构建历史消息 history := make([]KiroHistoryMessage, 0) var currentContent string var currentImages []KiroImage var currentToolResults []KiroToolResult for i, msg := range req.Messages { isLast := i == len(req.Messages)-1 if msg.Role == "user" { content, images, toolResults := extractClaudeUserContent(msg.Content) content = normalizeUserContent(content, len(images) > 0) if isLast { currentContent = content currentImages = images currentToolResults = toolResults } else { userMsg := KiroUserInputMessage{ Content: content, // ModelID: modelID, Origin: origin, } if len(images) > 0 { userMsg.Images = images } if len(toolResults) > 0 { userMsg.UserInputMessageContext = &UserInputMessageContext{ ToolResults: toolResults, } } history = append(history, KiroHistoryMessage{ UserInputMessage: &userMsg, }) } } else if msg.Role == "assistant" { content, toolUses := extractClaudeAssistantContent(msg.Content) history = append(history, KiroHistoryMessage{ AssistantResponseMessage: &KiroAssistantResponseMessage{ Content: content, ToolUses: toolUses, }, }) } } // 确保 history 以 user 开始 if len(history) > 0 && history[0].AssistantResponseMessage != nil { history = append([]KiroHistoryMessage{{ UserInputMessage: &KiroUserInputMessage{ Content: "Begin conversation", // ModelID: modelID, Origin: origin, }, }}, history...) } // 构建最终内容 finalContent := "" if systemPrompt != "" { finalContent = "--- SYSTEM PROMPT ---\n" + systemPrompt + "\n--- END SYSTEM PROMPT ---\n\n" } if currentContent != "" { finalContent += currentContent } else if len(currentImages) > 0 { finalContent += normalizeUserContent("", true) } else if len(currentToolResults) > 0 { finalContent += buildToolResultsContinuation(currentToolResults) } else { finalContent += minimalFallbackUserContent } // 转换工具 kiroTools := convertClaudeTools(req.Tools) // 构建 payload payload := &KiroPayload{} payload.ConversationState.ChatTriggerType = "MANUAL" payload.ConversationState.ConversationID = buildConversationID(modelID, systemPrompt, firstClaudeConversationAnchor(req.Messages)) payload.ConversationState.CurrentMessage.UserInputMessage = KiroUserInputMessage{ Content: finalContent, // ModelID: modelID, Origin: origin, Images: currentImages, } if len(kiroTools) > 0 || len(currentToolResults) > 0 { payload.ConversationState.CurrentMessage.UserInputMessage.UserInputMessageContext = &UserInputMessageContext{ Tools: kiroTools, ToolResults: currentToolResults, } } if len(history) > 0 { payload.ConversationState.History = history } if req.MaxTokens > 0 || req.Temperature > 0 || req.TopP > 0 { payload.InferenceConfig = &InferenceConfig{ MaxTokens: req.MaxTokens, Temperature: req.Temperature, TopP: req.TopP, } } return payload } func extractSystemPrompt(system interface{}) string { if system == nil { return "" } if s, ok := system.(string); ok { return s } if blocks, ok := system.([]interface{}); ok { var parts []string for _, b := range blocks { if block, ok := b.(map[string]interface{}); ok { if text, ok := block["text"].(string); ok { parts = append(parts, text) } } } return strings.Join(parts, "\n") } return "" } func extractClaudeUserContent(content interface{}) (string, []KiroImage, []KiroToolResult) { var text string var images []KiroImage var toolResults []KiroToolResult if s, ok := content.(string); ok { return s, nil, nil } if blocks, ok := content.([]interface{}); ok { for _, b := range blocks { block, ok := b.(map[string]interface{}) if !ok { continue } blockType, _ := block["type"].(string) switch blockType { case "text", "input_text": if t, ok := block["text"].(string); ok { text += t } case "image", "image_url", "input_image": if img := extractImageFromClaudeBlock(block); img != nil { images = append(images, *img) } case "tool_result": toolUseID, _ := block["tool_use_id"].(string) resultContent := extractToolResultContent(block["content"]) toolResults = append(toolResults, KiroToolResult{ ToolUseID: toolUseID, Content: []KiroResultContent{{Text: resultContent}}, Status: "success", }) } } } return text, images, toolResults } func extractImageFromClaudeBlock(block map[string]interface{}) *KiroImage { if source, ok := block["source"].(map[string]interface{}); ok { if data, ok := source["data"].(string); ok { if img := parseDataURL(data); img != nil { return img } mediaType, _ := source["media_type"].(string) if mediaType == "" { mediaType, _ = source["mediaType"].(string) } if mediaType == "" { mediaType, _ = source["mime_type"].(string) } format := strings.TrimPrefix(strings.ToLower(mediaType), "image/") if img := parseBase64Image(data, format); img != nil { return img } } if url, ok := source["url"].(string); ok { if img := parseDataURL(url); img != nil { return img } } } if img := extractImageFromOpenAIPart(block); img != nil { return img } if data, ok := block["data"].(string); ok { if img := parseDataURL(data); img != nil { return img } } return nil } func extractToolResultContent(content interface{}) string { if s, ok := content.(string); ok { return s } if blocks, ok := content.([]interface{}); ok { var parts []string for _, b := range blocks { if block, ok := b.(map[string]interface{}); ok { if text, ok := block["text"].(string); ok { parts = append(parts, text) } } } return strings.Join(parts, "") } return "" } func extractClaudeAssistantContent(content interface{}) (string, []KiroToolUse) { var text string var toolUses []KiroToolUse if s, ok := content.(string); ok { return s, nil } if blocks, ok := content.([]interface{}); ok { for _, b := range blocks { block, ok := b.(map[string]interface{}) if !ok { continue } blockType, _ := block["type"].(string) switch blockType { case "text": if t, ok := block["text"].(string); ok { text += t } case "tool_use": id, _ := block["id"].(string) name, _ := block["name"].(string) input, _ := block["input"].(map[string]interface{}) if input == nil { input = make(map[string]interface{}) } toolUses = append(toolUses, KiroToolUse{ ToolUseID: id, Name: name, Input: input, }) } } } return text, toolUses } func convertClaudeTools(tools []ClaudeTool) []KiroToolWrapper { if len(tools) == 0 { return nil } result := make([]KiroToolWrapper, len(tools)) for i, tool := range tools { desc := tool.Description if len(desc) > maxToolDescLen { desc = desc[:maxToolDescLen] + "..." } result[i] = KiroToolWrapper{} result[i].ToolSpecification.Name = shortenToolName(tool.Name) result[i].ToolSpecification.Description = desc result[i].ToolSpecification.InputSchema = InputSchema{JSON: tool.InputSchema} } return result } func shortenToolName(name string) string { if len(name) <= 64 { return name } // MCP tools: mcp__server__tool -> mcp__tool if strings.HasPrefix(name, "mcp__") { lastIdx := strings.LastIndex(name, "__") if lastIdx > 5 { shortened := "mcp__" + name[lastIdx+2:] if len(shortened) <= 64 { return shortened } } } return name[:64] } // ==================== Kiro -> Claude 转换 ==================== func KiroToClaudeResponse(content, thinkingContent string, toolUses []KiroToolUse, inputTokens, outputTokens int, model string) *ClaudeResponse { blocks := make([]ClaudeContentBlock, 0) if thinkingContent != "" { blocks = append(blocks, ClaudeContentBlock{ Type: "thinking", Thinking: thinkingContent, }) } if content != "" { blocks = append(blocks, ClaudeContentBlock{ Type: "text", Text: content, }) } for _, tu := range toolUses { blocks = append(blocks, ClaudeContentBlock{ Type: "tool_use", ID: tu.ToolUseID, Name: tu.Name, Input: tu.Input, }) } stopReason := "end_turn" if len(toolUses) > 0 { stopReason = "tool_use" } return &ClaudeResponse{ ID: "msg_" + uuid.New().String(), Type: "message", Role: "assistant", Content: blocks, Model: model, StopReason: stopReason, Usage: ClaudeUsage{ InputTokens: inputTokens, OutputTokens: outputTokens, }, } } // ==================== OpenAI API 类型 ==================== type OpenAIRequest struct { Model string `json:"model"` Messages []OpenAIMessage `json:"messages"` MaxTokens int `json:"max_tokens,omitempty"` Temperature float64 `json:"temperature,omitempty"` TopP float64 `json:"top_p,omitempty"` Stream bool `json:"stream,omitempty"` Tools []OpenAITool `json:"tools,omitempty"` } type OpenAIMessage struct { Role string `json:"role"` Content interface{} `json:"content"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` } type ToolCall struct { ID string `json:"id"` Type string `json:"type"` Function struct { Name string `json:"name"` Arguments string `json:"arguments"` } `json:"function"` } type OpenAITool struct { Type string `json:"type"` Function struct { Name string `json:"name"` Description string `json:"description"` Parameters interface{} `json:"parameters"` } `json:"function"` } type OpenAIResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []OpenAIChoice `json:"choices"` Usage OpenAIUsage `json:"usage"` } type OpenAIChoice struct { Index int `json:"index"` Message OpenAIMessage `json:"message"` FinishReason string `json:"finish_reason"` } type OpenAIUsage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } // ==================== OpenAI -> Kiro 转换 ==================== func OpenAIToKiro(req *OpenAIRequest, thinking bool) *KiroPayload { modelID := MapModel(req.Model) origin := "AI_EDITOR" // 提取系统提示 var systemPrompt string var nonSystemMessages []OpenAIMessage for _, msg := range req.Messages { if msg.Role == "system" { if s := extractOpenAIMessageText(msg.Content); s != "" { systemPrompt += s + "\n" } } else { nonSystemMessages = append(nonSystemMessages, msg) } } // 如果启用 thinking 模式,注入 thinking 提示 if thinking { systemPrompt = ThinkingModePrompt + "\n\n" + systemPrompt } // 构建历史消息 history := make([]KiroHistoryMessage, 0) var currentContent string var currentImages []KiroImage var currentToolResults []KiroToolResult systemMerged := false for i, msg := range nonSystemMessages { isLast := i == len(nonSystemMessages)-1 switch msg.Role { case "user": content, images := extractOpenAIUserContent(msg.Content) content = normalizeUserContent(content, len(images) > 0) // 第一条 user 消息合并 system prompt if !systemMerged && systemPrompt != "" { content = systemPrompt + "\n" + content systemMerged = true } if isLast { currentContent = content currentImages = images } else { history = append(history, KiroHistoryMessage{ UserInputMessage: &KiroUserInputMessage{ Content: content, // ModelID: modelID, Origin: origin, Images: images, }, }) } case "assistant": content := extractOpenAIMessageText(msg.Content) var toolUses []KiroToolUse for _, tc := range msg.ToolCalls { var input map[string]interface{} json.Unmarshal([]byte(tc.Function.Arguments), &input) if input == nil { input = make(map[string]interface{}) } toolUses = append(toolUses, KiroToolUse{ ToolUseID: tc.ID, Name: tc.Function.Name, Input: input, }) } history = append(history, KiroHistoryMessage{ AssistantResponseMessage: &KiroAssistantResponseMessage{ Content: content, ToolUses: toolUses, }, }) case "tool": content := extractOpenAIMessageText(msg.Content) currentToolResults = append(currentToolResults, KiroToolResult{ ToolUseID: msg.ToolCallID, Content: []KiroResultContent{{Text: content}}, Status: "success", }) // 检查下一条是否还是 tool nextIdx := i + 1 if nextIdx >= len(nonSystemMessages) || nonSystemMessages[nextIdx].Role != "tool" { if !isLast { history = append(history, KiroHistoryMessage{ UserInputMessage: &KiroUserInputMessage{ Content: buildToolResultsContinuation(currentToolResults), // ModelID: modelID, Origin: origin, UserInputMessageContext: &UserInputMessageContext{ ToolResults: currentToolResults, }, }, }) currentToolResults = nil } } } } // 构建最终内容 finalContent := currentContent if finalContent == "" { if len(currentImages) > 0 { finalContent = normalizeUserContent("", true) } else if len(currentToolResults) > 0 { finalContent = buildToolResultsContinuation(currentToolResults) } else { finalContent = minimalFallbackUserContent } } if !systemMerged && systemPrompt != "" { finalContent = systemPrompt + "\n" + finalContent } // 转换工具 kiroTools := convertOpenAITools(req.Tools) // 构建 payload payload := &KiroPayload{} payload.ConversationState.ChatTriggerType = "MANUAL" payload.ConversationState.ConversationID = buildConversationID(modelID, systemPrompt, firstOpenAIConversationAnchor(nonSystemMessages)) payload.ConversationState.CurrentMessage.UserInputMessage = KiroUserInputMessage{ Content: finalContent, // ModelID: modelID, Origin: origin, Images: currentImages, } if len(kiroTools) > 0 || len(currentToolResults) > 0 { payload.ConversationState.CurrentMessage.UserInputMessage.UserInputMessageContext = &UserInputMessageContext{ Tools: kiroTools, ToolResults: currentToolResults, } } if len(history) > 0 { payload.ConversationState.History = history } if req.MaxTokens > 0 || req.Temperature > 0 || req.TopP > 0 { payload.InferenceConfig = &InferenceConfig{ MaxTokens: req.MaxTokens, Temperature: req.Temperature, TopP: req.TopP, } } return payload } func extractOpenAIUserContent(content interface{}) (string, []KiroImage) { if s, ok := content.(string); ok { return s, nil } var text string var images []KiroImage if part, ok := content.(map[string]interface{}); ok { if t, ok := extractOpenAITextPart(part); ok { text += t } if img := extractImageFromOpenAIPart(part); img != nil { images = append(images, *img) } } if parts, ok := content.([]interface{}); ok { for _, p := range parts { part, ok := p.(map[string]interface{}) if !ok { continue } if t, ok := extractOpenAITextPart(part); ok { text += t } if img := extractImageFromOpenAIPart(part); img != nil { images = append(images, *img) } } } if len(images) > 0 { text = sanitizeImagePlaceholders(text) } return text, images } func extractOpenAIMessageText(content interface{}) string { if content == nil { return "" } if s, ok := content.(string); ok { return s } if text, _ := extractOpenAIUserContent(content); strings.TrimSpace(text) != "" { return text } switch v := content.(type) { case map[string]interface{}: if nested, ok := v["content"]; ok { if nestedText := extractOpenAIMessageText(nested); strings.TrimSpace(nestedText) != "" { return nestedText } } if raw, err := json.Marshal(v); err == nil { return string(raw) } case []interface{}: parts := make([]string, 0, len(v)) for _, item := range v { partText := extractOpenAIMessageText(item) if strings.TrimSpace(partText) != "" { parts = append(parts, partText) } } if len(parts) > 0 { return strings.Join(parts, "") } if raw, err := json.Marshal(v); err == nil { return string(raw) } default: if raw, err := json.Marshal(v); err == nil { return string(raw) } } return "" } func buildToolResultsContinuation(toolResults []KiroToolResult) string { if len(toolResults) == 0 { return minimalFallbackUserContent } parts := make([]string, 0, len(toolResults)) for _, tr := range toolResults { if len(tr.Content) == 0 { continue } for _, c := range tr.Content { if strings.TrimSpace(c.Text) != "" { parts = append(parts, c.Text) } } } if len(parts) == 0 { return minimalFallbackUserContent } joined := strings.Join(parts, "\n\n") if len(joined) > 4000 { return joined[:4000] } return joined } func firstClaudeConversationAnchor(messages []ClaudeMessage) string { for _, msg := range messages { if msg.Role != "user" { continue } text, _, toolResults := extractClaudeUserContent(msg.Content) if strings.TrimSpace(text) != "" { return strings.TrimSpace(text) } if len(toolResults) > 0 { return buildToolResultsContinuation(toolResults) } } for _, msg := range messages { if strings.TrimSpace(msg.Role) != "" { if text := extractOpenAIMessageText(msg.Content); strings.TrimSpace(text) != "" { return strings.TrimSpace(text) } } } return "" } func firstOpenAIConversationAnchor(messages []OpenAIMessage) string { for _, msg := range messages { if msg.Role != "user" { continue } text := extractOpenAIMessageText(msg.Content) if strings.TrimSpace(text) != "" { return strings.TrimSpace(text) } } for _, msg := range messages { text := extractOpenAIMessageText(msg.Content) if strings.TrimSpace(text) != "" { return strings.TrimSpace(text) } } return "" } func buildConversationID(modelID, systemPrompt, anchor string) string { anchor = strings.TrimSpace(anchor) if anchor == "" { return uuid.New().String() } seed := strings.Join([]string{modelID, strings.TrimSpace(systemPrompt), anchor}, "\n") return uuid.NewSHA1(uuid.NameSpaceURL, []byte(seed)).String() } func extractOpenAITextPart(part map[string]interface{}) (string, bool) { partType, _ := part["type"].(string) switch partType { case "text", "input_text": if t, ok := part["text"].(string); ok { return t, true } } if t, ok := part["text"].(string); ok { return t, true } return "", false } func extractImageFromOpenAIPart(part map[string]interface{}) *KiroImage { partType, _ := part["type"].(string) if partType != "" { switch partType { case "image", "image_url", "input_image", "file", "input_file": default: return nil } } if fileObj, ok := part["file"].(map[string]interface{}); ok { if img := extractImageFromOpenAIPart(fileObj); img != nil { return img } } if sourceObj, ok := part["source"].(map[string]interface{}); ok { if img := extractImageFromOpenAIPart(sourceObj); img != nil { return img } } if raw, ok := part["mime"].(string); ok && !strings.HasPrefix(strings.ToLower(raw), "image/") { return nil } if raw, ok := part["media_type"].(string); ok && !strings.HasPrefix(strings.ToLower(raw), "image/") { return nil } if raw, ok := part["mime_type"].(string); ok && !strings.HasPrefix(strings.ToLower(raw), "image/") { return nil } if raw, ok := part["url"].(string); ok { if img := parseDataURL(raw); img != nil { return img } } if raw, ok := part["b64_json"].(string); ok { if img := parseBase64Image(raw, "png"); img != nil { return img } } if raw, ok := part["image_url"]; ok { switch v := raw.(type) { case string: if img := parseDataURL(v); img != nil { return img } case map[string]interface{}: if u, ok := v["url"].(string); ok { if img := parseDataURL(u); img != nil { return img } } } } if raw, ok := part["image_base64"].(string); ok { if img := parseBase64Image(raw, "png"); img != nil { return img } } if raw, ok := part["data"].(string); ok { if img := parseDataURL(raw); img != nil { return img } if img := parseBase64Image(raw, "png"); img != nil { return img } } return nil } func sanitizeImagePlaceholders(text string) string { re := regexp.MustCompile(`\[Image\s+\d+\]`) cleaned := re.ReplaceAllString(text, "") cleaned = strings.Join(strings.Fields(cleaned), " ") return strings.TrimSpace(cleaned) } func normalizeUserContent(text string, hasImages bool) string { trimmed := strings.TrimSpace(text) if trimmed == "" && hasImages { return "Please analyze the attached image." } return trimmed } func parseDataURL(url string) *KiroImage { cleaned := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(url, "\n", ""), "\r", "")) if strings.Contains(cleaned, "[Image") { return nil } re := regexp.MustCompile(`^data:image/([a-zA-Z0-9+.-]+)(;[a-zA-Z0-9=._:+-]+)*;base64,(.+)$`) matches := re.FindStringSubmatch(cleaned) if len(matches) == 4 { return parseBase64Image(matches[3], matches[1]) } if len(matches) != 3 { return nil } return parseBase64Image(matches[2], matches[1]) } func parseBase64Image(data, format string) *KiroImage { format = strings.ToLower(format) if format == "jpg" { format = "jpeg" } // 验证 base64 if _, err := base64.StdEncoding.DecodeString(data); err != nil { if _, errRaw := base64.RawStdEncoding.DecodeString(data); errRaw != nil { if _, errURL := base64.URLEncoding.DecodeString(data); errURL != nil { if _, errRawURL := base64.RawURLEncoding.DecodeString(data); errRawURL != nil { return nil } } } } if format == "" { format = "png" } return &KiroImage{ Format: format, Source: struct { Bytes string `json:"bytes"` }{Bytes: data}, } } func convertOpenAITools(tools []OpenAITool) []KiroToolWrapper { if len(tools) == 0 { return nil } result := make([]KiroToolWrapper, 0, len(tools)) for _, tool := range tools { if tool.Type != "function" { continue } desc := tool.Function.Description if len(desc) > maxToolDescLen { desc = desc[:maxToolDescLen] + "..." } wrapper := KiroToolWrapper{} wrapper.ToolSpecification.Name = shortenToolName(tool.Function.Name) wrapper.ToolSpecification.Description = desc wrapper.ToolSpecification.InputSchema = InputSchema{JSON: tool.Function.Parameters} result = append(result, wrapper) } return result } // ==================== Kiro -> OpenAI 转换 ==================== func KiroToOpenAIResponse(content string, toolUses []KiroToolUse, inputTokens, outputTokens int, model string) *OpenAIResponse { msg := OpenAIMessage{ Role: "assistant", } finishReason := "stop" if len(toolUses) > 0 { msg.Content = nil msg.ToolCalls = make([]ToolCall, len(toolUses)) for i, tu := range toolUses { args, _ := json.Marshal(tu.Input) msg.ToolCalls[i] = ToolCall{ ID: tu.ToolUseID, Type: "function", } msg.ToolCalls[i].Function.Name = tu.Name msg.ToolCalls[i].Function.Arguments = string(args) } finishReason = "tool_calls" } else { msg.Content = content } return &OpenAIResponse{ ID: "chatcmpl-" + uuid.New().String(), Object: "chat.completion", Created: time.Now().Unix(), Model: model, Choices: []OpenAIChoice{{ Index: 0, Message: msg, FinishReason: finishReason, }}, Usage: OpenAIUsage{ PromptTokens: inputTokens, CompletionTokens: outputTokens, TotalTokens: inputTokens + outputTokens, }, } } // extractThinkingFromContent 从内容中提取 标签内的内容 func extractThinkingFromContent(content string) (string, string) { var reasoning string result := content for { start := strings.Index(result, "") if start == -1 { break } end := strings.Index(result[start:], "") if end == -1 { break } end += start // 提取 thinking 内容 thinkingContent := result[start+10 : end] reasoning += thinkingContent // 从结果中移除 thinking 标签 result = result[:start] + result[end+11:] } return strings.TrimSpace(result), reasoning } // KiroToOpenAIResponseWithReasoning 带 reasoning_content 的 OpenAI 响应 func KiroToOpenAIResponseWithReasoning(content, reasoningContent string, toolUses []KiroToolUse, inputTokens, outputTokens int, model, thinkingFormat string) map[string]interface{} { finishReason := "stop" message := map[string]interface{}{ "role": "assistant", } if len(toolUses) > 0 { message["content"] = nil toolCalls := make([]map[string]interface{}, len(toolUses)) for i, tu := range toolUses { args, _ := json.Marshal(tu.Input) toolCalls[i] = map[string]interface{}{ "id": tu.ToolUseID, "type": "function", "function": map[string]string{ "name": tu.Name, "arguments": string(args), }, } } message["tool_calls"] = toolCalls finishReason = "tool_calls" } else { // 根据配置格式化 thinking 输出 if reasoningContent != "" { switch thinkingFormat { case "thinking": message["content"] = "" + reasoningContent + "" + content case "think": message["content"] = "" + reasoningContent + "" + content default: // "reasoning_content" message["content"] = content message["reasoning_content"] = reasoningContent } } else { message["content"] = content } } return map[string]interface{}{ "id": "chatcmpl-" + uuid.New().String(), "object": "chat.completion", "created": time.Now().Unix(), "model": model, "choices": []map[string]interface{}{{ "index": 0, "message": message, "finish_reason": finishReason, }}, "usage": map[string]int{ "prompt_tokens": inputTokens, "completion_tokens": outputTokens, "total_tokens": inputTokens + outputTokens, }, } }