Merge remote-tracking branch 'origin/main' into pr-104

# Conflicts:
#	config.example.yaml
#	internal/config/config.go
#	sdk/cliproxy/auth/model_name_mappings.go
This commit is contained in:
Luis Pater
2026-01-16 09:40:07 +08:00
76 changed files with 15505 additions and 361 deletions

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/fsnotify/fsnotify"
kiroauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kiro"
log "github.com/sirupsen/logrus"
)
@@ -39,12 +40,35 @@ func (w *Watcher) start(ctx context.Context) error {
}
log.Debugf("watching auth directory: %s", w.authDir)
w.watchKiroIDETokenFile()
go w.processEvents(ctx)
w.reloadClients(true, nil, false)
return nil
}
func (w *Watcher) watchKiroIDETokenFile() {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Debugf("failed to get home directory for Kiro IDE token watch: %v", err)
return
}
kiroTokenDir := filepath.Join(homeDir, ".aws", "sso", "cache")
if _, statErr := os.Stat(kiroTokenDir); os.IsNotExist(statErr) {
log.Debugf("Kiro IDE token directory does not exist: %s", kiroTokenDir)
return
}
if errAdd := w.watcher.Add(kiroTokenDir); errAdd != nil {
log.Debugf("failed to watch Kiro IDE token directory %s: %v", kiroTokenDir, errAdd)
return
}
log.Debugf("watching Kiro IDE token directory: %s", kiroTokenDir)
}
func (w *Watcher) processEvents(ctx context.Context) {
for {
select {
@@ -73,11 +97,17 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
isConfigEvent := normalizedName == normalizedConfigPath && event.Op&configOps != 0
authOps := fsnotify.Create | fsnotify.Write | fsnotify.Remove | fsnotify.Rename
isAuthJSON := strings.HasPrefix(normalizedName, normalizedAuthDir) && strings.HasSuffix(normalizedName, ".json") && event.Op&authOps != 0
if !isConfigEvent && !isAuthJSON {
isKiroIDEToken := w.isKiroIDETokenFile(event.Name) && event.Op&authOps != 0
if !isConfigEvent && !isAuthJSON && !isKiroIDEToken {
// Ignore unrelated files (e.g., cookie snapshots *.cookie) and other noise.
return
}
if isKiroIDEToken {
w.handleKiroIDETokenChange(event)
return
}
now := time.Now()
log.Debugf("file system event detected: %s %s", event.Op.String(), event.Name)
@@ -124,6 +154,42 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
}
}
func (w *Watcher) isKiroIDETokenFile(path string) bool {
normalized := filepath.ToSlash(path)
return strings.HasSuffix(normalized, "kiro-auth-token.json") && strings.Contains(normalized, ".aws/sso/cache")
}
func (w *Watcher) handleKiroIDETokenChange(event fsnotify.Event) {
log.Debugf("Kiro IDE token file event detected: %s %s", event.Op.String(), event.Name)
if event.Op&(fsnotify.Remove|fsnotify.Rename) != 0 {
time.Sleep(replaceCheckDelay)
if _, statErr := os.Stat(event.Name); statErr != nil {
log.Debugf("Kiro IDE token file removed: %s", event.Name)
return
}
}
tokenData, err := kiroauth.LoadKiroIDEToken()
if err != nil {
log.Debugf("failed to load Kiro IDE token after change: %v", err)
return
}
log.Infof("Kiro IDE token file updated, access token refreshed (provider: %s)", tokenData.Provider)
w.refreshAuthState(true)
w.clientsMutex.RLock()
cfg := w.config
w.clientsMutex.RUnlock()
if w.reloadCallback != nil && cfg != nil {
log.Debugf("triggering server update callback after Kiro IDE token change")
w.reloadCallback(cfg)
}
}
func (w *Watcher) authFileUnchanged(path string) (bool, error) {
data, errRead := os.ReadFile(path)
if errRead != nil {

View File

@@ -5,8 +5,10 @@ import (
"strconv"
"strings"
kiroauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kiro"
"github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
)
// ConfigSynthesizer generates Auth entries from configuration API keys.
@@ -31,6 +33,8 @@ func (s *ConfigSynthesizer) Synthesize(ctx *SynthesisContext) ([]*coreauth.Auth,
out = append(out, s.synthesizeClaudeKeys(ctx)...)
// Codex API Keys
out = append(out, s.synthesizeCodexKeys(ctx)...)
// Kiro (AWS CodeWhisperer)
out = append(out, s.synthesizeKiroKeys(ctx)...)
// OpenAI-compat
out = append(out, s.synthesizeOpenAICompat(ctx)...)
// Vertex-compat
@@ -317,3 +321,96 @@ func (s *ConfigSynthesizer) synthesizeVertexCompat(ctx *SynthesisContext) []*cor
}
return out
}
// synthesizeKiroKeys creates Auth entries for Kiro (AWS CodeWhisperer) tokens.
func (s *ConfigSynthesizer) synthesizeKiroKeys(ctx *SynthesisContext) []*coreauth.Auth {
cfg := ctx.Config
now := ctx.Now
idGen := ctx.IDGenerator
if len(cfg.KiroKey) == 0 {
return nil
}
out := make([]*coreauth.Auth, 0, len(cfg.KiroKey))
kAuth := kiroauth.NewKiroAuth(cfg)
for i := range cfg.KiroKey {
kk := cfg.KiroKey[i]
var accessToken, profileArn, refreshToken string
// Try to load from token file first
if kk.TokenFile != "" && kAuth != nil {
tokenData, err := kAuth.LoadTokenFromFile(kk.TokenFile)
if err != nil {
log.Warnf("failed to load kiro token file %s: %v", kk.TokenFile, err)
} else {
accessToken = tokenData.AccessToken
profileArn = tokenData.ProfileArn
refreshToken = tokenData.RefreshToken
}
}
// Override with direct config values if provided
if kk.AccessToken != "" {
accessToken = kk.AccessToken
}
if kk.ProfileArn != "" {
profileArn = kk.ProfileArn
}
if kk.RefreshToken != "" {
refreshToken = kk.RefreshToken
}
if accessToken == "" {
log.Warnf("kiro config[%d] missing access_token, skipping", i)
continue
}
// profileArn is optional for AWS Builder ID users
id, token := idGen.Next("kiro:token", accessToken, profileArn)
attrs := map[string]string{
"source": fmt.Sprintf("config:kiro[%s]", token),
"access_token": accessToken,
}
if profileArn != "" {
attrs["profile_arn"] = profileArn
}
if kk.Region != "" {
attrs["region"] = kk.Region
}
if kk.AgentTaskType != "" {
attrs["agent_task_type"] = kk.AgentTaskType
}
if kk.PreferredEndpoint != "" {
attrs["preferred_endpoint"] = kk.PreferredEndpoint
} else if cfg.KiroPreferredEndpoint != "" {
// Apply global default if not overridden by specific key
attrs["preferred_endpoint"] = cfg.KiroPreferredEndpoint
}
if refreshToken != "" {
attrs["refresh_token"] = refreshToken
}
proxyURL := strings.TrimSpace(kk.ProxyURL)
a := &coreauth.Auth{
ID: id,
Provider: "kiro",
Label: "kiro-token",
Status: coreauth.StatusActive,
ProxyURL: proxyURL,
Attributes: attrs,
CreatedAt: now,
UpdatedAt: now,
}
if refreshToken != "" {
if a.Metadata == nil {
a.Metadata = make(map[string]any)
}
a.Metadata["refresh_token"] = refreshToken
}
out = append(out, a)
}
return out
}