feat: Kiro API Proxy - OpenAI/Anthropic compatible API service

- Multi-account pool with round-robin load balancing
- Auto token refresh for IAM IdC and Social auth
- Streaming support (SSE)
- Web admin panel with account management
- Docker support with GitHub Actions CI/CD
- Machine ID management per account
- Usage tracking (requests, tokens, credits)
This commit is contained in:
Quorinex
2026-02-04 00:37:05 +08:00
commit c5e6d42163
18 changed files with 5218 additions and 0 deletions

1392
proxy/handler.go Normal file

File diff suppressed because it is too large Load Diff

370
proxy/kiro.go Normal file
View File

@@ -0,0 +1,370 @@
// Package proxy Kiro API 代理核心
// 负责调用 Kiro API 并解析 AWS Event Stream 响应
package proxy
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"kiro-api-proxy/config"
"net/http"
"strings"
"time"
"github.com/google/uuid"
)
const (
KiroEndpoint = "https://codewhisperer.us-east-1.amazonaws.com/generateAssistantResponse"
KiroVersion = "0.6.18"
)
// ==================== 请求结构 ====================
// KiroPayload Kiro API 请求体
type KiroPayload struct {
ConversationState struct {
ChatTriggerType string `json:"chatTriggerType"`
ConversationID string `json:"conversationId"`
CurrentMessage struct {
UserInputMessage KiroUserInputMessage `json:"userInputMessage"`
} `json:"currentMessage"`
History []KiroHistoryMessage `json:"history,omitempty"`
} `json:"conversationState"`
ProfileArn string `json:"profileArn,omitempty"`
InferenceConfig *InferenceConfig `json:"inferenceConfig,omitempty"`
}
type KiroUserInputMessage struct {
Content string `json:"content"`
ModelID string `json:"modelId,omitempty"`
Origin string `json:"origin"`
Images []KiroImage `json:"images,omitempty"`
UserInputMessageContext *UserInputMessageContext `json:"userInputMessageContext,omitempty"`
}
type UserInputMessageContext struct {
Tools []KiroToolWrapper `json:"tools,omitempty"`
ToolResults []KiroToolResult `json:"toolResults,omitempty"`
}
type KiroToolWrapper struct {
ToolSpecification struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema InputSchema `json:"inputSchema"`
} `json:"toolSpecification"`
}
type InputSchema struct {
JSON interface{} `json:"json"`
}
type KiroToolResult struct {
ToolUseID string `json:"toolUseId"`
Content []KiroResultContent `json:"content"`
Status string `json:"status"`
}
type KiroResultContent struct {
Text string `json:"text"`
}
type KiroImage struct {
Format string `json:"format"`
Source struct {
Bytes string `json:"bytes"`
} `json:"source"`
}
type KiroHistoryMessage struct {
UserInputMessage *KiroUserInputMessage `json:"userInputMessage,omitempty"`
AssistantResponseMessage *KiroAssistantResponseMessage `json:"assistantResponseMessage,omitempty"`
}
type KiroAssistantResponseMessage struct {
Content string `json:"content"`
ToolUses []KiroToolUse `json:"toolUses,omitempty"`
}
type KiroToolUse struct {
ToolUseID string `json:"toolUseId"`
Name string `json:"name"`
Input map[string]interface{} `json:"input"`
}
type InferenceConfig struct {
MaxTokens int `json:"maxTokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"topP,omitempty"`
}
// ==================== 流式回调 ====================
// KiroStreamCallback 流式响应回调
type KiroStreamCallback struct {
OnText func(text string, isThinking bool)
OnToolUse func(toolUse KiroToolUse)
OnComplete func(inputTokens, outputTokens int)
OnError func(err error)
OnCredits func(credits float64)
}
// ==================== API 调用 ====================
// CallKiroAPI 调用 Kiro API流式
func CallKiroAPI(account *config.Account, payload *KiroPayload, callback *KiroStreamCallback) error {
body, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequest("POST", KiroEndpoint, bytes.NewReader(body))
if err != nil {
return err
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "*/*")
req.Header.Set("X-Amz-Target", "AmazonCodeWhispererStreamingService.GenerateAssistantResponse")
// User-Agent 包含机器码
machineId := account.MachineId
var userAgent, amzUserAgent string
if machineId != "" {
userAgent = fmt.Sprintf("aws-sdk-js/1.0.18 ua/2.1 os/linux lang/js md/nodejs#20.16.0 api/codewhispererstreaming#1.0.18 m/E KiroIDE-%s-%s", KiroVersion, machineId)
amzUserAgent = fmt.Sprintf("aws-sdk-js/1.0.18 KiroIDE %s %s", KiroVersion, machineId)
} else {
userAgent = fmt.Sprintf("aws-sdk-js/1.0.18 ua/2.1 os/linux lang/js md/nodejs#20.16.0 api/codewhispererstreaming#1.0.18 m/E KiroIDE-%s", KiroVersion)
amzUserAgent = fmt.Sprintf("aws-sdk-js/1.0.18 KiroIDE %s", KiroVersion)
}
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-Amz-User-Agent", amzUserAgent)
req.Header.Set("x-amzn-kiro-agent-mode", "spec")
req.Header.Set("x-amzn-codewhisperer-optout", "true")
req.Header.Set("Amz-Sdk-Request", "attempt=1; max=3")
req.Header.Set("Amz-Sdk-Invocation-Id", uuid.New().String())
req.Header.Set("Authorization", "Bearer "+account.AccessToken)
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
return parseEventStream(resp.Body, callback)
}
// ==================== Event Stream 解析 ====================
// parseEventStream 解析 AWS Event Stream 二进制格式
func parseEventStream(body io.Reader, callback *KiroStreamCallback) error {
reader := bufio.NewReader(body)
var inputTokens, outputTokens int
var totalOutputChars int
var totalCredits float64
var currentToolUse *toolUseState
for {
// Prelude: 12 bytes (total_len + headers_len + crc)
prelude := make([]byte, 12)
_, err := io.ReadFull(reader, prelude)
if err == io.EOF {
break
}
if err != nil {
return err
}
totalLength := int(prelude[0])<<24 | int(prelude[1])<<16 | int(prelude[2])<<8 | int(prelude[3])
headersLength := int(prelude[4])<<24 | int(prelude[5])<<16 | int(prelude[6])<<8 | int(prelude[7])
if totalLength < 16 {
continue
}
// 读取剩余部分
remaining := totalLength - 12
msgBuf := make([]byte, remaining)
_, err = io.ReadFull(reader, msgBuf)
if err != nil {
return err
}
if headersLength > len(msgBuf)-4 {
continue
}
eventType := extractEventType(msgBuf[0:headersLength])
payloadBytes := msgBuf[headersLength : len(msgBuf)-4]
if len(payloadBytes) == 0 {
continue
}
var event map[string]interface{}
if err := json.Unmarshal(payloadBytes, &event); err != nil {
continue
}
// 处理事件
switch eventType {
case "assistantResponseEvent":
if content, ok := event["content"].(string); ok && content != "" {
callback.OnText(content, false)
totalOutputChars += len(content)
}
case "reasoningContentEvent":
if text, ok := event["text"].(string); ok && text != "" {
callback.OnText(text, true)
totalOutputChars += len(text)
}
case "toolUseEvent":
currentToolUse = handleToolUseEvent(event, currentToolUse, callback)
case "messageMetadataEvent", "metadataEvent":
if tokenUsage, ok := event["tokenUsage"].(map[string]interface{}); ok {
if v, ok := tokenUsage["outputTokens"].(float64); ok {
outputTokens = int(v)
}
uncached, _ := tokenUsage["uncachedInputTokens"].(float64)
cacheRead, _ := tokenUsage["cacheReadInputTokens"].(float64)
cacheWrite, _ := tokenUsage["cacheWriteInputTokens"].(float64)
inputTokens = int(uncached + cacheRead + cacheWrite)
}
case "meteringEvent":
if usage, ok := event["usage"].(float64); ok {
totalCredits += usage
}
}
}
// 估算 token约 3 字符 = 1 token
if outputTokens == 0 && totalOutputChars > 0 {
outputTokens = max(1, totalOutputChars/3)
}
if callback.OnCredits != nil && totalCredits > 0 {
callback.OnCredits(totalCredits)
}
callback.OnComplete(inputTokens, outputTokens)
return nil
}
// ==================== Tool Use 处理 ====================
type toolUseState struct {
ToolUseID string
Name string
InputBuffer strings.Builder
}
func handleToolUseEvent(event map[string]interface{}, current *toolUseState, callback *KiroStreamCallback) *toolUseState {
toolUseID, _ := event["toolUseId"].(string)
name, _ := event["name"].(string)
isStop, _ := event["stop"].(bool)
if toolUseID != "" && name != "" {
if current == nil {
current = &toolUseState{ToolUseID: toolUseID, Name: name}
} else if current.ToolUseID != toolUseID {
finishToolUse(current, callback)
current = &toolUseState{ToolUseID: toolUseID, Name: name}
}
}
if current != nil {
if input, ok := event["input"].(string); ok {
current.InputBuffer.WriteString(input)
} else if inputObj, ok := event["input"].(map[string]interface{}); ok {
data, _ := json.Marshal(inputObj)
current.InputBuffer.Reset()
current.InputBuffer.Write(data)
}
}
if isStop && current != nil {
finishToolUse(current, callback)
return nil
}
return current
}
func finishToolUse(state *toolUseState, callback *KiroStreamCallback) {
var input map[string]interface{}
if state.InputBuffer.Len() > 0 {
json.Unmarshal([]byte(state.InputBuffer.String()), &input)
}
if input == nil {
input = make(map[string]interface{})
}
callback.OnToolUse(KiroToolUse{
ToolUseID: state.ToolUseID,
Name: state.Name,
Input: input,
})
}
// extractEventType 从 headers 中提取事件类型
func extractEventType(headers []byte) string {
offset := 0
for offset < len(headers) {
if offset >= len(headers) {
break
}
nameLen := int(headers[offset])
offset++
if offset+nameLen > len(headers) {
break
}
name := string(headers[offset : offset+nameLen])
offset += nameLen
if offset >= len(headers) {
break
}
valueType := headers[offset]
offset++
if valueType == 7 { // String
if offset+2 > len(headers) {
break
}
valueLen := int(headers[offset])<<8 | int(headers[offset+1])
offset += 2
if offset+valueLen > len(headers) {
break
}
value := string(headers[offset : offset+valueLen])
offset += valueLen
if name == ":event-type" {
return value
}
continue
}
// 跳过其他类型
skipSizes := map[byte]int{0: 0, 1: 0, 2: 1, 3: 2, 4: 4, 5: 8, 8: 8, 9: 16}
if valueType == 6 {
if offset+2 > len(headers) {
break
}
l := int(headers[offset])<<8 | int(headers[offset+1])
offset += 2 + l
} else if skip, ok := skipSizes[valueType]; ok {
offset += skip
} else {
break
}
}
return ""
}

271
proxy/kiro_api.go Normal file
View File

@@ -0,0 +1,271 @@
package proxy
import (
"encoding/json"
"fmt"
"io"
"kiro-api-proxy/config"
"net/http"
"strings"
"time"
)
const (
kiroRestAPIBase = "https://codewhisperer.us-east-1.amazonaws.com"
kiroVersion = "0.6.18"
)
// GetUsageLimits 获取账户使用量和订阅信息
func GetUsageLimits(account *config.Account) (*UsageLimitsResponse, error) {
url := fmt.Sprintf("%s/getUsageLimits?origin=AI_EDITOR&resourceType=AGENTIC_REQUEST&isEmailRequired=true", kiroRestAPIBase)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
setKiroHeaders(req, account)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var result UsageLimitsResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}
// GetUserInfo 获取用户信息
func GetUserInfo(account *config.Account) (*UserInfoResponse, error) {
url := fmt.Sprintf("%s/GetUserInfo", kiroRestAPIBase)
payload := `{"origin":"KIRO_IDE"}`
req, err := http.NewRequest("POST", url, strings.NewReader(payload))
if err != nil {
return nil, err
}
setKiroHeaders(req, account)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var result UserInfoResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}
// ListAvailableModels 获取可用模型列表
func ListAvailableModels(account *config.Account) ([]ModelInfo, error) {
url := fmt.Sprintf("%s/ListAvailableModels?origin=AI_EDITOR&maxResults=50", kiroRestAPIBase)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
setKiroHeaders(req, account)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var result struct {
Models []ModelInfo `json:"models"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Models, nil
}
func setKiroHeaders(req *http.Request, account *config.Account) {
machineId := account.MachineId
var userAgent, amzUserAgent string
if machineId != "" {
userAgent = fmt.Sprintf("aws-sdk-js/1.0.18 ua/2.1 os/windows lang/js md/nodejs#20.16.0 api/codewhispererstreaming#1.0.18 m/E KiroIDE-%s-%s", kiroVersion, machineId)
amzUserAgent = fmt.Sprintf("aws-sdk-js/1.0.18 KiroIDE %s %s", kiroVersion, machineId)
} else {
userAgent = fmt.Sprintf("aws-sdk-js/1.0.18 ua/2.1 os/windows lang/js md/nodejs#20.16.0 api/codewhispererstreaming#1.0.18 m/E KiroIDE-%s", kiroVersion)
amzUserAgent = fmt.Sprintf("aws-sdk-js/1.0.18 KiroIDE-%s", kiroVersion)
}
req.Header.Set("Authorization", "Bearer "+account.AccessToken)
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", userAgent)
req.Header.Set("x-amz-user-agent", amzUserAgent)
req.Header.Set("x-amzn-codewhisperer-optout", "true")
}
// RefreshAccountInfo 刷新账户信息(使用量、订阅等)
func RefreshAccountInfo(account *config.Account) (*config.AccountInfo, error) {
info := &config.AccountInfo{
LastRefresh: time.Now().Unix(),
}
// 获取使用量和订阅信息
usage, err := GetUsageLimits(account)
if err != nil {
return nil, fmt.Errorf("GetUsageLimits: %w", err)
}
// 解析用户信息
if usage.UserInfo != nil {
info.Email = usage.UserInfo.Email
info.UserId = usage.UserInfo.UserId
}
// 解析订阅信息
if usage.SubscriptionInfo != nil {
// 优先从 SubscriptionTitle 或 SubscriptionName 解析类型
titleOrName := usage.SubscriptionInfo.SubscriptionTitle
if titleOrName == "" {
titleOrName = usage.SubscriptionInfo.SubscriptionName
}
if titleOrName == "" {
titleOrName = usage.SubscriptionInfo.SubscriptionType
}
info.SubscriptionType = parseSubscriptionType(titleOrName)
info.SubscriptionTitle = usage.SubscriptionInfo.SubscriptionTitle
if info.SubscriptionTitle == "" {
info.SubscriptionTitle = usage.SubscriptionInfo.SubscriptionName
}
fmt.Printf("[RefreshAccountInfo] Subscription: type=%s, title=%s, name=%s, parsed=%s\n",
usage.SubscriptionInfo.SubscriptionType,
usage.SubscriptionInfo.SubscriptionTitle,
usage.SubscriptionInfo.SubscriptionName,
info.SubscriptionType)
}
// 解析使用量
if len(usage.UsageBreakdownList) > 0 {
breakdown := usage.UsageBreakdownList[0]
info.UsageCurrent = breakdown.CurrentUsage
info.UsageLimit = breakdown.UsageLimit
if info.UsageLimit > 0 {
info.UsagePercent = info.UsageCurrent / info.UsageLimit
}
}
// 解析重置日期
if usage.NextDateReset != "" {
if ts, err := usage.NextDateReset.Int64(); err == nil && ts > 0 {
info.NextResetDate = time.Unix(ts, 0).Format("2006-01-02")
} else if f, err := usage.NextDateReset.Float64(); err == nil && f > 0 {
info.NextResetDate = time.Unix(int64(f), 0).Format("2006-01-02")
}
}
return info, nil
}
func parseSubscriptionType(raw string) string {
upper := strings.ToUpper(raw)
if strings.Contains(upper, "PRO_PLUS") || strings.Contains(upper, "PROPLUS") {
return "PRO_PLUS"
}
if strings.Contains(upper, "POWER") {
return "POWER"
}
if strings.Contains(upper, "PRO") {
return "PRO"
}
return "FREE"
}
// 响应结构体
type UsageLimitsResponse struct {
UsageBreakdownList []UsageBreakdown `json:"usageBreakdownList"`
NextDateReset json.Number `json:"nextDateReset"`
SubscriptionInfo *SubscriptionInfo `json:"subscriptionInfo"`
UserInfo *UserInfo `json:"userInfo"`
}
type UsageBreakdown struct {
ResourceType string `json:"resourceType"`
CurrentUsage float64 `json:"currentUsage"`
UsageLimit float64 `json:"usageLimit"`
Currency string `json:"currency"`
Unit string `json:"unit"`
OverageRate float64 `json:"overageRate"`
FreeTrialInfo *FreeTrialInfo `json:"freeTrialInfo"`
Bonuses []BonusInfo `json:"bonuses"`
}
type FreeTrialInfo struct {
CurrentUsage float64 `json:"currentUsage"`
UsageLimit float64 `json:"usageLimit"`
FreeTrialStatus string `json:"freeTrialStatus"`
FreeTrialExpiry int64 `json:"freeTrialExpiry"`
}
type BonusInfo struct {
BonusCode string `json:"bonusCode"`
DisplayName string `json:"displayName"`
CurrentUsage float64 `json:"currentUsage"`
UsageLimit float64 `json:"usageLimit"`
ExpiresAt int64 `json:"expiresAt"`
Status string `json:"status"`
}
type SubscriptionInfo struct {
SubscriptionName string `json:"subscriptionName"`
SubscriptionTitle string `json:"subscriptionTitle"`
SubscriptionType string `json:"subscriptionType"`
Status string `json:"status"`
UpgradeCapability string `json:"upgradeCapability"`
}
type UserInfo struct {
Email string `json:"email"`
UserId string `json:"userId"`
}
type UserInfoResponse struct {
Email string `json:"email"`
UserId string `json:"userId"`
Idp string `json:"idp"`
Status string `json:"status"`
}
type ModelInfo struct {
ModelId string `json:"modelId"`
ModelName string `json:"modelName"`
Description string `json:"description"`
InputTypes []string `json:"supportedInputTypes"`
RateMultiplier float64 `json:"rateMultiplier"`
TokenLimits *struct {
MaxInputTokens int `json:"maxInputTokens"`
MaxOutputTokens int `json:"maxOutputTokens"`
} `json:"tokenLimits"`
}

811
proxy/translator.go Normal file
View File

@@ -0,0 +1,811 @@
package proxy
import (
"encoding/base64"
"encoding/json"
"regexp"
"strings"
"time"
"github.com/google/uuid"
)
// 模型映射
var modelMap = map[string]string{
"claude-sonnet-4-5": "claude-sonnet-4.5",
"claude-sonnet-4.5": "claude-sonnet-4.5",
"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-sonnet-4": "claude-sonnet-4",
"claude-sonnet-4-20250514": "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": "claude-sonnet-4.5",
"gpt-4o": "claude-sonnet-4.5",
"gpt-4-turbo": "claude-sonnet-4.5",
"gpt-3.5-turbo": "claude-sonnet-4.5",
}
func MapModel(model string) string {
lower := strings.ToLower(model)
for k, v := range modelMap {
if strings.Contains(lower, k) {
return v
}
}
// 如果已经是有效的 Kiro 模型,直接返回
if strings.HasPrefix(lower, "claude-") {
return model
}
return "claude-sonnet-4.5"
}
// ==================== 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"`
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) *KiroPayload {
modelID := MapModel(req.Model)
origin := "AI_EDITOR"
// 提取系统提示
systemPrompt := extractSystemPrompt(req.System)
// 注入时间戳
timestamp := time.Now().Format(time.RFC3339)
systemPrompt = "[Context: Current time is " + timestamp + "]\n\n" + systemPrompt
// 注入执行导向指令(防止 AI 在探索过程中丢失目标)
executionDirective := `
<execution_discipline>
当用户要求执行特定任务时,你必须遵循以下纪律:
1. **目标锁定**:在整个会话中始终牢记用户的原始目标,不要在代码探索过程中迷失方向
2. **行动优先**:优先执行任务而非仅分析或总结,除非用户明确只要求分析
3. **计划执行**:为任务创建明确的步骤计划,逐步执行并标记完成状态
4. **禁止确认性收尾**:在任务未完成前,禁止输出"需要我继续吗?"、"需要深入分析吗?"等确认性问题
5. **持续推进**:如果发现部分任务已完成,立即继续执行剩余未完成的任务
6. **完整交付**:直到所有任务步骤都执行完毕才算完成
</execution_discipline>
`
systemPrompt = systemPrompt + "\n\n" + executionDirective
// 构建历史消息
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)
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(currentToolResults) > 0 {
finalContent += "Tool results provided."
} else {
finalContent += "Continue"
}
// 转换工具
kiroTools := convertClaudeTools(req.Tools)
// 构建 payload
payload := &KiroPayload{}
payload.ConversationState.ChatTriggerType = "MANUAL"
payload.ConversationState.ConversationID = uuid.New().String()
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":
if t, ok := block["text"].(string); ok {
text += t
}
case "image":
if source, ok := block["source"].(map[string]interface{}); ok {
mediaType, _ := source["media_type"].(string)
data, _ := source["data"].(string)
format := strings.TrimPrefix(mediaType, "image/")
if format == "jpg" {
format = "jpeg"
}
images = append(images, KiroImage{
Format: format,
Source: struct {
Bytes string `json:"bytes"`
}{Bytes: data},
})
}
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 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,
})
}
}
}
if text == "" && len(toolUses) > 0 {
text = "Using tools."
}
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 string, toolUses []KiroToolUse, inputTokens, outputTokens int, model string) *ClaudeResponse {
blocks := make([]ClaudeContentBlock, 0)
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) *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, ok := msg.Content.(string); ok {
systemPrompt += s + "\n"
}
} else {
nonSystemMessages = append(nonSystemMessages, msg)
}
}
// 注入时间戳
timestamp := time.Now().Format(time.RFC3339)
systemPrompt = "[Context: Current time is " + timestamp + "]\n\n" + systemPrompt
// 注入执行导向指令(防止 AI 在探索过程中丢失目标)
executionDirective := `
<execution_discipline>
当用户要求执行特定任务时,你必须遵循以下纪律:
1. **目标锁定**:在整个会话中始终牢记用户的原始目标,不要在代码探索过程中迷失方向
2. **行动优先**:优先执行任务而非仅分析或总结,除非用户明确只要求分析
3. **计划执行**:为任务创建明确的步骤计划,逐步执行并标记完成状态
4. **禁止确认性收尾**:在任务未完成前,禁止输出"需要我继续吗?"、"需要深入分析吗?"等确认性问题
5. **持续推进**:如果发现部分任务已完成,立即继续执行剩余未完成的任务
6. **完整交付**:直到所有任务步骤都执行完毕才算完成
</execution_discipline>
`
systemPrompt = systemPrompt + "\n\n" + executionDirective
// 构建历史消息
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)
// 第一条 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, _ := msg.Content.(string)
if content == "" && len(msg.ToolCalls) > 0 {
content = "Using tools."
}
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, _ := msg.Content.(string)
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: "Tool results provided.",
ModelID: modelID,
Origin: origin,
UserInputMessageContext: &UserInputMessageContext{
ToolResults: currentToolResults,
},
},
})
currentToolResults = nil
}
}
}
}
// 构建最终内容
finalContent := currentContent
if finalContent == "" {
if len(currentToolResults) > 0 {
finalContent = "Tool results provided."
} else {
finalContent = "Continue"
}
}
if !systemMerged && systemPrompt != "" {
finalContent = systemPrompt + "\n" + finalContent
}
// 转换工具
kiroTools := convertOpenAITools(req.Tools)
// 构建 payload
payload := &KiroPayload{}
payload.ConversationState.ChatTriggerType = "MANUAL"
payload.ConversationState.ConversationID = uuid.New().String()
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 parts, ok := content.([]interface{}); ok {
for _, p := range parts {
part, ok := p.(map[string]interface{})
if !ok {
continue
}
partType, _ := part["type"].(string)
switch partType {
case "text":
if t, ok := part["text"].(string); ok {
text += t
}
case "image_url":
if imgUrl, ok := part["image_url"].(map[string]interface{}); ok {
if url, ok := imgUrl["url"].(string); ok {
if img := parseDataURL(url); img != nil {
images = append(images, *img)
}
}
}
}
}
}
return text, images
}
func parseDataURL(url string) *KiroImage {
// data:image/png;base64,xxxxx
re := regexp.MustCompile(`^data:image/(\w+);base64,(.+)$`)
matches := re.FindStringSubmatch(url)
if len(matches) != 3 {
return nil
}
format := matches[1]
if format == "jpg" {
format = "jpeg"
}
// 验证 base64
if _, err := base64.StdEncoding.DecodeString(matches[2]); err != nil {
return nil
}
return &KiroImage{
Format: format,
Source: struct {
Bytes string `json:"bytes"`
}{Bytes: matches[2]},
}
}
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,
},
}
}