- 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
520 lines
16 KiB
Go
520 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"`
|
||
|
||
// 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"
|
||
}
|
||
}
|