mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-08 06:43:41 +00:00
added kilocode auth, needs adjusting
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,8 +3,10 @@ cli-proxy-api
|
|||||||
cliproxy
|
cliproxy
|
||||||
*.exe
|
*.exe
|
||||||
|
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
config.yaml
|
config.yaml
|
||||||
|
my-config.yaml
|
||||||
.env
|
.env
|
||||||
.mcp.json
|
.mcp.json
|
||||||
# Generated content
|
# Generated content
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ func main() {
|
|||||||
var codexLogin bool
|
var codexLogin bool
|
||||||
var claudeLogin bool
|
var claudeLogin bool
|
||||||
var qwenLogin bool
|
var qwenLogin bool
|
||||||
|
var kiloLogin bool
|
||||||
var iflowLogin bool
|
var iflowLogin bool
|
||||||
var iflowCookie bool
|
var iflowCookie bool
|
||||||
var noBrowser bool
|
var noBrowser bool
|
||||||
@@ -96,6 +97,7 @@ func main() {
|
|||||||
flag.BoolVar(&codexLogin, "codex-login", false, "Login to Codex using OAuth")
|
flag.BoolVar(&codexLogin, "codex-login", false, "Login to Codex using OAuth")
|
||||||
flag.BoolVar(&claudeLogin, "claude-login", false, "Login to Claude using OAuth")
|
flag.BoolVar(&claudeLogin, "claude-login", false, "Login to Claude using OAuth")
|
||||||
flag.BoolVar(&qwenLogin, "qwen-login", false, "Login to Qwen using OAuth")
|
flag.BoolVar(&qwenLogin, "qwen-login", false, "Login to Qwen using OAuth")
|
||||||
|
flag.BoolVar(&kiloLogin, "kilo-login", false, "Login to Kilo AI using device flow")
|
||||||
flag.BoolVar(&iflowLogin, "iflow-login", false, "Login to iFlow using OAuth")
|
flag.BoolVar(&iflowLogin, "iflow-login", false, "Login to iFlow using OAuth")
|
||||||
flag.BoolVar(&iflowCookie, "iflow-cookie", false, "Login to iFlow using Cookie")
|
flag.BoolVar(&iflowCookie, "iflow-cookie", false, "Login to iFlow using Cookie")
|
||||||
flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
|
flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
|
||||||
@@ -499,6 +501,8 @@ func main() {
|
|||||||
cmd.DoClaudeLogin(cfg, options)
|
cmd.DoClaudeLogin(cfg, options)
|
||||||
} else if qwenLogin {
|
} else if qwenLogin {
|
||||||
cmd.DoQwenLogin(cfg, options)
|
cmd.DoQwenLogin(cfg, options)
|
||||||
|
} else if kiloLogin {
|
||||||
|
cmd.DoKiloLogin(cfg, options)
|
||||||
} else if iflowLogin {
|
} else if iflowLogin {
|
||||||
cmd.DoIFlowLogin(cfg, options)
|
cmd.DoIFlowLogin(cfg, options)
|
||||||
} else if iflowCookie {
|
} else if iflowCookie {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Server host/interface to bind to. Default is empty ("") to bind all interfaces (IPv4 + IPv6).
|
# Server host/interface to bind to. Default is empty ("") to bind all interfaces (IPv4 + IPv6).
|
||||||
# Use "127.0.0.1" or "localhost" to restrict access to local machine only.
|
# Use "127.0.0.1" or "localhost" to restrict access to local machine only.
|
||||||
host: ""
|
host: ''
|
||||||
|
|
||||||
# Server port
|
# Server port
|
||||||
port: 8317
|
port: 8317
|
||||||
@@ -8,8 +8,8 @@ port: 8317
|
|||||||
# TLS settings for HTTPS. When enabled, the server listens with the provided certificate and key.
|
# TLS settings for HTTPS. When enabled, the server listens with the provided certificate and key.
|
||||||
tls:
|
tls:
|
||||||
enable: false
|
enable: false
|
||||||
cert: ""
|
cert: ''
|
||||||
key: ""
|
key: ''
|
||||||
|
|
||||||
# Management API settings
|
# Management API settings
|
||||||
remote-management:
|
remote-management:
|
||||||
@@ -20,22 +20,22 @@ remote-management:
|
|||||||
# Management key. If a plaintext value is provided here, it will be hashed on startup.
|
# Management key. If a plaintext value is provided here, it will be hashed on startup.
|
||||||
# All management requests (even from localhost) require this key.
|
# All management requests (even from localhost) require this key.
|
||||||
# Leave empty to disable the Management API entirely (404 for all /v0/management routes).
|
# Leave empty to disable the Management API entirely (404 for all /v0/management routes).
|
||||||
secret-key: ""
|
secret-key: ''
|
||||||
|
|
||||||
# Disable the bundled management control panel asset download and HTTP route when true.
|
# Disable the bundled management control panel asset download and HTTP route when true.
|
||||||
disable-control-panel: false
|
disable-control-panel: false
|
||||||
|
|
||||||
# GitHub repository for the management control panel. Accepts a repository URL or releases API URL.
|
# GitHub repository for the management control panel. Accepts a repository URL or releases API URL.
|
||||||
panel-github-repository: "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"
|
panel-github-repository: 'https://github.com/router-for-me/Cli-Proxy-API-Management-Center'
|
||||||
|
|
||||||
# Authentication directory (supports ~ for home directory)
|
# Authentication directory (supports ~ for home directory)
|
||||||
auth-dir: "~/.cli-proxy-api"
|
auth-dir: '~/.cli-proxy-api'
|
||||||
|
|
||||||
# API keys for authentication
|
# API keys for authentication
|
||||||
api-keys:
|
api-keys:
|
||||||
- "your-api-key-1"
|
- 'your-api-key-1'
|
||||||
- "your-api-key-2"
|
- 'your-api-key-2'
|
||||||
- "your-api-key-3"
|
- 'your-api-key-3'
|
||||||
|
|
||||||
# Enable debug logging
|
# Enable debug logging
|
||||||
debug: false
|
debug: false
|
||||||
@@ -43,7 +43,7 @@ debug: false
|
|||||||
# Enable pprof HTTP debug server (host:port). Keep it bound to localhost for safety.
|
# Enable pprof HTTP debug server (host:port). Keep it bound to localhost for safety.
|
||||||
pprof:
|
pprof:
|
||||||
enable: false
|
enable: false
|
||||||
addr: "127.0.0.1:8316"
|
addr: '127.0.0.1:8316'
|
||||||
|
|
||||||
# When true, disable high-overhead HTTP middleware features to reduce per-request memory usage under high concurrency.
|
# When true, disable high-overhead HTTP middleware features to reduce per-request memory usage under high concurrency.
|
||||||
commercial-mode: false
|
commercial-mode: false
|
||||||
@@ -68,7 +68,7 @@ error-logs-max-files: 10
|
|||||||
usage-statistics-enabled: false
|
usage-statistics-enabled: false
|
||||||
|
|
||||||
# Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/
|
# Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/
|
||||||
proxy-url: ""
|
proxy-url: ''
|
||||||
|
|
||||||
# When true, unprefixed model requests only use credentials without a prefix (except when prefix == model name).
|
# When true, unprefixed model requests only use credentials without a prefix (except when prefix == model name).
|
||||||
force-model-prefix: false
|
force-model-prefix: false
|
||||||
@@ -86,7 +86,7 @@ quota-exceeded:
|
|||||||
|
|
||||||
# Routing strategy for selecting credentials when multiple match.
|
# Routing strategy for selecting credentials when multiple match.
|
||||||
routing:
|
routing:
|
||||||
strategy: "round-robin" # round-robin (default), fill-first
|
strategy: 'round-robin' # round-robin (default), fill-first
|
||||||
|
|
||||||
# When true, enable authentication for the WebSocket API (/v1/ws).
|
# When true, enable authentication for the WebSocket API (/v1/ws).
|
||||||
ws-auth: false
|
ws-auth: false
|
||||||
@@ -171,6 +171,21 @@ nonstream-keepalive-interval: 0
|
|||||||
# profile-arn: "arn:aws:codewhisperer:us-east-1:..."
|
# profile-arn: "arn:aws:codewhisperer:us-east-1:..."
|
||||||
# proxy-url: "socks5://proxy.example.com:1080" # optional: proxy override
|
# proxy-url: "socks5://proxy.example.com:1080" # optional: proxy override
|
||||||
|
|
||||||
|
# Kilocode (OAuth-based code assistant)
|
||||||
|
# Note: Kilocode uses OAuth device flow authentication.
|
||||||
|
# Use the CLI command: ./server --kilo-login
|
||||||
|
# This will save credentials to the auth directory (default: ~/.cli-proxy-api/)
|
||||||
|
# oauth-model-alias:
|
||||||
|
# kilo:
|
||||||
|
# - name: "minimax/minimax-m2.5:free"
|
||||||
|
# alias: "minimax-m2.5"
|
||||||
|
# - name: "z-ai/glm-5:free"
|
||||||
|
# alias: "glm-5"
|
||||||
|
# oauth-excluded-models:
|
||||||
|
# kilo:
|
||||||
|
# - "kilo-claude-opus-4-6" # exclude specific models (exact match)
|
||||||
|
# - "*:free" # wildcard matching suffix (e.g. all free models)
|
||||||
|
|
||||||
# OpenAI compatibility providers
|
# OpenAI compatibility providers
|
||||||
# openai-compatibility:
|
# openai-compatibility:
|
||||||
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
|
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot"
|
||||||
geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
geminiAuth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
||||||
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kilo"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi"
|
||||||
kiroauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kiro"
|
kiroauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kiro"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
|
||||||
@@ -2733,3 +2734,88 @@ func generateKiroPKCE() (verifier, challenge string, err error) {
|
|||||||
|
|
||||||
return verifier, challenge, nil
|
return verifier, challenge, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RequestKiloToken(c *gin.Context) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
fmt.Println("Initializing Kilo authentication...")
|
||||||
|
|
||||||
|
state := fmt.Sprintf("kil-%d", time.Now().UnixNano())
|
||||||
|
kilocodeAuth := kilo.NewKiloAuth()
|
||||||
|
|
||||||
|
resp, err := kilocodeAuth.InitiateDeviceFlow(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to initiate device flow: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initiate device flow"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterOAuthSession(state, "kilo")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
fmt.Printf("Please visit %s and enter code: %s\n", resp.VerificationURL, resp.Code)
|
||||||
|
|
||||||
|
status, err := kilocodeAuth.PollForToken(ctx, resp.Code)
|
||||||
|
if err != nil {
|
||||||
|
SetOAuthSessionError(state, "Authentication failed")
|
||||||
|
fmt.Printf("Authentication failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := kilocodeAuth.GetProfile(ctx, status.Token)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to fetch profile: %v", err)
|
||||||
|
profile = &kilo.Profile{Email: status.UserEmail}
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgID string
|
||||||
|
if len(profile.Orgs) > 0 {
|
||||||
|
orgID = profile.Orgs[0].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults, err := kilocodeAuth.GetDefaults(ctx, status.Token, orgID)
|
||||||
|
if err != nil {
|
||||||
|
defaults = &kilo.Defaults{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := &kilo.KiloTokenStorage{
|
||||||
|
Token: status.Token,
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Model: defaults.Model,
|
||||||
|
Email: status.UserEmail,
|
||||||
|
Type: "kilo",
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := kilo.CredentialFileName(status.UserEmail)
|
||||||
|
record := &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
|
Provider: "kilo",
|
||||||
|
FileName: fileName,
|
||||||
|
Storage: ts,
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"email": status.UserEmail,
|
||||||
|
"organization_id": orgID,
|
||||||
|
"model": defaults.Model,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
savedPath, errSave := h.saveTokenRecord(ctx, record)
|
||||||
|
if errSave != nil {
|
||||||
|
log.Errorf("Failed to save authentication tokens: %v", errSave)
|
||||||
|
SetOAuthSessionError(state, "Failed to save authentication tokens")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
|
||||||
|
CompleteOAuthSession(state)
|
||||||
|
CompleteOAuthSessionsByProvider("kilo")
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "ok",
|
||||||
|
"url": resp.VerificationURL,
|
||||||
|
"state": state,
|
||||||
|
"user_code": resp.Code,
|
||||||
|
"verification_uri": resp.VerificationURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -649,6 +649,7 @@ func (s *Server) registerManagementRoutes() {
|
|||||||
mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken)
|
mgmt.GET("/gemini-cli-auth-url", s.mgmt.RequestGeminiCLIToken)
|
||||||
mgmt.GET("/antigravity-auth-url", s.mgmt.RequestAntigravityToken)
|
mgmt.GET("/antigravity-auth-url", s.mgmt.RequestAntigravityToken)
|
||||||
mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken)
|
mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken)
|
||||||
|
mgmt.GET("/kilo-auth-url", s.mgmt.RequestKiloToken)
|
||||||
mgmt.GET("/kimi-auth-url", s.mgmt.RequestKimiToken)
|
mgmt.GET("/kimi-auth-url", s.mgmt.RequestKimiToken)
|
||||||
mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken)
|
mgmt.GET("/iflow-auth-url", s.mgmt.RequestIFlowToken)
|
||||||
mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken)
|
mgmt.POST("/iflow-auth-url", s.mgmt.RequestIFlowCookieToken)
|
||||||
|
|||||||
162
internal/auth/kilo/kilo_auth.go
Normal file
162
internal/auth/kilo/kilo_auth.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
// Package kilo provides authentication and token management functionality
|
||||||
|
// for Kilo AI services.
|
||||||
|
package kilo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BaseURL is the base URL for the Kilo AI API.
|
||||||
|
BaseURL = "https://api.kilo.ai/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeviceAuthResponse represents the response from initiating device flow.
|
||||||
|
type DeviceAuthResponse struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
VerificationURL string `json:"verificationUrl"`
|
||||||
|
ExpiresIn int `json:"expiresIn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceStatusResponse represents the response when polling for device flow status.
|
||||||
|
type DeviceStatusResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
UserEmail string `json:"userEmail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile represents the user profile from Kilo AI.
|
||||||
|
type Profile struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Orgs []Organization `json:"organizations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization represents a Kilo AI organization.
|
||||||
|
type Organization struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults represents default settings for an organization or user.
|
||||||
|
type Defaults struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KiloAuth provides methods for handling the Kilo AI authentication flow.
|
||||||
|
type KiloAuth struct {
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKiloAuth creates a new instance of KiloAuth.
|
||||||
|
func NewKiloAuth() *KiloAuth {
|
||||||
|
return &KiloAuth{
|
||||||
|
client: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitiateDeviceFlow starts the device authentication flow.
|
||||||
|
func (k *KiloAuth) InitiateDeviceFlow(ctx context.Context) (*DeviceAuthResponse, error) {
|
||||||
|
resp, err := k.client.Post(BaseURL+"/device-auth/codes", "application/json", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to initiate device flow: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data DeviceAuthResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PollForToken polls for the device flow completion.
|
||||||
|
func (k *KiloAuth) PollForToken(ctx context.Context, code string) (*DeviceStatusResponse, error) {
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-ticker.C:
|
||||||
|
resp, err := k.client.Get(BaseURL + "/device-auth/codes/" + code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var data DeviceStatusResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch data.Status {
|
||||||
|
case "approved":
|
||||||
|
return &data, nil
|
||||||
|
case "denied", "expired":
|
||||||
|
return nil, fmt.Errorf("device flow %s", data.Status)
|
||||||
|
case "pending":
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown status: %s", data.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProfile fetches the user's profile.
|
||||||
|
func (k *KiloAuth) GetProfile(ctx context.Context, token string) (*Profile, error) {
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, "GET", BaseURL+"/profile", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
|
resp, err := k.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to get profile: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var profile Profile
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults fetches default settings for an organization.
|
||||||
|
func (k *KiloAuth) GetDefaults(ctx context.Context, token, orgID string) (*Defaults, error) {
|
||||||
|
url := BaseURL + "/defaults"
|
||||||
|
if orgID != "" {
|
||||||
|
url = BaseURL + "/organizations/" + orgID + "/defaults"
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
|
resp, err := k.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to get defaults: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaults Defaults
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&defaults); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &defaults, nil
|
||||||
|
}
|
||||||
60
internal/auth/kilo/kilo_token.go
Normal file
60
internal/auth/kilo/kilo_token.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Package kilo provides authentication and token management functionality
|
||||||
|
// for Kilo AI services.
|
||||||
|
package kilo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KiloTokenStorage stores token information for Kilo AI authentication.
|
||||||
|
type KiloTokenStorage struct {
|
||||||
|
// Token is the Kilo access token.
|
||||||
|
Token string `json:"kilocodeToken"`
|
||||||
|
|
||||||
|
// OrganizationID is the Kilo organization ID.
|
||||||
|
OrganizationID string `json:"kilocodeOrganizationId"`
|
||||||
|
|
||||||
|
// Model is the default model to use.
|
||||||
|
Model string `json:"kilocodeModel"`
|
||||||
|
|
||||||
|
// Email is the email address of the authenticated user.
|
||||||
|
Email string `json:"email"`
|
||||||
|
|
||||||
|
// Type indicates the authentication provider type, always "kilo" for this storage.
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveTokenToFile serializes the Kilo token storage to a JSON file.
|
||||||
|
func (ts *KiloTokenStorage) SaveTokenToFile(authFilePath string) error {
|
||||||
|
misc.LogSavingCredentials(authFilePath)
|
||||||
|
ts.Type = "kilo"
|
||||||
|
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() {
|
||||||
|
if errClose := f.Close(); errClose != nil {
|
||||||
|
log.Errorf("failed to close file: %v", errClose)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = json.NewEncoder(f).Encode(ts); err != nil {
|
||||||
|
return fmt.Errorf("failed to write token to file: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialFileName returns the filename used to persist Kilo credentials.
|
||||||
|
func CredentialFileName(email string) string {
|
||||||
|
return fmt.Sprintf("kilo-%s.json", email)
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ func newAuthManager() *sdkAuth.Manager {
|
|||||||
sdkAuth.NewKimiAuthenticator(),
|
sdkAuth.NewKimiAuthenticator(),
|
||||||
sdkAuth.NewKiroAuthenticator(),
|
sdkAuth.NewKiroAuthenticator(),
|
||||||
sdkAuth.NewGitHubCopilotAuthenticator(),
|
sdkAuth.NewGitHubCopilotAuthenticator(),
|
||||||
|
sdkAuth.NewKiloAuthenticator(),
|
||||||
)
|
)
|
||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
|
|||||||
54
internal/cmd/kilo_login.go
Normal file
54
internal/cmd/kilo_login.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DoKiloLogin handles the Kilo device flow using the shared authentication manager.
|
||||||
|
// It initiates the device-based authentication process for Kilo AI services and saves
|
||||||
|
// the authentication tokens to the configured auth directory.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - cfg: The application configuration
|
||||||
|
// - options: Login options including browser behavior and prompts
|
||||||
|
func DoKiloLogin(cfg *config.Config, options *LoginOptions) {
|
||||||
|
if options == nil {
|
||||||
|
options = &LoginOptions{}
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := newAuthManager()
|
||||||
|
|
||||||
|
promptFn := options.Prompt
|
||||||
|
if promptFn == nil {
|
||||||
|
promptFn = func(prompt string) (string, error) {
|
||||||
|
fmt.Print(prompt)
|
||||||
|
var value string
|
||||||
|
fmt.Scanln(&value)
|
||||||
|
return strings.TrimSpace(value), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authOpts := &sdkAuth.LoginOptions{
|
||||||
|
NoBrowser: options.NoBrowser,
|
||||||
|
CallbackPort: options.CallbackPort,
|
||||||
|
Metadata: map[string]string{},
|
||||||
|
Prompt: promptFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, savedPath, err := manager.Login(context.Background(), "kilo", cfg, authOpts)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Kilo authentication failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if savedPath != "" {
|
||||||
|
fmt.Printf("Authentication saved to %s\n", savedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Kilo authentication successful!")
|
||||||
|
}
|
||||||
@@ -27,4 +27,7 @@ const (
|
|||||||
|
|
||||||
// Kiro represents the AWS CodeWhisperer (Kiro) provider identifier.
|
// Kiro represents the AWS CodeWhisperer (Kiro) provider identifier.
|
||||||
Kiro = "kiro"
|
Kiro = "kiro"
|
||||||
|
|
||||||
|
// Kilo represents the Kilo AI provider identifier.
|
||||||
|
Kilo = "kilo"
|
||||||
)
|
)
|
||||||
|
|||||||
21
internal/registry/kilo_models.go
Normal file
21
internal/registry/kilo_models.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Package registry provides model definitions for various AI service providers.
|
||||||
|
package registry
|
||||||
|
|
||||||
|
// GetKiloModels returns the Kilo model definitions
|
||||||
|
func GetKiloModels() []*ModelInfo {
|
||||||
|
return []*ModelInfo{
|
||||||
|
// --- Base Models ---
|
||||||
|
{
|
||||||
|
ID: "kilo-auto",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1732752000,
|
||||||
|
OwnedBy: "kilo",
|
||||||
|
Type: "kilo",
|
||||||
|
DisplayName: "Kilo Auto",
|
||||||
|
Description: "Automatic model selection by Kilo",
|
||||||
|
ContextLength: 200000,
|
||||||
|
MaxCompletionTokens: 64000,
|
||||||
|
Thinking: &ThinkingSupport{Min: 1024, Max: 32000, ZeroAllowed: true, DynamicAllowed: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
// - qwen
|
// - qwen
|
||||||
// - iflow
|
// - iflow
|
||||||
// - kiro
|
// - kiro
|
||||||
|
// - kilo
|
||||||
// - github-copilot
|
// - github-copilot
|
||||||
// - kiro
|
// - kiro
|
||||||
// - amazonq
|
// - amazonq
|
||||||
@@ -47,6 +48,8 @@ func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo {
|
|||||||
return GetGitHubCopilotModels()
|
return GetGitHubCopilotModels()
|
||||||
case "kiro":
|
case "kiro":
|
||||||
return GetKiroModels()
|
return GetKiroModels()
|
||||||
|
case "kilo":
|
||||||
|
return GetKiloModels()
|
||||||
case "amazonq":
|
case "amazonq":
|
||||||
return GetAmazonQModels()
|
return GetAmazonQModels()
|
||||||
case "antigravity":
|
case "antigravity":
|
||||||
@@ -95,6 +98,7 @@ func LookupStaticModelInfo(modelID string) *ModelInfo {
|
|||||||
GetIFlowModels(),
|
GetIFlowModels(),
|
||||||
GetGitHubCopilotModels(),
|
GetGitHubCopilotModels(),
|
||||||
GetKiroModels(),
|
GetKiroModels(),
|
||||||
|
GetKiloModels(),
|
||||||
GetAmazonQModels(),
|
GetAmazonQModels(),
|
||||||
}
|
}
|
||||||
for _, models := range allModels {
|
for _, models := range allModels {
|
||||||
|
|||||||
204
internal/runtime/executor/kilo_executor.go
Normal file
204
internal/runtime/executor/kilo_executor.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KiloExecutor handles requests to Kilo API.
|
||||||
|
type KiloExecutor struct {
|
||||||
|
cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKiloExecutor creates a new Kilo executor instance.
|
||||||
|
func NewKiloExecutor(cfg *config.Config) *KiloExecutor {
|
||||||
|
return &KiloExecutor{cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifier returns the unique identifier for this executor.
|
||||||
|
func (e *KiloExecutor) Identifier() string { return "kilo" }
|
||||||
|
|
||||||
|
// PrepareRequest prepares the HTTP request before execution.
|
||||||
|
func (e *KiloExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error {
|
||||||
|
if req == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
accessToken, _ := kiloCredentials(auth)
|
||||||
|
if strings.TrimSpace(accessToken) == "" {
|
||||||
|
return fmt.Errorf("kilo: missing access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(req, attrs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HttpRequest executes a raw HTTP request.
|
||||||
|
func (e *KiloExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) {
|
||||||
|
if req == nil {
|
||||||
|
return nil, fmt.Errorf("kilo executor: request is nil")
|
||||||
|
}
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = req.Context()
|
||||||
|
}
|
||||||
|
httpReq := req.WithContext(ctx)
|
||||||
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
|
return httpClient.Do(httpReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute performs a non-streaming request.
|
||||||
|
func (e *KiloExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
|
||||||
|
return cliproxyexecutor.Response{}, fmt.Errorf("kilo: execution not fully implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteStream performs a streaming request.
|
||||||
|
func (e *KiloExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) {
|
||||||
|
return nil, fmt.Errorf("kilo: streaming execution not fully implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh validates the Kilo token.
|
||||||
|
func (e *KiloExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
||||||
|
if auth == nil {
|
||||||
|
return nil, fmt.Errorf("missing auth")
|
||||||
|
}
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountTokens returns the token count for the given request.
|
||||||
|
func (e *KiloExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
|
||||||
|
return cliproxyexecutor.Response{}, fmt.Errorf("kilo: count tokens not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// kiloCredentials extracts access token and other info from auth.
|
||||||
|
func kiloCredentials(auth *cliproxyauth.Auth) (accessToken, orgID string) {
|
||||||
|
if auth == nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
if auth.Metadata != nil {
|
||||||
|
if token, ok := auth.Metadata["access_token"].(string); ok {
|
||||||
|
accessToken = token
|
||||||
|
}
|
||||||
|
if org, ok := auth.Metadata["organization_id"].(string); ok {
|
||||||
|
orgID = org
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if accessToken == "" && auth.Attributes != nil {
|
||||||
|
accessToken = auth.Attributes["access_token"]
|
||||||
|
orgID = auth.Attributes["organization_id"]
|
||||||
|
}
|
||||||
|
return accessToken, orgID
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchKiloModels fetches models from Kilo API.
|
||||||
|
func FetchKiloModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *config.Config) []*registry.ModelInfo {
|
||||||
|
accessToken, orgID := kiloCredentials(auth)
|
||||||
|
if accessToken == "" {
|
||||||
|
log.Infof("kilo: no access token found, skipping dynamic model fetch (using static kilo-auto)")
|
||||||
|
return registry.GetKiloModels()
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := newProxyAwareHTTPClient(ctx, cfg, auth, 0)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.kilo.ai/api/openrouter/models", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("kilo: failed to create model fetch request: %v", err)
|
||||||
|
return registry.GetKiloModels()
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
if orgID != "" {
|
||||||
|
req.Header.Set("X-Kilocode-OrganizationID", orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
log.Warnf("kilo: fetch models canceled: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Warnf("kilo: using static models (API fetch failed: %v)", err)
|
||||||
|
}
|
||||||
|
return registry.GetKiloModels()
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("kilo: failed to read models response: %v", err)
|
||||||
|
return registry.GetKiloModels()
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Warnf("kilo: fetch models failed: status %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
return registry.GetKiloModels()
|
||||||
|
}
|
||||||
|
|
||||||
|
result := gjson.GetBytes(body, "data")
|
||||||
|
if !result.Exists() {
|
||||||
|
// Try root if data field is missing
|
||||||
|
result = gjson.ParseBytes(body)
|
||||||
|
if !result.IsArray() {
|
||||||
|
log.Debugf("kilo: response body: %s", string(body))
|
||||||
|
log.Warn("kilo: invalid API response format (expected array or data field with array)")
|
||||||
|
return registry.GetKiloModels()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dynamicModels []*registry.ModelInfo
|
||||||
|
now := time.Now().Unix()
|
||||||
|
count := 0
|
||||||
|
totalCount := 0
|
||||||
|
|
||||||
|
result.ForEach(func(key, value gjson.Result) bool {
|
||||||
|
totalCount++
|
||||||
|
pIdxResult := value.Get("preferredIndex")
|
||||||
|
preferredIndex := pIdxResult.Int()
|
||||||
|
|
||||||
|
// Filter models where preferredIndex > 0 (Kilo-curated models)
|
||||||
|
if preferredIndex <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicModels = append(dynamicModels, ®istry.ModelInfo{
|
||||||
|
ID: value.Get("id").String(),
|
||||||
|
DisplayName: value.Get("name").String(),
|
||||||
|
ContextLength: int(value.Get("context_length").Int()),
|
||||||
|
OwnedBy: "kilo",
|
||||||
|
Type: "kilo",
|
||||||
|
Object: "model",
|
||||||
|
Created: now,
|
||||||
|
})
|
||||||
|
count++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Infof("kilo: fetched %d models from API, %d curated (preferredIndex > 0)", totalCount, count)
|
||||||
|
if count == 0 && totalCount > 0 {
|
||||||
|
log.Warn("kilo: no curated models found (all preferredIndex <= 0). Check API response.")
|
||||||
|
}
|
||||||
|
|
||||||
|
staticModels := registry.GetKiloModels()
|
||||||
|
// Always include kilo-auto (first static model)
|
||||||
|
allModels := append(staticModels[:1], dynamicModels...)
|
||||||
|
|
||||||
|
return allModels
|
||||||
|
}
|
||||||
|
|
||||||
121
sdk/auth/kilo.go
Normal file
121
sdk/auth/kilo.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kilo"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KiloAuthenticator implements the login flow for Kilo AI accounts.
|
||||||
|
type KiloAuthenticator struct{}
|
||||||
|
|
||||||
|
// NewKiloAuthenticator constructs a Kilo authenticator.
|
||||||
|
func NewKiloAuthenticator() *KiloAuthenticator {
|
||||||
|
return &KiloAuthenticator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *KiloAuthenticator) Provider() string {
|
||||||
|
return "kilo"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *KiloAuthenticator) RefreshLead() *time.Duration {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login manages the device flow authentication for Kilo AI.
|
||||||
|
func (a *KiloAuthenticator) 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 ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
if opts == nil {
|
||||||
|
opts = &LoginOptions{}
|
||||||
|
}
|
||||||
|
|
||||||
|
kilocodeAuth := kilo.NewKiloAuth()
|
||||||
|
|
||||||
|
fmt.Println("Initiating Kilo device authentication...")
|
||||||
|
resp, err := kilocodeAuth.InitiateDeviceFlow(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initiate device flow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Please visit: %s\n", resp.VerificationURL)
|
||||||
|
fmt.Printf("And enter code: %s\n", resp.Code)
|
||||||
|
|
||||||
|
fmt.Println("Waiting for authorization...")
|
||||||
|
status, err := kilocodeAuth.PollForToken(ctx, resp.Code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("authentication failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Authentication successful for %s\n", status.UserEmail)
|
||||||
|
|
||||||
|
profile, err := kilocodeAuth.GetProfile(ctx, status.Token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgID string
|
||||||
|
if len(profile.Orgs) > 1 {
|
||||||
|
fmt.Println("Multiple organizations found. Please select one:")
|
||||||
|
for i, org := range profile.Orgs {
|
||||||
|
fmt.Printf("[%d] %s (%s)\n", i+1, org.Name, org.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Prompt != nil {
|
||||||
|
input, err := opts.Prompt("Enter the number of the organization: ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var choice int
|
||||||
|
fmt.Sscanf(input, "%d", &choice)
|
||||||
|
if choice > 0 && choice <= len(profile.Orgs) {
|
||||||
|
orgID = profile.Orgs[choice-1].ID
|
||||||
|
} else {
|
||||||
|
orgID = profile.Orgs[0].ID
|
||||||
|
fmt.Printf("Invalid choice, defaulting to %s\n", profile.Orgs[0].Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
orgID = profile.Orgs[0].ID
|
||||||
|
fmt.Printf("Non-interactive mode, defaulting to organization: %s\n", profile.Orgs[0].Name)
|
||||||
|
}
|
||||||
|
} else if len(profile.Orgs) == 1 {
|
||||||
|
orgID = profile.Orgs[0].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults, err := kilocodeAuth.GetDefaults(ctx, status.Token, orgID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: failed to fetch defaults: %v\n", err)
|
||||||
|
defaults = &kilo.Defaults{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := &kilo.KiloTokenStorage{
|
||||||
|
Token: status.Token,
|
||||||
|
OrganizationID: orgID,
|
||||||
|
Model: defaults.Model,
|
||||||
|
Email: status.UserEmail,
|
||||||
|
Type: "kilo",
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := kilo.CredentialFileName(status.UserEmail)
|
||||||
|
metadata := map[string]any{
|
||||||
|
"email": status.UserEmail,
|
||||||
|
"organization_id": orgID,
|
||||||
|
"model": defaults.Model,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
|
Provider: a.Provider(),
|
||||||
|
FileName: fileName,
|
||||||
|
Storage: ts,
|
||||||
|
Metadata: metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -413,6 +413,8 @@ func (s *Service) ensureExecutorsForAuth(a *coreauth.Auth) {
|
|||||||
s.coreManager.RegisterExecutor(executor.NewKimiExecutor(s.cfg))
|
s.coreManager.RegisterExecutor(executor.NewKimiExecutor(s.cfg))
|
||||||
case "kiro":
|
case "kiro":
|
||||||
s.coreManager.RegisterExecutor(executor.NewKiroExecutor(s.cfg))
|
s.coreManager.RegisterExecutor(executor.NewKiroExecutor(s.cfg))
|
||||||
|
case "kilo":
|
||||||
|
s.coreManager.RegisterExecutor(executor.NewKiloExecutor(s.cfg))
|
||||||
case "github-copilot":
|
case "github-copilot":
|
||||||
s.coreManager.RegisterExecutor(executor.NewGitHubCopilotExecutor(s.cfg))
|
s.coreManager.RegisterExecutor(executor.NewGitHubCopilotExecutor(s.cfg))
|
||||||
default:
|
default:
|
||||||
@@ -844,6 +846,9 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
|
|||||||
case "kiro":
|
case "kiro":
|
||||||
models = s.fetchKiroModels(a)
|
models = s.fetchKiroModels(a)
|
||||||
models = applyExcludedModels(models, excluded)
|
models = applyExcludedModels(models, excluded)
|
||||||
|
case "kilo":
|
||||||
|
models = executor.FetchKiloModels(context.Background(), a, s.cfg)
|
||||||
|
models = applyExcludedModels(models, excluded)
|
||||||
default:
|
default:
|
||||||
// Handle OpenAI-compatibility providers by name using config
|
// Handle OpenAI-compatibility providers by name using config
|
||||||
if s.cfg != nil {
|
if s.cfg != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user