Files
kirogo/config/config.go
huangzhenpc 3b791a6926 feat: add INVALID_MODEL_ID retry config + detailed request logging
- Config: new InvalidModelRetries field (default 3, range 0-20)
- Admin API: /admin/api/general GET/POST for general settings
- Admin UI: new "通用设置" card with retry count input
- CallKiroAPI: same-endpoint retry on HTTP 400 INVALID_MODEL_ID
  before falling back to next endpoint
- CallKiroAPI: switched to log.Printf with timestamp, account,
  model, attempt counter, elapsed time, error body truncation
2026-05-11 19:15:49 +08:00

520 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package config provides configuration management for Kiro API Proxy.
//
// This package handles persistent storage and retrieval of:
// - Account credentials and authentication tokens
// - Server settings (port, host, API keys)
// - Usage statistics and metrics
// - Thinking mode configuration for AI responses
//
// All configuration is stored in a JSON file with thread-safe access
// via read-write mutex protection.
package config
import (
"crypto/rand"
"encoding/json"
"fmt"
"os"
"runtime"
"sync"
)
// GenerateMachineId generates a UUID v4 format machine identifier.
// This ID is used to uniquely identify the proxy instance in Kiro API requests,
// helping with request tracking and rate limiting on the server side.
func GenerateMachineId() string {
bytes := make([]byte, 16)
rand.Read(bytes)
bytes[6] = (bytes[6] & 0x0f) | 0x40 // 版本 4
bytes[8] = (bytes[8] & 0x3f) | 0x80 // 变体
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
bytes[0:4], bytes[4:6], bytes[6:8], bytes[8:10], bytes[10:16])
}
// Account represents a Kiro API account with authentication credentials and usage statistics.
type Account struct {
// Basic identification
ID string `json:"id"` // Unique account identifier (UUID)
Email string `json:"email,omitempty"` // User email address
UserId string `json:"userId,omitempty"` // Kiro user ID
Nickname string `json:"nickname,omitempty"` // Display name for admin panel
// Authentication credentials
AccessToken string `json:"accessToken"` // OAuth access token for API calls
RefreshToken string `json:"refreshToken"` // OAuth refresh token for token renewal
ClientID string `json:"clientId,omitempty"` // OIDC client ID (for IdC auth)
ClientSecret string `json:"clientSecret,omitempty"` // OIDC client secret (for IdC auth)
AuthMethod string `json:"authMethod"` // Authentication method: "idc" (AWS IdC) or "social" (GitHub/Google)
Provider string `json:"provider,omitempty"` // Identity provider name (e.g., "BuilderId", "GitHub")
Region string `json:"region"` // AWS region for OIDC endpoints
StartUrl string `json:"startUrl,omitempty"` // AWS SSO start URL
ExpiresAt int64 `json:"expiresAt,omitempty"` // Token expiration timestamp (Unix seconds)
MachineId string `json:"machineId,omitempty"` // UUID machine identifier for request tracking
// Priority weight for load balancing (higher = more requests)
Weight int `json:"weight,omitempty"` // 0 or 1 = normal, 2+ = higher priority
// Account status
Enabled bool `json:"enabled"` // Whether account is active in the pool
BanStatus string `json:"banStatus,omitempty"` // Ban status: "ACTIVE", "BANNED", "SUSPENDED"
BanReason string `json:"banReason,omitempty"` // Reason for ban/suspension
BanTime int64 `json:"banTime,omitempty"` // Timestamp when ban was detected
// Subscription information
SubscriptionType string `json:"subscriptionType,omitempty"` // Tier: FREE, PRO, PRO_PLUS, or POWER
SubscriptionTitle string `json:"subscriptionTitle,omitempty"` // Human-readable subscription name
DaysRemaining int `json:"daysRemaining,omitempty"` // Days until subscription expires
// Usage tracking
UsageCurrent float64 `json:"usageCurrent,omitempty"` // Current period usage (credits)
UsageLimit float64 `json:"usageLimit,omitempty"` // Maximum allowed usage per period
UsagePercent float64 `json:"usagePercent,omitempty"` // Usage percentage (0.0-1.0)
NextResetDate string `json:"nextResetDate,omitempty"` // Date when usage resets (YYYY-MM-DD)
LastRefresh int64 `json:"lastRefresh,omitempty"` // Last info refresh timestamp
// Trial usage tracking
TrialUsageCurrent float64 `json:"trialUsageCurrent,omitempty"` // Trial quota current usage
TrialUsageLimit float64 `json:"trialUsageLimit,omitempty"` // Trial quota total limit
TrialUsagePercent float64 `json:"trialUsagePercent,omitempty"` // Trial quota usage percentage (0.0-1.0)
TrialStatus string `json:"trialStatus,omitempty"` // Trial status: ACTIVE, EXPIRED, NONE
TrialExpiresAt int64 `json:"trialExpiresAt,omitempty"` // Trial expiration timestamp (Unix seconds)
// Runtime statistics (updated during operation)
RequestCount int `json:"requestCount,omitempty"` // Total requests processed
ErrorCount int `json:"errorCount,omitempty"` // Total errors encountered
LastUsed int64 `json:"lastUsed,omitempty"` // Last request timestamp
TotalTokens int `json:"totalTokens,omitempty"` // Cumulative tokens processed
TotalCredits float64 `json:"totalCredits,omitempty"` // Cumulative credits consumed
}
// Config represents the global application configuration.
type Config struct {
// Server settings
Password string `json:"password"` // Admin panel password
Port int `json:"port"` // HTTP server port (default: 8080)
Host string `json:"host"` // HTTP server bind address (default: 0.0.0.0)
ApiKey string `json:"apiKey,omitempty"` // API key for client authentication
RequireApiKey bool `json:"requireApiKey"` // Whether to enforce API key validation
KiroVersion string `json:"kiroVersion,omitempty"`
SystemVersion string `json:"systemVersion,omitempty"`
NodeVersion string `json:"nodeVersion,omitempty"`
Accounts []Account `json:"accounts"` // Registered Kiro accounts
// Thinking mode configuration for extended reasoning output
ThinkingSuffix string `json:"thinkingSuffix,omitempty"` // Model suffix to trigger thinking mode (default: "-thinking")
OpenAIThinkingFormat string `json:"openaiThinkingFormat,omitempty"` // OpenAI output format: "reasoning_content", "thinking", or "think"
ClaudeThinkingFormat string `json:"claudeThinkingFormat,omitempty"` // Claude output format: "reasoning_content", "thinking", or "think"
// Endpoint configuration: "auto", "codewhisperer", or "amazonq"
PreferredEndpoint string `json:"preferredEndpoint,omitempty"`
// General behavior settings
InvalidModelRetries int `json:"invalidModelRetries,omitempty"` // Same-endpoint retry count on INVALID_MODEL_ID (default: 3)
// Global statistics (persisted across restarts)
TotalRequests int `json:"totalRequests,omitempty"` // Total API requests received
SuccessRequests int `json:"successRequests,omitempty"` // Successful requests count
FailedRequests int `json:"failedRequests,omitempty"` // Failed requests count
TotalTokens int `json:"totalTokens,omitempty"` // Total tokens processed
TotalCredits float64 `json:"totalCredits,omitempty"` // Total credits consumed
}
// AccountInfo contains account metadata retrieved from Kiro API.
// Used for updating subscription and usage information.
type AccountInfo struct {
Email string
UserId string
SubscriptionType string
SubscriptionTitle string
DaysRemaining int
UsageCurrent float64
UsageLimit float64
UsagePercent float64
NextResetDate string
LastRefresh int64
TrialUsageCurrent float64
TrialUsageLimit float64
TrialUsagePercent float64
TrialStatus string
TrialExpiresAt int64
}
// Version 当前版本号
const Version = "1.0.4"
var (
cfg *Config
cfgLock sync.RWMutex
cfgPath string
)
// Init initializes the configuration system with the specified file path.
// If the file doesn't exist, a default configuration is created.
func Init(path string) error {
cfgPath = path
return Load()
}
func Load() error {
cfgLock.Lock()
defer cfgLock.Unlock()
data, err := os.ReadFile(cfgPath)
if err != nil {
if os.IsNotExist(err) {
// Create default configuration.
// Binds to 0.0.0.0 by default for Docker/container compatibility.
cfg = &Config{
Password: "changeme",
Port: 8080,
Host: "0.0.0.0",
RequireApiKey: false,
Accounts: []Account{},
}
return Save()
}
return err
}
var c Config
if err := json.Unmarshal(data, &c); err != nil {
return err
}
cfg = &c
return nil
}
// Save persists the current configuration to the JSON file.
// Uses indented formatting for human readability.
func Save() error {
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(cfgPath, data, 0600)
}
// SetPassword updates the admin password.
// Primarily used for environment variable override in containerized deployments.
func SetPassword(password string) {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.Password = password
}
func Get() *Config {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg
}
func GetPassword() string {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg.Password
}
func GetPort() int {
cfgLock.RLock()
defer cfgLock.RUnlock()
if cfg.Port == 0 {
return 8080
}
return cfg.Port
}
func GetHost() string {
cfgLock.RLock()
defer cfgLock.RUnlock()
if cfg.Host == "" {
return "127.0.0.1"
}
return cfg.Host
}
func GetAccounts() []Account {
cfgLock.RLock()
defer cfgLock.RUnlock()
accounts := make([]Account, len(cfg.Accounts))
copy(accounts, cfg.Accounts)
return accounts
}
func GetEnabledAccounts() []Account {
cfgLock.RLock()
defer cfgLock.RUnlock()
var accounts []Account
for _, a := range cfg.Accounts {
if a.Enabled {
accounts = append(accounts, a)
}
}
return accounts
}
func AddAccount(account Account) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.Accounts = append(cfg.Accounts, account)
return Save()
}
func UpdateAccount(id string, account Account) error {
cfgLock.Lock()
defer cfgLock.Unlock()
for i, a := range cfg.Accounts {
if a.ID == id {
cfg.Accounts[i] = account
return Save()
}
}
return nil
}
func DeleteAccount(id string) error {
cfgLock.Lock()
defer cfgLock.Unlock()
for i, a := range cfg.Accounts {
if a.ID == id {
cfg.Accounts = append(cfg.Accounts[:i], cfg.Accounts[i+1:]...)
return Save()
}
}
return nil
}
func UpdateAccountToken(id, accessToken, refreshToken string, expiresAt int64) error {
cfgLock.Lock()
defer cfgLock.Unlock()
for i, a := range cfg.Accounts {
if a.ID == id {
cfg.Accounts[i].AccessToken = accessToken
if refreshToken != "" {
cfg.Accounts[i].RefreshToken = refreshToken
}
cfg.Accounts[i].ExpiresAt = expiresAt
return Save()
}
}
return nil
}
func GetApiKey() string {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg.ApiKey
}
func IsApiKeyRequired() bool {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg.RequireApiKey
}
func UpdateSettings(apiKey string, requireApiKey bool, password string) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.ApiKey = apiKey
cfg.RequireApiKey = requireApiKey
if password != "" {
cfg.Password = password
}
return Save()
}
func UpdateStats(totalReq, successReq, failedReq, totalTokens int, totalCredits float64) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.TotalRequests = totalReq
cfg.SuccessRequests = successReq
cfg.FailedRequests = failedReq
cfg.TotalTokens = totalTokens
cfg.TotalCredits = totalCredits
return Save()
}
func GetStats() (int, int, int, int, float64) {
cfgLock.RLock()
defer cfgLock.RUnlock()
return cfg.TotalRequests, cfg.SuccessRequests, cfg.FailedRequests, cfg.TotalTokens, cfg.TotalCredits
}
func UpdateAccountStats(id string, requestCount, errorCount, totalTokens int, totalCredits float64, lastUsed int64) error {
cfgLock.Lock()
defer cfgLock.Unlock()
for i, a := range cfg.Accounts {
if a.ID == id {
cfg.Accounts[i].RequestCount = requestCount
cfg.Accounts[i].ErrorCount = errorCount
cfg.Accounts[i].TotalTokens = totalTokens
cfg.Accounts[i].TotalCredits = totalCredits
cfg.Accounts[i].LastUsed = lastUsed
return Save()
}
}
return nil
}
// UpdateAccountInfo updates an account's subscription and usage information.
// Called after refreshing account data from Kiro API.
func UpdateAccountInfo(id string, info AccountInfo) error {
cfgLock.Lock()
defer cfgLock.Unlock()
for i, a := range cfg.Accounts {
if a.ID == id {
if info.Email != "" {
cfg.Accounts[i].Email = info.Email
}
if info.UserId != "" {
cfg.Accounts[i].UserId = info.UserId
}
cfg.Accounts[i].SubscriptionType = info.SubscriptionType
cfg.Accounts[i].SubscriptionTitle = info.SubscriptionTitle
cfg.Accounts[i].DaysRemaining = info.DaysRemaining
cfg.Accounts[i].UsageCurrent = info.UsageCurrent
cfg.Accounts[i].UsageLimit = info.UsageLimit
cfg.Accounts[i].UsagePercent = info.UsagePercent
cfg.Accounts[i].NextResetDate = info.NextResetDate
cfg.Accounts[i].LastRefresh = info.LastRefresh
cfg.Accounts[i].TrialUsageCurrent = info.TrialUsageCurrent
cfg.Accounts[i].TrialUsageLimit = info.TrialUsageLimit
cfg.Accounts[i].TrialUsagePercent = info.TrialUsagePercent
cfg.Accounts[i].TrialStatus = info.TrialStatus
cfg.Accounts[i].TrialExpiresAt = info.TrialExpiresAt
return Save()
}
}
return nil
}
// ThinkingConfig holds settings for AI thinking/reasoning mode.
// When enabled, models output their reasoning process alongside the response.
type ThinkingConfig struct {
Suffix string `json:"suffix"` // Model name suffix that triggers thinking mode
OpenAIFormat string `json:"openaiFormat"` // Output format for OpenAI-compatible responses
ClaudeFormat string `json:"claudeFormat"` // Output format for Claude-compatible responses
}
// GetThinkingConfig 获取 thinking 配置
func GetThinkingConfig() ThinkingConfig {
cfgLock.RLock()
defer cfgLock.RUnlock()
suffix := cfg.ThinkingSuffix
if suffix == "" {
suffix = "-thinking"
}
openaiFormat := cfg.OpenAIThinkingFormat
if openaiFormat == "" {
openaiFormat = "reasoning_content"
}
claudeFormat := cfg.ClaudeThinkingFormat
if claudeFormat == "" {
claudeFormat = "thinking"
}
return ThinkingConfig{
Suffix: suffix,
OpenAIFormat: openaiFormat,
ClaudeFormat: claudeFormat,
}
}
// UpdateThinkingConfig 更新 thinking 配置
func UpdateThinkingConfig(suffix, openaiFormat, claudeFormat string) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.ThinkingSuffix = suffix
cfg.OpenAIThinkingFormat = openaiFormat
cfg.ClaudeThinkingFormat = claudeFormat
return Save()
}
// GetPreferredEndpoint 获取首选端点配置
func GetPreferredEndpoint() string {
cfgLock.RLock()
defer cfgLock.RUnlock()
if cfg.PreferredEndpoint == "" {
return "auto"
}
return cfg.PreferredEndpoint
}
// UpdatePreferredEndpoint 更新首选端点配置
func UpdatePreferredEndpoint(endpoint string) error {
cfgLock.Lock()
defer cfgLock.Unlock()
cfg.PreferredEndpoint = endpoint
return Save()
}
// GetInvalidModelRetries 返回 INVALID_MODEL_ID 同端点重试次数(默认 3
func GetInvalidModelRetries() int {
cfgLock.RLock()
defer cfgLock.RUnlock()
if cfg.InvalidModelRetries < 0 {
return 0
}
if cfg.InvalidModelRetries == 0 {
return 3
}
return cfg.InvalidModelRetries
}
// UpdateInvalidModelRetries 更新 INVALID_MODEL_ID 同端点重试次数
func UpdateInvalidModelRetries(n int) error {
cfgLock.Lock()
defer cfgLock.Unlock()
if n < 0 {
n = 0
}
cfg.InvalidModelRetries = n
return Save()
}
type KiroClientConfig struct {
KiroVersion string
SystemVersion string
NodeVersion string
}
func GetKiroClientConfig() KiroClientConfig {
cfgLock.RLock()
defer cfgLock.RUnlock()
kiroVersion := "0.11.107"
if cfg != nil && cfg.KiroVersion != "" {
kiroVersion = cfg.KiroVersion
}
systemVersion := ""
if cfg != nil {
systemVersion = cfg.SystemVersion
}
if systemVersion == "" {
systemVersion = defaultSystemVersion()
}
nodeVersion := "22.22.0"
if cfg != nil && cfg.NodeVersion != "" {
nodeVersion = cfg.NodeVersion
}
return KiroClientConfig{
KiroVersion: kiroVersion,
SystemVersion: systemVersion,
NodeVersion: nodeVersion,
}
}
func defaultSystemVersion() string {
switch runtime.GOOS {
case "windows":
return "win32#10.0.22631"
case "darwin":
return "darwin#24.6.0"
default:
return "linux#6.6.87"
}
}