514 lines
16 KiB
Go
514 lines
16 KiB
Go
// 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"`
|
|
|
|
// Proxy configuration: optional outbound proxy for Kiro API requests
|
|
// Format: "socks5://host:port", "socks5://user:pass@host:port",
|
|
// "http://host:port", "http://user:pass@host:port"
|
|
// Leave empty to connect directly.
|
|
ProxyURL string `json:"proxyURL,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 current version
|
|
const Version = "1.0.6"
|
|
|
|
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()
|
|
}
|
|
|
|
// GetProxyURL 获取出站代理地址
|
|
func GetProxyURL() string {
|
|
cfgLock.RLock()
|
|
defer cfgLock.RUnlock()
|
|
return cfg.ProxyURL
|
|
}
|
|
|
|
// UpdateProxySettings 更新出站代理配置
|
|
func UpdateProxySettings(proxyURL string) error {
|
|
cfgLock.Lock()
|
|
defer cfgLock.Unlock()
|
|
cfg.ProxyURL = proxyURL
|
|
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"
|
|
}
|
|
}
|