tkiro / kiro-go /proxy /handler.go
JXJBing's picture
Upload 113 files
6b86af2 verified
Raw
History Blame Contribute Delete
92.2 kB
package proxy
import (
"bytes"
"encoding/json"
"fmt"
"io"
"kiro-api-proxy/auth"
"kiro-api-proxy/config"
"kiro-api-proxy/pool"
"math/rand"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
)
type statusRecorder struct {
http.ResponseWriter
status int
}
func (sr *statusRecorder) WriteHeader(code int) {
sr.status = code
sr.ResponseWriter.WriteHeader(code)
}
func (sr *statusRecorder) Write(b []byte) (int, error) {
if sr.status == 0 {
sr.status = http.StatusOK
}
return sr.ResponseWriter.Write(b)
}
type RequestLogAttempt struct {
Try int `json:"try"`
AccountID string `json:"accountId,omitempty"`
Email string `json:"email,omitempty"`
StatusCode int `json:"statusCode"`
Error string `json:"error,omitempty"`
DurationMs int64 `json:"durationMs"`
}
type RequestLogEntry struct {
Time int64 `json:"time"`
Path string `json:"path"`
Model string `json:"model,omitempty"`
AccountID string `json:"accountId,omitempty"`
Email string `json:"email,omitempty"`
Attempts int `json:"attempts"`
FinalStatus int `json:"finalStatus"`
DurationMs int64 `json:"durationMs"`
Error string `json:"error,omitempty"`
AttemptItems []RequestLogAttempt `json:"attemptItems,omitempty"`
}
type requestLogRing struct {
mu sync.RWMutex
buf []RequestLogEntry
next int
count int
}
func newRequestLogRing(size int) *requestLogRing {
if size <= 0 {
size = 500
}
return &requestLogRing{buf: make([]RequestLogEntry, size)}
}
func (r *requestLogRing) Add(entry RequestLogEntry) {
r.mu.Lock()
defer r.mu.Unlock()
r.buf[r.next] = entry
r.next = (r.next + 1) % len(r.buf)
if r.count < len(r.buf) {
r.count++
}
}
func (r *requestLogRing) List(limit int) []RequestLogEntry {
r.mu.RLock()
defer r.mu.RUnlock()
if r.count == 0 {
return []RequestLogEntry{}
}
if limit <= 0 || limit > r.count {
limit = r.count
}
result := make([]RequestLogEntry, 0, limit)
for i := 0; i < limit; i++ {
idx := (r.next - 1 - i + len(r.buf)) % len(r.buf)
result = append(result, r.buf[idx])
}
return result
}
type RequestFinalMetrics struct {
Path string
Model string
AccountID string
AccountEmail string
Attempts int
FinalStatus int
DurationMs int64
Error string
AttemptItems []RequestLogAttempt
TotalTokens int
Credits float64
}
// Handler HTTP 处理器
type Handler struct {
pool *pool.AccountPool
// 运行时统计 (使用原子操作)
totalRequests int64
successRequests int64
failedRequests int64
attemptFailedRequests int64
totalRetries int64
totalTokens int64
totalCredits float64 // float64 需要用锁保护
creditsMu sync.RWMutex
startTime int64
stopRefresh chan struct{}
stopStatsSaver chan struct{}
requestLogs *requestLogRing
// 模型缓存
cachedModels []ModelInfo
modelsCacheMu sync.RWMutex
modelsCacheTime int64
gatewayBase string
gatewayAPIKey string
gatewayProxy *httputil.ReverseProxy
}
func NewHandler() *Handler {
totalReq, successReq, failedReq, attemptFailedReq, totalRetries, totalTokens, totalCredits := config.GetStats()
h := &Handler{
pool: pool.GetPool(),
totalRequests: int64(totalReq),
successRequests: int64(successReq),
failedRequests: int64(failedReq),
attemptFailedRequests: int64(attemptFailedReq),
totalRetries: int64(totalRetries),
totalTokens: int64(totalTokens),
totalCredits: totalCredits,
startTime: time.Now().Unix(),
stopRefresh: make(chan struct{}),
stopStatsSaver: make(chan struct{}),
requestLogs: newRequestLogRing(500),
gatewayBase: strings.TrimRight(os.Getenv("KIRO_GATEWAY_BASE"), "/"),
gatewayAPIKey: os.Getenv("KIRO_GATEWAY_API_KEY"),
}
if h.gatewayBase != "" {
if u, err := url.Parse(h.gatewayBase); err == nil {
h.gatewayProxy = httputil.NewSingleHostReverseProxy(u)
origDirector := h.gatewayProxy.Director
h.gatewayProxy.Director = func(req *http.Request) {
origDirector(req)
if h.gatewayAPIKey != "" {
req.Header.Set("Authorization", "Bearer "+h.gatewayAPIKey)
}
}
logger := fmt.Sprintf("[GatewayProxy] enabled -> %s", h.gatewayBase)
_ = logger
}
}
// 启动后台刷新
go h.backgroundRefresh()
// 启动后台统计保存 (每30秒保存一次)
go h.backgroundStatsSaver()
return h
}
// backgroundRefresh 后台定时刷新账户信息
func (h *Handler) backgroundRefresh() {
ticker := time.NewTicker(30 * time.Minute) // 每 30 分钟刷新一次
defer ticker.Stop()
// 启动时延迟 10 秒后执行一次
time.Sleep(10 * time.Second)
h.refreshModelsCache()
h.refreshAllAccounts()
for {
select {
case <-ticker.C:
h.refreshModelsCache()
h.refreshAllAccounts()
case <-h.stopRefresh:
return
}
}
}
// refreshAllAccounts 刷新所有账户信息
func (h *Handler) refreshAllAccounts() {
accounts := config.GetAccounts()
for i := range accounts {
account := &accounts[i]
if !account.Enabled || account.AccessToken == "" {
continue
}
// 检查 token 是否需要刷新
if account.ExpiresAt > 0 && time.Now().Unix() > account.ExpiresAt-300 {
newAccessToken, newRefreshToken, newExpiresAt, err := auth.RefreshToken(account)
if err != nil {
fmt.Printf("[BackgroundRefresh] Token refresh failed for %s: %v\n", account.Email, err)
continue
}
account.AccessToken = newAccessToken
if newRefreshToken != "" {
account.RefreshToken = newRefreshToken
}
account.ExpiresAt = newExpiresAt
config.UpdateAccountToken(account.ID, newAccessToken, newRefreshToken, newExpiresAt)
h.pool.UpdateToken(account.ID, newAccessToken, newRefreshToken, newExpiresAt)
}
// 刷新账户信息
info, err := RefreshAccountInfo(account)
if err != nil {
fmt.Printf("[BackgroundRefresh] Failed to refresh %s: %v\n", account.Email, err)
continue
}
config.UpdateAccountInfo(account.ID, *info)
fmt.Printf("[BackgroundRefresh] Refreshed %s: %s %.1f/%.1f\n", account.Email, info.SubscriptionType, info.UsageCurrent, info.UsageLimit)
}
h.pool.Reload()
}
// validateApiKey 验证 API Key
func (h *Handler) validateApiKey(r *http.Request) bool {
if !config.IsApiKeyRequired() {
return true
}
expectedKey := config.GetApiKey()
if expectedKey == "" {
return true
}
// 从 Authorization 头或 X-Api-Key 头获取
authHeader := r.Header.Get("Authorization")
apiKeyHeader := r.Header.Get("X-Api-Key")
var providedKey string
if strings.HasPrefix(authHeader, "Bearer ") {
providedKey = strings.TrimPrefix(authHeader, "Bearer ")
} else if apiKeyHeader != "" {
providedKey = apiKeyHeader
}
return providedKey == expectedKey
}
func (h *Handler) useGatewayProxy() bool {
return h.gatewayProxy != nil
}
func (h *Handler) selectGatewayAccountWithTried(tried map[string]bool) *config.Account {
accounts := h.pool.GetAllAccounts()
if len(accounts) == 0 {
return nil
}
candidates := make([]config.Account, 0, len(accounts))
for _, a := range accounts {
if !a.Enabled || a.RefreshToken == "" {
continue
}
if tried != nil && tried[a.ID] {
continue
}
candidates = append(candidates, a)
}
if len(candidates) == 0 {
return nil
}
// 优先 + 可降级:永远先试最高权重组,失败后在下一轮降级到次高权重组
maxWeight := 0
for i := range candidates {
w := candidates[i].Weight
if w <= 0 {
w = 100
}
if w > maxWeight {
maxWeight = w
}
}
if maxWeight <= 0 {
maxWeight = 100
}
top := make([]config.Account, 0, len(candidates))
for i := range candidates {
w := candidates[i].Weight
if w <= 0 {
w = 100
}
if w == maxWeight {
top = append(top, candidates[i])
}
}
if len(top) == 0 {
chosen := candidates[rand.Intn(len(candidates))]
return &chosen
}
chosen := top[rand.Intn(len(top))]
return &chosen
}
func (h *Handler) buildGatewayPools(accounts []config.Account) ([]config.Account, []config.Account) {
primary := make([]config.Account, 0)
fallback := make([]config.Account, 0)
for _, a := range accounts {
if !a.Enabled || a.RefreshToken == "" {
continue
}
if a.UsageCurrent >= 10 {
primary = append(primary, a)
} else {
fallback = append(fallback, a)
}
}
return primary, fallback
}
func (h *Handler) pickWeightedGateway(candidates []config.Account) *config.Account {
if len(candidates) == 0 {
return nil
}
// 严格优先:仅从最高权重组里选择(同权重再随机)
maxWeight := 0
for i := range candidates {
w := candidates[i].Weight
if w <= 0 {
w = 100
}
if w > maxWeight {
maxWeight = w
}
}
if maxWeight <= 0 {
maxWeight = 100
}
top := make([]config.Account, 0, len(candidates))
for i := range candidates {
w := candidates[i].Weight
if w <= 0 {
w = 100
}
if w == maxWeight {
top = append(top, candidates[i])
}
}
if len(top) == 0 {
chosen := candidates[rand.Intn(len(candidates))]
return &chosen
}
chosen := top[rand.Intn(len(top))]
return &chosen
}
func (h *Handler) proxyToGatewayWithAccount(w http.ResponseWriter, r *http.Request, account *config.Account) {
if h.gatewayProxy == nil {
http.Error(w, "gateway proxy not configured", 500)
return
}
if account != nil {
r.Header.Set("X-Kiro-Refresh-Token", account.RefreshToken)
r.Header.Set("X-Kiro-Region", account.Region)
r.Header.Set("X-Kiro-Auth-Method", account.AuthMethod)
r.Header.Set("X-Kiro-Client-Id", account.ClientID)
r.Header.Set("X-Kiro-Client-Secret", account.ClientSecret)
}
h.gatewayProxy.ServeHTTP(w, r)
}
func (h *Handler) proxyWithFailover(w http.ResponseWriter, r *http.Request) {
if h.gatewayProxy == nil {
http.Error(w, "gateway proxy not configured", 500)
return
}
reqStart := time.Now()
bodyBytes, _ := io.ReadAll(r.Body)
r.Body.Close()
model := extractModelFromRequestBody(bodyBytes)
maxAttempts := 4
if r.URL.Path == "/v1/models" || r.Method == http.MethodGet {
maxAttempts = 2
}
var (
lastStatus int
lastBody []byte
lastHeader http.Header
lastError string
lastAccID string
lastEmail string
)
attemptItems := make([]RequestLogAttempt, 0, maxAttempts)
tried := map[string]bool{}
for i := 0; i < maxAttempts; i++ {
acc := h.selectGatewayAccountWithTried(tried)
if acc == nil {
break
}
tried[acc.ID] = true
lastAccID = acc.ID
lastEmail = acc.Email
cloneReq := r.Clone(r.Context())
cloneReq.Body = io.NopCloser(bytes.NewReader(bodyBytes))
cloneReq.ContentLength = int64(len(bodyBytes))
if len(bodyBytes) > 0 {
cloneReq.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(bodyBytes)), nil
}
}
tryStart := time.Now()
rec := httptest.NewRecorder()
h.proxyToGatewayWithAccount(rec, cloneReq, acc)
resp := rec.Result()
respBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()
lastStatus = resp.StatusCode
lastBody = respBody
lastHeader = resp.Header.Clone()
if resp.StatusCode >= 400 {
lastError = string(respBody)
} else {
lastError = ""
}
attemptItems = append(attemptItems, RequestLogAttempt{
Try: len(attemptItems) + 1,
AccountID: acc.ID,
Email: acc.Email,
StatusCode: resp.StatusCode,
DurationMs: time.Since(tryStart).Milliseconds(),
Error: truncateError(lastError),
})
// success
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
for k, vals := range resp.Header {
for _, v := range vals {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
_, _ = w.Write(respBody)
totalTokens, credits := extractUsageMetrics(respBody)
h.finalizeRequest(RequestFinalMetrics{
Path: r.URL.Path,
Model: model,
AccountID: acc.ID,
AccountEmail: acc.Email,
Attempts: len(attemptItems),
FinalStatus: resp.StatusCode,
DurationMs: time.Since(reqStart).Milliseconds(),
AttemptItems: attemptItems,
TotalTokens: totalTokens,
Credits: credits,
})
return
}
// classify failover conditions
if resp.StatusCode == 402 || resp.StatusCode == 429 || resp.StatusCode >= 500 {
// mark cooldown in pool
h.pool.RecordError(acc.ID, resp.StatusCode == 402 || resp.StatusCode == 429)
continue
}
// non-retryable, return immediately
for k, vals := range resp.Header {
for _, v := range vals {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
_, _ = w.Write(respBody)
h.finalizeRequest(RequestFinalMetrics{
Path: r.URL.Path,
Model: model,
AccountID: acc.ID,
AccountEmail: acc.Email,
Attempts: len(attemptItems),
FinalStatus: resp.StatusCode,
DurationMs: time.Since(reqStart).Milliseconds(),
Error: truncateError(lastError),
AttemptItems: attemptItems,
})
return
}
if lastStatus == 0 {
lastStatus = http.StatusServiceUnavailable
lastError = "no available account"
http.Error(w, "no available account", http.StatusServiceUnavailable)
} else {
if lastHeader != nil {
for k, vals := range lastHeader {
for _, v := range vals {
w.Header().Add(k, v)
}
}
}
w.WriteHeader(lastStatus)
_, _ = w.Write(lastBody)
}
h.finalizeRequest(RequestFinalMetrics{
Path: r.URL.Path,
Model: model,
AccountID: lastAccID,
AccountEmail: lastEmail,
Attempts: max(1, len(attemptItems)),
FinalStatus: lastStatus,
DurationMs: time.Since(reqStart).Milliseconds(),
Error: truncateError(lastError),
AttemptItems: attemptItems,
})
}
func (h *Handler) proxyToGateway(w http.ResponseWriter, r *http.Request) {
// fallback legacy behavior
h.proxyToGatewayWithAccount(w, r, nil)
}
// ServeHTTP 路由分发
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// CORS - 完整的头部支持
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Api-Key, anthropic-version, anthropic-beta, x-api-key, x-stainless-os, x-stainless-lang, x-stainless-package-version, x-stainless-runtime, x-stainless-runtime-version, x-stainless-arch")
w.Header().Set("Access-Control-Expose-Headers", "x-request-id, x-ratelimit-limit-requests, x-ratelimit-limit-tokens, x-ratelimit-remaining-requests, x-ratelimit-remaining-tokens, x-ratelimit-reset-requests, x-ratelimit-reset-tokens")
if r.Method == "OPTIONS" {
w.WriteHeader(204)
return
}
// 路由
switch {
// API 端点(需要验证 API Key)
case path == "/v1/messages" || path == "/messages" || path == "/anthropic/v1/messages":
if !h.validateApiKey(r) {
h.sendClaudeError(w, 401, "authentication_error", "Invalid or missing API key")
return
}
if h.useGatewayProxy() {
h.proxyWithFailover(w, r)
return
}
h.handleClaudeMessages(w, r)
case path == "/v1/messages/count_tokens" || path == "/messages/count_tokens":
if !h.validateApiKey(r) {
h.sendClaudeError(w, 401, "authentication_error", "Invalid or missing API key")
return
}
if h.useGatewayProxy() {
h.proxyWithFailover(w, r)
return
}
h.handleCountTokens(w, r)
case path == "/v1/chat/completions" || path == "/chat/completions":
if !h.validateApiKey(r) {
h.sendOpenAIError(w, 401, "authentication_error", "Invalid or missing API key")
return
}
if h.useGatewayProxy() {
h.proxyWithFailover(w, r)
return
}
h.handleOpenAIChat(w, r)
case path == "/v1/models" || path == "/models":
if h.useGatewayProxy() {
h.proxyWithFailover(w, r)
return
}
h.handleModels(w, r)
case path == "/api/event_logging/batch":
// Claude Code 遥测端点 - 直接返回 200 OK
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"status":"ok"}`))
// 管理端点
case path == "/admin" || path == "/admin/":
h.serveAdminPage(w, r)
case strings.HasPrefix(path, "/admin/api/"):
h.handleAdminAPI(w, r)
case strings.HasPrefix(path, "/admin/"):
h.serveStaticFile(w, r)
// 健康检查
case path == "/health" || path == "/":
h.handleHealth(w, r)
// 统计端点(需要 API Key 鉴权)
case path == "/v1/stats":
if !h.validateApiKey(r) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(401)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid or missing API key"})
return
}
h.handleStats(w, r)
default:
http.Error(w, "Not Found", 404)
}
}
// handleHealth 健康检查(不暴露统计数据)
func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"version": config.Version,
"uptime": time.Now().Unix() - h.startTime,
})
}
// handleStats 统计数据(需要 API Key 鉴权)
func (h *Handler) handleStats(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"version": config.Version,
"accounts": h.pool.Count(),
"available": h.pool.AvailableCount(),
"totalRequests": atomic.LoadInt64(&h.totalRequests),
"successRequests": atomic.LoadInt64(&h.successRequests),
"failedRequests": atomic.LoadInt64(&h.failedRequests),
"totalTokens": atomic.LoadInt64(&h.totalTokens),
"totalCredits": h.getCredits(),
"uptime": time.Now().Unix() - h.startTime,
})
}
// handleModels 模型列表
func (h *Handler) handleModels(w http.ResponseWriter, r *http.Request) {
// 尝试用缓存的真实模型列表
h.modelsCacheMu.RLock()
cached := h.cachedModels
h.modelsCacheMu.RUnlock()
thinkingSuffix := config.GetThinkingConfig().Suffix
var models []map[string]interface{}
if len(cached) > 0 {
for _, m := range cached {
models = append(models, map[string]interface{}{
"id": m.ModelId, "object": "model", "owned_by": "anthropic",
})
// 自动生成 thinking 变体
models = append(models, map[string]interface{}{
"id": m.ModelId + thinkingSuffix, "object": "model", "owned_by": "anthropic",
})
}
} else {
// fallback 静态列表
models = []map[string]interface{}{
{"id": "claude-sonnet-4.6", "object": "model", "owned_by": "anthropic"},
{"id": "claude-sonnet-4.6" + thinkingSuffix, "object": "model", "owned_by": "anthropic"},
{"id": "claude-sonnet-4.5", "object": "model", "owned_by": "anthropic"},
{"id": "claude-sonnet-4.5" + thinkingSuffix, "object": "model", "owned_by": "anthropic"},
{"id": "claude-sonnet-4", "object": "model", "owned_by": "anthropic"},
{"id": "claude-sonnet-4" + thinkingSuffix, "object": "model", "owned_by": "anthropic"},
{"id": "claude-haiku-4.5", "object": "model", "owned_by": "anthropic"},
{"id": "claude-haiku-4.5" + thinkingSuffix, "object": "model", "owned_by": "anthropic"},
{"id": "claude-opus-4.5", "object": "model", "owned_by": "anthropic"},
{"id": "claude-opus-4.5" + thinkingSuffix, "object": "model", "owned_by": "anthropic"},
}
}
// 添加别名模型
models = append(models,
map[string]interface{}{"id": "auto", "object": "model", "owned_by": "kiro-proxy"},
map[string]interface{}{"id": "gpt-4o", "object": "model", "owned_by": "kiro-proxy"},
map[string]interface{}{"id": "gpt-4", "object": "model", "owned_by": "kiro-proxy"},
)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]interface{}{
"object": "list",
"data": models,
})
}
// refreshModelsCache 从 Kiro API 拉取模型列表并缓存
func (h *Handler) refreshModelsCache() {
account := h.pool.GetNext()
if account == nil {
return
}
// 确保 token 有效
if err := h.ensureValidToken(account); err != nil {
return
}
models, err := ListAvailableModels(account)
if err != nil {
fmt.Printf("[ModelsCache] Failed to refresh: %v\n", err)
return
}
if len(models) > 0 {
h.modelsCacheMu.Lock()
h.cachedModels = models
h.modelsCacheTime = time.Now().Unix()
h.modelsCacheMu.Unlock()
fmt.Printf("[ModelsCache] Cached %d models\n", len(models))
}
}
// handleCountTokens Token 计数(Claude Code 会调用)
func (h *Handler) handleCountTokens(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method Not Allowed", 405)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
h.sendClaudeError(w, 400, "invalid_request_error", "Failed to read request body")
return
}
var req struct {
Messages []struct {
Role string `json:"role"`
Content interface{} `json:"content"`
} `json:"messages"`
System interface{} `json:"system"`
}
if err := json.Unmarshal(body, &req); err != nil {
h.sendClaudeError(w, 400, "invalid_request_error", "Invalid JSON")
return
}
// 简单估算 token 数量(每 4 个字符约 1 个 token)
var totalChars int
for _, msg := range req.Messages {
switch content := msg.Content.(type) {
case string:
totalChars += len(content)
case []interface{}:
for _, part := range content {
if p, ok := part.(map[string]interface{}); ok {
if text, ok := p["text"].(string); ok {
totalChars += len(text)
}
}
}
}
}
// 系统提示
switch system := req.System.(type) {
case string:
totalChars += len(system)
case []interface{}:
for _, part := range system {
if p, ok := part.(map[string]interface{}); ok {
if text, ok := p["text"].(string); ok {
totalChars += len(text)
}
}
}
}
estimatedTokens := (totalChars + 3) / 4 // 向上取整
if estimatedTokens < 1 {
estimatedTokens = 1
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]int{"input_tokens": estimatedTokens})
}
// handleClaudeMessages Claude API 处理
func (h *Handler) handleClaudeMessages(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method Not Allowed", 405)
return
}
requestStart := time.Now()
// 读取请求
body, err := io.ReadAll(r.Body)
if err != nil {
h.sendClaudeError(w, 400, "invalid_request_error", "Failed to read request body")
return
}
var req ClaudeRequest
if err := json.Unmarshal(body, &req); err != nil {
h.sendClaudeError(w, 400, "invalid_request_error", "Invalid JSON: "+err.Error())
return
}
// 获取账号
account := h.pool.GetNext()
if account == nil {
h.sendClaudeError(w, 503, "api_error", "No available accounts")
h.finalizeRequest(RequestFinalMetrics{
Path: r.URL.Path,
Model: req.Model,
Attempts: 1,
FinalStatus: 503,
DurationMs: time.Since(requestStart).Milliseconds(),
Error: "No available accounts",
AttemptItems: []RequestLogAttempt{{
Try: 1,
StatusCode: 503,
Error: "No available accounts",
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
return
}
// 检查并刷新 token
if err := h.ensureValidToken(account); err != nil {
h.sendClaudeError(w, 503, "api_error", "Token refresh failed: "+err.Error())
h.finalizeRequest(RequestFinalMetrics{
Path: r.URL.Path,
Model: req.Model,
AccountID: account.ID,
AccountEmail: account.Email,
Attempts: 1,
FinalStatus: 503,
DurationMs: time.Since(requestStart).Milliseconds(),
Error: truncateError(err.Error()),
AttemptItems: []RequestLogAttempt{{
Try: 1,
AccountID: account.ID,
Email: account.Email,
StatusCode: 503,
Error: truncateError(err.Error()),
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
return
}
// 解析模型和 thinking 模式
thinkingCfg := config.GetThinkingConfig()
actualModel, thinking := ParseModelAndThinking(req.Model, thinkingCfg.Suffix)
req.Model = actualModel
// 转换请求
kiroPayload := ClaudeToKiro(&req, thinking)
// 流式或非流式
if req.Stream {
h.handleClaudeStream(w, account, kiroPayload, req.Model, requestStart)
} else {
h.handleClaudeNonStream(w, account, kiroPayload, req.Model, requestStart)
}
}
// handleClaudeStream Claude 流式响应
func (h *Handler) handleClaudeStream(w http.ResponseWriter, account *config.Account, payload *KiroPayload, model string, requestStart time.Time) {
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
h.sendClaudeError(w, 500, "api_error", "Streaming not supported")
return
}
// 获取 thinking 输出格式配置
thinkingFormat := config.GetThinkingConfig().ClaudeFormat
msgID := "msg_" + uuid.New().String()
var contentStarted bool
var toolUseIndex int
var inputTokens, outputTokens int
var credits float64
var toolUses []KiroToolUse
// Thinking 标签解析状态
var textBuffer string
var inThinkingBlock bool
// 发送文本的辅助函数
// thinkingState: 0=普通内容, 1=thinking开始, 2=thinking中间, 3=thinking结束
sendText := func(text string, thinkingState int) {
// 确保 content_block 已开始
if !contentStarted {
h.sendSSE(w, flusher, "content_block_start", map[string]interface{}{
"type": "content_block_start",
"index": 0,
"content_block": map[string]string{"type": "text", "text": ""},
})
contentStarted = true
}
if thinkingState == 0 {
// 普通内容
if text == "" {
return
}
h.sendSSE(w, flusher, "content_block_delta", map[string]interface{}{
"type": "content_block_delta",
"index": 0,
"delta": map[string]string{"type": "text_delta", "text": text},
})
} else {
// thinking 内容
var outputText string
switch thinkingFormat {
case "think":
switch thinkingState {
case 1:
outputText = "<think>" + text
case 2:
outputText = text
case 3:
outputText = text + "</think>"
}
case "reasoning_content":
// Claude 格式不支持 reasoning_content,直接输出内容
outputText = text
default: // "thinking"
switch thinkingState {
case 1:
outputText = "<thinking>" + text
case 2:
outputText = text
case 3:
outputText = text + "</thinking>"
}
}
if outputText == "" {
return
}
h.sendSSE(w, flusher, "content_block_delta", map[string]interface{}{
"type": "content_block_delta",
"index": 0,
"delta": map[string]string{"type": "text_delta", "text": outputText},
})
}
}
// 处理文本,解析 <thinking> 标签
var thinkingStarted bool
processClaudeText := func(text string, isThinking bool, forceFlush bool) {
// 如果是 reasoningContentEvent,直接输出
if isThinking {
if !thinkingStarted {
sendText(text, 1)
thinkingStarted = true
} else {
sendText(text, 2)
}
return
}
textBuffer += text
for {
if !inThinkingBlock {
thinkingStart := strings.Index(textBuffer, "<thinking>")
if thinkingStart != -1 {
if thinkingStart > 0 {
sendText(textBuffer[:thinkingStart], 0)
}
textBuffer = textBuffer[thinkingStart+10:]
inThinkingBlock = true
thinkingStarted = false
} else if forceFlush || len([]rune(textBuffer)) > 50 {
// 使用 rune 切片来正确处理 Unicode 字符
runes := []rune(textBuffer)
safeLen := len(runes)
if !forceFlush {
safeLen = max(0, len(runes)-15)
}
if safeLen > 0 {
sendText(string(runes[:safeLen]), 0)
textBuffer = string(runes[safeLen:])
}
break
} else {
break
}
} else {
thinkingEnd := strings.Index(textBuffer, "</thinking>")
if thinkingEnd != -1 {
content := textBuffer[:thinkingEnd]
if !thinkingStarted {
sendText(content, 1)
sendText("", 3)
} else {
sendText(content, 3)
}
textBuffer = textBuffer[thinkingEnd+11:]
inThinkingBlock = false
thinkingStarted = false
} else if forceFlush {
if textBuffer != "" {
if !thinkingStarted {
sendText(textBuffer, 1)
sendText("", 3)
} else {
sendText(textBuffer, 3)
}
textBuffer = ""
}
break
} else {
// 流式输出 thinking 块内的内容
runes := []rune(textBuffer)
if len(runes) > 20 {
safeLen := len(runes) - 15
if safeLen > 0 {
if !thinkingStarted {
sendText(string(runes[:safeLen]), 1)
thinkingStarted = true
} else {
sendText(string(runes[:safeLen]), 2)
}
textBuffer = string(runes[safeLen:])
}
}
break
}
}
}
}
// 发送 message_start
h.sendSSE(w, flusher, "message_start", map[string]interface{}{
"type": "message_start",
"message": map[string]interface{}{
"id": msgID,
"type": "message",
"role": "assistant",
"content": []interface{}{},
"model": model,
},
})
callback := &KiroStreamCallback{
OnText: func(text string, isThinking bool) {
if text == "" {
return
}
processClaudeText(text, isThinking, false)
},
OnToolUse: func(tu KiroToolUse) {
// 先刷新缓冲区
processClaudeText("", false, true)
toolUses = append(toolUses, tu)
// 关闭文本块
if contentStarted && toolUseIndex == 0 {
h.sendSSE(w, flusher, "content_block_stop", map[string]interface{}{
"type": "content_block_stop",
"index": 0,
})
}
idx := toolUseIndex
if contentStarted {
idx = toolUseIndex + 1
}
toolUseIndex++
h.sendSSE(w, flusher, "content_block_start", map[string]interface{}{
"type": "content_block_start",
"index": idx,
"content_block": map[string]interface{}{
"type": "tool_use",
"id": tu.ToolUseID,
"name": tu.Name,
"input": map[string]interface{}{},
},
})
inputJSON, _ := json.Marshal(tu.Input)
h.sendSSE(w, flusher, "content_block_delta", map[string]interface{}{
"type": "content_block_delta",
"index": idx,
"delta": map[string]interface{}{
"type": "input_json_delta",
"partial_json": string(inputJSON),
},
})
h.sendSSE(w, flusher, "content_block_stop", map[string]interface{}{
"type": "content_block_stop",
"index": idx,
})
},
OnComplete: func(inTok, outTok int) {
inputTokens = inTok
outputTokens = outTok
},
OnError: func(err error) {
h.pool.RecordError(account.ID, strings.Contains(err.Error(), "429") || strings.Contains(err.Error(), "quota"))
},
OnCredits: func(c float64) {
credits = c
},
}
err := CallKiroAPI(account, payload, callback)
if err != nil {
h.pool.RecordError(account.ID, strings.Contains(err.Error(), "429") || strings.Contains(err.Error(), "quota"))
h.sendSSE(w, flusher, "error", map[string]interface{}{
"type": "error",
"error": map[string]string{"type": "api_error", "message": err.Error()},
})
h.finalizeRequest(RequestFinalMetrics{
Path: "/v1/messages",
Model: model,
AccountID: account.ID,
AccountEmail: account.Email,
Attempts: 1,
FinalStatus: 500,
DurationMs: time.Since(requestStart).Milliseconds(),
Error: truncateError(err.Error()),
AttemptItems: []RequestLogAttempt{{
Try: 1,
AccountID: account.ID,
Email: account.Email,
StatusCode: 500,
Error: truncateError(err.Error()),
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
return
}
// 刷新剩余缓冲区
processClaudeText("", false, true)
h.recordSuccess(inputTokens, outputTokens, credits, 1)
h.pool.RecordSuccess(account.ID)
h.pool.UpdateStats(account.ID, inputTokens+outputTokens, credits)
// 关闭最后的内容块
if contentStarted && toolUseIndex == 0 {
h.sendSSE(w, flusher, "content_block_stop", map[string]interface{}{
"type": "content_block_stop",
"index": 0,
})
}
// 发送 message_delta
stopReason := "end_turn"
if len(toolUses) > 0 {
stopReason = "tool_use"
}
h.sendSSE(w, flusher, "message_delta", map[string]interface{}{
"type": "message_delta",
"delta": map[string]interface{}{
"stop_reason": stopReason,
},
"usage": map[string]int{
"input_tokens": inputTokens,
"output_tokens": outputTokens,
},
})
h.sendSSE(w, flusher, "message_stop", map[string]interface{}{
"type": "message_stop",
})
h.appendRequestLog(RequestFinalMetrics{
Path: "/v1/messages",
Model: model,
AccountID: account.ID,
AccountEmail: account.Email,
Attempts: 1,
FinalStatus: 200,
DurationMs: time.Since(requestStart).Milliseconds(),
AttemptItems: []RequestLogAttempt{{
Try: 1,
AccountID: account.ID,
Email: account.Email,
StatusCode: 200,
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
}
func (h *Handler) sendSSE(w http.ResponseWriter, flusher http.Flusher, event string, data interface{}) {
jsonData, _ := json.Marshal(data)
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, string(jsonData))
flusher.Flush()
}
// backgroundStatsSaver 后台定时保存统计数据
func (h *Handler) backgroundStatsSaver() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
h.saveStats()
case <-h.stopStatsSaver:
h.saveStats() // 退出前保存一次
return
}
}
}
// saveStats 保存统计到配置文件
func (h *Handler) saveStats() {
config.UpdateStats(
int(atomic.LoadInt64(&h.totalRequests)),
int(atomic.LoadInt64(&h.successRequests)),
int(atomic.LoadInt64(&h.failedRequests)),
int(atomic.LoadInt64(&h.attemptFailedRequests)),
int(atomic.LoadInt64(&h.totalRetries)),
int(atomic.LoadInt64(&h.totalTokens)),
h.getCredits(),
)
}
// getCredits 线程安全获取 credits
func (h *Handler) getCredits() float64 {
h.creditsMu.RLock()
defer h.creditsMu.RUnlock()
return h.totalCredits
}
// addCredits 线程安全增加 credits
func (h *Handler) addCredits(credits float64) {
h.creditsMu.Lock()
h.totalCredits += credits
h.creditsMu.Unlock()
}
// 统计记录 (使用原子操作)
func (h *Handler) recordSuccess(inputTokens, outputTokens int, credits float64, attempts int) {
atomic.AddInt64(&h.totalRequests, 1)
atomic.AddInt64(&h.successRequests, 1)
atomic.AddInt64(&h.totalTokens, int64(inputTokens+outputTokens))
if attempts > 1 {
atomic.AddInt64(&h.totalRetries, int64(attempts-1))
}
h.addCredits(credits)
}
func (h *Handler) recordFailure(attempts int) {
atomic.AddInt64(&h.totalRequests, 1)
atomic.AddInt64(&h.failedRequests, 1)
if attempts > 0 {
atomic.AddInt64(&h.attemptFailedRequests, int64(attempts))
}
if attempts > 1 {
atomic.AddInt64(&h.totalRetries, int64(attempts-1))
}
}
func (h *Handler) recordAttemptFailure(count int) {
if count > 0 {
atomic.AddInt64(&h.attemptFailedRequests, int64(count))
}
}
func (h *Handler) appendRequestLog(m RequestFinalMetrics) {
if h.requestLogs == nil {
return
}
entry := RequestLogEntry{
Time: time.Now().Unix(),
Path: m.Path,
Model: m.Model,
AccountID: m.AccountID,
Email: m.AccountEmail,
Attempts: max(1, m.Attempts),
FinalStatus: m.FinalStatus,
DurationMs: m.DurationMs,
Error: m.Error,
AttemptItems: m.AttemptItems,
}
h.requestLogs.Add(entry)
}
func (h *Handler) finalizeRequest(m RequestFinalMetrics) {
attempts := max(1, m.Attempts)
attemptFailures := 0
if len(m.AttemptItems) > 0 {
for _, a := range m.AttemptItems {
if a.StatusCode >= 400 || a.Error != "" {
attemptFailures++
}
}
} else if m.FinalStatus >= 400 {
attemptFailures = attempts
}
if m.FinalStatus >= 200 && m.FinalStatus < 400 {
h.recordSuccess(0, 0, 0, attempts)
h.recordAttemptFailure(max(0, attemptFailures))
} else {
h.recordFailure(max(attempts, attemptFailures))
}
// 累加真实 tokens/credits(优先使用回调提供值)
if m.TotalTokens > 0 {
atomic.AddInt64(&h.totalTokens, int64(m.TotalTokens))
}
if m.Credits > 0 {
h.addCredits(m.Credits)
}
h.appendRequestLog(m)
}
func extractUsageMetrics(body []byte) (int, float64) {
if len(body) == 0 {
return 0, 0
}
var obj map[string]interface{}
if err := json.Unmarshal(body, &obj); err != nil {
return 0, 0
}
totalTokens := 0
if usage, ok := obj["usage"].(map[string]interface{}); ok {
if v, ok := usage["total_tokens"].(float64); ok {
totalTokens = int(v)
} else {
pt, _ := usage["prompt_tokens"].(float64)
ct, _ := usage["completion_tokens"].(float64)
totalTokens = int(pt + ct)
}
}
credits := 0.0
if v, ok := obj["credits"].(float64); ok {
credits = v
}
return totalTokens, credits
}
func extractModelFromRequestBody(body []byte) string {
if len(body) == 0 {
return ""
}
var obj map[string]interface{}
if err := json.Unmarshal(body, &obj); err != nil {
return ""
}
if m, ok := obj["model"].(string); ok {
return m
}
return ""
}
func truncateError(err string) string {
err = strings.TrimSpace(err)
if len(err) > 300 {
return err[:300] + "..."
}
return err
}
// handleClaudeNonStream Claude 非流式响应
func (h *Handler) handleClaudeNonStream(w http.ResponseWriter, account *config.Account, payload *KiroPayload, model string, requestStart time.Time) {
var content string
var thinkingContent string
var toolUses []KiroToolUse
var inputTokens, outputTokens int
var credits float64
callback := &KiroStreamCallback{
OnText: func(text string, isThinking bool) {
if isThinking {
thinkingContent += text
} else {
content += text
}
},
OnToolUse: func(tu KiroToolUse) {
toolUses = append(toolUses, tu)
},
OnComplete: func(inTok, outTok int) {
inputTokens = inTok
outputTokens = outTok
},
OnError: func(err error) {
h.pool.RecordError(account.ID, strings.Contains(err.Error(), "429"))
},
OnCredits: func(c float64) {
credits = c
},
}
err := CallKiroAPI(account, payload, callback)
if err != nil {
h.pool.RecordError(account.ID, strings.Contains(err.Error(), "429"))
h.sendClaudeError(w, 500, "api_error", err.Error())
h.finalizeRequest(RequestFinalMetrics{
Path: "/v1/messages",
Model: model,
AccountID: account.ID,
AccountEmail: account.Email,
Attempts: 1,
FinalStatus: 500,
DurationMs: time.Since(requestStart).Milliseconds(),
Error: truncateError(err.Error()),
AttemptItems: []RequestLogAttempt{{
Try: 1,
AccountID: account.ID,
Email: account.Email,
StatusCode: 500,
Error: truncateError(err.Error()),
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
return
}
h.recordSuccess(inputTokens, outputTokens, credits, 1)
h.pool.RecordSuccess(account.ID)
h.pool.UpdateStats(account.ID, inputTokens+outputTokens, credits)
// 合并 thinking 内容(如果有 reasoningContentEvent 的内容)
thinkingFormat := config.GetThinkingConfig().ClaudeFormat
finalContent := content
if thinkingContent != "" {
switch thinkingFormat {
case "think":
finalContent = "<think>" + thinkingContent + "</think>" + content
case "reasoning_content":
finalContent = thinkingContent + content // Claude 格式不支持 reasoning_content,直接拼接
default: // "thinking"
finalContent = "<thinking>" + thinkingContent + "</thinking>" + content
}
}
resp := KiroToClaudeResponse(finalContent, toolUses, inputTokens, outputTokens, model)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(resp)
h.appendRequestLog(RequestFinalMetrics{
Path: "/v1/messages",
Model: model,
AccountID: account.ID,
AccountEmail: account.Email,
Attempts: 1,
FinalStatus: 200,
DurationMs: time.Since(requestStart).Milliseconds(),
AttemptItems: []RequestLogAttempt{{
Try: 1,
AccountID: account.ID,
Email: account.Email,
StatusCode: 200,
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
}
func (h *Handler) sendClaudeError(w http.ResponseWriter, status int, errType, message string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]interface{}{
"type": "error",
"error": map[string]string{
"type": errType,
"message": message,
},
})
}
// handleOpenAIChat OpenAI API 处理
func (h *Handler) handleOpenAIChat(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method Not Allowed", 405)
return
}
requestStart := time.Now()
body, err := io.ReadAll(r.Body)
if err != nil {
h.sendOpenAIError(w, 400, "invalid_request_error", "Failed to read request body")
return
}
var req OpenAIRequest
if err := json.Unmarshal(body, &req); err != nil {
h.sendOpenAIError(w, 400, "invalid_request_error", "Invalid JSON")
return
}
account := h.pool.GetNext()
if account == nil {
h.sendOpenAIError(w, 503, "server_error", "No available accounts")
h.finalizeRequest(RequestFinalMetrics{
Path: r.URL.Path,
Model: req.Model,
Attempts: 1,
FinalStatus: 503,
DurationMs: time.Since(requestStart).Milliseconds(),
Error: "No available accounts",
AttemptItems: []RequestLogAttempt{{
Try: 1,
StatusCode: 503,
Error: "No available accounts",
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
return
}
if err := h.ensureValidToken(account); err != nil {
h.sendOpenAIError(w, 503, "server_error", "Token refresh failed")
h.finalizeRequest(RequestFinalMetrics{
Path: r.URL.Path,
Model: req.Model,
AccountID: account.ID,
AccountEmail: account.Email,
Attempts: 1,
FinalStatus: 503,
DurationMs: time.Since(requestStart).Milliseconds(),
Error: truncateError(err.Error()),
AttemptItems: []RequestLogAttempt{{
Try: 1,
AccountID: account.ID,
Email: account.Email,
StatusCode: 503,
Error: truncateError(err.Error()),
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
return
}
// 解析模型和 thinking 模式
thinkingCfg := config.GetThinkingConfig()
actualModel, thinking := ParseModelAndThinking(req.Model, thinkingCfg.Suffix)
req.Model = actualModel
kiroPayload := OpenAIToKiro(&req, thinking)
if req.Stream {
h.handleOpenAIStream(w, account, kiroPayload, req.Model, requestStart)
} else {
h.handleOpenAINonStream(w, account, kiroPayload, req.Model, requestStart)
}
}
// handleOpenAIStream OpenAI 流式响应
func (h *Handler) handleOpenAIStream(w http.ResponseWriter, account *config.Account, payload *KiroPayload, model string, requestStart time.Time) {
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
h.sendOpenAIError(w, 500, "server_error", "Streaming not supported")
return
}
// 获取 thinking 输出格式配置
thinkingFormat := config.GetThinkingConfig().OpenAIFormat
chatID := "chatcmpl-" + uuid.New().String()
var toolCalls []ToolCall
var toolCallIndex int
var inputTokens, outputTokens int
var credits float64
// Thinking 标签解析状态
var textBuffer string
var inThinkingBlock bool
// 发送 chunk 的辅助函数
// thinkingState: 0=普通内容, 1=thinking开始, 2=thinking中间, 3=thinking结束
sendChunk := func(content string, thinkingState int) {
if content == "" && thinkingState == 2 {
return
}
var chunk map[string]interface{}
if thinkingState > 0 {
// thinking 内容
switch thinkingFormat {
case "thinking":
// 流式输出标签
var text string
switch thinkingState {
case 1: // 开始
text = "<thinking>" + content
case 2: // 中间
text = content
case 3: // 结束
text = content + "</thinking>"
}
if text == "" {
return
}
chunk = map[string]interface{}{
"id": chatID,
"object": "chat.completion.chunk",
"created": time.Now().Unix(),
"model": model,
"choices": []map[string]interface{}{{
"index": 0,
"delta": map[string]string{"content": text},
"finish_reason": nil,
}},
}
case "think":
var text string
switch thinkingState {
case 1:
text = "<think>" + content
case 2:
text = content
case 3:
text = content + "</think>"
}
if text == "" {
return
}
chunk = map[string]interface{}{
"id": chatID,
"object": "chat.completion.chunk",
"created": time.Now().Unix(),
"model": model,
"choices": []map[string]interface{}{{
"index": 0,
"delta": map[string]string{"content": text},
"finish_reason": nil,
}},
}
default: // "reasoning_content"
if content == "" {
return
}
chunk = map[string]interface{}{
"id": chatID,
"object": "chat.completion.chunk",
"created": time.Now().Unix(),
"model": model,
"choices": []map[string]interface{}{{
"index": 0,
"delta": map[string]string{"reasoning_content": content},
"finish_reason": nil,
}},
}
}
} else {
// 普通内容
if content == "" {
return
}
chunk = map[string]interface{}{
"id": chatID,
"object": "chat.completion.chunk",
"created": time.Now().Unix(),
"model": model,
"choices": []map[string]interface{}{{
"index": 0,
"delta": map[string]string{"content": content},
"finish_reason": nil,
}},
}
}
data, _ := json.Marshal(chunk)
fmt.Fprintf(w, "data: %s\n\n", string(data))
flusher.Flush()
}
// 处理文本,解析 <thinking> 标签
// thinkingStarted 用于跟踪是否已发送开始标签
var thinkingStarted bool
processText := func(text string, isThinking bool, forceFlush bool) {
// 如果是 reasoningContentEvent,直接输出
if isThinking {
if !thinkingStarted {
sendChunk(text, 1) // 开始
thinkingStarted = true
} else {
sendChunk(text, 2) // 中间
}
return
}
textBuffer += text
for {
if !inThinkingBlock {
// 查找 <thinking> 开始标签
thinkingStart := strings.Index(textBuffer, "<thinking>")
if thinkingStart != -1 {
// 输出 thinking 标签之前的内容
if thinkingStart > 0 {
sendChunk(textBuffer[:thinkingStart], 0)
}
textBuffer = textBuffer[thinkingStart+10:] // 移除 <thinking>
inThinkingBlock = true
thinkingStarted = false // 重置,准备发送新的开始标签
} else if forceFlush || len([]rune(textBuffer)) > 50 {
// 没有找到标签,安全输出(保留可能的部分标签)
runes := []rune(textBuffer)
safeLen := len(runes)
if !forceFlush {
safeLen = max(0, len(runes)-15)
}
if safeLen > 0 {
sendChunk(string(runes[:safeLen]), 0)
textBuffer = string(runes[safeLen:])
}
break
} else {
break
}
} else {
// 在 thinking 块内,查找 </thinking> 结束标签
thinkingEnd := strings.Index(textBuffer, "</thinking>")
if thinkingEnd != -1 {
// 输出 thinking 内容
content := textBuffer[:thinkingEnd]
if !thinkingStarted {
// 一次性输出完整内容(开始+内容+结束)
sendChunk(content, 1) // 开始
sendChunk("", 3) // 结束(空内容,只发结束标签)
} else {
// 已经开始了,发送剩余内容和结束
sendChunk(content, 3) // 结束
}
textBuffer = textBuffer[thinkingEnd+11:] // 移除 </thinking>
inThinkingBlock = false
thinkingStarted = false
} else if forceFlush {
// 强制刷新:输出剩余内容
if textBuffer != "" {
if !thinkingStarted {
sendChunk(textBuffer, 1) // 开始
sendChunk("", 3) // 结束
} else {
sendChunk(textBuffer, 3) // 结束
}
textBuffer = ""
}
break
} else {
// 流式输出 thinking 块内的内容
runes := []rune(textBuffer)
if len(runes) > 20 {
safeLen := len(runes) - 15 // 保留可能的 </thinking> 部分
if safeLen > 0 {
if !thinkingStarted {
sendChunk(string(runes[:safeLen]), 1) // 开始
thinkingStarted = true
} else {
sendChunk(string(runes[:safeLen]), 2) // 中间
}
textBuffer = string(runes[safeLen:])
}
}
break
}
}
}
}
callback := &KiroStreamCallback{
OnText: func(text string, isThinking bool) {
if text == "" {
return
}
processText(text, isThinking, false)
},
OnToolUse: func(tu KiroToolUse) {
// 先刷新缓冲区
processText("", false, true)
args, _ := json.Marshal(tu.Input)
tc := ToolCall{ID: tu.ToolUseID, Type: "function"}
tc.Function.Name = tu.Name
tc.Function.Arguments = string(args)
toolCalls = append(toolCalls, tc)
chunk := map[string]interface{}{
"id": chatID,
"object": "chat.completion.chunk",
"created": time.Now().Unix(),
"model": model,
"choices": []map[string]interface{}{{
"index": 0,
"delta": map[string]interface{}{
"tool_calls": []map[string]interface{}{{
"index": toolCallIndex,
"id": tu.ToolUseID,
"type": "function",
"function": map[string]string{
"name": tu.Name,
"arguments": string(args),
},
}},
},
"finish_reason": nil,
}},
}
toolCallIndex++
data, _ := json.Marshal(chunk)
fmt.Fprintf(w, "data: %s\n\n", string(data))
flusher.Flush()
},
OnComplete: func(inTok, outTok int) {
inputTokens = inTok
outputTokens = outTok
},
OnError: func(err error) {
h.pool.RecordError(account.ID, strings.Contains(err.Error(), "429"))
},
OnCredits: func(c float64) {
credits = c
},
}
err := CallKiroAPI(account, payload, callback)
if err != nil {
h.pool.RecordError(account.ID, strings.Contains(err.Error(), "429"))
h.finalizeRequest(RequestFinalMetrics{
Path: "/v1/chat/completions",
Model: model,
AccountID: account.ID,
AccountEmail: account.Email,
Attempts: 1,
FinalStatus: 500,
DurationMs: time.Since(requestStart).Milliseconds(),
Error: truncateError(err.Error()),
AttemptItems: []RequestLogAttempt{{
Try: 1,
AccountID: account.ID,
Email: account.Email,
StatusCode: 500,
Error: truncateError(err.Error()),
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
return
}
// 刷新剩余缓冲区
processText("", false, true)
h.recordSuccess(inputTokens, outputTokens, credits, 1)
h.pool.RecordSuccess(account.ID)
h.pool.UpdateStats(account.ID, inputTokens+outputTokens, credits)
// 发送结束
finishReason := "stop"
if len(toolCalls) > 0 {
finishReason = "tool_calls"
}
chunk := map[string]interface{}{
"id": chatID,
"object": "chat.completion.chunk",
"created": time.Now().Unix(),
"model": model,
"choices": []map[string]interface{}{{
"index": 0,
"delta": map[string]interface{}{},
"finish_reason": finishReason,
}},
"usage": map[string]int{
"prompt_tokens": inputTokens,
"completion_tokens": outputTokens,
"total_tokens": inputTokens + outputTokens,
},
}
data, _ := json.Marshal(chunk)
fmt.Fprintf(w, "data: %s\n\n", string(data))
fmt.Fprintf(w, "data: [DONE]\n\n")
flusher.Flush()
h.appendRequestLog(RequestFinalMetrics{
Path: "/v1/chat/completions",
Model: model,
AccountID: account.ID,
AccountEmail: account.Email,
Attempts: 1,
FinalStatus: 200,
DurationMs: time.Since(requestStart).Milliseconds(),
AttemptItems: []RequestLogAttempt{{
Try: 1,
AccountID: account.ID,
Email: account.Email,
StatusCode: 200,
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
}
// handleOpenAINonStream OpenAI 非流式响应
func (h *Handler) handleOpenAINonStream(w http.ResponseWriter, account *config.Account, payload *KiroPayload, model string, requestStart time.Time) {
var content string
var reasoningContent string
var toolUses []KiroToolUse
var inputTokens, outputTokens int
var credits float64
callback := &KiroStreamCallback{
OnText: func(text string, isThinking bool) {
if isThinking {
reasoningContent += text
} else {
content += text
}
},
OnToolUse: func(tu KiroToolUse) { toolUses = append(toolUses, tu) },
OnComplete: func(inTok, outTok int) { inputTokens = inTok; outputTokens = outTok },
OnError: func(err error) { h.pool.RecordError(account.ID, strings.Contains(err.Error(), "429")) },
OnCredits: func(c float64) { credits = c },
}
err := CallKiroAPI(account, payload, callback)
if err != nil {
h.pool.RecordError(account.ID, strings.Contains(err.Error(), "429"))
h.sendOpenAIError(w, 500, "server_error", err.Error())
h.finalizeRequest(RequestFinalMetrics{
Path: "/v1/chat/completions",
Model: model,
AccountID: account.ID,
AccountEmail: account.Email,
Attempts: 1,
FinalStatus: 500,
DurationMs: time.Since(requestStart).Milliseconds(),
Error: truncateError(err.Error()),
AttemptItems: []RequestLogAttempt{{
Try: 1,
AccountID: account.ID,
Email: account.Email,
StatusCode: 500,
Error: truncateError(err.Error()),
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
return
}
h.recordSuccess(inputTokens, outputTokens, credits, 1)
h.pool.RecordSuccess(account.ID)
h.pool.UpdateStats(account.ID, inputTokens+outputTokens, credits)
// 解析 content 中的 <thinking> 标签
finalContent, extractedReasoning := extractThinkingFromContent(content)
if extractedReasoning != "" {
reasoningContent = extractedReasoning + reasoningContent
}
thinkingFormat := config.GetThinkingConfig().OpenAIFormat
resp := KiroToOpenAIResponseWithReasoning(finalContent, reasoningContent, toolUses, inputTokens, outputTokens, model, thinkingFormat)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(resp)
h.appendRequestLog(RequestFinalMetrics{
Path: "/v1/chat/completions",
Model: model,
AccountID: account.ID,
AccountEmail: account.Email,
Attempts: 1,
FinalStatus: 200,
DurationMs: time.Since(requestStart).Milliseconds(),
AttemptItems: []RequestLogAttempt{{
Try: 1,
AccountID: account.ID,
Email: account.Email,
StatusCode: 200,
DurationMs: time.Since(requestStart).Milliseconds(),
}},
})
}
func (h *Handler) sendOpenAIError(w http.ResponseWriter, status int, errType, message string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": map[string]interface{}{
"type": errType,
"message": message,
},
})
}
// ensureValidToken 确保 token 有效
func (h *Handler) ensureValidToken(account *config.Account) error {
if account.ExpiresAt == 0 || time.Now().Unix() < account.ExpiresAt-300 {
return nil
}
accessToken, refreshToken, expiresAt, err := auth.RefreshToken(account)
if err != nil {
return err
}
// 更新内存
h.pool.UpdateToken(account.ID, accessToken, refreshToken, expiresAt)
account.AccessToken = accessToken
if refreshToken != "" {
account.RefreshToken = refreshToken
}
account.ExpiresAt = expiresAt
// 持久化
config.UpdateAccountToken(account.ID, accessToken, refreshToken, expiresAt)
return nil
}
// ==================== 管理 API ====================
func (h *Handler) handleAdminAPI(w http.ResponseWriter, r *http.Request) {
// 验证密码
password := r.Header.Get("X-Admin-Password")
if password == "" {
cookie, _ := r.Cookie("admin_password")
if cookie != nil {
password = cookie.Value
}
}
if password != config.GetPassword() {
w.WriteHeader(401)
json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized"})
return
}
path := strings.TrimPrefix(r.URL.Path, "/admin/api")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
switch {
case path == "/accounts" && r.Method == "GET":
h.apiGetAccounts(w, r)
case path == "/accounts" && r.Method == "POST":
h.apiAddAccount(w, r)
case strings.HasPrefix(path, "/accounts/") && strings.HasSuffix(path, "/refresh") && r.Method == "POST":
id := strings.TrimSuffix(strings.TrimPrefix(path, "/accounts/"), "/refresh")
h.apiRefreshAccount(w, r, id)
case strings.HasPrefix(path, "/accounts/") && strings.HasSuffix(path, "/models") && r.Method == "GET":
id := strings.TrimSuffix(strings.TrimPrefix(path, "/accounts/"), "/models")
h.apiGetAccountModels(w, r, id)
case strings.HasPrefix(path, "/accounts/") && strings.HasSuffix(path, "/full") && r.Method == "GET":
id := strings.TrimSuffix(strings.TrimPrefix(path, "/accounts/"), "/full")
h.apiGetAccountFull(w, r, id)
case strings.HasPrefix(path, "/accounts/") && r.Method == "DELETE":
h.apiDeleteAccount(w, r, strings.TrimPrefix(path, "/accounts/"))
case strings.HasPrefix(path, "/accounts/") && r.Method == "PUT":
h.apiUpdateAccount(w, r, strings.TrimPrefix(path, "/accounts/"))
case path == "/auth/iam-sso/start" && r.Method == "POST":
h.apiStartIamSso(w, r)
case path == "/auth/iam-sso/complete" && r.Method == "POST":
h.apiCompleteIamSso(w, r)
case path == "/auth/builderid/start" && r.Method == "POST":
h.apiStartBuilderIdLogin(w, r)
case path == "/auth/builderid/poll" && r.Method == "POST":
h.apiPollBuilderIdAuth(w, r)
case path == "/auth/sso-token" && r.Method == "POST":
h.apiImportSsoToken(w, r)
case path == "/auth/credentials" && r.Method == "POST":
h.apiImportCredentials(w, r)
case path == "/status" && r.Method == "GET":
h.apiGetStatus(w, r)
case path == "/settings" && r.Method == "GET":
h.apiGetSettings(w, r)
case path == "/settings" && r.Method == "POST":
h.apiUpdateSettings(w, r)
case path == "/stats" && r.Method == "GET":
h.apiGetStats(w, r)
case path == "/request-logs" && r.Method == "GET":
h.apiGetRequestLogs(w, r)
case path == "/accounts/weight" && r.Method == "POST":
h.apiUpdateAccountWeight(w, r)
case path == "/stats/reset" && r.Method == "POST":
h.apiResetStats(w, r)
case path == "/generate-machine-id" && r.Method == "GET":
h.apiGenerateMachineId(w, r)
case path == "/thinking" && r.Method == "GET":
h.apiGetThinkingConfig(w, r)
case path == "/thinking" && r.Method == "POST":
h.apiUpdateThinkingConfig(w, r)
case path == "/endpoint" && r.Method == "GET":
h.apiGetEndpointConfig(w, r)
case path == "/endpoint" && r.Method == "POST":
h.apiUpdateEndpointConfig(w, r)
case path == "/version" && r.Method == "GET":
h.apiGetVersion(w, r)
case path == "/export" && r.Method == "POST":
h.apiExportAccounts(w, r)
default:
w.WriteHeader(404)
json.NewEncoder(w).Encode(map[string]string{"error": "Not Found"})
}
}
func (h *Handler) apiGetAccounts(w http.ResponseWriter, r *http.Request) {
accounts := config.GetAccounts()
poolAccounts := h.pool.GetAllAccounts()
// 合并运行时统计
statsMap := make(map[string]config.Account)
for _, a := range poolAccounts {
statsMap[a.ID] = a
}
// 隐藏敏感信息
result := make([]map[string]interface{}, len(accounts))
for i, a := range accounts {
// 获取运行时统计
stats := statsMap[a.ID]
result[i] = map[string]interface{}{
"id": a.ID,
"email": a.Email,
"userId": a.UserId,
"nickname": a.Nickname,
"weight": a.Weight,
"authMethod": a.AuthMethod,
"provider": a.Provider,
"region": a.Region,
"enabled": a.Enabled,
"banStatus": a.BanStatus,
"banReason": a.BanReason,
"banTime": a.BanTime,
"expiresAt": a.ExpiresAt,
"hasToken": a.AccessToken != "",
"machineId": a.MachineId,
"subscriptionType": a.SubscriptionType,
"subscriptionTitle": a.SubscriptionTitle,
"daysRemaining": a.DaysRemaining,
"usageCurrent": a.UsageCurrent,
"usageLimit": a.UsageLimit,
"usagePercent": a.UsagePercent,
"nextResetDate": a.NextResetDate,
"lastRefresh": a.LastRefresh,
"trialUsageCurrent": a.TrialUsageCurrent,
"trialUsageLimit": a.TrialUsageLimit,
"trialUsagePercent": a.TrialUsagePercent,
"trialStatus": a.TrialStatus,
"trialExpiresAt": a.TrialExpiresAt,
"requestCount": stats.RequestCount,
"errorCount": stats.ErrorCount,
"totalTokens": stats.TotalTokens,
"totalCredits": stats.TotalCredits,
"lastUsed": stats.LastUsed,
}
}
json.NewEncoder(w).Encode(result)
}
func (h *Handler) apiAddAccount(w http.ResponseWriter, r *http.Request) {
var account config.Account
if err := json.NewDecoder(r.Body).Decode(&account); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
if account.ID == "" {
account.ID = auth.GenerateAccountID()
}
if account.Region == "" {
account.Region = "us-east-1"
}
if err := config.AddAccount(account); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
h.pool.Reload()
json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "id": account.ID})
}
func (h *Handler) apiDeleteAccount(w http.ResponseWriter, r *http.Request, id string) {
if err := config.DeleteAccount(id); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
h.pool.Reload()
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func (h *Handler) apiUpdateAccount(w http.ResponseWriter, r *http.Request, id string) {
var updates map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
// 获取现有账号
accounts := config.GetAccounts()
var existing *config.Account
for i := range accounts {
if accounts[i].ID == id {
existing = &accounts[i]
break
}
}
if existing == nil {
w.WriteHeader(404)
json.NewEncoder(w).Encode(map[string]string{"error": "Account not found"})
return
}
// 只更新传入的字段
if v, ok := updates["enabled"].(bool); ok {
existing.Enabled = v
}
if v, ok := updates["nickname"].(string); ok {
existing.Nickname = v
}
if v, ok := updates["machineId"].(string); ok {
existing.MachineId = v
}
if v, ok := updates["weight"]; ok {
switch vv := v.(type) {
case float64:
existing.Weight = int(vv)
case int:
existing.Weight = vv
}
if existing.Weight <= 0 {
existing.Weight = 100
}
}
if err := config.UpdateAccount(id, *existing); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
h.pool.Reload()
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func (h *Handler) apiStartIamSso(w http.ResponseWriter, r *http.Request) {
var req struct {
StartUrl string `json:"startUrl"`
Region string `json:"region"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
if req.StartUrl == "" {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "startUrl is required"})
return
}
sessionID, authorizeUrl, expiresIn, err := auth.StartIamSsoLogin(req.StartUrl, req.Region)
if err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"sessionId": sessionID,
"authorizeUrl": authorizeUrl,
"expiresIn": expiresIn,
})
}
func (h *Handler) apiCompleteIamSso(w http.ResponseWriter, r *http.Request) {
var req struct {
SessionID string `json:"sessionId"`
CallbackUrl string `json:"callbackUrl"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
accessToken, refreshToken, clientID, clientSecret, region, expiresIn, err := auth.CompleteIamSsoLogin(req.SessionID, req.CallbackUrl)
if err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// 获取用户信息
email, _, _ := auth.GetUserInfo(accessToken)
// 创建账号
account := config.Account{
ID: auth.GenerateAccountID(),
Email: email,
AccessToken: accessToken,
RefreshToken: refreshToken,
ClientID: clientID,
ClientSecret: clientSecret,
AuthMethod: "idc",
Region: region,
ExpiresAt: time.Now().Unix() + int64(expiresIn),
Enabled: true,
MachineId: config.GenerateMachineId(),
}
if err := config.AddAccount(account); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
h.pool.Reload()
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"account": map[string]interface{}{
"id": account.ID,
"email": account.Email,
},
})
}
func (h *Handler) apiStartBuilderIdLogin(w http.ResponseWriter, r *http.Request) {
var req struct {
Region string `json:"region"`
}
json.NewDecoder(r.Body).Decode(&req)
session, err := auth.StartBuilderIdLogin(req.Region)
if err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"sessionId": session.ID,
"userCode": session.UserCode,
"verificationUri": session.VerificationUri,
"interval": session.Interval,
})
}
func (h *Handler) apiPollBuilderIdAuth(w http.ResponseWriter, r *http.Request) {
var req struct {
SessionID string `json:"sessionId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
accessToken, refreshToken, clientID, clientSecret, region, expiresIn, status, err := auth.PollBuilderIdAuth(req.SessionID)
if err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": err.Error(),
})
return
}
if status == "pending" || status == "slow_down" {
// 获取当前间隔
interval := 5
if session := auth.GetBuilderIdSession(req.SessionID); session != nil {
interval = session.Interval
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"completed": false,
"status": status,
"interval": interval,
})
return
}
// 授权完成,获取用户信息
email, _, _ := auth.GetUserInfo(accessToken)
// 创建账号
account := config.Account{
ID: auth.GenerateAccountID(),
Email: email,
AccessToken: accessToken,
RefreshToken: refreshToken,
ClientID: clientID,
ClientSecret: clientSecret,
AuthMethod: "idc",
Provider: "BuilderId",
Region: region,
ExpiresAt: time.Now().Unix() + int64(expiresIn),
Enabled: true,
MachineId: config.GenerateMachineId(),
}
if err := config.AddAccount(account); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
h.pool.Reload()
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"completed": true,
"account": map[string]interface{}{
"id": account.ID,
"email": account.Email,
},
})
}
func (h *Handler) apiImportSsoToken(w http.ResponseWriter, r *http.Request) {
var req struct {
BearerToken string `json:"bearerToken"`
Region string `json:"region"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
if req.BearerToken == "" {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "bearerToken is required"})
return
}
// 支持批量导入,按行分割
tokens := strings.Split(strings.TrimSpace(req.BearerToken), "\n")
var imported []map[string]interface{}
var errors []string
for _, token := range tokens {
token = strings.TrimSpace(token)
if token == "" {
continue
}
accessToken, refreshToken, clientID, clientSecret, expiresIn, err := auth.ImportFromSsoToken(token, req.Region)
if err != nil {
errors = append(errors, err.Error())
continue
}
// 获取用户信息
email, _, _ := auth.GetUserInfo(accessToken)
// 创建账号
account := config.Account{
ID: auth.GenerateAccountID(),
Email: email,
AccessToken: accessToken,
RefreshToken: refreshToken,
ClientID: clientID,
ClientSecret: clientSecret,
AuthMethod: "idc",
Region: req.Region,
ExpiresAt: time.Now().Unix() + int64(expiresIn),
Enabled: true,
MachineId: config.GenerateMachineId(),
}
if err := config.AddAccount(account); err != nil {
errors = append(errors, err.Error())
continue
}
imported = append(imported, map[string]interface{}{
"id": account.ID,
"email": account.Email,
})
}
h.pool.Reload()
if len(imported) == 0 && len(errors) > 0 {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": strings.Join(errors, "; "),
})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"accounts": imported,
"errors": errors,
})
}
func (h *Handler) apiImportCredentials(w http.ResponseWriter, r *http.Request) {
var req struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
AuthMethod string `json:"authMethod"`
Provider string `json:"provider"`
Region string `json:"region"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
if req.RefreshToken == "" {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "refreshToken is required"})
return
}
// 设置默认值
if req.Region == "" {
req.Region = "us-east-1"
}
if req.AuthMethod == "" {
if req.ClientID != "" {
req.AuthMethod = "idc"
} else {
req.AuthMethod = "social"
}
}
// 标准化 authMethod
switch strings.ToLower(req.AuthMethod) {
case "idc", "builderid", "enterprise":
req.AuthMethod = "idc"
case "social", "google", "github":
req.AuthMethod = "social"
default:
if req.ClientID != "" && req.ClientSecret != "" {
req.AuthMethod = "idc"
} else {
req.AuthMethod = "social"
}
}
// 始终尝试用 refreshToken 刷新获取新的 accessToken
var accessToken string
var expiresAt int64
tempAccount := &config.Account{
RefreshToken: req.RefreshToken,
ClientID: req.ClientID,
ClientSecret: req.ClientSecret,
AuthMethod: req.AuthMethod,
Region: req.Region,
}
newAccessToken, newRefreshToken, newExpiresAt, err := auth.RefreshToken(tempAccount)
if err != nil {
// 刷新失败,如果有传入的 accessToken 则尝试使用
if req.AccessToken != "" {
accessToken = req.AccessToken
expiresAt = time.Now().Unix() + 300 // 可能已过期,设短一点
} else {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Token refresh failed: " + err.Error()})
return
}
} else {
accessToken = newAccessToken
if newRefreshToken != "" {
req.RefreshToken = newRefreshToken
}
expiresAt = newExpiresAt
}
// 获取用户信息
email, _, _ := auth.GetUserInfo(accessToken)
// 创建账号
account := config.Account{
ID: auth.GenerateAccountID(),
Email: email,
AccessToken: accessToken,
RefreshToken: req.RefreshToken,
ClientID: req.ClientID,
ClientSecret: req.ClientSecret,
AuthMethod: req.AuthMethod,
Provider: req.Provider,
Region: req.Region,
ExpiresAt: expiresAt,
Enabled: true,
MachineId: config.GenerateMachineId(),
}
if err := config.AddAccount(account); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
h.pool.Reload()
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"account": map[string]interface{}{
"id": account.ID,
"email": account.Email,
},
})
}
func (h *Handler) apiGetStatus(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"accounts": h.pool.Count(),
"available": h.pool.AvailableCount(),
"totalRequests": atomic.LoadInt64(&h.totalRequests),
"successRequests": atomic.LoadInt64(&h.successRequests),
"failedRequests": atomic.LoadInt64(&h.failedRequests),
"attemptFailedRequests": atomic.LoadInt64(&h.attemptFailedRequests),
"totalRetries": atomic.LoadInt64(&h.totalRetries),
"totalTokens": atomic.LoadInt64(&h.totalTokens),
"totalCredits": h.getCredits(),
"uptime": time.Now().Unix() - h.startTime,
"statsDescription": map[string]string{
"failedRequests": "最终失败请求数(重试后仍失败)",
"attemptFailedRequests": "尝试失败次数(包含重试中的失败)",
"totalRetries": "总重试次数(sum(attempts-1))",
},
})
}
func (h *Handler) apiGetSettings(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"apiKey": config.GetApiKey(),
"requireApiKey": config.IsApiKeyRequired(),
"port": config.GetPort(),
"host": config.GetHost(),
})
}
func (h *Handler) apiUpdateSettings(w http.ResponseWriter, r *http.Request) {
var req struct {
ApiKey string `json:"apiKey"`
RequireApiKey bool `json:"requireApiKey"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
if err := config.UpdateSettings(req.ApiKey, req.RequireApiKey, req.Password); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func (h *Handler) apiGetStats(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"totalRequests": atomic.LoadInt64(&h.totalRequests),
"successRequests": atomic.LoadInt64(&h.successRequests),
"failedRequests": atomic.LoadInt64(&h.failedRequests),
"attemptFailedRequests": atomic.LoadInt64(&h.attemptFailedRequests),
"totalRetries": atomic.LoadInt64(&h.totalRetries),
"totalTokens": atomic.LoadInt64(&h.totalTokens),
"totalCredits": h.getCredits(),
"uptime": time.Now().Unix() - h.startTime,
"statsDescription": map[string]string{
"failedRequests": "最终失败请求数(重试后仍失败)",
"attemptFailedRequests": "尝试失败次数(包含重试中的失败)",
"totalRetries": "总重试次数(sum(attempts-1))",
},
})
}
func (h *Handler) apiResetStats(w http.ResponseWriter, r *http.Request) {
atomic.StoreInt64(&h.totalRequests, 0)
atomic.StoreInt64(&h.successRequests, 0)
atomic.StoreInt64(&h.failedRequests, 0)
atomic.StoreInt64(&h.attemptFailedRequests, 0)
atomic.StoreInt64(&h.totalRetries, 0)
atomic.StoreInt64(&h.totalTokens, 0)
h.creditsMu.Lock()
h.totalCredits = 0
h.creditsMu.Unlock()
config.UpdateStats(0, 0, 0, 0, 0, 0, 0)
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func (h *Handler) apiGetRequestLogs(w http.ResponseWriter, r *http.Request) {
limit := 100
if raw := r.URL.Query().Get("limit"); raw != "" {
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
limit = v
}
}
if limit > 500 {
limit = 500
}
items := h.requestLogs.List(limit)
json.NewEncoder(w).Encode(map[string]interface{}{
"items": items,
"total": len(items),
})
}
func (h *Handler) apiUpdateAccountWeight(w http.ResponseWriter, r *http.Request) {
var raw map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
id, _ := raw["id"].(string)
if id == "" {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "id is required"})
return
}
weight := 100
switch v := raw["weight"].(type) {
case float64:
weight = int(v)
case int:
weight = v
case string:
if p, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {
weight = p
}
}
if weight <= 0 {
weight = 100
}
if weight > 10000 {
weight = 10000
}
accounts := config.GetAccounts()
var account *config.Account
for i := range accounts {
if accounts[i].ID == id {
account = &accounts[i]
break
}
}
if account == nil {
w.WriteHeader(404)
json.NewEncoder(w).Encode(map[string]string{"error": "Account not found"})
return
}
account.Weight = weight
if err := config.UpdateAccount(id, *account); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
h.pool.Reload()
json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "id": id, "weight": weight})
}
// apiGenerateMachineId 生成新的机器码
func (h *Handler) apiGenerateMachineId(w http.ResponseWriter, r *http.Request) {
machineId := config.GenerateMachineId()
json.NewEncoder(w).Encode(map[string]string{"machineId": machineId})
}
// apiRefreshAccount 刷新账户信息(使用量、订阅等)
func (h *Handler) apiRefreshAccount(w http.ResponseWriter, r *http.Request, id string) {
accounts := config.GetAccounts()
var account *config.Account
for i := range accounts {
if accounts[i].ID == id {
account = &accounts[i]
break
}
}
if account == nil {
w.WriteHeader(404)
json.NewEncoder(w).Encode(map[string]string{"error": "Account not found"})
return
}
// 先尝试刷新 token(不管是否过期,确保 token 有效)
refreshTokenIfNeeded := func() error {
if account.RefreshToken == "" {
return nil
}
newAccessToken, newRefreshToken, newExpiresAt, err := auth.RefreshToken(account)
if err != nil {
return err
}
account.AccessToken = newAccessToken
if newRefreshToken != "" {
account.RefreshToken = newRefreshToken
}
account.ExpiresAt = newExpiresAt
config.UpdateAccountToken(id, newAccessToken, newRefreshToken, newExpiresAt)
h.pool.UpdateToken(id, newAccessToken, newRefreshToken, newExpiresAt)
return nil
}
// 检查 token 是否快过期,先刷新
if account.ExpiresAt > 0 && time.Now().Unix() > account.ExpiresAt-300 {
if err := refreshTokenIfNeeded(); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": "Token refresh failed: " + err.Error()})
return
}
}
// 获取账户信息
info, err := RefreshAccountInfo(account)
if err != nil {
// 检查是否为封禁相关错误
errMsg := err.Error()
if strings.Contains(errMsg, "TEMPORARILY_SUSPENDED") || strings.Contains(errMsg, "Account suspended") {
// 封禁状态已在 RefreshAccountInfo 中处理,静默返回成功
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "Account status updated",
})
return
}
// 如果是 403/401,说明 token 无效,尝试刷新后重试
if strings.Contains(errMsg, "403") || strings.Contains(errMsg, "401") || strings.Contains(errMsg, "invalid") || strings.Contains(errMsg, "expired") {
if refreshErr := refreshTokenIfNeeded(); refreshErr == nil {
// 重试
info, err = RefreshAccountInfo(account)
if err != nil {
// 重试后仍然失败,检查是否为封禁状态
if strings.Contains(err.Error(), "TEMPORARILY_SUSPENDED") || strings.Contains(err.Error(), "Account suspended") {
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "Account status updated",
})
return
}
}
}
}
// 其他错误才显示错误信息
if err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
}
// 保存到配置
if err := config.UpdateAccountInfo(id, *info); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"info": info,
})
}
// apiGetAccountFull 获取单个账号的完整信息(包含敏感字段)
func (h *Handler) apiGetAccountFull(w http.ResponseWriter, r *http.Request, id string) {
accounts := config.GetAccounts()
poolAccounts := h.pool.GetAllAccounts()
// 查找指定账号
var account *config.Account
for i := range accounts {
if accounts[i].ID == id {
account = &accounts[i]
break
}
}
if account == nil {
w.WriteHeader(404)
json.NewEncoder(w).Encode(map[string]string{"error": "Account not found"})
return
}
// 获取运行时统计
var stats config.Account
for _, a := range poolAccounts {
if a.ID == id {
stats = a
break
}
}
// 返回完整账号信息(包含敏感字段)
result := map[string]interface{}{
"id": account.ID,
"email": account.Email,
"userId": account.UserId,
"nickname": account.Nickname,
"weight": account.Weight,
"accessToken": account.AccessToken,
"refreshToken": account.RefreshToken,
"clientId": account.ClientID,
"clientSecret": account.ClientSecret,
"authMethod": account.AuthMethod,
"provider": account.Provider,
"region": account.Region,
"expiresAt": account.ExpiresAt,
"machineId": account.MachineId,
"enabled": account.Enabled,
"banStatus": account.BanStatus,
"banReason": account.BanReason,
"banTime": account.BanTime,
"subscriptionType": account.SubscriptionType,
"subscriptionTitle": account.SubscriptionTitle,
"daysRemaining": account.DaysRemaining,
"usageCurrent": account.UsageCurrent,
"usageLimit": account.UsageLimit,
"usagePercent": account.UsagePercent,
"nextResetDate": account.NextResetDate,
"lastRefresh": account.LastRefresh,
"trialUsageCurrent": account.TrialUsageCurrent,
"trialUsageLimit": account.TrialUsageLimit,
"trialUsagePercent": account.TrialUsagePercent,
"trialStatus": account.TrialStatus,
"trialExpiresAt": account.TrialExpiresAt,
"requestCount": stats.RequestCount,
"errorCount": stats.ErrorCount,
"totalTokens": stats.TotalTokens,
"totalCredits": stats.TotalCredits,
"lastUsed": stats.LastUsed,
}
json.NewEncoder(w).Encode(result)
}
// apiGetAccountModels 获取账户可用模型
func (h *Handler) apiGetAccountModels(w http.ResponseWriter, r *http.Request, id string) {
accounts := config.GetAccounts()
var account *config.Account
for i := range accounts {
if accounts[i].ID == id {
account = &accounts[i]
break
}
}
if account == nil {
w.WriteHeader(404)
json.NewEncoder(w).Encode(map[string]string{"error": "Account not found"})
return
}
models, err := ListAvailableModels(account)
if err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"models": models,
})
}
// ==================== 静态文件服务 ====================
func (h *Handler) serveAdminPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "web/index.html")
}
func (h *Handler) serveStaticFile(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/admin/")
http.ServeFile(w, r, "web/"+path)
}
// apiGetThinkingConfig 获取 thinking 配置
func (h *Handler) apiGetThinkingConfig(w http.ResponseWriter, r *http.Request) {
cfg := config.GetThinkingConfig()
json.NewEncoder(w).Encode(map[string]interface{}{
"suffix": cfg.Suffix,
"openaiFormat": cfg.OpenAIFormat,
"claudeFormat": cfg.ClaudeFormat,
})
}
// apiUpdateThinkingConfig 更新 thinking 配置
func (h *Handler) apiUpdateThinkingConfig(w http.ResponseWriter, r *http.Request) {
var req struct {
Suffix string `json:"suffix"`
OpenAIFormat string `json:"openaiFormat"`
ClaudeFormat string `json:"claudeFormat"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
// 验证格式
validFormats := map[string]bool{"reasoning_content": true, "thinking": true, "think": true}
if req.OpenAIFormat != "" && !validFormats[req.OpenAIFormat] {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid openaiFormat, must be: reasoning_content, thinking, or think"})
return
}
if req.ClaudeFormat != "" && !validFormats[req.ClaudeFormat] {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid claudeFormat, must be: reasoning_content, thinking, or think"})
return
}
if err := config.UpdateThinkingConfig(req.Suffix, req.OpenAIFormat, req.ClaudeFormat); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// apiGetEndpointConfig 获取端点配置
func (h *Handler) apiGetEndpointConfig(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{
"preferredEndpoint": config.GetPreferredEndpoint(),
})
}
// apiUpdateEndpointConfig 更新端点配置
func (h *Handler) apiUpdateEndpointConfig(w http.ResponseWriter, r *http.Request) {
var req struct {
PreferredEndpoint string `json:"preferredEndpoint"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON"})
return
}
valid := map[string]bool{"auto": true, "codewhisperer": true, "amazonq": true}
if !valid[req.PreferredEndpoint] {
w.WriteHeader(400)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid endpoint, must be: auto, codewhisperer, or amazonq"})
return
}
if err := config.UpdatePreferredEndpoint(req.PreferredEndpoint); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// apiGetVersion 获取版本信息
func (h *Handler) apiGetVersion(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{
"version": config.Version,
})
}
// apiExportAccounts 导出账号凭证
func (h *Handler) apiExportAccounts(w http.ResponseWriter, r *http.Request) {
var req struct {
IDs []string `json:"ids"` // 为空则导出全部
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// 如果 body 为空或解析失败,导出全部
req.IDs = nil
}
accounts := config.GetAccounts()
// 如果指定了 ID,只导出指定的
if len(req.IDs) > 0 {
idSet := make(map[string]bool)
for _, id := range req.IDs {
idSet[id] = true
}
var filtered []config.Account
for _, a := range accounts {
if idSet[a.ID] {
filtered = append(filtered, a)
}
}
accounts = filtered
}
// 构建兼容 Kiro Account Manager 的导出格式
type ExportCredentials struct {
AccessToken string `json:"accessToken"`
CsrfToken string `json:"csrfToken"`
RefreshToken string `json:"refreshToken"`
ClientID string `json:"clientId,omitempty"`
ClientSecret string `json:"clientSecret,omitempty"`
Region string `json:"region,omitempty"`
ExpiresAt int64 `json:"expiresAt"`
AuthMethod string `json:"authMethod,omitempty"`
Provider string `json:"provider,omitempty"`
}
type ExportSubscription struct {
Type string `json:"type"`
Title string `json:"title,omitempty"`
}
type ExportUsage struct {
Current float64 `json:"current"`
Limit float64 `json:"limit"`
PercentUsed float64 `json:"percentUsed"`
LastUpdated int64 `json:"lastUpdated"`
}
type ExportAccount struct {
ID string `json:"id"`
Email string `json:"email"`
Nickname string `json:"nickname,omitempty"`
Idp string `json:"idp"`
UserId string `json:"userId,omitempty"`
MachineId string `json:"machineId,omitempty"`
Credentials ExportCredentials `json:"credentials"`
Subscription ExportSubscription `json:"subscription"`
Usage ExportUsage `json:"usage"`
Tags []string `json:"tags"`
Status string `json:"status"`
CreatedAt int64 `json:"createdAt"`
LastUsedAt int64 `json:"lastUsedAt"`
}
type ExportData struct {
Version string `json:"version"`
ExportedAt int64 `json:"exportedAt"`
Accounts []ExportAccount `json:"accounts"`
Groups []interface{} `json:"groups"`
Tags []interface{} `json:"tags"`
}
exportAccounts := make([]ExportAccount, 0, len(accounts))
for _, a := range accounts {
// 映射 provider 到 idp
idp := a.Provider
if idp == "" {
if a.AuthMethod == "social" {
idp = "Google"
} else {
idp = "BuilderId"
}
}
// 映射 authMethod
authMethod := a.AuthMethod
if authMethod == "idc" {
authMethod = "IdC"
}
// 映射订阅类型
subType := "Free"
rawType := strings.ToUpper(a.SubscriptionType)
if strings.Contains(rawType, "PRO_PLUS") || strings.Contains(rawType, "PROPLUS") {
subType = "Pro_Plus"
} else if strings.Contains(rawType, "PRO") {
subType = "Pro"
} else if strings.Contains(rawType, "POWER") {
subType = "Pro_Plus"
}
exportAccounts = append(exportAccounts, ExportAccount{
ID: a.ID,
Email: a.Email,
Nickname: a.Nickname,
Idp: idp,
UserId: a.UserId,
MachineId: a.MachineId,
Credentials: ExportCredentials{
AccessToken: a.AccessToken,
CsrfToken: "",
RefreshToken: a.RefreshToken,
ClientID: a.ClientID,
ClientSecret: a.ClientSecret,
Region: a.Region,
ExpiresAt: a.ExpiresAt * 1000, // 转为毫秒时间戳
AuthMethod: authMethod,
Provider: a.Provider,
},
Subscription: ExportSubscription{
Type: subType,
Title: a.SubscriptionTitle,
},
Usage: ExportUsage{
Current: a.UsageCurrent,
Limit: a.UsageLimit,
PercentUsed: a.UsagePercent,
LastUpdated: time.Now().UnixMilli(),
},
Tags: []string{},
Status: "active",
CreatedAt: time.Now().UnixMilli(),
LastUsedAt: time.Now().UnixMilli(),
})
}
data := ExportData{
Version: config.Version,
ExportedAt: time.Now().UnixMilli(),
Accounts: exportAccounts,
Groups: []interface{}{},
Tags: []interface{}{},
}
json.NewEncoder(w).Encode(data)
}