mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
Compare commits
13 Commits
d216adeffc
...
c82d8e250a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c82d8e250a | ||
|
|
73db4e64f6 | ||
|
|
69ca0a8fac | ||
|
|
3b04e11544 | ||
|
|
e0927afa40 | ||
|
|
f97d9f3e11 | ||
|
|
6d8609e457 | ||
|
|
13bb7cf704 | ||
|
|
8bce696a7c | ||
|
|
6db8d2a28e | ||
|
|
6da7ed53f2 | ||
|
|
fe6043aec7 | ||
|
|
bc32096e9c |
@@ -138,6 +138,10 @@ Windows desktop app built with Tauri + React for monitoring AI coding assistant
|
|||||||
|
|
||||||
A lightweight web admin panel for CLIProxyAPI with health checks, resource monitoring, real-time logs, auto-update, request statistics and pricing display. Supports one-click installation and systemd service.
|
A lightweight web admin panel for CLIProxyAPI with health checks, resource monitoring, real-time logs, auto-update, request statistics and pricing display. Supports one-click installation and systemd service.
|
||||||
|
|
||||||
|
### [CLIProxyAPI Tray](https://github.com/kitephp/CLIProxyAPI_Tray)
|
||||||
|
|
||||||
|
A Windows tray application implemented using PowerShell scripts, without relying on any third-party libraries. The main features include: automatic creation of shortcuts, silent running, password management, channel switching (Main / Plus), and automatic downloading and updating.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If you developed a project based on CLIProxyAPI, please open a PR to add it to this list.
|
> If you developed a project based on CLIProxyAPI, please open a PR to add it to this list.
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ Windows 桌面应用,基于 Tauri + React 构建,用于通过 CLIProxyAPI
|
|||||||
|
|
||||||
基于 Next.js 的实现,灵感来自 CLIProxyAPI,易于安装使用;自研格式转换(OpenAI/Claude/Gemini/Ollama)、组合系统与自动回退、多账户管理(指数退避)、Next.js Web 控制台,并支持 Cursor、Claude Code、Cline、RooCode 等 CLI 工具,无需 API 密钥。
|
基于 Next.js 的实现,灵感来自 CLIProxyAPI,易于安装使用;自研格式转换(OpenAI/Claude/Gemini/Ollama)、组合系统与自动回退、多账户管理(指数退避)、Next.js Web 控制台,并支持 Cursor、Claude Code、Cline、RooCode 等 CLI 工具,无需 API 密钥。
|
||||||
|
|
||||||
|
### [CLIProxyAPI Tray](https://github.com/kitephp/CLIProxyAPI_Tray)
|
||||||
|
|
||||||
|
Windows 托盘应用,基于 PowerShell 脚本实现,不依赖任何第三方库。主要功能包括:自动创建快捷方式、静默运行、密码管理、通道切换(Main / Plus)以及自动下载与更新。
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 如果你开发了 CLIProxyAPI 的移植或衍生项目,请提交 PR 将其添加到此列表中。
|
> 如果你开发了 CLIProxyAPI 的移植或衍生项目,请提交 PR 将其添加到此列表中。
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ logging-to-file: false
|
|||||||
# files are deleted until within the limit. Set to 0 to disable.
|
# files are deleted until within the limit. Set to 0 to disable.
|
||||||
logs-max-total-size-mb: 0
|
logs-max-total-size-mb: 0
|
||||||
|
|
||||||
|
# Maximum number of error log files retained when request logging is disabled.
|
||||||
|
# When exceeded, the oldest error log files are deleted. Default is 10. Set to 0 to disable cleanup.
|
||||||
|
error-logs-max-files: 10
|
||||||
|
|
||||||
# When false, disable in-memory usage statistics aggregation
|
# When false, disable in-memory usage statistics aggregation
|
||||||
usage-statistics-enabled: false
|
usage-statistics-enabled: false
|
||||||
|
|
||||||
@@ -285,24 +289,31 @@ oauth-model-alias:
|
|||||||
# default: # Default rules only set parameters when they are missing in the payload.
|
# default: # Default rules only set parameters when they are missing in the payload.
|
||||||
# - models:
|
# - models:
|
||||||
# - name: "gemini-2.5-pro" # Supports wildcards (e.g., "gemini-*")
|
# - name: "gemini-2.5-pro" # Supports wildcards (e.g., "gemini-*")
|
||||||
# protocol: "gemini" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex
|
# protocol: "gemini" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex, antigravity
|
||||||
# params: # JSON path (gjson/sjson syntax) -> value
|
# params: # JSON path (gjson/sjson syntax) -> value
|
||||||
# "generationConfig.thinkingConfig.thinkingBudget": 32768
|
# "generationConfig.thinkingConfig.thinkingBudget": 32768
|
||||||
# default-raw: # Default raw rules set parameters using raw JSON when missing (must be valid JSON).
|
# default-raw: # Default raw rules set parameters using raw JSON when missing (must be valid JSON).
|
||||||
# - models:
|
# - models:
|
||||||
# - name: "gemini-2.5-pro" # Supports wildcards (e.g., "gemini-*")
|
# - name: "gemini-2.5-pro" # Supports wildcards (e.g., "gemini-*")
|
||||||
# protocol: "gemini" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex
|
# protocol: "gemini" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex, antigravity
|
||||||
# params: # JSON path (gjson/sjson syntax) -> raw JSON value (strings are used as-is, must be valid JSON)
|
# params: # JSON path (gjson/sjson syntax) -> raw JSON value (strings are used as-is, must be valid JSON)
|
||||||
# "generationConfig.responseJsonSchema": "{\"type\":\"object\",\"properties\":{\"answer\":{\"type\":\"string\"}}}"
|
# "generationConfig.responseJsonSchema": "{\"type\":\"object\",\"properties\":{\"answer\":{\"type\":\"string\"}}}"
|
||||||
# override: # Override rules always set parameters, overwriting any existing values.
|
# override: # Override rules always set parameters, overwriting any existing values.
|
||||||
# - models:
|
# - models:
|
||||||
# - name: "gpt-*" # Supports wildcards (e.g., "gpt-*")
|
# - name: "gpt-*" # Supports wildcards (e.g., "gpt-*")
|
||||||
# protocol: "codex" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex
|
# protocol: "codex" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex, antigravity
|
||||||
# params: # JSON path (gjson/sjson syntax) -> value
|
# params: # JSON path (gjson/sjson syntax) -> value
|
||||||
# "reasoning.effort": "high"
|
# "reasoning.effort": "high"
|
||||||
# override-raw: # Override raw rules always set parameters using raw JSON (must be valid JSON).
|
# override-raw: # Override raw rules always set parameters using raw JSON (must be valid JSON).
|
||||||
# - models:
|
# - models:
|
||||||
# - name: "gpt-*" # Supports wildcards (e.g., "gpt-*")
|
# - name: "gpt-*" # Supports wildcards (e.g., "gpt-*")
|
||||||
# protocol: "codex" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex
|
# protocol: "codex" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex, antigravity
|
||||||
# params: # JSON path (gjson/sjson syntax) -> raw JSON value (strings are used as-is, must be valid JSON)
|
# params: # JSON path (gjson/sjson syntax) -> raw JSON value (strings are used as-is, must be valid JSON)
|
||||||
# "response_format": "{\"type\":\"json_schema\",\"json_schema\":{\"name\":\"answer\",\"schema\":{\"type\":\"object\"}}}"
|
# "response_format": "{\"type\":\"json_schema\",\"json_schema\":{\"name\":\"answer\",\"schema\":{\"type\":\"object\"}}}"
|
||||||
|
# filter: # Filter rules remove specified parameters from the payload.
|
||||||
|
# - models:
|
||||||
|
# - name: "gemini-2.5-pro" # Supports wildcards (e.g., "gemini-*")
|
||||||
|
# protocol: "gemini" # restricts the rule to a specific protocol, options: openai, gemini, claude, codex, antigravity
|
||||||
|
# params: # JSON paths (gjson/sjson syntax) to remove from the payload
|
||||||
|
# - "generationConfig.thinkingConfig.thinkingBudget"
|
||||||
|
# - "generationConfig.responseJsonSchema"
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ func main() {
|
|||||||
// Optional: add a simple middleware + custom request logger
|
// Optional: add a simple middleware + custom request logger
|
||||||
api.WithMiddleware(func(c *gin.Context) { c.Header("X-Example", "custom-provider"); c.Next() }),
|
api.WithMiddleware(func(c *gin.Context) { c.Header("X-Example", "custom-provider"); c.Next() }),
|
||||||
api.WithRequestLoggerFactory(func(cfg *config.Config, cfgPath string) logging.RequestLogger {
|
api.WithRequestLoggerFactory(func(cfg *config.Config, cfgPath string) logging.RequestLogger {
|
||||||
return logging.NewFileRequestLogger(true, "logs", filepath.Dir(cfgPath))
|
return logging.NewFileRequestLoggerWithOptions(true, "logs", filepath.Dir(cfgPath), cfg.ErrorLogsMaxFiles)
|
||||||
}),
|
}),
|
||||||
).
|
).
|
||||||
WithHooks(hooks).
|
WithHooks(hooks).
|
||||||
|
|||||||
@@ -222,6 +222,26 @@ func (h *Handler) PutLogsMaxTotalSizeMB(c *gin.Context) {
|
|||||||
h.persist(c)
|
h.persist(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrorLogsMaxFiles
|
||||||
|
func (h *Handler) GetErrorLogsMaxFiles(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{"error-logs-max-files": h.cfg.ErrorLogsMaxFiles})
|
||||||
|
}
|
||||||
|
func (h *Handler) PutErrorLogsMaxFiles(c *gin.Context) {
|
||||||
|
var body struct {
|
||||||
|
Value *int `json:"value"`
|
||||||
|
}
|
||||||
|
if errBindJSON := c.ShouldBindJSON(&body); errBindJSON != nil || body.Value == nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value := *body.Value
|
||||||
|
if value < 0 {
|
||||||
|
value = 10
|
||||||
|
}
|
||||||
|
h.cfg.ErrorLogsMaxFiles = value
|
||||||
|
h.persist(c)
|
||||||
|
}
|
||||||
|
|
||||||
// Request log
|
// Request log
|
||||||
func (h *Handler) GetRequestLog(c *gin.Context) { c.JSON(200, gin.H{"request-log": h.cfg.RequestLog}) }
|
func (h *Handler) GetRequestLog(c *gin.Context) { c.JSON(200, gin.H{"request-log": h.cfg.RequestLog}) }
|
||||||
func (h *Handler) PutRequestLog(c *gin.Context) {
|
func (h *Handler) PutRequestLog(c *gin.Context) {
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ type ServerOption func(*serverOptionConfig)
|
|||||||
func defaultRequestLoggerFactory(cfg *config.Config, configPath string) logging.RequestLogger {
|
func defaultRequestLoggerFactory(cfg *config.Config, configPath string) logging.RequestLogger {
|
||||||
configDir := filepath.Dir(configPath)
|
configDir := filepath.Dir(configPath)
|
||||||
if base := util.WritablePath(); base != "" {
|
if base := util.WritablePath(); base != "" {
|
||||||
return logging.NewFileRequestLogger(cfg.RequestLog, filepath.Join(base, "logs"), configDir)
|
return logging.NewFileRequestLogger(cfg.RequestLog, filepath.Join(base, "logs"), configDir, cfg.ErrorLogsMaxFiles)
|
||||||
}
|
}
|
||||||
return logging.NewFileRequestLogger(cfg.RequestLog, "logs", configDir)
|
return logging.NewFileRequestLogger(cfg.RequestLog, "logs", configDir, cfg.ErrorLogsMaxFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithMiddleware appends additional Gin middleware during server construction.
|
// WithMiddleware appends additional Gin middleware during server construction.
|
||||||
@@ -497,6 +497,10 @@ func (s *Server) registerManagementRoutes() {
|
|||||||
mgmt.PUT("/logs-max-total-size-mb", s.mgmt.PutLogsMaxTotalSizeMB)
|
mgmt.PUT("/logs-max-total-size-mb", s.mgmt.PutLogsMaxTotalSizeMB)
|
||||||
mgmt.PATCH("/logs-max-total-size-mb", s.mgmt.PutLogsMaxTotalSizeMB)
|
mgmt.PATCH("/logs-max-total-size-mb", s.mgmt.PutLogsMaxTotalSizeMB)
|
||||||
|
|
||||||
|
mgmt.GET("/error-logs-max-files", s.mgmt.GetErrorLogsMaxFiles)
|
||||||
|
mgmt.PUT("/error-logs-max-files", s.mgmt.PutErrorLogsMaxFiles)
|
||||||
|
mgmt.PATCH("/error-logs-max-files", s.mgmt.PutErrorLogsMaxFiles)
|
||||||
|
|
||||||
mgmt.GET("/usage-statistics-enabled", s.mgmt.GetUsageStatisticsEnabled)
|
mgmt.GET("/usage-statistics-enabled", s.mgmt.GetUsageStatisticsEnabled)
|
||||||
mgmt.PUT("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled)
|
mgmt.PUT("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled)
|
||||||
mgmt.PATCH("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled)
|
mgmt.PATCH("/usage-statistics-enabled", s.mgmt.PutUsageStatisticsEnabled)
|
||||||
@@ -907,6 +911,15 @@ func (s *Server) UpdateClients(cfg *config.Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.requestLogger != nil && (oldCfg == nil || oldCfg.ErrorLogsMaxFiles != cfg.ErrorLogsMaxFiles) {
|
||||||
|
if setter, ok := s.requestLogger.(interface{ SetErrorLogsMaxFiles(int) }); ok {
|
||||||
|
setter.SetErrorLogsMaxFiles(cfg.ErrorLogsMaxFiles)
|
||||||
|
}
|
||||||
|
if oldCfg != nil {
|
||||||
|
log.Debugf("error_logs_max_files updated from %d to %d", oldCfg.ErrorLogsMaxFiles, cfg.ErrorLogsMaxFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if oldCfg == nil || oldCfg.DisableCooling != cfg.DisableCooling {
|
if oldCfg == nil || oldCfg.DisableCooling != cfg.DisableCooling {
|
||||||
auth.SetQuotaCooldownDisabled(cfg.DisableCooling)
|
auth.SetQuotaCooldownDisabled(cfg.DisableCooling)
|
||||||
if oldCfg != nil {
|
if oldCfg != nil {
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ type Config struct {
|
|||||||
// When exceeded, the oldest log files are deleted until within the limit. Set to 0 to disable.
|
// When exceeded, the oldest log files are deleted until within the limit. Set to 0 to disable.
|
||||||
LogsMaxTotalSizeMB int `yaml:"logs-max-total-size-mb" json:"logs-max-total-size-mb"`
|
LogsMaxTotalSizeMB int `yaml:"logs-max-total-size-mb" json:"logs-max-total-size-mb"`
|
||||||
|
|
||||||
|
// ErrorLogsMaxFiles limits the number of error log files retained when request logging is disabled.
|
||||||
|
// When exceeded, the oldest error log files are deleted. Default is 10. Set to 0 to disable cleanup.
|
||||||
|
ErrorLogsMaxFiles int `yaml:"error-logs-max-files" json:"error-logs-max-files"`
|
||||||
|
|
||||||
// UsageStatisticsEnabled toggles in-memory usage aggregation; when false, usage data is discarded.
|
// UsageStatisticsEnabled toggles in-memory usage aggregation; when false, usage data is discarded.
|
||||||
UsageStatisticsEnabled bool `yaml:"usage-statistics-enabled" json:"usage-statistics-enabled"`
|
UsageStatisticsEnabled bool `yaml:"usage-statistics-enabled" json:"usage-statistics-enabled"`
|
||||||
|
|
||||||
@@ -229,6 +233,16 @@ type PayloadConfig struct {
|
|||||||
Override []PayloadRule `yaml:"override" json:"override"`
|
Override []PayloadRule `yaml:"override" json:"override"`
|
||||||
// OverrideRaw defines rules that always set raw JSON values, overwriting any existing values.
|
// OverrideRaw defines rules that always set raw JSON values, overwriting any existing values.
|
||||||
OverrideRaw []PayloadRule `yaml:"override-raw" json:"override-raw"`
|
OverrideRaw []PayloadRule `yaml:"override-raw" json:"override-raw"`
|
||||||
|
// Filter defines rules that remove parameters from the payload by JSON path.
|
||||||
|
Filter []PayloadFilterRule `yaml:"filter" json:"filter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayloadFilterRule describes a rule to remove specific JSON paths from matching model payloads.
|
||||||
|
type PayloadFilterRule struct {
|
||||||
|
// Models lists model entries with name pattern and protocol constraint.
|
||||||
|
Models []PayloadModelRule `yaml:"models" json:"models"`
|
||||||
|
// Params lists JSON paths (gjson/sjson syntax) to remove from the payload.
|
||||||
|
Params []string `yaml:"params" json:"params"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PayloadRule describes a single rule targeting a list of models with parameter updates.
|
// PayloadRule describes a single rule targeting a list of models with parameter updates.
|
||||||
@@ -502,6 +516,7 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
cfg.Host = "" // Default empty: binds to all interfaces (IPv4 + IPv6)
|
cfg.Host = "" // Default empty: binds to all interfaces (IPv4 + IPv6)
|
||||||
cfg.LoggingToFile = false
|
cfg.LoggingToFile = false
|
||||||
cfg.LogsMaxTotalSizeMB = 0
|
cfg.LogsMaxTotalSizeMB = 0
|
||||||
|
cfg.ErrorLogsMaxFiles = 10
|
||||||
cfg.UsageStatisticsEnabled = false
|
cfg.UsageStatisticsEnabled = false
|
||||||
cfg.DisableCooling = false
|
cfg.DisableCooling = false
|
||||||
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
||||||
@@ -550,6 +565,10 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
cfg.LogsMaxTotalSizeMB = 0
|
cfg.LogsMaxTotalSizeMB = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.ErrorLogsMaxFiles < 0 {
|
||||||
|
cfg.ErrorLogsMaxFiles = 10
|
||||||
|
}
|
||||||
|
|
||||||
// Sync request authentication providers with inline API keys for backwards compatibility.
|
// Sync request authentication providers with inline API keys for backwards compatibility.
|
||||||
syncInlineAccessProvider(&cfg)
|
syncInlineAccessProvider(&cfg)
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ type FileRequestLogger struct {
|
|||||||
|
|
||||||
// logsDir is the directory where log files are stored.
|
// logsDir is the directory where log files are stored.
|
||||||
logsDir string
|
logsDir string
|
||||||
|
|
||||||
|
// errorLogsMaxFiles limits the number of error log files retained.
|
||||||
|
errorLogsMaxFiles int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFileRequestLogger creates a new file-based request logger.
|
// NewFileRequestLogger creates a new file-based request logger.
|
||||||
@@ -141,10 +144,11 @@ type FileRequestLogger struct {
|
|||||||
// - logsDir: The directory where log files should be stored (can be relative)
|
// - logsDir: The directory where log files should be stored (can be relative)
|
||||||
// - configDir: The directory of the configuration file; when logsDir is
|
// - configDir: The directory of the configuration file; when logsDir is
|
||||||
// relative, it will be resolved relative to this directory
|
// relative, it will be resolved relative to this directory
|
||||||
|
// - errorLogsMaxFiles: Maximum number of error log files to retain (0 = no cleanup)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - *FileRequestLogger: A new file-based request logger instance
|
// - *FileRequestLogger: A new file-based request logger instance
|
||||||
func NewFileRequestLogger(enabled bool, logsDir string, configDir string) *FileRequestLogger {
|
func NewFileRequestLogger(enabled bool, logsDir string, configDir string, errorLogsMaxFiles int) *FileRequestLogger {
|
||||||
// Resolve logsDir relative to the configuration file directory when it's not absolute.
|
// Resolve logsDir relative to the configuration file directory when it's not absolute.
|
||||||
if !filepath.IsAbs(logsDir) {
|
if !filepath.IsAbs(logsDir) {
|
||||||
// If configDir is provided, resolve logsDir relative to it.
|
// If configDir is provided, resolve logsDir relative to it.
|
||||||
@@ -155,6 +159,7 @@ func NewFileRequestLogger(enabled bool, logsDir string, configDir string) *FileR
|
|||||||
return &FileRequestLogger{
|
return &FileRequestLogger{
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
logsDir: logsDir,
|
logsDir: logsDir,
|
||||||
|
errorLogsMaxFiles: errorLogsMaxFiles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +180,11 @@ func (l *FileRequestLogger) SetEnabled(enabled bool) {
|
|||||||
l.enabled = enabled
|
l.enabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetErrorLogsMaxFiles updates the maximum number of error log files to retain.
|
||||||
|
func (l *FileRequestLogger) SetErrorLogsMaxFiles(maxFiles int) {
|
||||||
|
l.errorLogsMaxFiles = maxFiles
|
||||||
|
}
|
||||||
|
|
||||||
// LogRequest logs a complete non-streaming request/response cycle to a file.
|
// LogRequest logs a complete non-streaming request/response cycle to a file.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
@@ -433,8 +443,12 @@ func (l *FileRequestLogger) sanitizeForFilename(path string) string {
|
|||||||
return sanitized
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanupOldErrorLogs keeps only the newest 10 forced error log files.
|
// cleanupOldErrorLogs keeps only the newest errorLogsMaxFiles forced error log files.
|
||||||
func (l *FileRequestLogger) cleanupOldErrorLogs() error {
|
func (l *FileRequestLogger) cleanupOldErrorLogs() error {
|
||||||
|
if l.errorLogsMaxFiles <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
entries, errRead := os.ReadDir(l.logsDir)
|
entries, errRead := os.ReadDir(l.logsDir)
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
return errRead
|
return errRead
|
||||||
@@ -462,7 +476,7 @@ func (l *FileRequestLogger) cleanupOldErrorLogs() error {
|
|||||||
files = append(files, logFile{name: name, modTime: info.ModTime()})
|
files = append(files, logFile{name: name, modTime: info.ModTime()})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(files) <= 10 {
|
if len(files) <= l.errorLogsMaxFiles {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +484,7 @@ func (l *FileRequestLogger) cleanupOldErrorLogs() error {
|
|||||||
return files[i].modTime.After(files[j].modTime)
|
return files[i].modTime.After(files[j].modTime)
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, file := range files[10:] {
|
for _, file := range files[l.errorLogsMaxFiles:] {
|
||||||
if errRemove := os.Remove(filepath.Join(l.logsDir, file.name)); errRemove != nil {
|
if errRemove := os.Remove(filepath.Join(l.logsDir, file.name)); errRemove != nil {
|
||||||
log.WithError(errRemove).Warnf("failed to remove old error log: %s", file.name)
|
log.WithError(errRemove).Warnf("failed to remove old error log: %s", file.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
rules := cfg.Payload
|
rules := cfg.Payload
|
||||||
if len(rules.Default) == 0 && len(rules.DefaultRaw) == 0 && len(rules.Override) == 0 && len(rules.OverrideRaw) == 0 {
|
if len(rules.Default) == 0 && len(rules.DefaultRaw) == 0 && len(rules.Override) == 0 && len(rules.OverrideRaw) == 0 && len(rules.Filter) == 0 {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
model = strings.TrimSpace(model)
|
model = strings.TrimSpace(model)
|
||||||
@@ -39,7 +39,7 @@ func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
// Apply default rules: first write wins per field across all matching rules.
|
// Apply default rules: first write wins per field across all matching rules.
|
||||||
for i := range rules.Default {
|
for i := range rules.Default {
|
||||||
rule := &rules.Default[i]
|
rule := &rules.Default[i]
|
||||||
if !payloadRuleMatchesModels(rule, protocol, candidates) {
|
if !payloadModelRulesMatch(rule.Models, protocol, candidates) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for path, value := range rule.Params {
|
for path, value := range rule.Params {
|
||||||
@@ -64,7 +64,7 @@ func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
// Apply default raw rules: first write wins per field across all matching rules.
|
// Apply default raw rules: first write wins per field across all matching rules.
|
||||||
for i := range rules.DefaultRaw {
|
for i := range rules.DefaultRaw {
|
||||||
rule := &rules.DefaultRaw[i]
|
rule := &rules.DefaultRaw[i]
|
||||||
if !payloadRuleMatchesModels(rule, protocol, candidates) {
|
if !payloadModelRulesMatch(rule.Models, protocol, candidates) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for path, value := range rule.Params {
|
for path, value := range rule.Params {
|
||||||
@@ -93,7 +93,7 @@ func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
// Apply override rules: last write wins per field across all matching rules.
|
// Apply override rules: last write wins per field across all matching rules.
|
||||||
for i := range rules.Override {
|
for i := range rules.Override {
|
||||||
rule := &rules.Override[i]
|
rule := &rules.Override[i]
|
||||||
if !payloadRuleMatchesModels(rule, protocol, candidates) {
|
if !payloadModelRulesMatch(rule.Models, protocol, candidates) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for path, value := range rule.Params {
|
for path, value := range rule.Params {
|
||||||
@@ -111,7 +111,7 @@ func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
// Apply override raw rules: last write wins per field across all matching rules.
|
// Apply override raw rules: last write wins per field across all matching rules.
|
||||||
for i := range rules.OverrideRaw {
|
for i := range rules.OverrideRaw {
|
||||||
rule := &rules.OverrideRaw[i]
|
rule := &rules.OverrideRaw[i]
|
||||||
if !payloadRuleMatchesModels(rule, protocol, candidates) {
|
if !payloadModelRulesMatch(rule.Models, protocol, candidates) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for path, value := range rule.Params {
|
for path, value := range rule.Params {
|
||||||
@@ -130,29 +130,33 @@ func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string
|
|||||||
out = updated
|
out = updated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Apply filter rules: remove matching paths from payload.
|
||||||
|
for i := range rules.Filter {
|
||||||
|
rule := &rules.Filter[i]
|
||||||
|
if !payloadModelRulesMatch(rule.Models, protocol, candidates) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, path := range rule.Params {
|
||||||
|
fullPath := buildPayloadPath(root, path)
|
||||||
|
if fullPath == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updated, errDel := sjson.DeleteBytes(out, fullPath)
|
||||||
|
if errDel != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = updated
|
||||||
|
}
|
||||||
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func payloadRuleMatchesModels(rule *config.PayloadRule, protocol string, models []string) bool {
|
func payloadModelRulesMatch(rules []config.PayloadModelRule, protocol string, models []string) bool {
|
||||||
if rule == nil || len(models) == 0 {
|
if len(rules) == 0 || len(models) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, model := range models {
|
for _, model := range models {
|
||||||
if payloadRuleMatchesModel(rule, model, protocol) {
|
for _, entry := range rules {
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func payloadRuleMatchesModel(rule *config.PayloadRule, model, protocol string) bool {
|
|
||||||
if rule == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(rule.Models) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, entry := range rule.Models {
|
|
||||||
name := strings.TrimSpace(entry.Name)
|
name := strings.TrimSpace(entry.Name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
continue
|
continue
|
||||||
@@ -164,6 +168,7 @@ func payloadRuleMatchesModel(rule *config.PayloadRule, model, protocol string) b
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -386,11 +386,12 @@ func (s *ObjectTokenStore) syncConfigFromBucket(ctx context.Context, example str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ObjectTokenStore) syncAuthFromBucket(ctx context.Context) error {
|
func (s *ObjectTokenStore) syncAuthFromBucket(ctx context.Context) error {
|
||||||
if err := os.RemoveAll(s.authDir); err != nil {
|
// NOTE: We intentionally do NOT use os.RemoveAll here.
|
||||||
return fmt.Errorf("object store: reset auth directory: %w", err)
|
// Wiping the directory triggers file watcher delete events, which then
|
||||||
}
|
// propagate deletions to the remote object store (race condition).
|
||||||
|
// Instead, we just ensure the directory exists and overwrite files incrementally.
|
||||||
if err := os.MkdirAll(s.authDir, 0o700); err != nil {
|
if err := os.MkdirAll(s.authDir, 0o700); err != nil {
|
||||||
return fmt.Errorf("object store: recreate auth directory: %w", err)
|
return fmt.Errorf("object store: create auth directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix := s.prefixedKey(objectStoreAuthPrefix + "/")
|
prefix := s.prefixedKey(objectStoreAuthPrefix + "/")
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import (
|
|||||||
type convertCliResponseToOpenAIChatParams struct {
|
type convertCliResponseToOpenAIChatParams struct {
|
||||||
UnixTimestamp int64
|
UnixTimestamp int64
|
||||||
FunctionIndex int
|
FunctionIndex int
|
||||||
|
SawToolCall bool // Tracks if any tool call was seen in the entire stream
|
||||||
|
UpstreamFinishReason string // Caches the upstream finish reason for final chunk
|
||||||
}
|
}
|
||||||
|
|
||||||
// functionCallIDCounter provides a process-wide unique counter for function call identifiers.
|
// functionCallIDCounter provides a process-wide unique counter for function call identifiers.
|
||||||
@@ -79,10 +81,9 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
|
|||||||
template, _ = sjson.Set(template, "id", responseIDResult.String())
|
template, _ = sjson.Set(template, "id", responseIDResult.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and set the finish reason.
|
// Cache the finish reason - do NOT set it in output yet (will be set on final chunk)
|
||||||
if finishReasonResult := gjson.GetBytes(rawJSON, "response.candidates.0.finishReason"); finishReasonResult.Exists() {
|
if finishReasonResult := gjson.GetBytes(rawJSON, "response.candidates.0.finishReason"); finishReasonResult.Exists() {
|
||||||
template, _ = sjson.Set(template, "choices.0.finish_reason", strings.ToLower(finishReasonResult.String()))
|
(*param).(*convertCliResponseToOpenAIChatParams).UpstreamFinishReason = strings.ToUpper(finishReasonResult.String())
|
||||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", strings.ToLower(finishReasonResult.String()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and set usage metadata (token counts).
|
// Extract and set usage metadata (token counts).
|
||||||
@@ -112,7 +113,6 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
|
|||||||
|
|
||||||
// Process the main content part of the response.
|
// Process the main content part of the response.
|
||||||
partsResult := gjson.GetBytes(rawJSON, "response.candidates.0.content.parts")
|
partsResult := gjson.GetBytes(rawJSON, "response.candidates.0.content.parts")
|
||||||
hasFunctionCall := false
|
|
||||||
if partsResult.IsArray() {
|
if partsResult.IsArray() {
|
||||||
partResults := partsResult.Array()
|
partResults := partsResult.Array()
|
||||||
for i := 0; i < len(partResults); i++ {
|
for i := 0; i < len(partResults); i++ {
|
||||||
@@ -148,7 +148,7 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
|
|||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||||
} else if functionCallResult.Exists() {
|
} else if functionCallResult.Exists() {
|
||||||
// Handle function call content.
|
// Handle function call content.
|
||||||
hasFunctionCall = true
|
(*param).(*convertCliResponseToOpenAIChatParams).SawToolCall = true // Persist across chunks
|
||||||
toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls")
|
toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls")
|
||||||
functionCallIndex := (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex
|
functionCallIndex := (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex
|
||||||
(*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex++
|
(*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex++
|
||||||
@@ -195,9 +195,25 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasFunctionCall {
|
// Determine finish_reason only on the final chunk (has both finishReason and usage metadata)
|
||||||
template, _ = sjson.Set(template, "choices.0.finish_reason", "tool_calls")
|
params := (*param).(*convertCliResponseToOpenAIChatParams)
|
||||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "tool_calls")
|
upstreamFinishReason := params.UpstreamFinishReason
|
||||||
|
sawToolCall := params.SawToolCall
|
||||||
|
|
||||||
|
usageExists := gjson.GetBytes(rawJSON, "response.usageMetadata").Exists()
|
||||||
|
isFinalChunk := upstreamFinishReason != "" && usageExists
|
||||||
|
|
||||||
|
if isFinalChunk {
|
||||||
|
var finishReason string
|
||||||
|
if sawToolCall {
|
||||||
|
finishReason = "tool_calls"
|
||||||
|
} else if upstreamFinishReason == "MAX_TOKENS" {
|
||||||
|
finishReason = "max_tokens"
|
||||||
|
} else {
|
||||||
|
finishReason = "stop"
|
||||||
|
}
|
||||||
|
template, _ = sjson.Set(template, "choices.0.finish_reason", finishReason)
|
||||||
|
template, _ = sjson.Set(template, "choices.0.native_finish_reason", strings.ToLower(upstreamFinishReason))
|
||||||
}
|
}
|
||||||
|
|
||||||
return []string{template}
|
return []string{template}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package chat_completions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFinishReasonToolCallsNotOverwritten(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
var param any
|
||||||
|
|
||||||
|
// Chunk 1: Contains functionCall - should set SawToolCall = true
|
||||||
|
chunk1 := []byte(`{"response":{"candidates":[{"content":{"parts":[{"functionCall":{"name":"list_files","args":{"path":"."}}}]}}]}}`)
|
||||||
|
result1 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk1, ¶m)
|
||||||
|
|
||||||
|
// Verify chunk1 has no finish_reason (null)
|
||||||
|
if len(result1) != 1 {
|
||||||
|
t.Fatalf("Expected 1 result from chunk1, got %d", len(result1))
|
||||||
|
}
|
||||||
|
fr1 := gjson.Get(result1[0], "choices.0.finish_reason")
|
||||||
|
if fr1.Exists() && fr1.String() != "" && fr1.Type.String() != "Null" {
|
||||||
|
t.Errorf("Expected finish_reason to be null in chunk1, got: %v", fr1.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk 2: Contains finishReason STOP + usage (final chunk, no functionCall)
|
||||||
|
// This simulates what the upstream sends AFTER the tool call chunk
|
||||||
|
chunk2 := []byte(`{"response":{"candidates":[{"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":20,"totalTokenCount":30}}}`)
|
||||||
|
result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m)
|
||||||
|
|
||||||
|
// Verify chunk2 has finish_reason: "tool_calls" (not "stop")
|
||||||
|
if len(result2) != 1 {
|
||||||
|
t.Fatalf("Expected 1 result from chunk2, got %d", len(result2))
|
||||||
|
}
|
||||||
|
fr2 := gjson.Get(result2[0], "choices.0.finish_reason").String()
|
||||||
|
if fr2 != "tool_calls" {
|
||||||
|
t.Errorf("Expected finish_reason 'tool_calls', got: %s", fr2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify native_finish_reason is lowercase upstream value
|
||||||
|
nfr2 := gjson.Get(result2[0], "choices.0.native_finish_reason").String()
|
||||||
|
if nfr2 != "stop" {
|
||||||
|
t.Errorf("Expected native_finish_reason 'stop', got: %s", nfr2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFinishReasonStopForNormalText(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
var param any
|
||||||
|
|
||||||
|
// Chunk 1: Text content only
|
||||||
|
chunk1 := []byte(`{"response":{"candidates":[{"content":{"parts":[{"text":"Hello world"}]}}]}}`)
|
||||||
|
ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk1, ¶m)
|
||||||
|
|
||||||
|
// Chunk 2: Final chunk with STOP
|
||||||
|
chunk2 := []byte(`{"response":{"candidates":[{"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":5,"totalTokenCount":15}}}`)
|
||||||
|
result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m)
|
||||||
|
|
||||||
|
// Verify finish_reason is "stop" (no tool calls were made)
|
||||||
|
fr := gjson.Get(result2[0], "choices.0.finish_reason").String()
|
||||||
|
if fr != "stop" {
|
||||||
|
t.Errorf("Expected finish_reason 'stop', got: %s", fr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFinishReasonMaxTokens(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
var param any
|
||||||
|
|
||||||
|
// Chunk 1: Text content
|
||||||
|
chunk1 := []byte(`{"response":{"candidates":[{"content":{"parts":[{"text":"Hello"}]}}]}}`)
|
||||||
|
ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk1, ¶m)
|
||||||
|
|
||||||
|
// Chunk 2: Final chunk with MAX_TOKENS
|
||||||
|
chunk2 := []byte(`{"response":{"candidates":[{"finishReason":"MAX_TOKENS"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":100,"totalTokenCount":110}}}`)
|
||||||
|
result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m)
|
||||||
|
|
||||||
|
// Verify finish_reason is "max_tokens"
|
||||||
|
fr := gjson.Get(result2[0], "choices.0.finish_reason").String()
|
||||||
|
if fr != "max_tokens" {
|
||||||
|
t.Errorf("Expected finish_reason 'max_tokens', got: %s", fr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToolCallTakesPriorityOverMaxTokens(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
var param any
|
||||||
|
|
||||||
|
// Chunk 1: Contains functionCall
|
||||||
|
chunk1 := []byte(`{"response":{"candidates":[{"content":{"parts":[{"functionCall":{"name":"test","args":{}}}]}}]}}`)
|
||||||
|
ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk1, ¶m)
|
||||||
|
|
||||||
|
// Chunk 2: Final chunk with MAX_TOKENS (but we had a tool call, so tool_calls should win)
|
||||||
|
chunk2 := []byte(`{"response":{"candidates":[{"finishReason":"MAX_TOKENS"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":100,"totalTokenCount":110}}}`)
|
||||||
|
result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m)
|
||||||
|
|
||||||
|
// Verify finish_reason is "tool_calls" (takes priority over max_tokens)
|
||||||
|
fr := gjson.Get(result2[0], "choices.0.finish_reason").String()
|
||||||
|
if fr != "tool_calls" {
|
||||||
|
t.Errorf("Expected finish_reason 'tool_calls', got: %s", fr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoFinishReasonOnIntermediateChunks(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
var param any
|
||||||
|
|
||||||
|
// Chunk 1: Text content (no finish reason, no usage)
|
||||||
|
chunk1 := []byte(`{"response":{"candidates":[{"content":{"parts":[{"text":"Hello"}]}}]}}`)
|
||||||
|
result1 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk1, ¶m)
|
||||||
|
|
||||||
|
// Verify no finish_reason on intermediate chunk
|
||||||
|
fr1 := gjson.Get(result1[0], "choices.0.finish_reason")
|
||||||
|
if fr1.Exists() && fr1.String() != "" && fr1.Type.String() != "Null" {
|
||||||
|
t.Errorf("Expected no finish_reason on intermediate chunk, got: %v", fr1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk 2: More text (no finish reason, no usage)
|
||||||
|
chunk2 := []byte(`{"response":{"candidates":[{"content":{"parts":[{"text":" world"}]}}]}}`)
|
||||||
|
result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m)
|
||||||
|
|
||||||
|
// Verify no finish_reason on intermediate chunk
|
||||||
|
fr2 := gjson.Get(result2[0], "choices.0.finish_reason")
|
||||||
|
if fr2.Exists() && fr2.String() != "" && fr2.Type.String() != "Null" {
|
||||||
|
t.Errorf("Expected no finish_reason on intermediate chunk, got: %v", fr2)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -651,7 +651,7 @@ func (h *BaseAPIHandler) getRequestDetails(modelName string) (providers []string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(providers) == 0 {
|
if len(providers) == 0 {
|
||||||
return nil, "", &interfaces.ErrorMessage{StatusCode: http.StatusBadRequest, Error: fmt.Errorf("unknown provider for model %s", modelName)}
|
return nil, "", &interfaces.ErrorMessage{StatusCode: http.StatusBadGateway, Error: fmt.Errorf("unknown provider for model %s", modelName)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The thinking suffix is preserved in the model name itself, so no
|
// The thinking suffix is preserved in the model name itself, so no
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type AmpCode = internalconfig.AmpCode
|
|||||||
type OAuthModelAlias = internalconfig.OAuthModelAlias
|
type OAuthModelAlias = internalconfig.OAuthModelAlias
|
||||||
type PayloadConfig = internalconfig.PayloadConfig
|
type PayloadConfig = internalconfig.PayloadConfig
|
||||||
type PayloadRule = internalconfig.PayloadRule
|
type PayloadRule = internalconfig.PayloadRule
|
||||||
|
type PayloadFilterRule = internalconfig.PayloadFilterRule
|
||||||
type PayloadModelRule = internalconfig.PayloadModelRule
|
type PayloadModelRule = internalconfig.PayloadModelRule
|
||||||
|
|
||||||
type GeminiKey = internalconfig.GeminiKey
|
type GeminiKey = internalconfig.GeminiKey
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package logging
|
|||||||
|
|
||||||
import internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
|
import internallogging "github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
|
||||||
|
|
||||||
|
const defaultErrorLogsMaxFiles = 10
|
||||||
|
|
||||||
// RequestLogger defines the interface for logging HTTP requests and responses.
|
// RequestLogger defines the interface for logging HTTP requests and responses.
|
||||||
type RequestLogger = internallogging.RequestLogger
|
type RequestLogger = internallogging.RequestLogger
|
||||||
|
|
||||||
@@ -12,7 +14,12 @@ type StreamingLogWriter = internallogging.StreamingLogWriter
|
|||||||
// FileRequestLogger implements RequestLogger using file-based storage.
|
// FileRequestLogger implements RequestLogger using file-based storage.
|
||||||
type FileRequestLogger = internallogging.FileRequestLogger
|
type FileRequestLogger = internallogging.FileRequestLogger
|
||||||
|
|
||||||
// NewFileRequestLogger creates a new file-based request logger.
|
// NewFileRequestLogger creates a new file-based request logger with default error log retention (10 files).
|
||||||
func NewFileRequestLogger(enabled bool, logsDir string, configDir string) *FileRequestLogger {
|
func NewFileRequestLogger(enabled bool, logsDir string, configDir string) *FileRequestLogger {
|
||||||
return internallogging.NewFileRequestLogger(enabled, logsDir, configDir)
|
return internallogging.NewFileRequestLogger(enabled, logsDir, configDir, defaultErrorLogsMaxFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileRequestLoggerWithOptions creates a new file-based request logger with configurable error log retention.
|
||||||
|
func NewFileRequestLoggerWithOptions(enabled bool, logsDir string, configDir string, errorLogsMaxFiles int) *FileRequestLogger {
|
||||||
|
return internallogging.NewFileRequestLogger(enabled, logsDir, configDir, errorLogsMaxFiles)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user