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:
1392
proxy/handler.go
Normal file
1392
proxy/handler.go
Normal file
File diff suppressed because it is too large
Load Diff
370
proxy/kiro.go
Normal file
370
proxy/kiro.go
Normal 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
271
proxy/kiro_api.go
Normal 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
811
proxy/translator.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user