mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 12:20:52 +08:00
Add Kimi (Moonshot AI) provider support
- OAuth2 device authorization grant flow (RFC 8628) for authentication - Streaming and non-streaming chat completions via OpenAI-compatible API - Models: kimi-k2, kimi-k2-thinking, kimi-k2.5 - CLI `--kimi-login` command for device flow auth - Token management with automatic refresh - Thinking/reasoning effort support for thinking-enabled models Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
119
sdk/auth/kimi.go
Normal file
119
sdk/auth/kimi.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// kimiRefreshLead is the duration before token expiry when refresh should occur.
|
||||
var kimiRefreshLead = 5 * time.Minute
|
||||
|
||||
// KimiAuthenticator implements the OAuth device flow login for Kimi (Moonshot AI).
|
||||
type KimiAuthenticator struct{}
|
||||
|
||||
// NewKimiAuthenticator constructs a new Kimi authenticator.
|
||||
func NewKimiAuthenticator() Authenticator {
|
||||
return &KimiAuthenticator{}
|
||||
}
|
||||
|
||||
// Provider returns the provider key for kimi.
|
||||
func (KimiAuthenticator) Provider() string {
|
||||
return "kimi"
|
||||
}
|
||||
|
||||
// RefreshLead returns the duration before token expiry when refresh should occur.
|
||||
// Kimi tokens expire and need to be refreshed before expiry.
|
||||
func (KimiAuthenticator) RefreshLead() *time.Duration {
|
||||
return &kimiRefreshLead
|
||||
}
|
||||
|
||||
// Login initiates the Kimi device flow authentication.
|
||||
func (a KimiAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||
}
|
||||
if opts == nil {
|
||||
opts = &LoginOptions{}
|
||||
}
|
||||
|
||||
authSvc := kimi.NewKimiAuth(cfg)
|
||||
|
||||
// Start the device flow
|
||||
fmt.Println("Starting Kimi authentication...")
|
||||
deviceCode, err := authSvc.StartDeviceFlow(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("kimi: failed to start device flow: %w", err)
|
||||
}
|
||||
|
||||
// Display the verification URL
|
||||
verificationURL := deviceCode.VerificationURIComplete
|
||||
if verificationURL == "" {
|
||||
verificationURL = deviceCode.VerificationURI
|
||||
}
|
||||
|
||||
fmt.Printf("\nTo authenticate, please visit:\n%s\n\n", verificationURL)
|
||||
if deviceCode.UserCode != "" {
|
||||
fmt.Printf("User code: %s\n\n", deviceCode.UserCode)
|
||||
}
|
||||
|
||||
// Try to open the browser automatically
|
||||
if !opts.NoBrowser {
|
||||
if browser.IsAvailable() {
|
||||
if errOpen := browser.OpenURL(verificationURL); errOpen != nil {
|
||||
log.Warnf("Failed to open browser automatically: %v", errOpen)
|
||||
} else {
|
||||
fmt.Println("Browser opened automatically.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Waiting for authorization...")
|
||||
if deviceCode.ExpiresIn > 0 {
|
||||
fmt.Printf("(This will timeout in %d seconds if not authorized)\n", deviceCode.ExpiresIn)
|
||||
}
|
||||
|
||||
// Wait for user authorization
|
||||
authBundle, err := authSvc.WaitForAuthorization(ctx, deviceCode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("kimi: %w", err)
|
||||
}
|
||||
|
||||
// Create the token storage
|
||||
tokenStorage := authSvc.CreateTokenStorage(authBundle)
|
||||
|
||||
// Build metadata with token information
|
||||
metadata := map[string]any{
|
||||
"type": "kimi",
|
||||
"access_token": authBundle.TokenData.AccessToken,
|
||||
"refresh_token": authBundle.TokenData.RefreshToken,
|
||||
"token_type": authBundle.TokenData.TokenType,
|
||||
"scope": authBundle.TokenData.Scope,
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
if authBundle.TokenData.ExpiresAt > 0 {
|
||||
exp := time.Unix(authBundle.TokenData.ExpiresAt, 0).UTC().Format(time.RFC3339)
|
||||
metadata["expired"] = exp
|
||||
}
|
||||
|
||||
// Generate a unique filename
|
||||
fileName := fmt.Sprintf("kimi-%d.json", time.Now().UnixMilli())
|
||||
|
||||
fmt.Println("\nKimi authentication successful!")
|
||||
|
||||
return &coreauth.Auth{
|
||||
ID: fileName,
|
||||
Provider: a.Provider(),
|
||||
FileName: fileName,
|
||||
Label: "Kimi User",
|
||||
Storage: tokenStorage,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
@@ -14,6 +14,7 @@ func init() {
|
||||
registerRefreshLead("gemini", func() Authenticator { return NewGeminiAuthenticator() })
|
||||
registerRefreshLead("gemini-cli", func() Authenticator { return NewGeminiAuthenticator() })
|
||||
registerRefreshLead("antigravity", func() Authenticator { return NewAntigravityAuthenticator() })
|
||||
registerRefreshLead("kimi", func() Authenticator { return NewKimiAuthenticator() })
|
||||
}
|
||||
|
||||
func registerRefreshLead(provider string, factory func() Authenticator) {
|
||||
|
||||
Reference in New Issue
Block a user