// 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"` // 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() } 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" } }