Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ OmniRoute is an AI gateway for multi-provider LLMs: an OpenAI-compatible endpoin

A public CLIProxyAPI-compatible fork and bundled management panel. It keeps upstream-style usage while restoring built-in usage statistics, adding cache hit rate, first-byte latency, TPS tracking, and Docker-oriented self-hosted installation docs.

### [Codex Switch](https://github.com/9ycrooked/CodexSwitch)

This is a tool built with Tauri 2 + Vue 3 for managing multiple OpenAI Codex desktop accounts. Switch between saved ChatGPT/Codex certification profiles, check 5-hour and weekly quota usage in real time, verify token health, view active account details, and import or save auth.json files without manual copying.

> [!NOTE]
> If you have developed a port of CLIProxyAPI or a project inspired by it, please open a PR to add it to this list.

Expand Down
4 changes: 4 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ OmniRoute 是一个面向多供应商大语言模型的 AI 网关:它提供兼

一个公开的 CLIProxyAPI 兼容二开版本和配套管理面板,尽量保持与上游一致的使用方式,同时恢复内置使用量统计,并补充缓存命中率、首字响应时间、TPS 记录和面向 Docker 自托管的安装说明。

### [Codex Switch](https://github.com/9ycrooked/CodexSwitch)

这是一个使用 Tauri 2 + Vue 3 构建的工具,用于管理多个 OpenAI Codex 桌面账户。它可以在已保存的 ChatGPT/Codex 认证配置之间切换,实时查看 5 小时和每周配额使用情况,验证 token 健康状态,查看当前账户详情,并在无需手动复制的情况下导入或保存 auth.json 文件。

> [!NOTE]
> 如果你开发了 CLIProxyAPI 的移植或衍生项目,请提交 PR 将其添加到此列表中。

Expand Down
4 changes: 4 additions & 0 deletions README_JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ OmniRouteはマルチプロバイダーLLM向けのAIゲートウェイです:

上流に近い使い方を維持する公開CLIProxyAPI互換フォーク兼管理パネルです。内蔵の使用量統計を復元し、キャッシュヒット率、初回バイト待ち時間、TPSの記録、Docker向けのセルフホスト手順を追加しています。

### [Codex Switch](https://github.com/9ycrooked/CodexSwitch)

Tauri 2 + Vue 3で構築された、複数のOpenAI Codexデスクトップアカウントを管理するためのツールです。保存済みのChatGPT/Codex認証プロファイルを切り替え、5時間および週次クォータ使用量をリアルタイムで確認し、tokenの状態を検証し、現在のアカウント詳細を表示し、手動コピーなしでauth.jsonファイルをインポートまたは保存できます。

> [!NOTE]
> CLIProxyAPIの移植版またはそれに触発されたプロジェクトを開発した場合は、PRを送ってこのリストに追加してください。

Expand Down
77 changes: 0 additions & 77 deletions cmd/server/home_flag_test.go

This file was deleted.

180 changes: 1 addition & 179 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ import (
"fmt"
"io"
"io/fs"
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -53,120 +51,6 @@ func init() {
buildinfo.BuildDate = BuildDate
}

func parseHomeFlagConfig(rawAddr string, password string) (config.HomeConfig, error) {
rawAddr = strings.TrimSpace(rawAddr)
if rawAddr == "" {
return config.HomeConfig{}, fmt.Errorf("address is empty")
}

if strings.Contains(rawAddr, "://") {
return parseHomeURLConfig(rawAddr, password)
}

host, portStr, errSplit := net.SplitHostPort(rawAddr)
if errSplit != nil {
return config.HomeConfig{}, fmt.Errorf("expected host:port, redis://host:port, or rediss://host:port: %w", errSplit)
}

host = strings.TrimSpace(host)
if host == "" {
return config.HomeConfig{}, fmt.Errorf("host is empty")
}

port, errPort := parseHomePort(portStr)
if errPort != nil {
return config.HomeConfig{}, errPort
}

return config.HomeConfig{
Enabled: true,
Host: host,
Port: port,
Password: password,
}, nil
}

func parseHomeURLConfig(rawAddr string, password string) (config.HomeConfig, error) {
parsed, errParse := url.Parse(rawAddr)
if errParse != nil {
return config.HomeConfig{}, fmt.Errorf("parse URL: %w", errParse)
}

scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
if scheme != "redis" && scheme != "rediss" {
return config.HomeConfig{}, fmt.Errorf("unsupported URL scheme %q", parsed.Scheme)
}

host := strings.TrimSpace(parsed.Hostname())
if host == "" {
return config.HomeConfig{}, fmt.Errorf("host is empty")
}

port, errPort := parseHomePort(parsed.Port())
if errPort != nil {
return config.HomeConfig{}, errPort
}

if password == "" && parsed.User != nil {
if urlPassword, ok := parsed.User.Password(); ok {
password = urlPassword
}
}

homeCfg := config.HomeConfig{
Enabled: true,
Host: host,
Port: port,
Password: password,
}
query := parsed.Query()
homeCfg.DisableClusterDiscovery = parseHomeBoolQuery(query, "disable-cluster-discovery", "disable_cluster_discovery")

if scheme == "rediss" {
homeCfg.TLS.Enable = true
homeCfg.TLS.ServerName = strings.TrimSpace(firstHomeQueryValue(query, "server-name", "server_name"))
homeCfg.TLS.InsecureSkipVerify = parseHomeBoolQuery(query, "insecure-skip-verify", "insecure_skip_verify", "skip_verify")
homeCfg.TLS.CACert = strings.TrimSpace(firstHomeQueryValue(query, "ca-cert", "ca_cert"))
}

return homeCfg, nil
}

func parseHomePort(rawPort string) (int, error) {
rawPort = strings.TrimSpace(rawPort)
if rawPort == "" {
return 0, fmt.Errorf("port is empty")
}

port, errPort := strconv.Atoi(rawPort)
if errPort != nil || port <= 0 || port > 65535 {
return 0, fmt.Errorf("invalid port %q", rawPort)
}

return port, nil
}

func firstHomeQueryValue(values url.Values, keys ...string) string {
for _, key := range keys {
if value := values.Get(key); value != "" {
return value
}
}
return ""
}

func parseHomeBoolQuery(values url.Values, keys ...string) bool {
for _, key := range keys {
value := strings.TrimSpace(values.Get(key))
if value == "" {
continue
}
parsed, errParse := strconv.ParseBool(value)
return errParse == nil && parsed
}
return false
}

// main is the entry point of the application.
// It parses command-line flags, loads configuration, and starts the appropriate
// service based on the provided flags (login, codex-login, or server mode).
Expand All @@ -188,8 +72,6 @@ func main() {
var vertexImportPrefix string
var configPath string
var password string
var homeAddr string
var homePassword string
var homeJWT string
var homeDisableClusterDiscovery bool
var tuiMode bool
Expand All @@ -211,10 +93,8 @@ func main() {
flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file")
flag.StringVar(&vertexImportPrefix, "vertex-import-prefix", "", "Prefix for Vertex model namespacing (use with -vertex-import)")
flag.StringVar(&password, "password", "", "")
flag.StringVar(&homeAddr, "home", "", "Home control plane address in host:port, redis://host:port, or rediss://host:port format (loads config from home and skips local config file)")
flag.StringVar(&homePassword, "home-password", "", "Home control plane password (Redis AUTH)")
flag.StringVar(&homeJWT, "home-jwt", "", "Home control plane JWT for mTLS certificate bootstrap and connection")
flag.BoolVar(&homeDisableClusterDiscovery, "home-disable-cluster-discovery", false, "Disable Home CLUSTER NODES discovery and keep using the configured -home address")
flag.BoolVar(&homeDisableClusterDiscovery, "home-disable-cluster-discovery", false, "Disable Home CLUSTER NODES discovery and keep using the configured -home-jwt address")
flag.BoolVar(&tuiMode, "tui", false, "Start with terminal management UI")
flag.BoolVar(&standalone, "standalone", false, "In TUI mode, start an embedded local server")
flag.BoolVar(&localModel, "local-model", false, "Use embedded model catalog only, skip remote model fetching")
Expand Down Expand Up @@ -302,17 +182,6 @@ func main() {
}
writableBase := util.WritablePath()

// Allow env var fallback for home flags so they can be configured without command args.
if strings.TrimSpace(homeAddr) == "" {
if v, ok := lookupEnv("HOME_ADDR", "home_addr"); ok {
homeAddr = v
}
}
if strings.TrimSpace(homePassword) == "" {
if v, ok := lookupEnv("HOME_PASSWORD", "home_password"); ok {
homePassword = v
}
}
if strings.TrimSpace(homeJWT) == "" {
if v, ok := lookupEnv("HOME_JWT", "home_jwt"); ok {
homeJWT = v
Expand Down Expand Up @@ -426,53 +295,6 @@ func main() {
configFilePath = filepath.Join(wd, "config.yaml")
}

// Local stores are intentionally disabled when config is loaded from home.
usePostgresStore = false
useObjectStore = false
useGitStore = false
} else if strings.TrimSpace(homeAddr) != "" {
configLoadedFromHome = true
trimmedHomePassword := strings.TrimSpace(homePassword)
homeCfg, errHomeCfg := parseHomeFlagConfig(homeAddr, trimmedHomePassword)
if errHomeCfg != nil {
log.Errorf("invalid -home address %q: %v", homeAddr, errHomeCfg)
return
}
if homeDisableClusterDiscovery {
homeCfg.DisableClusterDiscovery = true
}
homeClient := home.New(homeCfg)
defer homeClient.Close()

ctxHome, cancelHome := context.WithTimeout(context.Background(), 30*time.Second)
raw, errGetConfig := homeClient.GetConfig(ctxHome)
cancelHome()
if errGetConfig != nil {
log.Errorf("failed to fetch config from home: %v", errGetConfig)
return
}

parsed, errParseConfig := config.ParseConfigBytes(raw)
if errParseConfig != nil {
log.Errorf("failed to parse config payload from home: %v", errParseConfig)
return
}
if parsed == nil {
parsed = &config.Config{}
}
parsed.Home = homeCfg
parsed.Port = 8317 // Default to 8317 for home mode, can be overridden by home config
parsed.UsageStatisticsEnabled = true
cfg = parsed

// Keep a non-empty config path for downstream components (log paths, management assets, etc),
// but do not require the file to exist when loading config from home.
if strings.TrimSpace(configPath) != "" {
configFilePath = configPath
} else {
configFilePath = filepath.Join(wd, "config.yaml")
}

// Local stores are intentionally disabled when config is loaded from home.
usePostgresStore = false
useObjectStore = false
Expand Down
24 changes: 2 additions & 22 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,6 @@ tls:
cert: ""
key: ""

# Optional "home" control plane integration over Redis protocol.
home:
enabled: false
host: "127.0.0.1"
port: 6379
password: ""
# Keep CPA pinned to the configured home address instead of switching to CLUSTER NODES entries.
# Useful when Home is behind NAT, Docker networking, or a reverse proxy.
disable-cluster-discovery: false
# Optional TLS for the outbound Redis connection to the home control plane.
# Enable this when connecting through rediss:// or an SSL stream proxy.
tls:
enable: false
# Optional SNI/certificate name override. Leave empty to use the configured home host.
server-name: ""
# Trust a private CA bundle in addition to system roots.
ca-cert: ""
# Only for testing self-signed endpoints; disables certificate verification.
insecure-skip-verify: false

# Management API settings
remote-management:
# Whether to allow remote (non-localhost) management access.
Expand Down Expand Up @@ -86,8 +66,8 @@ error-logs-max-files: 10
# When false, disable in-memory usage statistics aggregation
usage-statistics-enabled: false

# How long (in seconds) Redis usage queue items are retained in memory for the RESP interface (LPOP/RPOP).
# Note: the in-process Redis RESP usage output is disabled when home.enabled is true.
# How long (in seconds) usage queue items are retained in memory for the Management API.
# The local Redis RESP usage output is disabled.
# Default: 60. Max: 3600.
redis-usage-queue-retention-seconds: 60

Expand Down
14 changes: 1 addition & 13 deletions internal/api/protocol_multiplexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,8 @@ func (s *Server) routeMuxConnection(conn net.Conn, httpListener *muxListener) {
}

if isRedisRESPPrefix(prefix[0]) {
if s.cfg != nil && s.cfg.Home.Enabled {
if errClose := conn.Close(); errClose != nil {
log.Errorf("failed to close redis connection while home mode is enabled: %v", errClose)
}
return
}
if !s.managementRoutesEnabled.Load() {
if errClose := conn.Close(); errClose != nil {
log.Errorf("failed to close redis connection while management is disabled: %v", errClose)
}
return
}
_ = conn.SetReadDeadline(time.Time{})
s.handleRedisConnection(conn, reader)
s.handleRedisConnection(conn)
return
}

Expand Down
Loading
Loading