mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 04:10:51 +08:00
- 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>
113 lines
3.9 KiB
Go
113 lines
3.9 KiB
Go
// Package kimi provides authentication and token management functionality
|
|
// for Kimi (Moonshot AI) services. It handles OAuth2 device flow token storage,
|
|
// serialization, and retrieval for maintaining authenticated sessions with the Kimi API.
|
|
package kimi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
|
)
|
|
|
|
// KimiTokenStorage stores OAuth2 token information for Kimi API authentication.
|
|
type KimiTokenStorage struct {
|
|
// AccessToken is the OAuth2 access token used for authenticating API requests.
|
|
AccessToken string `json:"access_token"`
|
|
// RefreshToken is the OAuth2 refresh token used to obtain new access tokens.
|
|
RefreshToken string `json:"refresh_token"`
|
|
// TokenType is the type of token, typically "Bearer".
|
|
TokenType string `json:"token_type"`
|
|
// Scope is the OAuth2 scope granted to the token.
|
|
Scope string `json:"scope,omitempty"`
|
|
// Expired is the RFC3339 timestamp when the access token expires.
|
|
Expired string `json:"expired,omitempty"`
|
|
// Type indicates the authentication provider type, always "kimi" for this storage.
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// KimiTokenData holds the raw OAuth token response from Kimi.
|
|
type KimiTokenData struct {
|
|
// AccessToken is the OAuth2 access token.
|
|
AccessToken string `json:"access_token"`
|
|
// RefreshToken is the OAuth2 refresh token.
|
|
RefreshToken string `json:"refresh_token"`
|
|
// TokenType is the type of token, typically "Bearer".
|
|
TokenType string `json:"token_type"`
|
|
// ExpiresAt is the Unix timestamp when the token expires.
|
|
ExpiresAt int64 `json:"expires_at"`
|
|
// Scope is the OAuth2 scope granted to the token.
|
|
Scope string `json:"scope"`
|
|
}
|
|
|
|
// KimiAuthBundle bundles authentication data for storage.
|
|
type KimiAuthBundle struct {
|
|
// TokenData contains the OAuth token information.
|
|
TokenData *KimiTokenData
|
|
}
|
|
|
|
// DeviceCodeResponse represents Kimi's device code response.
|
|
type DeviceCodeResponse struct {
|
|
// DeviceCode is the device verification code.
|
|
DeviceCode string `json:"device_code"`
|
|
// UserCode is the code the user must enter at the verification URI.
|
|
UserCode string `json:"user_code"`
|
|
// VerificationURI is the URL where the user should enter the code.
|
|
VerificationURI string `json:"verification_uri,omitempty"`
|
|
// VerificationURIComplete is the URL with the code pre-filled.
|
|
VerificationURIComplete string `json:"verification_uri_complete"`
|
|
// ExpiresIn is the number of seconds until the device code expires.
|
|
ExpiresIn int `json:"expires_in"`
|
|
// Interval is the minimum number of seconds to wait between polling requests.
|
|
Interval int `json:"interval"`
|
|
}
|
|
|
|
// SaveTokenToFile serializes the Kimi token storage to a JSON file.
|
|
func (ts *KimiTokenStorage) SaveTokenToFile(authFilePath string) error {
|
|
misc.LogSavingCredentials(authFilePath)
|
|
ts.Type = "kimi"
|
|
|
|
if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil {
|
|
return fmt.Errorf("failed to create directory: %v", err)
|
|
}
|
|
|
|
f, err := os.Create(authFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create token file: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = f.Close()
|
|
}()
|
|
|
|
encoder := json.NewEncoder(f)
|
|
encoder.SetIndent("", " ")
|
|
if err = encoder.Encode(ts); err != nil {
|
|
return fmt.Errorf("failed to write token to file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsExpired checks if the token has expired.
|
|
func (ts *KimiTokenStorage) IsExpired() bool {
|
|
if ts.Expired == "" {
|
|
return false // No expiry set, assume valid
|
|
}
|
|
t, err := time.Parse(time.RFC3339, ts.Expired)
|
|
if err != nil {
|
|
return true // Has expiry string but can't parse
|
|
}
|
|
// Consider expired if within refresh threshold
|
|
return time.Now().Add(time.Duration(refreshThresholdSeconds) * time.Second).After(t)
|
|
}
|
|
|
|
// NeedsRefresh checks if the token should be refreshed.
|
|
func (ts *KimiTokenStorage) NeedsRefresh() bool {
|
|
if ts.RefreshToken == "" {
|
|
return false // Can't refresh without refresh token
|
|
}
|
|
return ts.IsExpired()
|
|
}
|