Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99c9f3069c | ||
|
|
f9f2333997 | ||
|
|
179b8aa88f | ||
|
|
040d66f0bb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ docs/*
|
|||||||
logs/*
|
logs/*
|
||||||
auths/*
|
auths/*
|
||||||
!auths/.gitkeep
|
!auths/.gitkeep
|
||||||
|
AGENTS.md
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/luispater/CLIProxyAPI/internal/auth/codex"
|
"github.com/luispater/CLIProxyAPI/internal/auth/codex"
|
||||||
"github.com/luispater/CLIProxyAPI/internal/browser"
|
"github.com/luispater/CLIProxyAPI/internal/browser"
|
||||||
"github.com/luispater/CLIProxyAPI/internal/config"
|
"github.com/luispater/CLIProxyAPI/internal/config"
|
||||||
|
"github.com/luispater/CLIProxyAPI/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
@@ -250,11 +251,13 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
|
|||||||
// Check if browser is available
|
// Check if browser is available
|
||||||
if !browser.IsAvailable() {
|
if !browser.IsAvailable() {
|
||||||
log.Warn("No browser available on this system")
|
log.Warn("No browser available on this system")
|
||||||
|
util.PrintSSHTunnelInstructions(8085)
|
||||||
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
||||||
} else {
|
} else {
|
||||||
if err := browser.OpenURL(authURL); err != nil {
|
if err := browser.OpenURL(authURL); err != nil {
|
||||||
authErr := codex.NewAuthenticationError(codex.ErrBrowserOpenFailed, err)
|
authErr := codex.NewAuthenticationError(codex.ErrBrowserOpenFailed, err)
|
||||||
log.Warn(codex.GetUserFriendlyMessage(authErr))
|
log.Warn(codex.GetUserFriendlyMessage(authErr))
|
||||||
|
util.PrintSSHTunnelInstructions(8085)
|
||||||
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
||||||
|
|
||||||
// Log platform info for debugging
|
// Log platform info for debugging
|
||||||
@@ -265,6 +268,7 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
util.PrintSSHTunnelInstructions(8085)
|
||||||
log.Infof("Please open this URL in your browser:\n\n%s\n", authURL)
|
log.Infof("Please open this URL in your browser:\n\n%s\n", authURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ func (c *GeminiCLIClient) SendRawTokenCount(ctx context.Context, modelName strin
|
|||||||
if newModelName != "" {
|
if newModelName != "" {
|
||||||
log.Debugf("Model %s is quota exceeded. Switch to preview model %s", modelName, newModelName)
|
log.Debugf("Model %s is quota exceeded. Switch to preview model %s", modelName, newModelName)
|
||||||
rawJSON, _ = sjson.SetBytes(rawJSON, "model", newModelName)
|
rawJSON, _ = sjson.SetBytes(rawJSON, "model", newModelName)
|
||||||
|
modelName = newModelName
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ type OpenAICompatibilityClient struct {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - *OpenAICompatibilityClient: A new OpenAI compatibility client instance.
|
// - *OpenAICompatibilityClient: A new OpenAI compatibility client instance.
|
||||||
// - error: An error if the client creation fails.
|
// - error: An error if the client creation fails.
|
||||||
func NewOpenAICompatibilityClient(cfg *config.Config, compatConfig *config.OpenAICompatibility) (*OpenAICompatibilityClient, error) {
|
func NewOpenAICompatibilityClient(cfg *config.Config, compatConfig *config.OpenAICompatibility, apiKeyIndex int) (*OpenAICompatibilityClient, error) {
|
||||||
if compatConfig == nil {
|
if compatConfig == nil {
|
||||||
return nil, fmt.Errorf("compatibility configuration is required")
|
return nil, fmt.Errorf("compatibility configuration is required")
|
||||||
}
|
}
|
||||||
@@ -53,10 +53,14 @@ func NewOpenAICompatibilityClient(cfg *config.Config, compatConfig *config.OpenA
|
|||||||
return nil, fmt.Errorf("at least one API key is required for OpenAI compatibility provider: %s", compatConfig.Name)
|
return nil, fmt.Errorf("at least one API key is required for OpenAI compatibility provider: %s", compatConfig.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(compatConfig.APIKeys) <= apiKeyIndex {
|
||||||
|
return nil, fmt.Errorf("invalid API key index for OpenAI compatibility provider: %s", compatConfig.Name)
|
||||||
|
}
|
||||||
|
|
||||||
httpClient := util.SetProxy(cfg, &http.Client{})
|
httpClient := util.SetProxy(cfg, &http.Client{})
|
||||||
|
|
||||||
// Generate unique client ID
|
// Generate unique client ID
|
||||||
clientID := fmt.Sprintf("openai-compatibility-%s-%d", compatConfig.Name, time.Now().UnixNano())
|
clientID := fmt.Sprintf("openai-compatibility-%s-%d-%d", compatConfig.Name, apiKeyIndex, time.Now().UnixNano())
|
||||||
|
|
||||||
client := &OpenAICompatibilityClient{
|
client := &OpenAICompatibilityClient{
|
||||||
ClientBase: ClientBase{
|
ClientBase: ClientBase{
|
||||||
@@ -66,7 +70,7 @@ func NewOpenAICompatibilityClient(cfg *config.Config, compatConfig *config.OpenA
|
|||||||
modelQuotaExceeded: make(map[string]*time.Time),
|
modelQuotaExceeded: make(map[string]*time.Time),
|
||||||
},
|
},
|
||||||
compatConfig: compatConfig,
|
compatConfig: compatConfig,
|
||||||
currentAPIKeyIndex: 0,
|
currentAPIKeyIndex: apiKeyIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize model registry
|
// Initialize model registry
|
||||||
@@ -134,8 +138,6 @@ func (c *OpenAICompatibilityClient) GetCurrentAPIKey() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
key := c.compatConfig.APIKeys[c.currentAPIKeyIndex]
|
key := c.compatConfig.APIKeys[c.currentAPIKeyIndex]
|
||||||
// Rotate to next key for load balancing
|
|
||||||
c.currentAPIKeyIndex = (c.currentAPIKeyIndex + 1) % len(c.compatConfig.APIKeys)
|
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/luispater/CLIProxyAPI/internal/browser"
|
"github.com/luispater/CLIProxyAPI/internal/browser"
|
||||||
"github.com/luispater/CLIProxyAPI/internal/client"
|
"github.com/luispater/CLIProxyAPI/internal/client"
|
||||||
"github.com/luispater/CLIProxyAPI/internal/config"
|
"github.com/luispater/CLIProxyAPI/internal/config"
|
||||||
|
"github.com/luispater/CLIProxyAPI/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,11 +87,13 @@ func DoClaudeLogin(cfg *config.Config, options *LoginOptions) {
|
|||||||
// Check if browser is available
|
// Check if browser is available
|
||||||
if !browser.IsAvailable() {
|
if !browser.IsAvailable() {
|
||||||
log.Warn("No browser available on this system")
|
log.Warn("No browser available on this system")
|
||||||
|
util.PrintSSHTunnelInstructions(54545)
|
||||||
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
||||||
} else {
|
} else {
|
||||||
if err = browser.OpenURL(authURL); err != nil {
|
if err = browser.OpenURL(authURL); err != nil {
|
||||||
authErr := claude.NewAuthenticationError(claude.ErrBrowserOpenFailed, err)
|
authErr := claude.NewAuthenticationError(claude.ErrBrowserOpenFailed, err)
|
||||||
log.Warn(claude.GetUserFriendlyMessage(authErr))
|
log.Warn(claude.GetUserFriendlyMessage(authErr))
|
||||||
|
util.PrintSSHTunnelInstructions(54545)
|
||||||
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
||||||
|
|
||||||
// Log platform info for debugging
|
// Log platform info for debugging
|
||||||
@@ -101,6 +104,7 @@ func DoClaudeLogin(cfg *config.Config, options *LoginOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
util.PrintSSHTunnelInstructions(54545)
|
||||||
log.Infof("Please open this URL in your browser:\n\n%s\n", authURL)
|
log.Infof("Please open this URL in your browser:\n\n%s\n", authURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/luispater/CLIProxyAPI/internal/browser"
|
"github.com/luispater/CLIProxyAPI/internal/browser"
|
||||||
"github.com/luispater/CLIProxyAPI/internal/client"
|
"github.com/luispater/CLIProxyAPI/internal/client"
|
||||||
"github.com/luispater/CLIProxyAPI/internal/config"
|
"github.com/luispater/CLIProxyAPI/internal/config"
|
||||||
|
"github.com/luispater/CLIProxyAPI/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -94,11 +95,13 @@ func DoCodexLogin(cfg *config.Config, options *LoginOptions) {
|
|||||||
// Check if browser is available
|
// Check if browser is available
|
||||||
if !browser.IsAvailable() {
|
if !browser.IsAvailable() {
|
||||||
log.Warn("No browser available on this system")
|
log.Warn("No browser available on this system")
|
||||||
|
util.PrintSSHTunnelInstructions(1455)
|
||||||
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
||||||
} else {
|
} else {
|
||||||
if err = browser.OpenURL(authURL); err != nil {
|
if err = browser.OpenURL(authURL); err != nil {
|
||||||
authErr := codex.NewAuthenticationError(codex.ErrBrowserOpenFailed, err)
|
authErr := codex.NewAuthenticationError(codex.ErrBrowserOpenFailed, err)
|
||||||
log.Warn(codex.GetUserFriendlyMessage(authErr))
|
log.Warn(codex.GetUserFriendlyMessage(authErr))
|
||||||
|
util.PrintSSHTunnelInstructions(1455)
|
||||||
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
|
||||||
|
|
||||||
// Log platform info for debugging
|
// Log platform info for debugging
|
||||||
@@ -109,6 +112,7 @@ func DoCodexLogin(cfg *config.Config, options *LoginOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
util.PrintSSHTunnelInstructions(1455)
|
||||||
log.Infof("Please open this URL in your browser:\n\n%s\n", authURL)
|
log.Infof("Please open this URL in your browser:\n\n%s\n", authURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -335,14 +335,16 @@ func buildAPIKeyClients(cfg *config.Config) (map[string]interfaces.Client, int,
|
|||||||
|
|
||||||
if len(cfg.OpenAICompatibility) > 0 {
|
if len(cfg.OpenAICompatibility) > 0 {
|
||||||
for _, compatConfig := range cfg.OpenAICompatibility {
|
for _, compatConfig := range cfg.OpenAICompatibility {
|
||||||
log.Debugf("Initializing OpenAI compatibility client for provider: %s", compatConfig.Name)
|
for i := 0; i < len(compatConfig.APIKeys); i++ {
|
||||||
compatClient, errClient := client.NewOpenAICompatibilityClient(cfg, &compatConfig)
|
log.Debugf("Initializing OpenAI compatibility client for provider: %s", compatConfig.Name)
|
||||||
if errClient != nil {
|
compatClient, errClient := client.NewOpenAICompatibilityClient(cfg, &compatConfig, i)
|
||||||
log.Errorf("failed to create OpenAI compatibility client for %s: %v", compatConfig.Name, errClient)
|
if errClient != nil {
|
||||||
continue
|
log.Errorf("failed to create OpenAI compatibility client for %s: %v", compatConfig.Name, errClient)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apiKeyClients[compatClient.GetClientID()] = compatClient
|
||||||
|
openAICompatCount++
|
||||||
}
|
}
|
||||||
apiKeyClients[compatClient.GetClientID()] = compatClient
|
|
||||||
openAICompatCount++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
135
internal/util/ssh_helper.go
Normal file
135
internal/util/ssh_helper.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// Package util provides helper functions for SSH tunnel instructions and network-related tasks.
|
||||||
|
// This includes detecting the appropriate IP address and printing commands
|
||||||
|
// to help users connect to the local server from a remote machine.
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ipServices = []string{
|
||||||
|
"https://api.ipify.org",
|
||||||
|
"https://ifconfig.me/ip",
|
||||||
|
"https://icanhazip.com",
|
||||||
|
"https://ipinfo.io/ip",
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPublicIP attempts to retrieve the public IP address from a list of external services.
|
||||||
|
// It iterates through the ipServices and returns the first successful response.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The public IP address as a string
|
||||||
|
// - error: An error if all services fail, nil otherwise
|
||||||
|
func getPublicIP() (string, error) {
|
||||||
|
for _, service := range ipServices {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", service, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Failed to create request to %s: %v", service, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Failed to get public IP from %s: %v", service, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if closeErr := resp.Body.Close(); closeErr != nil {
|
||||||
|
log.Warnf("Failed to close response body from %s: %v", service, closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Debugf("bad status code from %s: %d", service, resp.StatusCode)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Failed to read response body from %s: %v", service, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(ip)), nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("all IP services failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOutboundIP retrieves the preferred outbound IP address of this machine.
|
||||||
|
// It uses a UDP connection to a public DNS server to determine the local IP
|
||||||
|
// address that would be used for outbound traffic.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The outbound IP address as a string
|
||||||
|
// - error: An error if the IP address cannot be determined, nil otherwise
|
||||||
|
func getOutboundIP() (string, error) {
|
||||||
|
conn, err := net.Dial("udp", "8.8.8.8:80")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if closeErr := conn.Close(); closeErr != nil {
|
||||||
|
log.Warnf("Failed to close UDP connection: %v", closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
localAddr, ok := conn.LocalAddr().(*net.UDPAddr)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("could not assert UDP address type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return localAddr.IP.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIPAddress attempts to find the best-available IP address.
|
||||||
|
// It first tries to get the public IP address, and if that fails,
|
||||||
|
// it falls back to getting the local outbound IP address.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The determined IP address (preferring public IPv4)
|
||||||
|
func GetIPAddress() string {
|
||||||
|
publicIP, err := getPublicIP()
|
||||||
|
if err == nil {
|
||||||
|
log.Debugf("Public IP detected: %s", publicIP)
|
||||||
|
return publicIP
|
||||||
|
}
|
||||||
|
log.Warnf("Failed to get public IP, falling back to outbound IP: %v", err)
|
||||||
|
outboundIP, err := getOutboundIP()
|
||||||
|
if err == nil {
|
||||||
|
log.Debugf("Outbound IP detected: %s", outboundIP)
|
||||||
|
return outboundIP
|
||||||
|
}
|
||||||
|
log.Errorf("Failed to get any IP address: %v", err)
|
||||||
|
return "127.0.0.1" // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintSSHTunnelInstructions detects the IP address and prints SSH tunnel instructions
|
||||||
|
// for the user to connect to the local OAuth callback server from a remote machine.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - port: The local port number for the SSH tunnel
|
||||||
|
func PrintSSHTunnelInstructions(port int) {
|
||||||
|
ipAddress := GetIPAddress()
|
||||||
|
border := "================================================================================"
|
||||||
|
log.Infof("To authenticate from a remote machine, an SSH tunnel may be required.")
|
||||||
|
fmt.Println(border)
|
||||||
|
fmt.Println(" Run one of the following commands on your local machine (NOT the server):")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf(" # Standard SSH command (assumes SSH port 22):\n")
|
||||||
|
fmt.Printf(" ssh -L %d:127.0.0.1:%d root@%s -p 22\n", port, port, ipAddress)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf(" # If using an SSH key (assumes SSH port 22):\n")
|
||||||
|
fmt.Printf(" ssh -i <path_to_your_key> -L %d:127.0.0.1:%d root@%s -p 22\n", port, port, ipAddress)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(" NOTE: If your server's SSH port is not 22, please modify the '-p 22' part accordingly.")
|
||||||
|
fmt.Println(border)
|
||||||
|
}
|
||||||
@@ -549,13 +549,15 @@ func buildAPIKeyClients(cfg *config.Config) (map[string]interfaces.Client, int,
|
|||||||
}
|
}
|
||||||
if len(cfg.OpenAICompatibility) > 0 {
|
if len(cfg.OpenAICompatibility) > 0 {
|
||||||
for _, compatConfig := range cfg.OpenAICompatibility {
|
for _, compatConfig := range cfg.OpenAICompatibility {
|
||||||
compatClient, errClient := client.NewOpenAICompatibilityClient(cfg, &compatConfig)
|
for i := 0; i < len(compatConfig.APIKeys); i++ {
|
||||||
if errClient != nil {
|
compatClient, errClient := client.NewOpenAICompatibilityClient(cfg, &compatConfig, i)
|
||||||
log.Errorf("failed to create OpenAI-compatibility client for %s: %v", compatConfig.Name, errClient)
|
if errClient != nil {
|
||||||
continue
|
log.Errorf("failed to create OpenAI-compatibility client for %s: %v", compatConfig.Name, errClient)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apiKeyClients[compatClient.GetClientID()] = compatClient
|
||||||
|
openAICompatCount++
|
||||||
}
|
}
|
||||||
apiKeyClients[compatClient.GetClientID()] = compatClient
|
|
||||||
openAICompatCount++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return apiKeyClients, glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount
|
return apiKeyClients, glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, openAICompatCount
|
||||||
|
|||||||
Reference in New Issue
Block a user