Compare commits

..

19 Commits

Author SHA1 Message Date
Luis Pater
172f282e9e feat: add graceful shutdown for streaming response handling
- Introduced `streamDone` channel to signal the completion of streaming goroutines.
- Updated `processStreamingChunks` to incorporate proper cleanup and defer close operations.
- Ensured `streamWriter` and associated resources are released when streaming completes.
2025-09-17 19:46:57 +08:00
Luis Pater
7f0c9b1942 Merge pull request #43 from kyle-the-dev/fix/gemini-schema-sanitization
fix: comprehensive JSON Schema sanitization for Claude to Gemini
2025-09-16 20:26:32 +08:00
Luis Pater
8c2063aeea Fix log key name from gpt-5-codex to force-gpt-5-codex for consistency 2025-09-16 20:03:48 +08:00
Luis Pater
ed6e7750e2 Update config key gpt-5-codex to force-gpt-5-codex for clarity in example configuration file 2025-09-16 20:00:21 +08:00
Kyle Ryan
a0c389a854 fix: comprehensive JSON Schema sanitization for Claude to Gemini
- Add SanitizeSchemaForGemini utility handling union types, allOf, exclusiveMinimum
- Fix both gemini-cli and gemini API translators
- Resolve "Proto field is not repeating, cannot start list" errors
- Maintain backward compatibility with fallback logic

This fixes Claude Code CLI compatibility issues when using tools with either
Gemini CLI credentials or direct Gemini API keys by properly sanitizing
JSON Schema fields that are incompatible with Gemini's Protocol Buffer validation.
2025-09-16 18:24:49 +08:00
hkfires
e9037fceb0 fix(windows): Improve path handling and file read reliability 2025-09-16 10:01:58 +08:00
Luis Pater
2406cc775e Add GPT-5 Codex model support and configuration options in documentation 2025-09-16 04:53:25 +08:00
Luis Pater
b84cbee77a Add support for forcing GPT-5 Codex model configuration
- Introduced a new `ForceGPT5Codex` configuration option in settings.
- Added relevant API endpoints for managing `ForceGPT5Codex`.
- Enhanced Codex client to handle GPT-5 Codex-specific logic and mapping.
- Updated example configuration file to include the new option.

Add GPT-5 Codex model support and configuration options in documentation
2025-09-16 04:40:19 +08:00
Luis Pater
fa762e69a4 Merge branch 'dev' of github.com:luispater/CLIProxyAPI into dev 2025-09-14 22:55:18 +08:00
Luis Pater
7e0fd1e260 Add Keep-Alive header 2025-09-14 22:54:36 +08:00
hkfires
d6037e5549 Refine .gitignore and .dockerignore files 2025-09-14 16:29:06 +08:00
Luis Pater
9fce13fe03 Update internal module imports to use v5 package path
- Updated all `github.com/luispater/CLIProxyAPI/internal/...` imports to point to `github.com/luispater/CLIProxyAPI/v5/internal/...`.
- Adjusted `go.mod` to specify `module github.com/luispater/CLIProxyAPI/v5`.
2025-09-13 23:34:32 +08:00
Luis Pater
4375822cbb Resolve logsDir path relative to configuration directory in FileRequestLogger 2025-09-12 15:28:07 +08:00
Luis Pater
e0d13148ef Merge branch 'main' into dev 2025-09-12 13:12:02 +08:00
Luis Pater
bd68472d3c Merge pull request #41 from kaixxx/main
Codex CLI - setting 'store = false' to prevent the request being rejected by OpenAI
2025-09-12 13:10:22 +08:00
kaixxx
b3c534bae5 Build instructions reformatting 2025-09-12 01:16:54 +02:00
kaixxx
b7d6ae1b48 Windows build instructions 2025-09-12 01:12:51 +02:00
kaixxx
aacfcae382 Codex CLI - setting 'store = false'
store = true leads to:
BadRequestError("Error code: 400 - {'detail': 'Store must be set to false'}")
2025-09-12 00:59:49 +02:00
Luis Pater
1c92034191 Implement IP-based rate limiting and ban mechanism for management API
- Introduced a new `attemptInfo` structure to track failed login attempts per IP.
- Added logic to temporarily ban IPs exceeding the allowed number of failures.
- Enhanced middleware to reset failed attempt counters on successful authentication.
- Updated `Handler` to include a `failedAttempts` map with thread-safe access.
2025-09-12 03:00:37 +08:00
88 changed files with 729 additions and 334 deletions

View File

@@ -1,6 +1,6 @@
# Git and GitHub folders
.git
.github
.git/*
.github/*
# Docker and CI/CD related files
docker-compose.yml
@@ -10,6 +10,7 @@ docker-compose.yml
Dockerfile
# Documentation and license
docs/*
README.md
README_CN.md
MANAGEMENT_API.md
@@ -20,6 +21,11 @@ LICENSE
config.example.yaml
# Runtime data folders (should be mounted as volumes)
auths
logs
auths/*
logs/*
conv/*
config.yaml
# Development/editor
.claude/*
.vscode/*

8
.gitignore vendored
View File

@@ -1,6 +1,12 @@
config.yaml
*.exe
bin/*
docs/*
logs/*
conv/*
auths/*
!auths/.gitkeep
AGENTS.md
.vscode/*
.claude/*
AGENTS.md
CLAUDE.md

View File

@@ -47,9 +47,16 @@ The first Chinese provider has now been added: [Qwen Code](https://github.com/Qw
```
2. Build the application:
Linux, macOS:
```bash
go build -o cli-proxy-api ./cmd/server
```
Windows:
```bash
go build -o cli-proxy-api.exe ./cmd/server
```
## Usage
@@ -222,6 +229,7 @@ console.log(await claudeResponse.json());
- gemini-2.5-flash
- gemini-2.5-flash-lite
- gpt-5
- gpt-5-codex
- claude-opus-4-1-20250805
- claude-opus-4-20250514
- claude-sonnet-4-20250514
@@ -255,6 +263,7 @@ The server uses a YAML configuration file (`config.yaml`) located in the project
| `debug` | boolean | false | Enable debug mode for verbose logging. |
| `api-keys` | string[] | [] | List of API keys that can be used to authenticate requests. |
| `generative-language-api-key` | string[] | [] | List of Generative Language API keys. |
| `force-gpt-5-codex` | bool | false | Force the conversion of GPT-5 calls to GPT-5 Codex. |
| `codex-api-key` | object | {} | List of Codex API keys. |
| `codex-api-key.api-key` | string | "" | Codex API key. |
| `codex-api-key.base-url` | string | "" | Custom Codex API endpoint, if you use a third-party API endpoint. |
@@ -315,6 +324,9 @@ generative-language-api-key:
- "AIzaSy...03"
- "AIzaSy...04"
# Force the conversion of GPT-5 calls to GPT-5 Codex.
force-gpt-5-codex: true
# Codex API keys
codex-api-key:
- api-key: "sk-atSM..."
@@ -416,7 +428,7 @@ export ANTHROPIC_MODEL=gemini-2.5-pro
export ANTHROPIC_SMALL_FAST_MODEL=gemini-2.5-flash
```
Using OpenAI models:
Using OpenAI GPT 5 models:
```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8317
export ANTHROPIC_AUTH_TOKEN=sk-dummy
@@ -424,6 +436,14 @@ export ANTHROPIC_MODEL=gpt-5
export ANTHROPIC_SMALL_FAST_MODEL=gpt-5-minimal
```
Using OpenAI GPT 5 Codex models:
```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8317
export ANTHROPIC_AUTH_TOKEN=sk-dummy
export ANTHROPIC_MODEL=gpt-5-codex
export ANTHROPIC_SMALL_FAST_MODEL=gpt-5-codex-low
```
Using Claude models:
```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8317
@@ -447,7 +467,7 @@ Start CLI Proxy API server, and then edit the `~/.codex/config.toml` and `~/.cod
config.toml:
```toml
model_provider = "cliproxyapi"
model = "gpt-5" # You can use any of the models that we support.
model = "gpt-5-codex" # Or gpt-5, you can also use any of the models that we support.
model_reasoning_effort = "high"
[model_providers.cliproxyapi]

View File

@@ -241,6 +241,7 @@ console.log(await claudeResponse.json());
- gemini-2.5-flash
- gemini-2.5-flash-lite
- gpt-5
- gpt-5-codex
- claude-opus-4-1-20250805
- claude-opus-4-20250514
- claude-sonnet-4-20250514
@@ -274,6 +275,7 @@ console.log(await claudeResponse.json());
| `debug` | boolean | false | 启用调试模式以获取详细日志。 |
| `api-keys` | string[] | [] | 可用于验证请求的API密钥列表。 |
| `generative-language-api-key` | string[] | [] | 生成式语言API密钥列表。 |
| `force-gpt-5-codex` | bool | false | 强制将 GPT-5 调用转换成 GPT-5 Codex。 |
| `codex-api-key` | object | {} | Codex API密钥列表。 |
| `codex-api-key.api-key` | string | "" | Codex API密钥。 |
| `codex-api-key.base-url` | string | "" | 自定义的Codex API端点 |
@@ -334,6 +336,9 @@ generative-language-api-key:
- "AIzaSy...03"
- "AIzaSy...04"
# 强制将 GPT-5 调用转换成 GPT-5 Codex.
force-gpt-5-codex: true
# Codex API 密钥
codex-api-key:
- api-key: "sk-atSM..."
@@ -430,7 +435,7 @@ export ANTHROPIC_MODEL=gemini-2.5-pro
export ANTHROPIC_SMALL_FAST_MODEL=gemini-2.5-flash
```
使用 OpenAI 模型:
使用 OpenAI GPT 5 模型:
```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8317
export ANTHROPIC_AUTH_TOKEN=sk-dummy
@@ -438,6 +443,15 @@ export ANTHROPIC_MODEL=gpt-5
export ANTHROPIC_SMALL_FAST_MODEL=gpt-5-minimal
```
使用 OpenAI GPT 5 Codex 模型:
```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8317
export ANTHROPIC_AUTH_TOKEN=sk-dummy
export ANTHROPIC_MODEL=gpt-5-codex
export ANTHROPIC_SMALL_FAST_MODEL=gpt-5-codex-low
```
使用 Claude 模型:
```bash
export ANTHROPIC_BASE_URL=http://127.0.0.1:8317
@@ -461,7 +475,7 @@ export ANTHROPIC_SMALL_FAST_MODEL=qwen3-coder-flash
config.toml:
```toml
model_provider = "cliproxyapi"
model = "gpt-5" # 你可以使用任何我们支持的模型
model = "gpt-5-codex" # 或者是gpt-5你也可以使用任何我们支持的模型
model_reasoning_effort = "high"
[model_providers.cliproxyapi]

View File

@@ -11,10 +11,10 @@ import (
"path/filepath"
"strings"
"github.com/luispater/CLIProxyAPI/internal/cmd"
"github.com/luispater/CLIProxyAPI/internal/config"
_ "github.com/luispater/CLIProxyAPI/internal/translator"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/cmd"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
)
@@ -122,13 +122,14 @@ func main() {
log.Fatalf("failed to get home directory: %v", errUserHomeDir)
}
// Reconstruct the path by replacing the tilde with the user's home directory.
parts := strings.Split(cfg.AuthDir, string(os.PathSeparator))
if len(parts) > 1 {
parts[0] = home
cfg.AuthDir = filepath.Join(parts...)
} else {
// If the path is just "~", set it to the home directory.
remainder := strings.TrimPrefix(cfg.AuthDir, "~")
remainder = strings.TrimLeft(remainder, "/\\")
if remainder == "" {
cfg.AuthDir = home
} else {
// Normalize any slash style in the remainder so Windows paths keep nested directories.
normalized := strings.ReplaceAll(remainder, "\\", "/")
cfg.AuthDir = filepath.Join(home, filepath.FromSlash(normalized))
}
}

View File

@@ -41,6 +41,9 @@ generative-language-api-key:
- "AIzaSy...03"
- "AIzaSy...04"
# forces the use of GPT-5 Codex model.
force-gpt-5-codex: true
# Codex API keys
codex-api-key:
- api-key: "sk-atSM..."

2
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/luispater/CLIProxyAPI
module github.com/luispater/CLIProxyAPI/v5
go 1.24

View File

@@ -13,11 +13,11 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/api/handlers"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/api/handlers"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)

View File

@@ -14,10 +14,10 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/api/handlers"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/api/handlers"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)

View File

@@ -13,11 +13,11 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/api/handlers"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/api/handlers"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
)

View File

@@ -8,10 +8,10 @@ import (
"sync"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/client"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"golang.org/x/net/context"
)

View File

@@ -13,13 +13,13 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/internal/auth/codex"
geminiAuth "github.com/luispater/CLIProxyAPI/internal/auth/gemini"
"github.com/luispater/CLIProxyAPI/internal/auth/qwen"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/codex"
geminiAuth "github.com/luispater/CLIProxyAPI/v5/internal/auth/gemini"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/qwen"
"github.com/luispater/CLIProxyAPI/v5/internal/client"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"golang.org/x/oauth2"

View File

@@ -12,6 +12,14 @@ func (h *Handler) GetConfig(c *gin.Context) {
func (h *Handler) GetDebug(c *gin.Context) { c.JSON(200, gin.H{"debug": h.cfg.Debug}) }
func (h *Handler) PutDebug(c *gin.Context) { h.updateBoolField(c, func(v bool) { h.cfg.Debug = v }) }
// ForceGPT5Codex
func (h *Handler) GetForceGPT5Codex(c *gin.Context) {
c.JSON(200, gin.H{"gpt-5-codex": h.cfg.ForceGPT5Codex})
}
func (h *Handler) PutForceGPT5Codex(c *gin.Context) {
h.updateBoolField(c, func(v bool) { h.cfg.ForceGPT5Codex = v })
}
// Request log
func (h *Handler) GetRequestLog(c *gin.Context) { c.JSON(200, gin.H{"request-log": h.cfg.RequestLog}) }
func (h *Handler) PutRequestLog(c *gin.Context) {

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
)
// Generic helpers for list[string]

View File

@@ -7,22 +7,31 @@ import (
"net/http"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"golang.org/x/crypto/bcrypt"
)
type attemptInfo struct {
count int
blockedUntil time.Time
}
// Handler aggregates config reference, persistence path and helpers.
type Handler struct {
cfg *config.Config
configFilePath string
mu sync.Mutex
attemptsMu sync.Mutex
failedAttempts map[string]*attemptInfo // keyed by client IP
}
// NewHandler creates a new management handler instance.
func NewHandler(cfg *config.Config, configFilePath string) *Handler {
return &Handler{cfg: cfg, configFilePath: configFilePath}
return &Handler{cfg: cfg, configFilePath: configFilePath, failedAttempts: make(map[string]*attemptInfo)}
}
// SetConfig updates the in-memory config reference when the server hot-reloads.
@@ -32,11 +41,32 @@ func (h *Handler) SetConfig(cfg *config.Config) { h.cfg = cfg }
// All requests (local and remote) require a valid management key.
// Additionally, remote access requires allow-remote-management=true.
func (h *Handler) Middleware() gin.HandlerFunc {
const maxFailures = 5
const banDuration = 30 * time.Minute
return func(c *gin.Context) {
clientIP := c.ClientIP()
// Remote access control: when not loopback, must be enabled
// For remote IPs, enforce allow-remote-management and ban checks
if !(clientIP == "127.0.0.1" || clientIP == "::1") {
// Check if IP is currently blocked
h.attemptsMu.Lock()
ai := h.failedAttempts[clientIP]
if ai != nil {
if !ai.blockedUntil.IsZero() {
if time.Now().Before(ai.blockedUntil) {
remaining := time.Until(ai.blockedUntil).Round(time.Second)
h.attemptsMu.Unlock()
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": fmt.Sprintf("IP banned due to too many failed attempts. Try again in %s", remaining)})
return
}
// Ban expired, reset state
ai.blockedUntil = time.Time{}
ai.count = 0
}
}
h.attemptsMu.Unlock()
allowRemote := h.cfg.RemoteManagement.AllowRemote
if !allowRemote {
allowRemote = true
@@ -67,15 +97,41 @@ func (h *Handler) Middleware() gin.HandlerFunc {
}
if !(clientIP == "127.0.0.1" || clientIP == "::1") {
// For remote IPs, enforce key and track failures
fail := func() {
h.attemptsMu.Lock()
ai := h.failedAttempts[clientIP]
if ai == nil {
ai = &attemptInfo{}
h.failedAttempts[clientIP] = ai
}
ai.count++
if ai.count >= maxFailures {
ai.blockedUntil = time.Now().Add(banDuration)
ai.count = 0
}
h.attemptsMu.Unlock()
}
if provided == "" {
fail()
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing management key"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(secret), []byte(provided)); err != nil {
fail()
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid management key"})
return
}
// Success: reset failed count for this IP
h.attemptsMu.Lock()
if ai := h.failedAttempts[clientIP]; ai != nil {
ai.count = 0
ai.blockedUntil = time.Time{}
}
h.attemptsMu.Unlock()
}
c.Next()

View File

@@ -14,11 +14,11 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/api/handlers"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/api/handlers"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

View File

@@ -13,11 +13,11 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/api/handlers"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/api/handlers"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)

View File

@@ -8,7 +8,7 @@ import (
"io"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/logging"
"github.com/luispater/CLIProxyAPI/v5/internal/logging"
)
// RequestLoggingMiddleware creates a Gin middleware that logs HTTP requests and responses.

View File

@@ -8,8 +8,8 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/logging"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/logging"
)
// RequestInfo holds essential details of an incoming HTTP request for logging purposes.
@@ -28,6 +28,7 @@ type ResponseWriterWrapper struct {
isStreaming bool // isStreaming indicates whether the response is a streaming type (e.g., text/event-stream).
streamWriter logging.StreamingLogWriter // streamWriter is a writer for handling streaming log entries.
chunkChannel chan []byte // chunkChannel is a channel for asynchronously passing response chunks to the logger.
streamDone chan struct{} // streamDone signals when the streaming goroutine completes.
logger logging.RequestLogger // logger is the instance of the request logger service.
requestInfo *RequestInfo // requestInfo holds the details of the original request.
statusCode int // statusCode stores the HTTP status code of the response.
@@ -108,9 +109,11 @@ func (w *ResponseWriterWrapper) WriteHeader(statusCode int) {
if err == nil {
w.streamWriter = streamWriter
w.chunkChannel = make(chan []byte, 100) // Buffered channel for async writes
doneChan := make(chan struct{})
w.streamDone = doneChan
// Start async chunk processor
go w.processStreamingChunks()
go w.processStreamingChunks(doneChan)
// Write status immediately
_ = streamWriter.WriteStatus(statusCode, w.headers)
@@ -168,7 +171,13 @@ func (w *ResponseWriterWrapper) detectStreaming(contentType string) bool {
// processStreamingChunks runs in a separate goroutine to process response chunks from the chunkChannel.
// It asynchronously writes each chunk to the streaming log writer.
func (w *ResponseWriterWrapper) processStreamingChunks() {
func (w *ResponseWriterWrapper) processStreamingChunks(done chan struct{}) {
if done == nil {
return
}
defer close(done)
if w.streamWriter == nil || w.chunkChannel == nil {
return
}
@@ -194,8 +203,15 @@ func (w *ResponseWriterWrapper) Finalize(c *gin.Context) error {
w.chunkChannel = nil
}
if w.streamDone != nil {
<-w.streamDone
w.streamDone = nil
}
if w.streamWriter != nil {
return w.streamWriter.Close()
err := w.streamWriter.Close()
w.streamWriter = nil
return err
}
} else {
// Capture final status code and headers if not already captured

View File

@@ -10,20 +10,21 @@ import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/api/handlers"
"github.com/luispater/CLIProxyAPI/internal/api/handlers/claude"
"github.com/luispater/CLIProxyAPI/internal/api/handlers/gemini"
managementHandlers "github.com/luispater/CLIProxyAPI/internal/api/handlers/management"
"github.com/luispater/CLIProxyAPI/internal/api/handlers/openai"
"github.com/luispater/CLIProxyAPI/internal/api/middleware"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/logging"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/api/handlers"
"github.com/luispater/CLIProxyAPI/v5/internal/api/handlers/claude"
"github.com/luispater/CLIProxyAPI/v5/internal/api/handlers/gemini"
managementHandlers "github.com/luispater/CLIProxyAPI/v5/internal/api/handlers/management"
"github.com/luispater/CLIProxyAPI/v5/internal/api/handlers/openai"
"github.com/luispater/CLIProxyAPI/v5/internal/api/middleware"
"github.com/luispater/CLIProxyAPI/v5/internal/client"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/logging"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
)
@@ -75,7 +76,8 @@ func NewServer(cfg *config.Config, cliClients []interfaces.Client, configFilePat
engine.Use(gin.Recovery())
// Add request logging middleware (positioned after recovery, before auth)
requestLogger := logging.NewFileRequestLogger(cfg.RequestLog, "logs")
// Resolve logs directory relative to the configuration file directory.
requestLogger := logging.NewFileRequestLogger(cfg.RequestLog, "logs", filepath.Dir(configFilePath))
engine.Use(middleware.RequestLoggingMiddleware(requestLogger))
engine.Use(corsMiddleware())
@@ -198,6 +200,10 @@ func (s *Server) setupRoutes() {
mgmt.PUT("/debug", s.mgmt.PutDebug)
mgmt.PATCH("/debug", s.mgmt.PutDebug)
mgmt.GET("/force-gpt-5-codex", s.mgmt.GetForceGPT5Codex)
mgmt.PUT("/force-gpt-5-codex", s.mgmt.PutForceGPT5Codex)
mgmt.PATCH("/force-gpt-5-codex", s.mgmt.PutForceGPT5Codex)
mgmt.GET("/proxy-url", s.mgmt.GetProxyURL)
mgmt.PUT("/proxy-url", s.mgmt.PutProxyURL)
mgmt.PATCH("/proxy-url", s.mgmt.PutProxyURL)

View File

@@ -13,8 +13,8 @@ import (
"strings"
"time"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
)

View File

@@ -14,8 +14,8 @@ import (
"strings"
"time"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
)

View File

@@ -15,10 +15,10 @@ import (
"net/url"
"time"
"github.com/luispater/CLIProxyAPI/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/internal/browser"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/v5/internal/browser"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"golang.org/x/net/proxy"

View File

@@ -13,8 +13,8 @@ import (
"strings"
"time"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
)

View File

@@ -16,16 +16,16 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/auth"
"github.com/luispater/CLIProxyAPI/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/internal/auth/empty"
"github.com/luispater/CLIProxyAPI/internal/config"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/auth"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/empty"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

View File

@@ -11,9 +11,9 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/auth"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/auth"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
)
// ClientBase provides a common base structure for all AI API clients.

View File

@@ -17,15 +17,15 @@ import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/luispater/CLIProxyAPI/internal/auth"
"github.com/luispater/CLIProxyAPI/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/internal/auth/empty"
"github.com/luispater/CLIProxyAPI/internal/config"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/auth"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/empty"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
@@ -136,6 +136,10 @@ func (c *CodexClient) CanProvideModel(modelName string) bool {
"gpt-5-low",
"gpt-5-medium",
"gpt-5-high",
"gpt-5-codex",
"gpt-5-codex-low",
"gpt-5-codex-medium",
"gpt-5-codex-high",
"codex-mini-latest",
}
return util.InArray(models, modelName)
@@ -415,6 +419,25 @@ func (c *CodexClient) APIRequest(ctx context.Context, modelName, endpoint string
case "gpt-5-high":
jsonBody, _ = sjson.SetBytes(jsonBody, "reasoning.effort", "high")
}
} else if util.InArray([]string{"gpt-5-codex", "gpt-5-codex-low", "gpt-5-codex-medium", "gpt-5-codex-high"}, modelName) {
jsonBody, _ = sjson.SetBytes(jsonBody, "model", "gpt-5-codex")
switch modelName {
case "gpt-5-codex":
jsonBody, _ = sjson.SetBytes(jsonBody, "reasoning.effort", "medium")
case "gpt-5-codex-low":
jsonBody, _ = sjson.SetBytes(jsonBody, "reasoning.effort", "low")
case "gpt-5-codex-medium":
jsonBody, _ = sjson.SetBytes(jsonBody, "reasoning.effort", "medium")
case "gpt-5-codex-high":
jsonBody, _ = sjson.SetBytes(jsonBody, "reasoning.effort", "high")
}
} else if c.cfg.ForceGPT5Codex {
if gjson.GetBytes(jsonBody, "model").String() == "gpt-5" {
if gjson.GetBytes(jsonBody, "reasoning.effort").String() == "minimal" {
jsonBody, _ = sjson.SetBytes(jsonBody, "reasoning.effort", "low")
}
jsonBody, _ = sjson.SetBytes(jsonBody, "model", "gpt-5-codex")
}
}
url := fmt.Sprintf("%s%s", chatGPTEndpoint, endpoint)
@@ -447,6 +470,7 @@ func (c *CodexClient) APIRequest(ctx context.Context, modelName, endpoint string
req.Header.Set("Openai-Beta", "responses=experimental")
req.Header.Set("Session_id", sessionID)
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Connection", "Keep-Alive")
if c.apiKeyIndex != -1 {
// Using API key authentication

View File

@@ -18,13 +18,13 @@ import (
"time"
"github.com/gin-gonic/gin"
geminiAuth "github.com/luispater/CLIProxyAPI/internal/auth/gemini"
"github.com/luispater/CLIProxyAPI/internal/config"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/internal/util"
geminiAuth "github.com/luispater/CLIProxyAPI/v5/internal/auth/gemini"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

View File

@@ -15,12 +15,12 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/config"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
)

View File

@@ -15,13 +15,13 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/auth"
"github.com/luispater/CLIProxyAPI/internal/config"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/auth"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/sjson"
)

View File

@@ -17,14 +17,14 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/luispater/CLIProxyAPI/internal/auth"
"github.com/luispater/CLIProxyAPI/internal/auth/qwen"
"github.com/luispater/CLIProxyAPI/internal/config"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/registry"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/auth"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/qwen"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/registry"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

View File

@@ -11,12 +11,12 @@ import (
"strings"
"time"
"github.com/luispater/CLIProxyAPI/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/internal/browser"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/v5/internal/browser"
"github.com/luispater/CLIProxyAPI/v5/internal/client"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
)

View File

@@ -7,9 +7,9 @@ import (
"context"
"os"
"github.com/luispater/CLIProxyAPI/internal/auth/gemini"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/gemini"
"github.com/luispater/CLIProxyAPI/v5/internal/client"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
log "github.com/sirupsen/logrus"
)

View File

@@ -11,12 +11,12 @@ import (
"strings"
"time"
"github.com/luispater/CLIProxyAPI/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/internal/browser"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/v5/internal/browser"
"github.com/luispater/CLIProxyAPI/v5/internal/client"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
)

View File

@@ -8,10 +8,10 @@ import (
"fmt"
"os"
"github.com/luispater/CLIProxyAPI/internal/auth/qwen"
"github.com/luispater/CLIProxyAPI/internal/browser"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/qwen"
"github.com/luispater/CLIProxyAPI/v5/internal/browser"
"github.com/luispater/CLIProxyAPI/v5/internal/client"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
log "github.com/sirupsen/logrus"
)

View File

@@ -18,16 +18,16 @@ import (
"syscall"
"time"
"github.com/luispater/CLIProxyAPI/internal/api"
"github.com/luispater/CLIProxyAPI/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/internal/auth/gemini"
"github.com/luispater/CLIProxyAPI/internal/auth/qwen"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/internal/watcher"
"github.com/luispater/CLIProxyAPI/v5/internal/api"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/gemini"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/qwen"
"github.com/luispater/CLIProxyAPI/v5/internal/client"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/watcher"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)

View File

@@ -44,6 +44,9 @@ type Config struct {
// ClaudeKey defines a list of Claude API key configurations as specified in the YAML configuration file.
ClaudeKey []ClaudeKey `yaml:"claude-api-key" json:"claude-api-key"`
// ForceGPT5Codex forces the use of GPT-5 Codex model.
ForceGPT5Codex bool `yaml:"force-gpt-5-codex" json:"force-gpt-5-codex"`
// Codex defines a list of Codex API key configurations as specified in the YAML configuration file.
CodexKey []CodexKey `yaml:"codex-api-key" json:"codex-api-key"`

View File

@@ -15,7 +15,7 @@ import (
"strings"
"time"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
)
// RequestLogger defines the interface for logging HTTP requests and responses.
@@ -98,20 +98,18 @@ type FileRequestLogger struct {
//
// Parameters:
// - enabled: Whether request logging should be enabled
// - logsDir: The directory where log files should be stored
// - logsDir: The directory where log files should be stored (can be relative)
// - configDir: The directory of the configuration file; when logsDir is
// relative, it will be resolved relative to this directory
//
// Returns:
// - *FileRequestLogger: A new file-based request logger instance
func NewFileRequestLogger(enabled bool, logsDir string) *FileRequestLogger {
// Resolve logsDir relative to the executable directory when it's not absolute.
func NewFileRequestLogger(enabled bool, logsDir string, configDir string) *FileRequestLogger {
// Resolve logsDir relative to the configuration file directory when it's not absolute.
if !filepath.IsAbs(logsDir) {
if exePath, err := os.Executable(); err == nil {
// Resolve symlinks to get the real executable path
if realExe, errEvalSymlinks := filepath.EvalSymlinks(exePath); errEvalSymlinks == nil {
exePath = realExe
}
execDir := filepath.Dir(exePath)
logsDir = filepath.Join(execDir, logsDir)
// If configDir is provided, resolve logsDir relative to it.
if configDir != "" {
logsDir = filepath.Join(configDir, logsDir)
}
}
return &FileRequestLogger{

View File

@@ -215,6 +215,58 @@ func GetOpenAIModels() []*ModelInfo {
MaxCompletionTokens: 128000,
SupportedParameters: []string{"tools"},
},
{
ID: "gpt-5-codex",
Object: "model",
Created: time.Now().Unix(),
OwnedBy: "openai",
Type: "openai",
Version: "gpt-5-2025-09-15",
DisplayName: "GPT 5 Codex",
Description: "Stable version of GPT 5 Codex, The best model for coding and agentic tasks across domains.",
ContextLength: 400000,
MaxCompletionTokens: 128000,
SupportedParameters: []string{"tools"},
},
{
ID: "gpt-5-codex-low",
Object: "model",
Created: time.Now().Unix(),
OwnedBy: "openai",
Type: "openai",
Version: "gpt-5-2025-09-15",
DisplayName: "GPT 5 Codex Low",
Description: "Stable version of GPT 5 Codex, The best model for coding and agentic tasks across domains.",
ContextLength: 400000,
MaxCompletionTokens: 128000,
SupportedParameters: []string{"tools"},
},
{
ID: "gpt-5-codex-medium",
Object: "model",
Created: time.Now().Unix(),
OwnedBy: "openai",
Type: "openai",
Version: "gpt-5-2025-09-15",
DisplayName: "GPT 5 Codex Medium",
Description: "Stable version of GPT 5 Codex, The best model for coding and agentic tasks across domains.",
ContextLength: 400000,
MaxCompletionTokens: 128000,
SupportedParameters: []string{"tools"},
},
{
ID: "gpt-5-codex-high",
Object: "model",
Created: time.Now().Unix(),
OwnedBy: "openai",
Type: "openai",
Version: "gpt-5-2025-09-15",
DisplayName: "GPT 5 Codex High",
Description: "Stable version of GPT 5 Codex, The best model for coding and agentic tasks across domains.",
ContextLength: 400000,
MaxCompletionTokens: 128000,
SupportedParameters: []string{"tools"},
},
{
ID: "codex-mini-latest",
Object: "model",

View File

@@ -8,7 +8,7 @@ package geminiCLI
import (
"bytes"
. "github.com/luispater/CLIProxyAPI/internal/translator/claude/gemini"
. "github.com/luispater/CLIProxyAPI/v5/internal/translator/claude/gemini"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

View File

@@ -7,7 +7,7 @@ package geminiCLI
import (
"context"
. "github.com/luispater/CLIProxyAPI/internal/translator/claude/gemini"
. "github.com/luispater/CLIProxyAPI/v5/internal/translator/claude/gemini"
"github.com/tidwall/sjson"
)

View File

@@ -1,9 +1,9 @@
package geminiCLI
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -12,7 +12,7 @@ import (
"math/big"
"strings"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

View File

@@ -1,9 +1,9 @@
package gemini
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -1,9 +1,9 @@
package chat_completions
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -1,9 +1,9 @@
package responses
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -11,7 +11,7 @@ import (
"strconv"
"strings"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

View File

@@ -1,9 +1,9 @@
package claude
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -8,7 +8,7 @@ package geminiCLI
import (
"bytes"
. "github.com/luispater/CLIProxyAPI/internal/translator/codex/gemini"
. "github.com/luispater/CLIProxyAPI/v5/internal/translator/codex/gemini"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

View File

@@ -7,7 +7,7 @@ package geminiCLI
import (
"context"
. "github.com/luispater/CLIProxyAPI/internal/translator/codex/gemini"
. "github.com/luispater/CLIProxyAPI/v5/internal/translator/codex/gemini"
"github.com/tidwall/sjson"
)

View File

@@ -1,9 +1,9 @@
package geminiCLI
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -13,8 +13,8 @@ import (
"strconv"
"strings"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

View File

@@ -1,9 +1,9 @@
package gemini
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -12,7 +12,7 @@ import (
"strconv"
"strings"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -259,9 +259,6 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
out, _ = sjson.Set(out, "text.verbosity", v.Value())
}
}
// The examples include store: true when response_format is provided
store = true
} else if text.Exists() {
// If only text.verbosity present (no response_format), map verbosity
if v := text.Get("verbosity"); v.Exists() {
@@ -306,10 +303,6 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
out, _ = sjson.SetRaw(out, "tools.-1", item)
}
}
// The examples include store: true when tools and formatting are used; be conservative
if rf.Exists() {
store = true
}
}
out, _ = sjson.Set(out, "store", store)

View File

@@ -1,9 +1,9 @@
package chat_completions
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -3,7 +3,7 @@ package responses
import (
"bytes"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

View File

@@ -6,7 +6,7 @@ import (
"context"
"fmt"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

View File

@@ -1,9 +1,9 @@
package responses
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -10,8 +10,8 @@ import (
"encoding/json"
"strings"
client "github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/util"
client "github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -136,8 +136,14 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
inputSchemaResult := toolResult.Get("input_schema")
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
inputSchema := inputSchemaResult.Raw
inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
inputSchema, _ = sjson.Delete(inputSchema, "$schema")
// Use comprehensive schema sanitization for Gemini API compatibility
if sanitizedSchema, sanitizeErr := util.SanitizeSchemaForGemini(inputSchema); sanitizeErr == nil {
inputSchema = sanitizedSchema
} else {
// Fallback to basic cleanup if sanitization fails
inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
inputSchema, _ = sjson.Delete(inputSchema, "$schema")
}
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)
var toolDeclaration any

View File

@@ -1,9 +1,9 @@
package claude
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -1,9 +1,9 @@
package gemini
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -7,8 +7,8 @@ import (
"fmt"
"strings"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

View File

@@ -11,7 +11,7 @@ import (
"fmt"
"time"
. "github.com/luispater/CLIProxyAPI/internal/translator/gemini/openai/chat-completions"
. "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/chat-completions"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

View File

@@ -1,9 +1,9 @@
package chat_completions
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -3,8 +3,8 @@ package responses
import (
"bytes"
. "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/gemini"
. "github.com/luispater/CLIProxyAPI/internal/translator/gemini/openai/responses"
. "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini-cli/gemini"
. "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/responses"
)
func ConvertOpenAIResponsesRequestToGeminiCLI(modelName string, inputRawJSON []byte, stream bool) []byte {

View File

@@ -3,7 +3,7 @@ package responses
import (
"context"
. "github.com/luispater/CLIProxyAPI/internal/translator/gemini/openai/responses"
. "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/responses"
"github.com/tidwall/gjson"
)

View File

@@ -1,9 +1,9 @@
package responses
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -10,8 +10,8 @@ import (
"encoding/json"
"strings"
client "github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/util"
client "github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -129,8 +129,14 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
inputSchemaResult := toolResult.Get("input_schema")
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
inputSchema := inputSchemaResult.Raw
inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
inputSchema, _ = sjson.Delete(inputSchema, "$schema")
// Use comprehensive schema sanitization for Gemini API compatibility
if sanitizedSchema, sanitizeErr := util.SanitizeSchemaForGemini(inputSchema); sanitizeErr == nil {
inputSchema = sanitizedSchema
} else {
// Fallback to basic cleanup if sanitization fails
inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
inputSchema, _ = sjson.Delete(inputSchema, "$schema")
}
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)
var toolDeclaration any

View File

@@ -1,9 +1,9 @@
package claude
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -1,9 +1,9 @@
package geminiCLI
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -1,9 +1,9 @@
package gemini
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
// Register a no-op response translator and a request normalizer for Gemini→Gemini.

View File

@@ -7,8 +7,8 @@ import (
"fmt"
"strings"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/misc"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

View File

@@ -1,9 +1,9 @@
package chat_completions
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -1,9 +1,9 @@
package responses
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -1,30 +1,30 @@
package translator
import (
_ "github.com/luispater/CLIProxyAPI/internal/translator/claude/gemini"
_ "github.com/luispater/CLIProxyAPI/internal/translator/claude/gemini-cli"
_ "github.com/luispater/CLIProxyAPI/internal/translator/claude/openai/chat-completions"
_ "github.com/luispater/CLIProxyAPI/internal/translator/claude/openai/responses"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/claude/gemini"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/claude/gemini-cli"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/claude/openai/chat-completions"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/claude/openai/responses"
_ "github.com/luispater/CLIProxyAPI/internal/translator/codex/claude"
_ "github.com/luispater/CLIProxyAPI/internal/translator/codex/gemini"
_ "github.com/luispater/CLIProxyAPI/internal/translator/codex/gemini-cli"
_ "github.com/luispater/CLIProxyAPI/internal/translator/codex/openai/chat-completions"
_ "github.com/luispater/CLIProxyAPI/internal/translator/codex/openai/responses"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/codex/claude"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/codex/gemini"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/codex/gemini-cli"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/codex/openai/chat-completions"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/codex/openai/responses"
_ "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/claude"
_ "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/gemini"
_ "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/openai/chat-completions"
_ "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/openai/responses"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini-cli/claude"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini-cli/gemini"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini-cli/openai/chat-completions"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini-cli/openai/responses"
_ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/claude"
_ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/gemini"
_ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/gemini-cli"
_ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/openai/chat-completions"
_ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/openai/responses"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/claude"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/gemini"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/gemini-cli"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/chat-completions"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/responses"
_ "github.com/luispater/CLIProxyAPI/internal/translator/openai/claude"
_ "github.com/luispater/CLIProxyAPI/internal/translator/openai/gemini"
_ "github.com/luispater/CLIProxyAPI/internal/translator/openai/gemini-cli"
_ "github.com/luispater/CLIProxyAPI/internal/translator/openai/openai/responses"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/claude"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/gemini"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/gemini-cli"
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/openai/responses"
)

View File

@@ -1,9 +1,9 @@
package claude
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -10,7 +10,7 @@ import (
"encoding/json"
"strings"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
"github.com/tidwall/gjson"
)

View File

@@ -1,9 +1,9 @@
package geminiCLI
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -8,7 +8,7 @@ package geminiCLI
import (
"bytes"
. "github.com/luispater/CLIProxyAPI/internal/translator/openai/gemini"
. "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/gemini"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

View File

@@ -8,7 +8,7 @@ package geminiCLI
import (
"context"
. "github.com/luispater/CLIProxyAPI/internal/translator/openai/gemini"
. "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/gemini"
"github.com/tidwall/sjson"
)

View File

@@ -1,9 +1,9 @@
package gemini
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -1,9 +1,9 @@
package responses
import (
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
)
func init() {

View File

@@ -3,7 +3,7 @@ package translator
import (
"context"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
log "github.com/sirupsen/logrus"
)

View File

@@ -6,7 +6,7 @@ package util
import (
"strings"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
)
// GetProviderName determines the AI service provider based on the model name.

View File

@@ -9,7 +9,7 @@ import (
"net/http"
"net/url"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
log "github.com/sirupsen/logrus"
"golang.org/x/net/proxy"
)

View File

@@ -212,3 +212,160 @@ func FixJSON(input string) string {
return out.String()
}
// SanitizeSchemaForGemini removes JSON Schema fields that are incompatible with Gemini API
// to prevent "Proto field is not repeating, cannot start list" errors.
//
// Parameters:
// - schemaJSON: The JSON schema string to sanitize
//
// Returns:
// - string: The sanitized schema string
// - error: An error if the operation fails
//
// This function removes the following incompatible fields:
// - additionalProperties: Not supported in Gemini function declarations
// - $schema: JSON Schema meta-schema identifier, not needed for API
// - allOf/anyOf/oneOf: Union type constructs not supported
// - exclusiveMinimum/exclusiveMaximum: Advanced validation constraints
// - patternProperties: Advanced property pattern matching
// - dependencies: Property dependencies not supported
// - type arrays: Converts ["string", "null"] to just "string"
func SanitizeSchemaForGemini(schemaJSON string) (string, error) {
// Remove top-level incompatible fields
fieldsToRemove := []string{
"additionalProperties",
"$schema",
"allOf",
"anyOf",
"oneOf",
"exclusiveMinimum",
"exclusiveMaximum",
"patternProperties",
"dependencies",
}
result := schemaJSON
var err error
for _, field := range fieldsToRemove {
result, err = sjson.Delete(result, field)
if err != nil {
continue // Continue even if deletion fails
}
}
// Handle type arrays by converting them to single types
result = sanitizeTypeFields(result)
// Recursively clean nested objects
result = cleanNestedSchemas(result)
return result, nil
}
// sanitizeTypeFields converts type arrays to single types for Gemini compatibility
func sanitizeTypeFields(jsonStr string) string {
// Parse the JSON to find all "type" fields
parsed := gjson.Parse(jsonStr)
result := jsonStr
// Walk through all paths to find type fields
var typeFields []string
walkForTypeFields(parsed, "", &typeFields)
// Process each type field
for _, path := range typeFields {
typeValue := gjson.Get(result, path)
if typeValue.IsArray() {
// Convert array to single type (prioritize string, then others)
arr := typeValue.Array()
if len(arr) > 0 {
var preferredType string
for _, t := range arr {
typeStr := t.String()
if typeStr == "string" {
preferredType = "string"
break
} else if typeStr == "number" || typeStr == "integer" {
preferredType = typeStr
if preferredType == "" {
preferredType = typeStr
}
} else if preferredType == "" {
preferredType = typeStr
}
}
if preferredType != "" {
result, _ = sjson.Set(result, path, preferredType)
}
}
}
}
return result
}
// walkForTypeFields recursively finds all "type" field paths in the JSON
func walkForTypeFields(value gjson.Result, path string, paths *[]string) {
switch value.Type {
case gjson.JSON:
value.ForEach(func(key, val gjson.Result) bool {
var childPath string
if path == "" {
childPath = key.String()
} else {
childPath = path + "." + key.String()
}
if key.String() == "type" {
*paths = append(*paths, childPath)
}
walkForTypeFields(val, childPath, paths)
return true
})
}
}
// cleanNestedSchemas recursively removes incompatible fields from nested schema objects
func cleanNestedSchemas(jsonStr string) string {
fieldsToRemove := []string{"allOf", "anyOf", "oneOf", "exclusiveMinimum", "exclusiveMaximum"}
// Find all nested paths that might contain these fields
var pathsToClean []string
parsed := gjson.Parse(jsonStr)
findNestedSchemaPaths(parsed, "", fieldsToRemove, &pathsToClean)
result := jsonStr
// Remove fields from all found paths
for _, path := range pathsToClean {
result, _ = sjson.Delete(result, path)
}
return result
}
// findNestedSchemaPaths recursively finds paths containing incompatible schema fields
func findNestedSchemaPaths(value gjson.Result, path string, fieldsToFind []string, paths *[]string) {
switch value.Type {
case gjson.JSON:
value.ForEach(func(key, val gjson.Result) bool {
var childPath string
if path == "" {
childPath = key.String()
} else {
childPath = path + "." + key.String()
}
// Check if this key is one we want to remove
for _, field := range fieldsToFind {
if key.String() == field {
*paths = append(*paths, childPath)
break
}
}
findNestedSchemaPaths(val, childPath, fieldsToFind, paths)
return true
})
}
}

View File

@@ -1,7 +1,7 @@
package util
import (
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
log "github.com/sirupsen/logrus"
)

View File

@@ -18,14 +18,14 @@ import (
"time"
"github.com/fsnotify/fsnotify"
"github.com/luispater/CLIProxyAPI/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/internal/auth/gemini"
"github.com/luispater/CLIProxyAPI/internal/auth/qwen"
"github.com/luispater/CLIProxyAPI/internal/client"
"github.com/luispater/CLIProxyAPI/internal/config"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/util"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/codex"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/gemini"
"github.com/luispater/CLIProxyAPI/v5/internal/auth/qwen"
"github.com/luispater/CLIProxyAPI/v5/internal/client"
"github.com/luispater/CLIProxyAPI/v5/internal/config"
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
"github.com/luispater/CLIProxyAPI/v5/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)
@@ -44,6 +44,11 @@ type Watcher struct {
lastConfigHash string
}
const (
authFileReadMaxAttempts = 5
authFileReadRetryDelay = 100 * time.Millisecond
)
// NewWatcher creates a new file watcher instance
func NewWatcher(configPath, authDir string, reloadCallback func(map[string]interfaces.Client, *config.Config)) (*Watcher, error) {
watcher, errNewWatcher := fsnotify.NewWatcher()
@@ -240,6 +245,9 @@ func (w *Watcher) reloadConfig() bool {
if oldConfig.RemoteManagement.AllowRemote != newConfig.RemoteManagement.AllowRemote {
log.Debugf(" remote-management.allow-remote: %t -> %t", oldConfig.RemoteManagement.AllowRemote, newConfig.RemoteManagement.AllowRemote)
}
if oldConfig.ForceGPT5Codex != newConfig.ForceGPT5Codex {
log.Debugf(" force-gpt-5-codex: %t -> %t", oldConfig.ForceGPT5Codex, newConfig.ForceGPT5Codex)
}
}
log.Infof("config successfully reloaded, triggering client reload")
@@ -295,7 +303,7 @@ func (w *Watcher) reloadClients() {
// Rebuild auth file hash cache for current clients
w.lastAuthHashes = make(map[string]string, len(newFileClients))
for path := range newFileClients {
if data, err := os.ReadFile(path); err == nil && len(data) > 0 {
if data, err := readAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay); err == nil && len(data) > 0 {
sum := sha256.Sum256(data)
w.lastAuthHashes[path] = hex.EncodeToString(sum[:])
}
@@ -324,7 +332,7 @@ func (w *Watcher) reloadClients() {
// createClientFromFile creates a single client instance from a given token file path.
func (w *Watcher) createClientFromFile(path string, cfg *config.Config) (interfaces.Client, error) {
data, errReadFile := os.ReadFile(path)
data, errReadFile := readAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay)
if errReadFile != nil {
return nil, errReadFile
}
@@ -382,8 +390,38 @@ func (w *Watcher) clientsToSlice(clientMap map[string]interfaces.Client) []inter
return s
}
// readAuthFileWithRetry attempts to read the auth file multiple times to work around
// short-lived locks on Windows while token files are being written.
func readAuthFileWithRetry(path string, attempts int, delay time.Duration) ([]byte, error) {
var lastErr error
for i := 0; i < attempts; i++ {
data, err := os.ReadFile(path)
if err == nil {
return data, nil
}
lastErr = err
if i < attempts-1 {
time.Sleep(delay)
}
}
return nil, lastErr
}
// addOrUpdateClient handles the addition or update of a single client.
func (w *Watcher) addOrUpdateClient(path string) {
data, errRead := readAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay)
if errRead != nil {
log.Errorf("failed to read auth file %s: %v", filepath.Base(path), errRead)
return
}
if len(data) == 0 {
log.Debugf("ignoring empty auth file: %s", filepath.Base(path))
return
}
sum := sha256.Sum256(data)
curHash := hex.EncodeToString(sum[:])
w.clientsMutex.Lock()
cfg := w.config
@@ -392,24 +430,6 @@ func (w *Watcher) addOrUpdateClient(path string) {
w.clientsMutex.Unlock()
return
}
// Read file to check for emptiness and calculate hash
data, errRead := os.ReadFile(path)
if errRead != nil {
log.Errorf("failed to read auth file %s: %v", filepath.Base(path), errRead)
w.clientsMutex.Unlock()
return
}
if len(data) == 0 {
// Empty file: ignore (wait for a subsequent WRITE)
log.Debugf("ignoring empty auth file: %s", filepath.Base(path))
w.clientsMutex.Unlock()
return
}
// Calculate a hash of the current content and compare with the cache
sum := sha256.Sum256(data)
curHash := hex.EncodeToString(sum[:])
if prev, ok := w.lastAuthHashes[path]; ok && prev == curHash {
log.Debugf("auth file unchanged (hash match), skipping reload: %s", filepath.Base(path))
w.clientsMutex.Unlock()