mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 04:10:51 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5a25dec57 | ||
|
|
c71905e5e8 | ||
|
|
bc78d668ac | ||
|
|
5bd0896ad7 | ||
|
|
09ecfbcaed | ||
|
|
f0bd14b64f | ||
|
|
f7d82fda3f | ||
|
|
25c6b479c7 | ||
|
|
7cf9ff0345 | ||
|
|
209d74062a | ||
|
|
d86b13c9cb | ||
|
|
075e3ab69e | ||
|
|
c1c9483752 | ||
|
|
6c65fdf54b | ||
|
|
4874253d1e | ||
|
|
b72250349f | ||
|
|
116573311f | ||
|
|
4af712544d | ||
|
|
3f9c9591bd | ||
|
|
1548c567ab | ||
|
|
5b23fc570c | ||
|
|
04e1c7a05a | ||
|
|
9181e72204 | ||
|
|
4939865f6d | ||
|
|
3da7f7482e | ||
|
|
9072b029b2 | ||
|
|
c296cfb8c0 | ||
|
|
2707377fcb | ||
|
|
259f586ff7 | ||
|
|
d885b81f23 | ||
|
|
fe6bffd080 | ||
|
|
250f212fa3 | ||
|
|
a275db3fdb |
@@ -30,6 +30,10 @@ Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB
|
|||||||
<td width="180"><a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa"><img src="./assets/cubence.png" alt="Cubence" width="150"></a></td>
|
<td width="180"><a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa"><img src="./assets/cubence.png" alt="Cubence" width="150"></a></td>
|
||||||
<td>Thanks to Cubence for sponsoring this project! Cubence is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. Cubence provides special discounts for our software users: register using <a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa">this link</a> and enter the "CLIPROXYAPI" promo code during recharge to get 10% off.</td>
|
<td>Thanks to Cubence for sponsoring this project! Cubence is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. Cubence provides special discounts for our software users: register using <a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa">this link</a> and enter the "CLIPROXYAPI" promo code during recharge to get 10% off.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=TJNAIF"><img src="./assets/aicodemirror.png" alt="AICodeMirror" width="150"></a></td>
|
||||||
|
<td>Thanks to AICodeMirror for sponsoring this project! AICodeMirror provides official high-stability relay services for Claude Code / Codex / Gemini CLI, with enterprise-grade concurrency, fast invoicing, and 24/7 dedicated technical support. Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original price, with extra discounts on top-ups! AICodeMirror offers special benefits for CLIProxyAPI users: register via <a href="https://www.aicodemirror.com/register?invitecode=TJNAIF">this link</a> to enjoy 20% off your first top-up, and enterprise customers can get up to 25% off!</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -142,6 +146,10 @@ A lightweight web admin panel for CLIProxyAPI with health checks, resource monit
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
### [霖君](https://github.com/wangdabaoqq/LinJun)
|
||||||
|
|
||||||
|
霖君 is a cross-platform desktop application for managing AI programming assistants, supporting macOS, Windows, and Linux systems. Unified management of Claude Code, Gemini CLI, OpenAI Codex, Qwen Code, and other AI coding tools, with local proxy for multi-account quota tracking and one-click configuration.
|
||||||
|
|
||||||
> [!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.
|
||||||
|
|
||||||
|
|||||||
16
README_CN.md
16
README_CN.md
@@ -30,6 +30,10 @@ GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元
|
|||||||
<td width="180"><a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa"><img src="./assets/cubence.png" alt="Cubence" width="150"></a></td>
|
<td width="180"><a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa"><img src="./assets/cubence.png" alt="Cubence" width="150"></a></td>
|
||||||
<td>感谢 Cubence 对本项目的赞助!Cubence 是一家可靠高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种服务的中转。Cubence 为本软件用户提供了特别优惠:使用<a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa">此链接</a>注册,并在充值时输入 "CLIPROXYAPI" 优惠码即可享受九折优惠。</td>
|
<td>感谢 Cubence 对本项目的赞助!Cubence 是一家可靠高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种服务的中转。Cubence 为本软件用户提供了特别优惠:使用<a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa">此链接</a>注册,并在充值时输入 "CLIPROXYAPI" 优惠码即可享受九折优惠。</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=TJNAIF"><img src="./assets/aicodemirror.png" alt="AICodeMirror" width="150"></a></td>
|
||||||
|
<td>感谢 AICodeMirror 赞助了本项目!AICodeMirror 提供 Claude Code / Codex / Gemini CLI 官方高稳定中转服务,支持企业级高并发、极速开票、7×24 专属技术支持。 Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更有折上折!AICodeMirror 为 CLIProxyAPI 的用户提供了特别福利,通过<a href="https://www.aicodemirror.com/register?invitecode=TJNAIF">此链接</a>注册的用户,可享受首充8折,企业客户最高可享 7.5 折!</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -137,6 +141,14 @@ Windows 桌面应用,基于 Tauri + React 构建,用于通过 CLIProxyAPI
|
|||||||
|
|
||||||
面向 CLIProxyAPI 的 Web 管理面板,提供健康检查、资源监控、日志查看、自动更新、请求统计与定价展示,支持一键安装与 systemd 服务。
|
面向 CLIProxyAPI 的 Web 管理面板,提供健康检查、资源监控、日志查看、自动更新、请求统计与定价展示,支持一键安装与 systemd 服务。
|
||||||
|
|
||||||
|
### [CLIProxyAPI Tray](https://github.com/kitephp/CLIProxyAPI_Tray)
|
||||||
|
|
||||||
|
Windows 托盘应用,基于 PowerShell 脚本实现,不依赖任何第三方库。主要功能包括:自动创建快捷方式、静默运行、密码管理、通道切换(Main / Plus)以及自动下载与更新。
|
||||||
|
|
||||||
|
### [霖君](https://github.com/wangdabaoqq/LinJun)
|
||||||
|
|
||||||
|
霖君是一款用于管理AI编程助手的跨平台桌面应用,支持macOS、Windows、Linux系统。统一管理Claude Code、Gemini CLI、OpenAI Codex、Qwen Code等AI编程工具,本地代理实现多账户配额跟踪和一键配置。
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 如果你开发了基于 CLIProxyAPI 的项目,请提交一个 PR(拉取请求)将其添加到此列表中。
|
> 如果你开发了基于 CLIProxyAPI 的项目,请提交一个 PR(拉取请求)将其添加到此列表中。
|
||||||
|
|
||||||
@@ -148,10 +160,6 @@ 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 将其添加到此列表中。
|
||||||
|
|
||||||
|
|||||||
BIN
assets/aicodemirror.png
Normal file
BIN
assets/aicodemirror.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@@ -40,6 +40,11 @@ api-keys:
|
|||||||
# Enable debug logging
|
# Enable debug logging
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
|
# Enable pprof HTTP debug server (host:port). Keep it bound to localhost for safety.
|
||||||
|
pprof:
|
||||||
|
enable: false
|
||||||
|
addr: "127.0.0.1:8316"
|
||||||
|
|
||||||
# When true, disable high-overhead HTTP middleware features to reduce per-request memory usage under high concurrency.
|
# When true, disable high-overhead HTTP middleware features to reduce per-request memory usage under high concurrency.
|
||||||
commercial-mode: false
|
commercial-mode: false
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultPanelGitHubRepository = "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"
|
const (
|
||||||
|
DefaultPanelGitHubRepository = "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"
|
||||||
|
DefaultPprofAddr = "127.0.0.1:8316"
|
||||||
|
)
|
||||||
|
|
||||||
// Config represents the application's configuration, loaded from a YAML file.
|
// Config represents the application's configuration, loaded from a YAML file.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -41,6 +44,9 @@ type Config struct {
|
|||||||
// Debug enables or disables debug-level logging and other debug features.
|
// Debug enables or disables debug-level logging and other debug features.
|
||||||
Debug bool `yaml:"debug" json:"debug"`
|
Debug bool `yaml:"debug" json:"debug"`
|
||||||
|
|
||||||
|
// Pprof config controls the optional pprof HTTP debug server.
|
||||||
|
Pprof PprofConfig `yaml:"pprof" json:"pprof"`
|
||||||
|
|
||||||
// CommercialMode disables high-overhead HTTP middleware features to minimize per-request memory usage.
|
// CommercialMode disables high-overhead HTTP middleware features to minimize per-request memory usage.
|
||||||
CommercialMode bool `yaml:"commercial-mode" json:"commercial-mode"`
|
CommercialMode bool `yaml:"commercial-mode" json:"commercial-mode"`
|
||||||
|
|
||||||
@@ -121,6 +127,14 @@ type TLSConfig struct {
|
|||||||
Key string `yaml:"key" json:"key"`
|
Key string `yaml:"key" json:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PprofConfig holds pprof HTTP server settings.
|
||||||
|
type PprofConfig struct {
|
||||||
|
// Enable toggles the pprof HTTP debug server.
|
||||||
|
Enable bool `yaml:"enable" json:"enable"`
|
||||||
|
// Addr is the host:port address for the pprof HTTP server.
|
||||||
|
Addr string `yaml:"addr" json:"addr"`
|
||||||
|
}
|
||||||
|
|
||||||
// RemoteManagement holds management API configuration under 'remote-management'.
|
// RemoteManagement holds management API configuration under 'remote-management'.
|
||||||
type RemoteManagement struct {
|
type RemoteManagement struct {
|
||||||
// AllowRemote toggles remote (non-localhost) access to management API.
|
// AllowRemote toggles remote (non-localhost) access to management API.
|
||||||
@@ -514,6 +528,8 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
cfg.ErrorLogsMaxFiles = 10
|
cfg.ErrorLogsMaxFiles = 10
|
||||||
cfg.UsageStatisticsEnabled = false
|
cfg.UsageStatisticsEnabled = false
|
||||||
cfg.DisableCooling = false
|
cfg.DisableCooling = false
|
||||||
|
cfg.Pprof.Enable = false
|
||||||
|
cfg.Pprof.Addr = DefaultPprofAddr
|
||||||
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
||||||
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
||||||
if err = yaml.Unmarshal(data, &cfg); err != nil {
|
if err = yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
@@ -556,6 +572,11 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Pprof.Addr = strings.TrimSpace(cfg.Pprof.Addr)
|
||||||
|
if cfg.Pprof.Addr == "" {
|
||||||
|
cfg.Pprof.Addr = DefaultPprofAddr
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.LogsMaxTotalSizeMB < 0 {
|
if cfg.LogsMaxTotalSizeMB < 0 {
|
||||||
cfg.LogsMaxTotalSizeMB = 0
|
cfg.LogsMaxTotalSizeMB = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ var antigravityModelConversionTable = map[string]string{
|
|||||||
"gemini-claude-sonnet-4-5": "claude-sonnet-4-5",
|
"gemini-claude-sonnet-4-5": "claude-sonnet-4-5",
|
||||||
"gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
"gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
||||||
"gemini-claude-opus-4-5-thinking": "claude-opus-4-5-thinking",
|
"gemini-claude-opus-4-5-thinking": "claude-opus-4-5-thinking",
|
||||||
|
"gemini-claude-opus-4-6-thinking": "claude-opus-4-6-thinking",
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultAntigravityAliases returns the default oauth-model-alias configuration
|
// defaultAntigravityAliases returns the default oauth-model-alias configuration
|
||||||
@@ -30,6 +31,7 @@ func defaultAntigravityAliases() []OAuthModelAlias {
|
|||||||
{Name: "claude-sonnet-4-5", Alias: "gemini-claude-sonnet-4-5"},
|
{Name: "claude-sonnet-4-5", Alias: "gemini-claude-sonnet-4-5"},
|
||||||
{Name: "claude-sonnet-4-5-thinking", Alias: "gemini-claude-sonnet-4-5-thinking"},
|
{Name: "claude-sonnet-4-5-thinking", Alias: "gemini-claude-sonnet-4-5-thinking"},
|
||||||
{Name: "claude-opus-4-5-thinking", Alias: "gemini-claude-opus-4-5-thinking"},
|
{Name: "claude-opus-4-5-thinking", Alias: "gemini-claude-opus-4-5-thinking"},
|
||||||
|
{Name: "claude-opus-4-6-thinking", Alias: "gemini-claude-opus-4-6-thinking"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ func TestMigrateOAuthModelAlias_ConvertsAntigravityModels(t *testing.T) {
|
|||||||
if !strings.Contains(content, "claude-opus-4-5-thinking") {
|
if !strings.Contains(content, "claude-opus-4-5-thinking") {
|
||||||
t.Fatal("expected missing default alias claude-opus-4-5-thinking to be added")
|
t.Fatal("expected missing default alias claude-opus-4-5-thinking to be added")
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(content, "claude-opus-4-6-thinking") {
|
||||||
|
t.Fatal("expected missing default alias claude-opus-4-6-thinking to be added")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMigrateOAuthModelAlias_AddsDefaultIfNeitherExists(t *testing.T) {
|
func TestMigrateOAuthModelAlias_AddsDefaultIfNeitherExists(t *testing.T) {
|
||||||
|
|||||||
@@ -131,7 +131,10 @@ func ResolveLogDirectory(cfg *config.Config) string {
|
|||||||
return logDir
|
return logDir
|
||||||
}
|
}
|
||||||
if !isDirWritable(logDir) {
|
if !isDirWritable(logDir) {
|
||||||
authDir := strings.TrimSpace(cfg.AuthDir)
|
authDir, err := util.ResolveAuthDir(cfg.AuthDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to resolve auth-dir %q for log directory: %v", cfg.AuthDir, err)
|
||||||
|
}
|
||||||
if authDir != "" {
|
if authDir != "" {
|
||||||
logDir = filepath.Join(authDir, "logs")
|
logDir = filepath.Join(authDir, "logs")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ func GetClaudeModels() []*ModelInfo {
|
|||||||
MaxCompletionTokens: 64000,
|
MaxCompletionTokens: 64000,
|
||||||
Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: false},
|
Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: false},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "claude-opus-4-6-20260205",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1770318000, // 2026-02-05
|
||||||
|
OwnedBy: "anthropic",
|
||||||
|
Type: "claude",
|
||||||
|
DisplayName: "Claude 4.6 Opus",
|
||||||
|
Description: "Premium model combining maximum intelligence with practical performance",
|
||||||
|
ContextLength: 200000,
|
||||||
|
MaxCompletionTokens: 64000,
|
||||||
|
Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: false},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ID: "claude-opus-4-5-20251101",
|
ID: "claude-opus-4-5-20251101",
|
||||||
Object: "model",
|
Object: "model",
|
||||||
@@ -716,6 +728,20 @@ func GetOpenAIModels() []*ModelInfo {
|
|||||||
SupportedParameters: []string{"tools"},
|
SupportedParameters: []string{"tools"},
|
||||||
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high", "xhigh"}},
|
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high", "xhigh"}},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "gpt-5.3-codex",
|
||||||
|
Object: "model",
|
||||||
|
Created: 1770307200,
|
||||||
|
OwnedBy: "openai",
|
||||||
|
Type: "openai",
|
||||||
|
Version: "gpt-5.3",
|
||||||
|
DisplayName: "GPT 5.3 Codex",
|
||||||
|
Description: "Stable version of GPT 5.3 Codex, The best model for coding and agentic tasks across domains.",
|
||||||
|
ContextLength: 400000,
|
||||||
|
MaxCompletionTokens: 128000,
|
||||||
|
SupportedParameters: []string{"tools"},
|
||||||
|
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high", "xhigh"}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,6 +829,7 @@ func GetIFlowModels() []*ModelInfo {
|
|||||||
{ID: "minimax-m2", DisplayName: "MiniMax-M2", Description: "MiniMax M2", Created: 1758672000, Thinking: iFlowThinkingSupport},
|
{ID: "minimax-m2", DisplayName: "MiniMax-M2", Description: "MiniMax M2", Created: 1758672000, Thinking: iFlowThinkingSupport},
|
||||||
{ID: "minimax-m2.1", DisplayName: "MiniMax-M2.1", Description: "MiniMax M2.1", Created: 1766448000, Thinking: iFlowThinkingSupport},
|
{ID: "minimax-m2.1", DisplayName: "MiniMax-M2.1", Description: "MiniMax M2.1", Created: 1766448000, Thinking: iFlowThinkingSupport},
|
||||||
{ID: "iflow-rome-30ba3b", DisplayName: "iFlow-ROME", Description: "iFlow Rome 30BA3B model", Created: 1736899200},
|
{ID: "iflow-rome-30ba3b", DisplayName: "iFlow-ROME", Description: "iFlow Rome 30BA3B model", Created: 1736899200},
|
||||||
|
{ID: "kimi-k2.5", DisplayName: "Kimi-K2.5", Description: "Moonshot Kimi K2.5", Created: 1769443200, Thinking: iFlowThinkingSupport},
|
||||||
}
|
}
|
||||||
models := make([]*ModelInfo, 0, len(entries))
|
models := make([]*ModelInfo, 0, len(entries))
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
@@ -839,6 +866,7 @@ func GetAntigravityModelConfig() map[string]*AntigravityModelConfig {
|
|||||||
"gemini-3-flash": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}}},
|
"gemini-3-flash": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}}},
|
||||||
"claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
"claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
||||||
"claude-opus-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
"claude-opus-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
||||||
|
"claude-opus-4-6-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000},
|
||||||
"claude-sonnet-4-5": {MaxCompletionTokens: 64000},
|
"claude-sonnet-4-5": {MaxCompletionTokens: 64000},
|
||||||
"gpt-oss-120b-medium": {},
|
"gpt-oss-120b-medium": {},
|
||||||
"tab_flash_lite_preview": {},
|
"tab_flash_lite_preview": {},
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
URL: endpoint,
|
URL: endpoint,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: wsReq.Headers.Clone(),
|
Headers: wsReq.Headers.Clone(),
|
||||||
Body: bytes.Clone(body.payload),
|
Body: body.payload,
|
||||||
Provider: e.Identifier(),
|
Provider: e.Identifier(),
|
||||||
AuthID: authID,
|
AuthID: authID,
|
||||||
AuthLabel: authLabel,
|
AuthLabel: authLabel,
|
||||||
@@ -156,14 +156,14 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, wsResp.Status, wsResp.Headers.Clone())
|
recordAPIResponseMetadata(ctx, e.cfg, wsResp.Status, wsResp.Headers.Clone())
|
||||||
if len(wsResp.Body) > 0 {
|
if len(wsResp.Body) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bytes.Clone(wsResp.Body))
|
appendAPIResponseChunk(ctx, e.cfg, wsResp.Body)
|
||||||
}
|
}
|
||||||
if wsResp.Status < 200 || wsResp.Status >= 300 {
|
if wsResp.Status < 200 || wsResp.Status >= 300 {
|
||||||
return resp, statusErr{code: wsResp.Status, msg: string(wsResp.Body)}
|
return resp, statusErr{code: wsResp.Status, msg: string(wsResp.Body)}
|
||||||
}
|
}
|
||||||
reporter.publish(ctx, parseGeminiUsage(wsResp.Body))
|
reporter.publish(ctx, parseGeminiUsage(wsResp.Body))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, body.toFormat, opts.SourceFormat, req.Model, bytes.Clone(opts.OriginalRequest), bytes.Clone(translatedReq), bytes.Clone(wsResp.Body), ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, wsResp.Body, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: ensureColonSpacedJSON([]byte(out))}
|
resp = cliproxyexecutor.Response{Payload: ensureColonSpacedJSON([]byte(out))}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
URL: endpoint,
|
URL: endpoint,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: wsReq.Headers.Clone(),
|
Headers: wsReq.Headers.Clone(),
|
||||||
Body: bytes.Clone(body.payload),
|
Body: body.payload,
|
||||||
Provider: e.Identifier(),
|
Provider: e.Identifier(),
|
||||||
AuthID: authID,
|
AuthID: authID,
|
||||||
AuthLabel: authLabel,
|
AuthLabel: authLabel,
|
||||||
@@ -225,7 +225,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
}
|
}
|
||||||
var body bytes.Buffer
|
var body bytes.Buffer
|
||||||
if len(firstEvent.Payload) > 0 {
|
if len(firstEvent.Payload) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bytes.Clone(firstEvent.Payload))
|
appendAPIResponseChunk(ctx, e.cfg, firstEvent.Payload)
|
||||||
body.Write(firstEvent.Payload)
|
body.Write(firstEvent.Payload)
|
||||||
}
|
}
|
||||||
if firstEvent.Type == wsrelay.MessageTypeStreamEnd {
|
if firstEvent.Type == wsrelay.MessageTypeStreamEnd {
|
||||||
@@ -244,7 +244,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
metadataLogged = true
|
metadataLogged = true
|
||||||
}
|
}
|
||||||
if len(event.Payload) > 0 {
|
if len(event.Payload) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bytes.Clone(event.Payload))
|
appendAPIResponseChunk(ctx, e.cfg, event.Payload)
|
||||||
body.Write(event.Payload)
|
body.Write(event.Payload)
|
||||||
}
|
}
|
||||||
if event.Type == wsrelay.MessageTypeStreamEnd {
|
if event.Type == wsrelay.MessageTypeStreamEnd {
|
||||||
@@ -274,12 +274,12 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
}
|
}
|
||||||
case wsrelay.MessageTypeStreamChunk:
|
case wsrelay.MessageTypeStreamChunk:
|
||||||
if len(event.Payload) > 0 {
|
if len(event.Payload) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bytes.Clone(event.Payload))
|
appendAPIResponseChunk(ctx, e.cfg, event.Payload)
|
||||||
filtered := FilterSSEUsageMetadata(event.Payload)
|
filtered := FilterSSEUsageMetadata(event.Payload)
|
||||||
if detail, ok := parseGeminiStreamUsage(filtered); ok {
|
if detail, ok := parseGeminiStreamUsage(filtered); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.publish(ctx, detail)
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, bytes.Clone(opts.OriginalRequest), translatedReq, bytes.Clone(filtered), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, filtered, ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON([]byte(lines[i]))}
|
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON([]byte(lines[i]))}
|
||||||
}
|
}
|
||||||
@@ -293,9 +293,9 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
metadataLogged = true
|
metadataLogged = true
|
||||||
}
|
}
|
||||||
if len(event.Payload) > 0 {
|
if len(event.Payload) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bytes.Clone(event.Payload))
|
appendAPIResponseChunk(ctx, e.cfg, event.Payload)
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, bytes.Clone(opts.OriginalRequest), translatedReq, bytes.Clone(event.Payload), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, event.Payload, ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON([]byte(lines[i]))}
|
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON([]byte(lines[i]))}
|
||||||
}
|
}
|
||||||
@@ -350,7 +350,7 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A
|
|||||||
URL: endpoint,
|
URL: endpoint,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: wsReq.Headers.Clone(),
|
Headers: wsReq.Headers.Clone(),
|
||||||
Body: bytes.Clone(body.payload),
|
Body: body.payload,
|
||||||
Provider: e.Identifier(),
|
Provider: e.Identifier(),
|
||||||
AuthID: authID,
|
AuthID: authID,
|
||||||
AuthLabel: authLabel,
|
AuthLabel: authLabel,
|
||||||
@@ -364,7 +364,7 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A
|
|||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, resp.Status, resp.Headers.Clone())
|
recordAPIResponseMetadata(ctx, e.cfg, resp.Status, resp.Headers.Clone())
|
||||||
if len(resp.Body) > 0 {
|
if len(resp.Body) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bytes.Clone(resp.Body))
|
appendAPIResponseChunk(ctx, e.cfg, resp.Body)
|
||||||
}
|
}
|
||||||
if resp.Status < 200 || resp.Status >= 300 {
|
if resp.Status < 200 || resp.Status >= 300 {
|
||||||
return cliproxyexecutor.Response{}, statusErr{code: resp.Status, msg: string(resp.Body)}
|
return cliproxyexecutor.Response{}, statusErr{code: resp.Status, msg: string(resp.Body)}
|
||||||
@@ -373,7 +373,7 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A
|
|||||||
if totalTokens <= 0 {
|
if totalTokens <= 0 {
|
||||||
return cliproxyexecutor.Response{}, fmt.Errorf("wsrelay: totalTokens missing in response")
|
return cliproxyexecutor.Response{}, fmt.Errorf("wsrelay: totalTokens missing in response")
|
||||||
}
|
}
|
||||||
translated := sdktranslator.TranslateTokenCount(ctx, body.toFormat, opts.SourceFormat, totalTokens, bytes.Clone(resp.Body))
|
translated := sdktranslator.TranslateTokenCount(ctx, body.toFormat, opts.SourceFormat, totalTokens, resp.Body)
|
||||||
return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
|
return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,12 +393,13 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, stream)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, stream)
|
||||||
payload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), stream)
|
payload := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, stream)
|
||||||
payload, err := thinking.ApplyThinking(payload, req.Model, from.String(), to.String(), e.Identifier())
|
payload, err := thinking.ApplyThinking(payload, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, translatedPayload{}, err
|
return nil, translatedPayload{}, err
|
||||||
|
|||||||
@@ -133,12 +133,13 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("antigravity")
|
to := sdktranslator.FromString("antigravity")
|
||||||
|
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
|
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -230,7 +231,7 @@ attemptLoop:
|
|||||||
|
|
||||||
reporter.publish(ctx, parseAntigravityUsage(bodyBytes))
|
reporter.publish(ctx, parseAntigravityUsage(bodyBytes))
|
||||||
var param any
|
var param any
|
||||||
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, bodyBytes, ¶m)
|
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bodyBytes, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(converted)}
|
resp = cliproxyexecutor.Response{Payload: []byte(converted)}
|
||||||
reporter.ensurePublished(ctx)
|
reporter.ensurePublished(ctx)
|
||||||
return resp, nil
|
return resp, nil
|
||||||
@@ -274,12 +275,13 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("antigravity")
|
to := sdktranslator.FromString("antigravity")
|
||||||
|
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
|
|
||||||
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
|
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -433,7 +435,7 @@ attemptLoop:
|
|||||||
|
|
||||||
reporter.publish(ctx, parseAntigravityUsage(resp.Payload))
|
reporter.publish(ctx, parseAntigravityUsage(resp.Payload))
|
||||||
var param any
|
var param any
|
||||||
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, resp.Payload, ¶m)
|
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, resp.Payload, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(converted)}
|
resp = cliproxyexecutor.Response{Payload: []byte(converted)}
|
||||||
reporter.ensurePublished(ctx)
|
reporter.ensurePublished(ctx)
|
||||||
|
|
||||||
@@ -665,12 +667,13 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("antigravity")
|
to := sdktranslator.FromString("antigravity")
|
||||||
|
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
|
|
||||||
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
|
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -800,12 +803,12 @@ attemptLoop:
|
|||||||
reporter.publish(ctx, detail)
|
reporter.publish(ctx, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, bytes.Clone(payload), ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(payload), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tail := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, []byte("[DONE]"), ¶m)
|
tail := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, []byte("[DONE]"), ¶m)
|
||||||
for i := range tail {
|
for i := range tail {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(tail[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(tail[i])}
|
||||||
}
|
}
|
||||||
@@ -872,7 +875,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
|
|||||||
respCtx := context.WithValue(ctx, "alt", opts.Alt)
|
respCtx := context.WithValue(ctx, "alt", opts.Alt)
|
||||||
|
|
||||||
// Prepare payload once (doesn't depend on baseURL)
|
// Prepare payload once (doesn't depend on baseURL)
|
||||||
payload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
payload := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
payload, err := thinking.ApplyThinking(payload, req.Model, from.String(), to.String(), e.Identifier())
|
payload, err := thinking.ApplyThinking(payload, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1280,51 +1283,40 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
|
|||||||
payload = geminiToAntigravity(modelName, payload, projectID)
|
payload = geminiToAntigravity(modelName, payload, projectID)
|
||||||
payload, _ = sjson.SetBytes(payload, "model", modelName)
|
payload, _ = sjson.SetBytes(payload, "model", modelName)
|
||||||
|
|
||||||
if strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") {
|
useAntigravitySchema := strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high")
|
||||||
strJSON := string(payload)
|
payloadStr := string(payload)
|
||||||
paths := make([]string, 0)
|
paths := make([]string, 0)
|
||||||
util.Walk(gjson.ParseBytes(payload), "", "parametersJsonSchema", &paths)
|
util.Walk(gjson.Parse(payloadStr), "", "parametersJsonSchema", &paths)
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
strJSON, _ = util.RenameKey(strJSON, p, p[:len(p)-len("parametersJsonSchema")]+"parameters")
|
payloadStr, _ = util.RenameKey(payloadStr, p, p[:len(p)-len("parametersJsonSchema")]+"parameters")
|
||||||
}
|
|
||||||
|
|
||||||
// Use the centralized schema cleaner to handle unsupported keywords,
|
|
||||||
// const->enum conversion, and flattening of types/anyOf.
|
|
||||||
strJSON = util.CleanJSONSchemaForAntigravity(strJSON)
|
|
||||||
payload = []byte(strJSON)
|
|
||||||
} else {
|
|
||||||
strJSON := string(payload)
|
|
||||||
paths := make([]string, 0)
|
|
||||||
util.Walk(gjson.Parse(strJSON), "", "parametersJsonSchema", &paths)
|
|
||||||
for _, p := range paths {
|
|
||||||
strJSON, _ = util.RenameKey(strJSON, p, p[:len(p)-len("parametersJsonSchema")]+"parameters")
|
|
||||||
}
|
|
||||||
// Clean tool schemas for Gemini to remove unsupported JSON Schema keywords
|
|
||||||
// without adding empty-schema placeholders.
|
|
||||||
strJSON = util.CleanJSONSchemaForGemini(strJSON)
|
|
||||||
payload = []byte(strJSON)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") {
|
if useAntigravitySchema {
|
||||||
systemInstructionPartsResult := gjson.GetBytes(payload, "request.systemInstruction.parts")
|
payloadStr = util.CleanJSONSchemaForAntigravity(payloadStr)
|
||||||
payload, _ = sjson.SetBytes(payload, "request.systemInstruction.role", "user")
|
} else {
|
||||||
payload, _ = sjson.SetBytes(payload, "request.systemInstruction.parts.0.text", systemInstruction)
|
payloadStr = util.CleanJSONSchemaForGemini(payloadStr)
|
||||||
payload, _ = sjson.SetBytes(payload, "request.systemInstruction.parts.1.text", fmt.Sprintf("Please ignore following [ignore]%s[/ignore]", systemInstruction))
|
}
|
||||||
|
|
||||||
|
if useAntigravitySchema {
|
||||||
|
systemInstructionPartsResult := gjson.Get(payloadStr, "request.systemInstruction.parts")
|
||||||
|
payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.role", "user")
|
||||||
|
payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.parts.0.text", systemInstruction)
|
||||||
|
payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.parts.1.text", fmt.Sprintf("Please ignore following [ignore]%s[/ignore]", systemInstruction))
|
||||||
|
|
||||||
if systemInstructionPartsResult.Exists() && systemInstructionPartsResult.IsArray() {
|
if systemInstructionPartsResult.Exists() && systemInstructionPartsResult.IsArray() {
|
||||||
for _, partResult := range systemInstructionPartsResult.Array() {
|
for _, partResult := range systemInstructionPartsResult.Array() {
|
||||||
payload, _ = sjson.SetRawBytes(payload, "request.systemInstruction.parts.-1", []byte(partResult.Raw))
|
payloadStr, _ = sjson.SetRaw(payloadStr, "request.systemInstruction.parts.-1", partResult.Raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(modelName, "claude") {
|
if strings.Contains(modelName, "claude") {
|
||||||
payload, _ = sjson.SetBytes(payload, "request.toolConfig.functionCallingConfig.mode", "VALIDATED")
|
payloadStr, _ = sjson.Set(payloadStr, "request.toolConfig.functionCallingConfig.mode", "VALIDATED")
|
||||||
} else {
|
} else {
|
||||||
payload, _ = sjson.DeleteBytes(payload, "request.generationConfig.maxOutputTokens")
|
payloadStr, _ = sjson.Delete(payloadStr, "request.generationConfig.maxOutputTokens")
|
||||||
}
|
}
|
||||||
|
|
||||||
httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(payload))
|
httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), strings.NewReader(payloadStr))
|
||||||
if errReq != nil {
|
if errReq != nil {
|
||||||
return nil, errReq
|
return nil, errReq
|
||||||
}
|
}
|
||||||
@@ -1346,11 +1338,15 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
|
var payloadLog []byte
|
||||||
|
if e.cfg != nil && e.cfg.RequestLog {
|
||||||
|
payloadLog = []byte(payloadStr)
|
||||||
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
||||||
URL: requestURL.String(),
|
URL: requestURL.String(),
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
Body: payload,
|
Body: payloadLog,
|
||||||
Provider: e.Identifier(),
|
Provider: e.Identifier(),
|
||||||
AuthID: authID,
|
AuthID: authID,
|
||||||
AuthLabel: authLabel,
|
AuthLabel: authLabel,
|
||||||
|
|||||||
@@ -100,12 +100,13 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
to := sdktranslator.FromString("claude")
|
to := sdktranslator.FromString("claude")
|
||||||
// Use streaming translation to preserve function calling, except for claude.
|
// Use streaming translation to preserve function calling, except for claude.
|
||||||
stream := from != to
|
stream := from != to
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, stream)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, stream)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), stream)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, stream)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
@@ -216,7 +217,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
to,
|
to,
|
||||||
from,
|
from,
|
||||||
req.Model,
|
req.Model,
|
||||||
bytes.Clone(opts.OriginalRequest),
|
opts.OriginalRequest,
|
||||||
bodyForTranslation,
|
bodyForTranslation,
|
||||||
data,
|
data,
|
||||||
¶m,
|
¶m,
|
||||||
@@ -240,12 +241,13 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.trackFailure(ctx, &err)
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("claude")
|
to := sdktranslator.FromString("claude")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
@@ -381,7 +383,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
to,
|
to,
|
||||||
from,
|
from,
|
||||||
req.Model,
|
req.Model,
|
||||||
bytes.Clone(opts.OriginalRequest),
|
opts.OriginalRequest,
|
||||||
bodyForTranslation,
|
bodyForTranslation,
|
||||||
bytes.Clone(line),
|
bytes.Clone(line),
|
||||||
¶m,
|
¶m,
|
||||||
@@ -411,7 +413,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
to := sdktranslator.FromString("claude")
|
to := sdktranslator.FromString("claude")
|
||||||
// Use streaming translation to preserve function calling, except for claude.
|
// Use streaming translation to preserve function calling, except for claude.
|
||||||
stream := from != to
|
stream := from != to
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), stream)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, stream)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
if !strings.HasPrefix(baseModel, "claude-3-5-haiku") {
|
if !strings.HasPrefix(baseModel, "claude-3-5-haiku") {
|
||||||
|
|||||||
@@ -88,12 +88,13 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("codex")
|
to := sdktranslator.FromString("codex")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -176,7 +177,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(originalPayload), body, line, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, line, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -197,12 +198,13 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai-response")
|
to := sdktranslator.FromString("openai-response")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -265,7 +267,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
|||||||
reporter.publish(ctx, parseOpenAIUsage(data))
|
reporter.publish(ctx, parseOpenAIUsage(data))
|
||||||
reporter.ensurePublished(ctx)
|
reporter.ensurePublished(ctx)
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(originalPayload), body, data, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -286,12 +288,13 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("codex")
|
to := sdktranslator.FromString("codex")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -378,7 +381,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(originalPayload), body, bytes.Clone(line), ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, originalPayload, body, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
||||||
}
|
}
|
||||||
@@ -397,7 +400,7 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("codex")
|
to := sdktranslator.FromString("codex")
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
body, err := thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err := thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -119,12 +119,13 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini-cli")
|
to := sdktranslator.FromString("gemini-cli")
|
||||||
|
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
basePayload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
basePayload := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
basePayload, err = thinking.ApplyThinking(basePayload, req.Model, from.String(), to.String(), e.Identifier())
|
basePayload, err = thinking.ApplyThinking(basePayload, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -223,7 +224,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
if httpResp.StatusCode >= 200 && httpResp.StatusCode < 300 {
|
if httpResp.StatusCode >= 200 && httpResp.StatusCode < 300 {
|
||||||
reporter.publish(ctx, parseGeminiCLIUsage(data))
|
reporter.publish(ctx, parseGeminiCLIUsage(data))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, bytes.Clone(opts.OriginalRequest), payload, data, ¶m)
|
out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, opts.OriginalRequest, payload, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -272,12 +273,13 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini-cli")
|
to := sdktranslator.FromString("gemini-cli")
|
||||||
|
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
basePayload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
basePayload := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
|
|
||||||
basePayload, err = thinking.ApplyThinking(basePayload, req.Model, from.String(), to.String(), e.Identifier())
|
basePayload, err = thinking.ApplyThinking(basePayload, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -399,14 +401,14 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
reporter.publish(ctx, detail)
|
reporter.publish(ctx, detail)
|
||||||
}
|
}
|
||||||
if bytes.HasPrefix(line, dataTag) {
|
if bytes.HasPrefix(line, dataTag) {
|
||||||
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, bytes.Clone(opts.OriginalRequest), reqBody, bytes.Clone(line), ¶m)
|
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, bytes.Clone(line), ¶m)
|
||||||
for i := range segments {
|
for i := range segments {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, bytes.Clone(opts.OriginalRequest), reqBody, bytes.Clone([]byte("[DONE]")), ¶m)
|
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, []byte("[DONE]"), ¶m)
|
||||||
for i := range segments {
|
for i := range segments {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])}
|
||||||
}
|
}
|
||||||
@@ -428,12 +430,12 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
appendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseGeminiCLIUsage(data))
|
reporter.publish(ctx, parseGeminiCLIUsage(data))
|
||||||
var param any
|
var param any
|
||||||
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, bytes.Clone(opts.OriginalRequest), reqBody, data, ¶m)
|
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, data, ¶m)
|
||||||
for i := range segments {
|
for i := range segments {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])}
|
||||||
}
|
}
|
||||||
|
|
||||||
segments = sdktranslator.TranslateStream(respCtx, to, from, attemptModel, bytes.Clone(opts.OriginalRequest), reqBody, bytes.Clone([]byte("[DONE]")), ¶m)
|
segments = sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, []byte("[DONE]"), ¶m)
|
||||||
for i := range segments {
|
for i := range segments {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])}
|
||||||
}
|
}
|
||||||
@@ -485,7 +487,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.
|
|||||||
// The loop variable attemptModel is only used as the concrete model id sent to the upstream
|
// The loop variable attemptModel is only used as the concrete model id sent to the upstream
|
||||||
// Gemini CLI endpoint when iterating fallback variants.
|
// Gemini CLI endpoint when iterating fallback variants.
|
||||||
for range models {
|
for range models {
|
||||||
payload := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
payload := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
payload, err = thinking.ApplyThinking(payload, req.Model, from.String(), to.String(), e.Identifier())
|
payload, err = thinking.ApplyThinking(payload, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -116,12 +116,13 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
// Official Gemini API via API key or OAuth bearer
|
// Official Gemini API via API key or OAuth bearer
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -203,7 +204,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
appendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseGeminiUsage(data))
|
reporter.publish(ctx, parseGeminiUsage(data))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -222,12 +223,13 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -318,12 +320,12 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
if detail, ok := parseGeminiStreamUsage(payload); ok {
|
if detail, ok := parseGeminiStreamUsage(payload); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.publish(ctx, detail)
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(payload), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(payload), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone([]byte("[DONE]")), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
||||||
}
|
}
|
||||||
@@ -344,7 +346,7 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String(), e.Identifier())
|
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -318,12 +318,13 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
|
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
body = sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body = sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -417,7 +418,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -432,12 +433,13 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
|
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -521,7 +523,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
appendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseGeminiUsage(data))
|
reporter.publish(ctx, parseGeminiUsage(data))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -536,12 +538,13 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
|
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -632,12 +635,12 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
if detail, ok := parseGeminiStreamUsage(line); ok {
|
if detail, ok := parseGeminiStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.publish(ctx, detail)
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, []byte("[DONE]"), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
||||||
}
|
}
|
||||||
@@ -660,12 +663,13 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
|
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -756,12 +760,12 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
|
|||||||
if detail, ok := parseGeminiStreamUsage(line); ok {
|
if detail, ok := parseGeminiStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.publish(ctx, detail)
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, []byte("[DONE]"), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
||||||
}
|
}
|
||||||
@@ -781,7 +785,7 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
|
|
||||||
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String(), e.Identifier())
|
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -865,7 +869,7 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *
|
|||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
|
|
||||||
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String(), e.Identifier())
|
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1003,6 +1007,8 @@ func vertexBaseURL(location string) string {
|
|||||||
loc := strings.TrimSpace(location)
|
loc := strings.TrimSpace(location)
|
||||||
if loc == "" {
|
if loc == "" {
|
||||||
loc = "us-central1"
|
loc = "us-central1"
|
||||||
|
} else if loc == "global" {
|
||||||
|
return "https://aiplatform.googleapis.com"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("https://%s-aiplatform.googleapis.com", loc)
|
return fmt.Sprintf("https://%s-aiplatform.googleapis.com", loc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,12 +87,13 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), "iflow", e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), "iflow", e.Identifier())
|
||||||
@@ -163,7 +164,7 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
var param any
|
var param any
|
||||||
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
||||||
// the original model name in the response for client compatibility.
|
// the original model name in the response for client compatibility.
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -189,12 +190,13 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), "iflow", e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), "iflow", e.Identifier())
|
||||||
@@ -274,7 +276,7 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
if detail, ok := parseOpenAIStreamUsage(line); ok {
|
if detail, ok := parseOpenAIStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.publish(ctx, detail)
|
||||||
}
|
}
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
||||||
}
|
}
|
||||||
@@ -296,7 +298,7 @@ func (e *IFlowExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
enc, err := tokenizerForModel(baseModel)
|
enc, err := tokenizerForModel(baseModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func recordAPIRequest(ctx context.Context, cfg *config.Config, info upstreamRequ
|
|||||||
writeHeaders(builder, info.Headers)
|
writeHeaders(builder, info.Headers)
|
||||||
builder.WriteString("\nBody:\n")
|
builder.WriteString("\nBody:\n")
|
||||||
if len(info.Body) > 0 {
|
if len(info.Body) > 0 {
|
||||||
builder.WriteString(string(bytes.Clone(info.Body)))
|
builder.WriteString(string(info.Body))
|
||||||
} else {
|
} else {
|
||||||
builder.WriteString("<empty>")
|
builder.WriteString("<empty>")
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ func appendAPIResponseChunk(ctx context.Context, cfg *config.Config, chunk []byt
|
|||||||
if cfg == nil || !cfg.RequestLog {
|
if cfg == nil || !cfg.RequestLog {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data := bytes.TrimSpace(bytes.Clone(chunk))
|
data := bytes.TrimSpace(chunk)
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,12 +88,13 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
to = sdktranslator.FromString("openai-response")
|
to = sdktranslator.FromString("openai-response")
|
||||||
endpoint = "/responses/compact"
|
endpoint = "/responses/compact"
|
||||||
}
|
}
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, opts.Stream)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, opts.Stream)
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), opts.Stream)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, opts.Stream)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := payloadRequestedModel(opts, req.Model)
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel)
|
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel)
|
||||||
if opts.Alt == "responses/compact" {
|
if opts.Alt == "responses/compact" {
|
||||||
@@ -170,7 +171,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
reporter.ensurePublished(ctx)
|
reporter.ensurePublished(ctx)
|
||||||
// Translate response back to source format when needed
|
// Translate response back to source format when needed
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, body, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, body, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -189,12 +190,13 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := payloadRequestedModel(opts, req.Model)
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel)
|
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel)
|
||||||
|
|
||||||
@@ -283,7 +285,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
|
|
||||||
// OpenAI-compatible streams are SSE: lines typically prefixed with "data: ".
|
// OpenAI-compatible streams are SSE: lines typically prefixed with "data: ".
|
||||||
// Pass through translator; it yields one or more chunks for the target schema.
|
// Pass through translator; it yields one or more chunks for the target schema.
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, bytes.Clone(line), ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
||||||
}
|
}
|
||||||
@@ -304,7 +306,7 @@ func (e *OpenAICompatExecutor) CountTokens(ctx context.Context, auth *cliproxyau
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
modelForCounting := baseModel
|
modelForCounting := baseModel
|
||||||
|
|
||||||
|
|||||||
@@ -81,12 +81,13 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
@@ -150,7 +151,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
var param any
|
var param any
|
||||||
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
||||||
// the original model name in the response for client compatibility.
|
// the original model name in the response for client compatibility.
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -171,12 +172,13 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayloadSource := req.Payload
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
@@ -253,12 +255,12 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
if detail, ok := parseOpenAIStreamUsage(line); ok {
|
if detail, ok := parseOpenAIStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.publish(ctx, detail)
|
||||||
}
|
}
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
doneChunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone([]byte("[DONE]")), ¶m)
|
doneChunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m)
|
||||||
for i := range doneChunks {
|
for i := range doneChunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(doneChunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(doneChunks[i])}
|
||||||
}
|
}
|
||||||
@@ -276,7 +278,7 @@ func (e *QwenExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
modelName := gjson.GetBytes(body, "model").String()
|
modelName := gjson.GetBytes(body, "model").String()
|
||||||
if strings.TrimSpace(modelName) == "" {
|
if strings.TrimSpace(modelName) == "" {
|
||||||
|
|||||||
@@ -388,7 +388,12 @@ func extractGeminiConfig(body []byte, provider string) ThinkingConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check thinkingLevel first (Gemini 3 format takes precedence)
|
// Check thinkingLevel first (Gemini 3 format takes precedence)
|
||||||
if level := gjson.GetBytes(body, prefix+".thinkingLevel"); level.Exists() {
|
level := gjson.GetBytes(body, prefix+".thinkingLevel")
|
||||||
|
if !level.Exists() {
|
||||||
|
// Google official Gemini Python SDK sends snake_case field names
|
||||||
|
level = gjson.GetBytes(body, prefix+".thinking_level")
|
||||||
|
}
|
||||||
|
if level.Exists() {
|
||||||
value := level.String()
|
value := level.String()
|
||||||
switch value {
|
switch value {
|
||||||
case "none":
|
case "none":
|
||||||
@@ -401,7 +406,12 @@ func extractGeminiConfig(body []byte, provider string) ThinkingConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check thinkingBudget (Gemini 2.5 format)
|
// Check thinkingBudget (Gemini 2.5 format)
|
||||||
if budget := gjson.GetBytes(body, prefix+".thinkingBudget"); budget.Exists() {
|
budget := gjson.GetBytes(body, prefix+".thinkingBudget")
|
||||||
|
if !budget.Exists() {
|
||||||
|
// Google official Gemini Python SDK sends snake_case field names
|
||||||
|
budget = gjson.GetBytes(body, prefix+".thinking_budget")
|
||||||
|
}
|
||||||
|
if budget.Exists() {
|
||||||
value := int(budget.Int())
|
value := int(budget.Int())
|
||||||
switch value {
|
switch value {
|
||||||
case 0:
|
case 0:
|
||||||
|
|||||||
@@ -94,8 +94,10 @@ func (a *Applier) applyCompatible(body []byte, config thinking.ThinkingConfig, m
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) ([]byte, error) {
|
func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) ([]byte, error) {
|
||||||
// Remove conflicting field to avoid both thinkingLevel and thinkingBudget in output
|
// Remove conflicting fields to avoid both thinkingLevel and thinkingBudget in output
|
||||||
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget")
|
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.thinking_budget")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.thinking_level")
|
||||||
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
||||||
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts")
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts")
|
||||||
|
|
||||||
@@ -114,28 +116,30 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig)
|
|||||||
|
|
||||||
level := string(config.Level)
|
level := string(config.Level)
|
||||||
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", level)
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", level)
|
||||||
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
|
||||||
|
// Respect user's explicit includeThoughts setting from original body; default to true if not set
|
||||||
|
// Support both camelCase and snake_case variants
|
||||||
|
includeThoughts := true
|
||||||
|
if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
} else if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.include_thoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
}
|
||||||
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig, modelInfo *registry.ModelInfo, isClaude bool) ([]byte, error) {
|
func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig, modelInfo *registry.ModelInfo, isClaude bool) ([]byte, error) {
|
||||||
// Remove conflicting field to avoid both thinkingLevel and thinkingBudget in output
|
// Remove conflicting fields to avoid both thinkingLevel and thinkingBudget in output
|
||||||
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig.thinkingLevel")
|
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig.thinkingLevel")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.thinking_level")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.thinking_budget")
|
||||||
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
||||||
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts")
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts")
|
||||||
|
|
||||||
budget := config.Budget
|
budget := config.Budget
|
||||||
includeThoughts := false
|
|
||||||
switch config.Mode {
|
|
||||||
case thinking.ModeNone:
|
|
||||||
includeThoughts = false
|
|
||||||
case thinking.ModeAuto:
|
|
||||||
includeThoughts = true
|
|
||||||
default:
|
|
||||||
includeThoughts = budget > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply Claude-specific constraints
|
// Apply Claude-specific constraints first to get the final budget value
|
||||||
if isClaude && modelInfo != nil {
|
if isClaude && modelInfo != nil {
|
||||||
budget, result = a.normalizeClaudeBudget(budget, result, modelInfo)
|
budget, result = a.normalizeClaudeBudget(budget, result, modelInfo)
|
||||||
// Check if budget was removed entirely
|
// Check if budget was removed entirely
|
||||||
@@ -144,6 +148,37 @@ func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For ModeNone, always set includeThoughts to false regardless of user setting.
|
||||||
|
// This ensures that when user requests budget=0 (disable thinking output),
|
||||||
|
// the includeThoughts is correctly set to false even if budget is clamped to min.
|
||||||
|
if config.Mode == thinking.ModeNone {
|
||||||
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", false)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine includeThoughts: respect user's explicit setting from original body if provided
|
||||||
|
// Support both camelCase and snake_case variants
|
||||||
|
var includeThoughts bool
|
||||||
|
var userSetIncludeThoughts bool
|
||||||
|
if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
userSetIncludeThoughts = true
|
||||||
|
} else if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.include_thoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
userSetIncludeThoughts = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userSetIncludeThoughts {
|
||||||
|
// No explicit setting, use default logic based on mode
|
||||||
|
switch config.Mode {
|
||||||
|
case thinking.ModeAuto:
|
||||||
|
includeThoughts = true
|
||||||
|
default:
|
||||||
|
includeThoughts = budget > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts)
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts)
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
@@ -118,8 +118,10 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig)
|
|||||||
// - ModeNone + Budget>0: forced to think but hide output (includeThoughts=false)
|
// - ModeNone + Budget>0: forced to think but hide output (includeThoughts=false)
|
||||||
// ValidateConfig sets config.Level to the lowest level when ModeNone + Budget > 0.
|
// ValidateConfig sets config.Level to the lowest level when ModeNone + Budget > 0.
|
||||||
|
|
||||||
// Remove conflicting field to avoid both thinkingLevel and thinkingBudget in output
|
// Remove conflicting fields to avoid both thinkingLevel and thinkingBudget in output
|
||||||
result, _ := sjson.DeleteBytes(body, "generationConfig.thinkingConfig.thinkingBudget")
|
result, _ := sjson.DeleteBytes(body, "generationConfig.thinkingConfig.thinkingBudget")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig.thinking_budget")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig.thinking_level")
|
||||||
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
||||||
result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig.include_thoughts")
|
result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig.include_thoughts")
|
||||||
|
|
||||||
@@ -138,29 +140,58 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig)
|
|||||||
|
|
||||||
level := string(config.Level)
|
level := string(config.Level)
|
||||||
result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.thinkingLevel", level)
|
result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.thinkingLevel", level)
|
||||||
result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.includeThoughts", true)
|
|
||||||
|
// Respect user's explicit includeThoughts setting from original body; default to true if not set
|
||||||
|
// Support both camelCase and snake_case variants
|
||||||
|
includeThoughts := true
|
||||||
|
if inc := gjson.GetBytes(body, "generationConfig.thinkingConfig.includeThoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
} else if inc := gjson.GetBytes(body, "generationConfig.thinkingConfig.include_thoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
}
|
||||||
|
result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.includeThoughts", includeThoughts)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig) ([]byte, error) {
|
func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig) ([]byte, error) {
|
||||||
// Remove conflicting field to avoid both thinkingLevel and thinkingBudget in output
|
// Remove conflicting fields to avoid both thinkingLevel and thinkingBudget in output
|
||||||
result, _ := sjson.DeleteBytes(body, "generationConfig.thinkingConfig.thinkingLevel")
|
result, _ := sjson.DeleteBytes(body, "generationConfig.thinkingConfig.thinkingLevel")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig.thinking_level")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig.thinking_budget")
|
||||||
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
||||||
result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig.include_thoughts")
|
result, _ = sjson.DeleteBytes(result, "generationConfig.thinkingConfig.include_thoughts")
|
||||||
|
|
||||||
budget := config.Budget
|
budget := config.Budget
|
||||||
// ModeNone semantics:
|
|
||||||
// - ModeNone + Budget=0: completely disable thinking
|
// For ModeNone, always set includeThoughts to false regardless of user setting.
|
||||||
// - ModeNone + Budget>0: forced to think but hide output (includeThoughts=false)
|
// This ensures that when user requests budget=0 (disable thinking output),
|
||||||
// When ZeroAllowed=false, ValidateConfig clamps Budget to Min while preserving ModeNone.
|
// the includeThoughts is correctly set to false even if budget is clamped to min.
|
||||||
includeThoughts := false
|
if config.Mode == thinking.ModeNone {
|
||||||
switch config.Mode {
|
result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
case thinking.ModeNone:
|
result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.includeThoughts", false)
|
||||||
includeThoughts = false
|
return result, nil
|
||||||
case thinking.ModeAuto:
|
}
|
||||||
includeThoughts = true
|
|
||||||
default:
|
// Determine includeThoughts: respect user's explicit setting from original body if provided
|
||||||
includeThoughts = budget > 0
|
// Support both camelCase and snake_case variants
|
||||||
|
var includeThoughts bool
|
||||||
|
var userSetIncludeThoughts bool
|
||||||
|
if inc := gjson.GetBytes(body, "generationConfig.thinkingConfig.includeThoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
userSetIncludeThoughts = true
|
||||||
|
} else if inc := gjson.GetBytes(body, "generationConfig.thinkingConfig.include_thoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
userSetIncludeThoughts = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userSetIncludeThoughts {
|
||||||
|
// No explicit setting, use default logic based on mode
|
||||||
|
switch config.Mode {
|
||||||
|
case thinking.ModeAuto:
|
||||||
|
includeThoughts = true
|
||||||
|
default:
|
||||||
|
includeThoughts = budget > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.thinkingBudget", budget)
|
result, _ = sjson.SetBytes(result, "generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
|
|||||||
@@ -79,8 +79,10 @@ func (a *Applier) applyCompatible(body []byte, config thinking.ThinkingConfig) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) ([]byte, error) {
|
func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig) ([]byte, error) {
|
||||||
// Remove conflicting field to avoid both thinkingLevel and thinkingBudget in output
|
// Remove conflicting fields to avoid both thinkingLevel and thinkingBudget in output
|
||||||
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget")
|
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig.thinkingBudget")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.thinking_budget")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.thinking_level")
|
||||||
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
||||||
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts")
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts")
|
||||||
|
|
||||||
@@ -99,25 +101,58 @@ func (a *Applier) applyLevelFormat(body []byte, config thinking.ThinkingConfig)
|
|||||||
|
|
||||||
level := string(config.Level)
|
level := string(config.Level)
|
||||||
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", level)
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingLevel", level)
|
||||||
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
|
||||||
|
// Respect user's explicit includeThoughts setting from original body; default to true if not set
|
||||||
|
// Support both camelCase and snake_case variants
|
||||||
|
includeThoughts := true
|
||||||
|
if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
} else if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.include_thoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
}
|
||||||
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig) ([]byte, error) {
|
func (a *Applier) applyBudgetFormat(body []byte, config thinking.ThinkingConfig) ([]byte, error) {
|
||||||
// Remove conflicting field to avoid both thinkingLevel and thinkingBudget in output
|
// Remove conflicting fields to avoid both thinkingLevel and thinkingBudget in output
|
||||||
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig.thinkingLevel")
|
result, _ := sjson.DeleteBytes(body, "request.generationConfig.thinkingConfig.thinkingLevel")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.thinking_level")
|
||||||
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.thinking_budget")
|
||||||
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
// Normalize includeThoughts field name to avoid oneof conflicts in upstream JSON parsing.
|
||||||
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts")
|
result, _ = sjson.DeleteBytes(result, "request.generationConfig.thinkingConfig.include_thoughts")
|
||||||
|
|
||||||
budget := config.Budget
|
budget := config.Budget
|
||||||
includeThoughts := false
|
|
||||||
switch config.Mode {
|
// For ModeNone, always set includeThoughts to false regardless of user setting.
|
||||||
case thinking.ModeNone:
|
// This ensures that when user requests budget=0 (disable thinking output),
|
||||||
includeThoughts = false
|
// the includeThoughts is correctly set to false even if budget is clamped to min.
|
||||||
case thinking.ModeAuto:
|
if config.Mode == thinking.ModeNone {
|
||||||
includeThoughts = true
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
default:
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.includeThoughts", false)
|
||||||
includeThoughts = budget > 0
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine includeThoughts: respect user's explicit setting from original body if provided
|
||||||
|
// Support both camelCase and snake_case variants
|
||||||
|
var includeThoughts bool
|
||||||
|
var userSetIncludeThoughts bool
|
||||||
|
if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.includeThoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
userSetIncludeThoughts = true
|
||||||
|
} else if inc := gjson.GetBytes(body, "request.generationConfig.thinkingConfig.include_thoughts"); inc.Exists() {
|
||||||
|
includeThoughts = inc.Bool()
|
||||||
|
userSetIncludeThoughts = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userSetIncludeThoughts {
|
||||||
|
// No explicit setting, use default logic based on mode
|
||||||
|
switch config.Mode {
|
||||||
|
case thinking.ModeAuto:
|
||||||
|
includeThoughts = true
|
||||||
|
default:
|
||||||
|
includeThoughts = budget > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
result, _ = sjson.SetBytes(result, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package claude
|
package claude
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
||||||
@@ -37,7 +36,7 @@ import (
|
|||||||
// - []byte: The transformed request data in Gemini CLI API format
|
// - []byte: The transformed request data in Gemini CLI API format
|
||||||
func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
enableThoughtTranslate := true
|
enableThoughtTranslate := true
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
// system instruction
|
// system instruction
|
||||||
systemInstructionJSON := ""
|
systemInstructionJSON := ""
|
||||||
@@ -115,7 +114,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
if signatureResult.Exists() && signatureResult.String() != "" {
|
if signatureResult.Exists() && signatureResult.String() != "" {
|
||||||
arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2)
|
arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2)
|
||||||
if len(arrayClientSignatures) == 2 {
|
if len(arrayClientSignatures) == 2 {
|
||||||
if modelName == arrayClientSignatures[0] {
|
if cache.GetModelGroup(modelName) == arrayClientSignatures[0] {
|
||||||
clientSignature = arrayClientSignatures[1]
|
clientSignature = arrayClientSignatures[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package gemini
|
package gemini
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ import (
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Gemini API format
|
// - []byte: The transformed request data in Gemini API format
|
||||||
func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
template := ""
|
template := ""
|
||||||
template = `{"project":"","request":{},"model":""}`
|
template = `{"project":"","request":{},"model":""}`
|
||||||
template, _ = sjson.SetRaw(template, "request", string(rawJSON))
|
template, _ = sjson.SetRaw(template, "request", string(rawJSON))
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package chat_completions
|
package chat_completions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ const geminiCLIFunctionThoughtSignature = "skip_thought_signature_validator"
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Gemini CLI API format
|
// - []byte: The transformed request data in Gemini CLI API format
|
||||||
func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
// Base envelope (no default thinkingConfig)
|
// Base envelope (no default thinkingConfig)
|
||||||
out := []byte(`{"project":"","request":{"contents":[]},"model":"gemini-2.5-pro"}`)
|
out := []byte(`{"project":"","request":{"contents":[]},"model":"gemini-2.5-pro"}`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package responses
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
|
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/gemini"
|
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/antigravity/gemini"
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses"
|
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ConvertOpenAIResponsesRequestToAntigravity(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIResponsesRequestToAntigravity(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
rawJSON = ConvertOpenAIResponsesRequestToGemini(modelName, rawJSON, stream)
|
rawJSON = ConvertOpenAIResponsesRequestToGemini(modelName, rawJSON, stream)
|
||||||
return ConvertGeminiRequestToAntigravity(modelName, rawJSON, stream)
|
return ConvertGeminiRequestToAntigravity(modelName, rawJSON, stream)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
package geminiCLI
|
package geminiCLI
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
|
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini"
|
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -30,7 +28,7 @@ import (
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Claude Code API format
|
// - []byte: The transformed request data in Claude Code API format
|
||||||
func ConvertGeminiCLIRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertGeminiCLIRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
modelResult := gjson.GetBytes(rawJSON, "model")
|
modelResult := gjson.GetBytes(rawJSON, "model")
|
||||||
// Extract the inner request object and promote it to the top level
|
// Extract the inner request object and promote it to the top level
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package gemini
|
package gemini
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -46,7 +45,7 @@ var (
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Claude Code API format
|
// - []byte: The transformed request data in Claude Code API format
|
||||||
func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
if account == "" {
|
if account == "" {
|
||||||
u, _ := uuid.NewRandom()
|
u, _ := uuid.NewRandom()
|
||||||
@@ -116,7 +115,11 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
// Include thoughts configuration for reasoning process visibility
|
// Include thoughts configuration for reasoning process visibility
|
||||||
// Translator only does format conversion, ApplyThinking handles model capability validation.
|
// Translator only does format conversion, ApplyThinking handles model capability validation.
|
||||||
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
||||||
if thinkingLevel := thinkingConfig.Get("thinkingLevel"); thinkingLevel.Exists() {
|
thinkingLevel := thinkingConfig.Get("thinkingLevel")
|
||||||
|
if !thinkingLevel.Exists() {
|
||||||
|
thinkingLevel = thinkingConfig.Get("thinking_level")
|
||||||
|
}
|
||||||
|
if thinkingLevel.Exists() {
|
||||||
level := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
level := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
||||||
switch level {
|
switch level {
|
||||||
case "":
|
case "":
|
||||||
@@ -132,23 +135,29 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() {
|
} else {
|
||||||
budget := int(thinkingBudget.Int())
|
thinkingBudget := thinkingConfig.Get("thinkingBudget")
|
||||||
switch budget {
|
if !thinkingBudget.Exists() {
|
||||||
case 0:
|
thinkingBudget = thinkingConfig.Get("thinking_budget")
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
}
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
if thinkingBudget.Exists() {
|
||||||
case -1:
|
budget := int(thinkingBudget.Int())
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
switch budget {
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
case 0:
|
||||||
default:
|
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
case -1:
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
||||||
|
default:
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
||||||
|
}
|
||||||
|
} else if includeThoughts := thinkingConfig.Get("includeThoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
} else if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
}
|
}
|
||||||
} else if includeThoughts := thinkingConfig.Get("includeThoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
|
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
|
||||||
} else if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
|
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package chat_completions
|
package chat_completions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -44,7 +43,7 @@ var (
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Claude Code API format
|
// - []byte: The transformed request data in Claude Code API format
|
||||||
func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
if account == "" {
|
if account == "" {
|
||||||
u, _ := uuid.NewRandom()
|
u, _ := uuid.NewRandom()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package responses
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -32,7 +31,7 @@ var (
|
|||||||
// - max_output_tokens -> max_tokens
|
// - max_output_tokens -> max_tokens
|
||||||
// - stream passthrough via parameter
|
// - stream passthrough via parameter
|
||||||
func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
if account == "" {
|
if account == "" {
|
||||||
u, _ := uuid.NewRandom()
|
u, _ := uuid.NewRandom()
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package claude
|
package claude
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -35,7 +34,7 @@ import (
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in internal client format
|
// - []byte: The transformed request data in internal client format
|
||||||
func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
template := `{"model":"","instructions":"","input":[]}`
|
template := `{"model":"","instructions":"","input":[]}`
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
package geminiCLI
|
package geminiCLI
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
|
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini"
|
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -30,7 +28,7 @@ import (
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Codex API format
|
// - []byte: The transformed request data in Codex API format
|
||||||
func ConvertGeminiCLIRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertGeminiCLIRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw)
|
rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw)
|
||||||
rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelName)
|
rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelName)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package gemini
|
package gemini
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
@@ -37,7 +36,7 @@ import (
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Codex API format
|
// - []byte: The transformed request data in Codex API format
|
||||||
func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
// Base template
|
// Base template
|
||||||
out := `{"model":"","instructions":"","input":[]}`
|
out := `{"model":"","instructions":"","input":[]}`
|
||||||
|
|
||||||
@@ -243,19 +242,30 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
out, _ = sjson.Set(out, "parallel_tool_calls", true)
|
out, _ = sjson.Set(out, "parallel_tool_calls", true)
|
||||||
|
|
||||||
// Convert Gemini thinkingConfig to Codex reasoning.effort.
|
// Convert Gemini thinkingConfig to Codex reasoning.effort.
|
||||||
|
// Note: Google official Python SDK sends snake_case fields (thinking_level/thinking_budget).
|
||||||
effortSet := false
|
effortSet := false
|
||||||
if genConfig := root.Get("generationConfig"); genConfig.Exists() {
|
if genConfig := root.Get("generationConfig"); genConfig.Exists() {
|
||||||
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
||||||
if thinkingLevel := thinkingConfig.Get("thinkingLevel"); thinkingLevel.Exists() {
|
thinkingLevel := thinkingConfig.Get("thinkingLevel")
|
||||||
|
if !thinkingLevel.Exists() {
|
||||||
|
thinkingLevel = thinkingConfig.Get("thinking_level")
|
||||||
|
}
|
||||||
|
if thinkingLevel.Exists() {
|
||||||
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
||||||
if effort != "" {
|
if effort != "" {
|
||||||
out, _ = sjson.Set(out, "reasoning.effort", effort)
|
out, _ = sjson.Set(out, "reasoning.effort", effort)
|
||||||
effortSet = true
|
effortSet = true
|
||||||
}
|
}
|
||||||
} else if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() {
|
} else {
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok {
|
thinkingBudget := thinkingConfig.Get("thinkingBudget")
|
||||||
out, _ = sjson.Set(out, "reasoning.effort", effort)
|
if !thinkingBudget.Exists() {
|
||||||
effortSet = true
|
thinkingBudget = thinkingConfig.Get("thinking_budget")
|
||||||
|
}
|
||||||
|
if thinkingBudget.Exists() {
|
||||||
|
if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok {
|
||||||
|
out, _ = sjson.Set(out, "reasoning.effort", effort)
|
||||||
|
effortSet = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
package chat_completions
|
package chat_completions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -29,7 +27,7 @@ import (
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in OpenAI Responses API format
|
// - []byte: The transformed request data in OpenAI Responses API format
|
||||||
func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
// Start with empty JSON object
|
// Start with empty JSON object
|
||||||
out := `{"instructions":""}`
|
out := `{"instructions":""}`
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package responses
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -9,7 +8,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
|
inputResult := gjson.GetBytes(rawJSON, "input")
|
||||||
|
if inputResult.Type == gjson.String {
|
||||||
|
input, _ := sjson.Set(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`, "0.content.0.text", inputResult.String())
|
||||||
|
rawJSON, _ = sjson.SetRawBytes(rawJSON, "input", []byte(input))
|
||||||
|
}
|
||||||
|
|
||||||
rawJSON, _ = sjson.SetBytes(rawJSON, "stream", true)
|
rawJSON, _ = sjson.SetBytes(rawJSON, "stream", true)
|
||||||
rawJSON, _ = sjson.SetBytes(rawJSON, "store", false)
|
rawJSON, _ = sjson.SetBytes(rawJSON, "store", false)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const geminiCLIClaudeThoughtSignature = "skip_thought_signature_validator"
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Gemini CLI API format
|
// - []byte: The transformed request data in Gemini CLI API format
|
||||||
func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
||||||
|
|
||||||
// Build output Gemini CLI request JSON
|
// Build output Gemini CLI request JSON
|
||||||
@@ -116,6 +116,19 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
part, _ = sjson.Set(part, "functionResponse.name", funcName)
|
part, _ = sjson.Set(part, "functionResponse.name", funcName)
|
||||||
part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
|
part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||||
|
|
||||||
|
case "image":
|
||||||
|
source := contentResult.Get("source")
|
||||||
|
if source.Get("type").String() == "base64" {
|
||||||
|
mimeType := source.Get("media_type").String()
|
||||||
|
data := source.Get("data").String()
|
||||||
|
if mimeType != "" && data != "" {
|
||||||
|
part := `{"inlineData":{"mime_type":"","data":""}}`
|
||||||
|
part, _ = sjson.Set(part, "inlineData.mime_type", mimeType)
|
||||||
|
part, _ = sjson.Set(part, "inlineData.data", data)
|
||||||
|
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package gemini
|
package gemini
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
@@ -33,7 +32,7 @@ import (
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Gemini API format
|
// - []byte: The transformed request data in Gemini API format
|
||||||
func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
template := ""
|
template := ""
|
||||||
template = `{"project":"","request":{},"model":""}`
|
template = `{"project":"","request":{},"model":""}`
|
||||||
template, _ = sjson.SetRaw(template, "request", string(rawJSON))
|
template, _ = sjson.SetRaw(template, "request", string(rawJSON))
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package chat_completions
|
package chat_completions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ const geminiCLIFunctionThoughtSignature = "skip_thought_signature_validator"
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Gemini CLI API format
|
// - []byte: The transformed request data in Gemini CLI API format
|
||||||
func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
// Base envelope (no default thinkingConfig)
|
// Base envelope (no default thinkingConfig)
|
||||||
out := []byte(`{"project":"","request":{"contents":[]},"model":"gemini-2.5-pro"}`)
|
out := []byte(`{"project":"","request":{"contents":[]},"model":"gemini-2.5-pro"}`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package responses
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
|
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/gemini"
|
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini-cli/gemini"
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses"
|
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/responses"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ConvertOpenAIResponsesRequestToGeminiCLI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIResponsesRequestToGeminiCLI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
rawJSON = ConvertOpenAIResponsesRequestToGemini(modelName, rawJSON, stream)
|
rawJSON = ConvertOpenAIResponsesRequestToGemini(modelName, rawJSON, stream)
|
||||||
return ConvertGeminiRequestToGeminiCLI(modelName, rawJSON, stream)
|
return ConvertGeminiRequestToGeminiCLI(modelName, rawJSON, stream)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const geminiClaudeThoughtSignature = "skip_thought_signature_validator"
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request in Gemini CLI format.
|
// - []byte: The transformed request in Gemini CLI format.
|
||||||
func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
||||||
|
|
||||||
// Build output Gemini CLI request JSON
|
// Build output Gemini CLI request JSON
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package geminiCLI
|
package geminiCLI
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
@@ -19,7 +18,7 @@ import (
|
|||||||
// It extracts the model name, system instruction, message contents, and tool declarations
|
// It extracts the model name, system instruction, message contents, and tool declarations
|
||||||
// from the raw JSON request and returns them in the format expected by the internal client.
|
// from the raw JSON request and returns them in the format expected by the internal client.
|
||||||
func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
modelResult := gjson.GetBytes(rawJSON, "model")
|
modelResult := gjson.GetBytes(rawJSON, "model")
|
||||||
rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw)
|
rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw)
|
||||||
rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelResult.String())
|
rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelResult.String())
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package gemini
|
package gemini
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
@@ -19,7 +18,7 @@ import (
|
|||||||
//
|
//
|
||||||
// It keeps the payload otherwise unchanged.
|
// It keeps the payload otherwise unchanged.
|
||||||
func ConvertGeminiRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertGeminiRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
// Fast path: if no contents field, only attach safety settings
|
// Fast path: if no contents field, only attach safety settings
|
||||||
contents := gjson.GetBytes(rawJSON, "contents")
|
contents := gjson.GetBytes(rawJSON, "contents")
|
||||||
if !contents.Exists() {
|
if !contents.Exists() {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package chat_completions
|
package chat_completions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ const geminiFunctionThoughtSignature = "skip_thought_signature_validator"
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in Gemini API format
|
// - []byte: The transformed request data in Gemini API format
|
||||||
func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
// Base envelope (no default thinkingConfig)
|
// Base envelope (no default thinkingConfig)
|
||||||
out := []byte(`{"contents":[]}`)
|
out := []byte(`{"contents":[]}`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package responses
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||||
@@ -12,7 +11,7 @@ import (
|
|||||||
const geminiResponsesThoughtSignature = "skip_thought_signature_validator"
|
const geminiResponsesThoughtSignature = "skip_thought_signature_validator"
|
||||||
|
|
||||||
func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
// Note: modelName and stream parameters are part of the fixed method signature
|
// Note: modelName and stream parameters are part of the fixed method signature
|
||||||
_ = modelName // Unused but required by interface
|
_ = modelName // Unused but required by interface
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package claude
|
package claude
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
@@ -18,7 +17,7 @@ import (
|
|||||||
// It extracts the model name, system instruction, message contents, and tool declarations
|
// It extracts the model name, system instruction, message contents, and tool declarations
|
||||||
// from the raw JSON request and returns them in the format expected by the OpenAI API.
|
// from the raw JSON request and returns them in the format expected by the OpenAI API.
|
||||||
func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
// Base OpenAI Chat Completions API template
|
// Base OpenAI Chat Completions API template
|
||||||
out := `{"model":"","messages":[]}`
|
out := `{"model":"","messages":[]}`
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
package geminiCLI
|
package geminiCLI
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
|
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini"
|
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -17,7 +15,7 @@ import (
|
|||||||
// It extracts the model name, generation config, message contents, and tool declarations
|
// It extracts the model name, generation config, message contents, and tool declarations
|
||||||
// from the raw JSON request and returns them in the format expected by the OpenAI API.
|
// from the raw JSON request and returns them in the format expected by the OpenAI API.
|
||||||
func ConvertGeminiCLIRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertGeminiCLIRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw)
|
rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw)
|
||||||
rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelName)
|
rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelName)
|
||||||
if gjson.GetBytes(rawJSON, "systemInstruction").Exists() {
|
if gjson.GetBytes(rawJSON, "systemInstruction").Exists() {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package gemini
|
package gemini
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
@@ -21,7 +20,7 @@ import (
|
|||||||
// It extracts the model name, generation config, message contents, and tool declarations
|
// It extracts the model name, generation config, message contents, and tool declarations
|
||||||
// from the raw JSON request and returns them in the format expected by the OpenAI API.
|
// from the raw JSON request and returns them in the format expected by the OpenAI API.
|
||||||
func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
// Base OpenAI Chat Completions API template
|
// Base OpenAI Chat Completions API template
|
||||||
out := `{"model":"","messages":[]}`
|
out := `{"model":"","messages":[]}`
|
||||||
|
|
||||||
@@ -83,16 +82,27 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map Gemini thinkingConfig to OpenAI reasoning_effort.
|
// Map Gemini thinkingConfig to OpenAI reasoning_effort.
|
||||||
// Always perform conversion to support allowCompat models that may not be in registry
|
// Always perform conversion to support allowCompat models that may not be in registry.
|
||||||
|
// Note: Google official Python SDK sends snake_case fields (thinking_level/thinking_budget).
|
||||||
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
||||||
if thinkingLevel := thinkingConfig.Get("thinkingLevel"); thinkingLevel.Exists() {
|
thinkingLevel := thinkingConfig.Get("thinkingLevel")
|
||||||
|
if !thinkingLevel.Exists() {
|
||||||
|
thinkingLevel = thinkingConfig.Get("thinking_level")
|
||||||
|
}
|
||||||
|
if thinkingLevel.Exists() {
|
||||||
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
||||||
if effort != "" {
|
if effort != "" {
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||||
}
|
}
|
||||||
} else if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() {
|
} else {
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok {
|
thinkingBudget := thinkingConfig.Get("thinkingBudget")
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
if !thinkingBudget.Exists() {
|
||||||
|
thinkingBudget = thinkingConfig.Get("thinking_budget")
|
||||||
|
}
|
||||||
|
if thinkingBudget.Exists() {
|
||||||
|
if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok {
|
||||||
|
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package chat_completions
|
package chat_completions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ func ConvertOpenAIRequestToOpenAI(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
// If there's an error, return the original JSON or handle the error appropriately.
|
// If there's an error, return the original JSON or handle the error appropriately.
|
||||||
// For now, we'll return the original, but in a real scenario, logging or a more robust error
|
// For now, we'll return the original, but in a real scenario, logging or a more robust error
|
||||||
// handling mechanism would be needed.
|
// handling mechanism would be needed.
|
||||||
return bytes.Clone(inputRawJSON)
|
return inputRawJSON
|
||||||
}
|
}
|
||||||
return updatedJSON
|
return updatedJSON
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package responses
|
package responses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -28,7 +27,7 @@ import (
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - []byte: The transformed request data in OpenAI chat completions format
|
// - []byte: The transformed request data in OpenAI chat completions format
|
||||||
func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := inputRawJSON
|
||||||
// Base OpenAI chat completions template with default values
|
// Base OpenAI chat completions template with default values
|
||||||
out := `{"model":"","messages":[],"stream":false}`
|
out := `{"model":"","messages":[],"stream":false}`
|
||||||
|
|
||||||
@@ -68,6 +67,9 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
|||||||
case "message", "":
|
case "message", "":
|
||||||
// Handle regular message conversion
|
// Handle regular message conversion
|
||||||
role := item.Get("role").String()
|
role := item.Get("role").String()
|
||||||
|
if role == "developer" {
|
||||||
|
role = "user"
|
||||||
|
}
|
||||||
message := `{"role":"","content":""}`
|
message := `{"role":"","content":""}`
|
||||||
message, _ = sjson.Set(message, "role", role)
|
message, _ = sjson.Set(message, "role", role)
|
||||||
|
|
||||||
@@ -167,7 +169,8 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
|||||||
// Only function tools need structural conversion because Chat Completions nests details under "function".
|
// Only function tools need structural conversion because Chat Completions nests details under "function".
|
||||||
toolType := tool.Get("type").String()
|
toolType := tool.Get("type").String()
|
||||||
if toolType != "" && toolType != "function" && tool.IsObject() {
|
if toolType != "" && toolType != "function" && tool.IsObject() {
|
||||||
chatCompletionsTools = append(chatCompletionsTools, tool.Value())
|
// Almost all providers lack built-in tools, so we just ignore them.
|
||||||
|
// chatCompletionsTools = append(chatCompletionsTools, tool.Value())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ func TestIsClaudeThinkingModel(t *testing.T) {
|
|||||||
// Claude thinking models - should return true
|
// Claude thinking models - should return true
|
||||||
{"claude-sonnet-4-5-thinking", "claude-sonnet-4-5-thinking", true},
|
{"claude-sonnet-4-5-thinking", "claude-sonnet-4-5-thinking", true},
|
||||||
{"claude-opus-4-5-thinking", "claude-opus-4-5-thinking", true},
|
{"claude-opus-4-5-thinking", "claude-opus-4-5-thinking", true},
|
||||||
|
{"claude-opus-4-6-thinking", "claude-opus-4-6-thinking", true},
|
||||||
{"Claude-Sonnet-Thinking uppercase", "Claude-Sonnet-4-5-Thinking", true},
|
{"Claude-Sonnet-Thinking uppercase", "Claude-Sonnet-4-5-Thinking", true},
|
||||||
{"claude thinking mixed case", "Claude-THINKING-Model", true},
|
{"claude thinking mixed case", "Claude-THINKING-Model", true},
|
||||||
|
|
||||||
|
|||||||
@@ -61,14 +61,20 @@ func cleanJSONSchema(jsonStr string, addPlaceholder bool) string {
|
|||||||
|
|
||||||
// removeKeywords removes all occurrences of specified keywords from the JSON schema.
|
// removeKeywords removes all occurrences of specified keywords from the JSON schema.
|
||||||
func removeKeywords(jsonStr string, keywords []string) string {
|
func removeKeywords(jsonStr string, keywords []string) string {
|
||||||
|
deletePaths := make([]string, 0)
|
||||||
|
pathsByField := findPathsByFields(jsonStr, keywords)
|
||||||
for _, key := range keywords {
|
for _, key := range keywords {
|
||||||
for _, p := range findPaths(jsonStr, key) {
|
for _, p := range pathsByField[key] {
|
||||||
if isPropertyDefinition(trimSuffix(p, "."+key)) {
|
if isPropertyDefinition(trimSuffix(p, "."+key)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
jsonStr, _ = sjson.Delete(jsonStr, p)
|
deletePaths = append(deletePaths, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sortByDepth(deletePaths)
|
||||||
|
for _, p := range deletePaths {
|
||||||
|
jsonStr, _ = sjson.Delete(jsonStr, p)
|
||||||
|
}
|
||||||
return jsonStr
|
return jsonStr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,8 +241,9 @@ var unsupportedConstraints = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func moveConstraintsToDescription(jsonStr string) string {
|
func moveConstraintsToDescription(jsonStr string) string {
|
||||||
|
pathsByField := findPathsByFields(jsonStr, unsupportedConstraints)
|
||||||
for _, key := range unsupportedConstraints {
|
for _, key := range unsupportedConstraints {
|
||||||
for _, p := range findPaths(jsonStr, key) {
|
for _, p := range pathsByField[key] {
|
||||||
val := gjson.Get(jsonStr, p)
|
val := gjson.Get(jsonStr, p)
|
||||||
if !val.Exists() || val.IsObject() || val.IsArray() {
|
if !val.Exists() || val.IsObject() || val.IsArray() {
|
||||||
continue
|
continue
|
||||||
@@ -424,14 +431,21 @@ func removeUnsupportedKeywords(jsonStr string) string {
|
|||||||
"$schema", "$defs", "definitions", "const", "$ref", "additionalProperties",
|
"$schema", "$defs", "definitions", "const", "$ref", "additionalProperties",
|
||||||
"propertyNames", // Gemini doesn't support property name validation
|
"propertyNames", // Gemini doesn't support property name validation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
deletePaths := make([]string, 0)
|
||||||
|
pathsByField := findPathsByFields(jsonStr, keywords)
|
||||||
for _, key := range keywords {
|
for _, key := range keywords {
|
||||||
for _, p := range findPaths(jsonStr, key) {
|
for _, p := range pathsByField[key] {
|
||||||
if isPropertyDefinition(trimSuffix(p, "."+key)) {
|
if isPropertyDefinition(trimSuffix(p, "."+key)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
jsonStr, _ = sjson.Delete(jsonStr, p)
|
deletePaths = append(deletePaths, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sortByDepth(deletePaths)
|
||||||
|
for _, p := range deletePaths {
|
||||||
|
jsonStr, _ = sjson.Delete(jsonStr, p)
|
||||||
|
}
|
||||||
// Remove x-* extension fields (e.g., x-google-enum-descriptions) that are not supported by Gemini API
|
// Remove x-* extension fields (e.g., x-google-enum-descriptions) that are not supported by Gemini API
|
||||||
jsonStr = removeExtensionFields(jsonStr)
|
jsonStr = removeExtensionFields(jsonStr)
|
||||||
return jsonStr
|
return jsonStr
|
||||||
@@ -581,6 +595,42 @@ func findPaths(jsonStr, field string) []string {
|
|||||||
return paths
|
return paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findPathsByFields(jsonStr string, fields []string) map[string][]string {
|
||||||
|
set := make(map[string]struct{}, len(fields))
|
||||||
|
for _, field := range fields {
|
||||||
|
set[field] = struct{}{}
|
||||||
|
}
|
||||||
|
paths := make(map[string][]string, len(set))
|
||||||
|
walkForFields(gjson.Parse(jsonStr), "", set, paths)
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
func walkForFields(value gjson.Result, path string, fields map[string]struct{}, paths map[string][]string) {
|
||||||
|
switch value.Type {
|
||||||
|
case gjson.JSON:
|
||||||
|
value.ForEach(func(key, val gjson.Result) bool {
|
||||||
|
keyStr := key.String()
|
||||||
|
safeKey := escapeGJSONPathKey(keyStr)
|
||||||
|
|
||||||
|
var childPath string
|
||||||
|
if path == "" {
|
||||||
|
childPath = safeKey
|
||||||
|
} else {
|
||||||
|
childPath = path + "." + safeKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := fields[keyStr]; ok {
|
||||||
|
paths[keyStr] = append(paths[keyStr], childPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
walkForFields(val, childPath, fields, paths)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
case gjson.String, gjson.Number, gjson.True, gjson.False, gjson.Null:
|
||||||
|
// Terminal types - no further traversal needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sortByDepth(paths []string) {
|
func sortByDepth(paths []string) {
|
||||||
sort.Slice(paths, func(i, j int) bool { return len(paths[i]) > len(paths[j]) })
|
sort.Slice(paths, func(i, j int) bool { return len(paths[i]) > len(paths[j]) })
|
||||||
}
|
}
|
||||||
@@ -667,6 +717,9 @@ func orDefault(val, def string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func escapeGJSONPathKey(key string) string {
|
func escapeGJSONPathKey(key string) string {
|
||||||
|
if strings.IndexAny(key, ".*?") == -1 {
|
||||||
|
return key
|
||||||
|
}
|
||||||
return gjsonPathKeyReplacer.Replace(key)
|
return gjsonPathKeyReplacer.Replace(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package util
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -33,15 +32,15 @@ func Walk(value gjson.Result, path, field string, paths *[]string) {
|
|||||||
// . -> \.
|
// . -> \.
|
||||||
// * -> \*
|
// * -> \*
|
||||||
// ? -> \?
|
// ? -> \?
|
||||||
var keyReplacer = strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\?")
|
keyStr := key.String()
|
||||||
safeKey := keyReplacer.Replace(key.String())
|
safeKey := escapeGJSONPathKey(keyStr)
|
||||||
|
|
||||||
if path == "" {
|
if path == "" {
|
||||||
childPath = safeKey
|
childPath = safeKey
|
||||||
} else {
|
} else {
|
||||||
childPath = path + "." + safeKey
|
childPath = path + "." + safeKey
|
||||||
}
|
}
|
||||||
if key.String() == field {
|
if keyStr == field {
|
||||||
*paths = append(*paths, childPath)
|
*paths = append(*paths, childPath)
|
||||||
}
|
}
|
||||||
Walk(val, childPath, field, paths)
|
Walk(val, childPath, field, paths)
|
||||||
@@ -87,15 +86,6 @@ func RenameKey(jsonStr, oldKeyPath, newKeyPath string) (string, error) {
|
|||||||
return finalJson, nil
|
return finalJson, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteKey(jsonStr, keyName string) string {
|
|
||||||
paths := make([]string, 0)
|
|
||||||
Walk(gjson.Parse(jsonStr), "", keyName, &paths)
|
|
||||||
for _, p := range paths {
|
|
||||||
jsonStr, _ = sjson.Delete(jsonStr, p)
|
|
||||||
}
|
|
||||||
return jsonStr
|
|
||||||
}
|
|
||||||
|
|
||||||
// FixJSON converts non-standard JSON that uses single quotes for strings into
|
// FixJSON converts non-standard JSON that uses single quotes for strings into
|
||||||
// RFC 8259-compliant JSON by converting those single-quoted strings to
|
// RFC 8259-compliant JSON by converting those single-quoted strings to
|
||||||
// double-quoted strings with proper escaping.
|
// double-quoted strings with proper escaping.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/diff"
|
||||||
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -72,6 +74,7 @@ func (w *Watcher) reloadClients(rescanAuth bool, affectedOAuthProviders []string
|
|||||||
w.clientsMutex.Lock()
|
w.clientsMutex.Lock()
|
||||||
|
|
||||||
w.lastAuthHashes = make(map[string]string)
|
w.lastAuthHashes = make(map[string]string)
|
||||||
|
w.lastAuthContents = make(map[string]*coreauth.Auth)
|
||||||
if resolvedAuthDir, errResolveAuthDir := util.ResolveAuthDir(cfg.AuthDir); errResolveAuthDir != nil {
|
if resolvedAuthDir, errResolveAuthDir := util.ResolveAuthDir(cfg.AuthDir); errResolveAuthDir != nil {
|
||||||
log.Errorf("failed to resolve auth directory for hash cache: %v", errResolveAuthDir)
|
log.Errorf("failed to resolve auth directory for hash cache: %v", errResolveAuthDir)
|
||||||
} else if resolvedAuthDir != "" {
|
} else if resolvedAuthDir != "" {
|
||||||
@@ -84,6 +87,11 @@ func (w *Watcher) reloadClients(rescanAuth bool, affectedOAuthProviders []string
|
|||||||
sum := sha256.Sum256(data)
|
sum := sha256.Sum256(data)
|
||||||
normalizedPath := w.normalizeAuthPath(path)
|
normalizedPath := w.normalizeAuthPath(path)
|
||||||
w.lastAuthHashes[normalizedPath] = hex.EncodeToString(sum[:])
|
w.lastAuthHashes[normalizedPath] = hex.EncodeToString(sum[:])
|
||||||
|
// Parse and cache auth content for future diff comparisons
|
||||||
|
var auth coreauth.Auth
|
||||||
|
if errParse := json.Unmarshal(data, &auth); errParse == nil {
|
||||||
|
w.lastAuthContents[normalizedPath] = &auth
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -127,6 +135,13 @@ func (w *Watcher) addOrUpdateClient(path string) {
|
|||||||
curHash := hex.EncodeToString(sum[:])
|
curHash := hex.EncodeToString(sum[:])
|
||||||
normalized := w.normalizeAuthPath(path)
|
normalized := w.normalizeAuthPath(path)
|
||||||
|
|
||||||
|
// Parse new auth content for diff comparison
|
||||||
|
var newAuth coreauth.Auth
|
||||||
|
if errParse := json.Unmarshal(data, &newAuth); errParse != nil {
|
||||||
|
log.Errorf("failed to parse auth file %s: %v", filepath.Base(path), errParse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
w.clientsMutex.Lock()
|
w.clientsMutex.Lock()
|
||||||
|
|
||||||
cfg := w.config
|
cfg := w.config
|
||||||
@@ -141,7 +156,26 @@ func (w *Watcher) addOrUpdateClient(path string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get old auth for diff comparison
|
||||||
|
var oldAuth *coreauth.Auth
|
||||||
|
if w.lastAuthContents != nil {
|
||||||
|
oldAuth = w.lastAuthContents[normalized]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute and log field changes
|
||||||
|
if changes := diff.BuildAuthChangeDetails(oldAuth, &newAuth); len(changes) > 0 {
|
||||||
|
log.Debugf("auth field changes for %s:", filepath.Base(path))
|
||||||
|
for _, c := range changes {
|
||||||
|
log.Debugf(" %s", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update caches
|
||||||
w.lastAuthHashes[normalized] = curHash
|
w.lastAuthHashes[normalized] = curHash
|
||||||
|
if w.lastAuthContents == nil {
|
||||||
|
w.lastAuthContents = make(map[string]*coreauth.Auth)
|
||||||
|
}
|
||||||
|
w.lastAuthContents[normalized] = &newAuth
|
||||||
|
|
||||||
w.clientsMutex.Unlock() // Unlock before the callback
|
w.clientsMutex.Unlock() // Unlock before the callback
|
||||||
|
|
||||||
@@ -160,6 +194,7 @@ func (w *Watcher) removeClient(path string) {
|
|||||||
|
|
||||||
cfg := w.config
|
cfg := w.config
|
||||||
delete(w.lastAuthHashes, normalized)
|
delete(w.lastAuthHashes, normalized)
|
||||||
|
delete(w.lastAuthContents, normalized)
|
||||||
|
|
||||||
w.clientsMutex.Unlock() // Release the lock before the callback
|
w.clientsMutex.Unlock() // Release the lock before the callback
|
||||||
|
|
||||||
|
|||||||
44
internal/watcher/diff/auth_diff.go
Normal file
44
internal/watcher/diff/auth_diff.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// auth_diff.go computes human-readable diffs for auth file field changes.
|
||||||
|
package diff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildAuthChangeDetails computes a redacted, human-readable list of auth field changes.
|
||||||
|
// Only prefix, proxy_url, and disabled fields are tracked; sensitive data is never printed.
|
||||||
|
func BuildAuthChangeDetails(oldAuth, newAuth *coreauth.Auth) []string {
|
||||||
|
changes := make([]string, 0, 3)
|
||||||
|
|
||||||
|
// Handle nil cases by using empty Auth as default
|
||||||
|
if oldAuth == nil {
|
||||||
|
oldAuth = &coreauth.Auth{}
|
||||||
|
}
|
||||||
|
if newAuth == nil {
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare prefix
|
||||||
|
oldPrefix := strings.TrimSpace(oldAuth.Prefix)
|
||||||
|
newPrefix := strings.TrimSpace(newAuth.Prefix)
|
||||||
|
if oldPrefix != newPrefix {
|
||||||
|
changes = append(changes, fmt.Sprintf("prefix: %s -> %s", oldPrefix, newPrefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare proxy_url (redacted)
|
||||||
|
oldProxy := strings.TrimSpace(oldAuth.ProxyURL)
|
||||||
|
newProxy := strings.TrimSpace(newAuth.ProxyURL)
|
||||||
|
if oldProxy != newProxy {
|
||||||
|
changes = append(changes, fmt.Sprintf("proxy_url: %s -> %s", formatProxyURL(oldProxy), formatProxyURL(newProxy)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare disabled
|
||||||
|
if oldAuth.Disabled != newAuth.Disabled {
|
||||||
|
changes = append(changes, fmt.Sprintf("disabled: %t -> %t", oldAuth.Disabled, newAuth.Disabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes
|
||||||
|
}
|
||||||
@@ -27,6 +27,12 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
|||||||
if oldCfg.Debug != newCfg.Debug {
|
if oldCfg.Debug != newCfg.Debug {
|
||||||
changes = append(changes, fmt.Sprintf("debug: %t -> %t", oldCfg.Debug, newCfg.Debug))
|
changes = append(changes, fmt.Sprintf("debug: %t -> %t", oldCfg.Debug, newCfg.Debug))
|
||||||
}
|
}
|
||||||
|
if oldCfg.Pprof.Enable != newCfg.Pprof.Enable {
|
||||||
|
changes = append(changes, fmt.Sprintf("pprof.enable: %t -> %t", oldCfg.Pprof.Enable, newCfg.Pprof.Enable))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(oldCfg.Pprof.Addr) != strings.TrimSpace(newCfg.Pprof.Addr) {
|
||||||
|
changes = append(changes, fmt.Sprintf("pprof.addr: %s -> %s", strings.TrimSpace(oldCfg.Pprof.Addr), strings.TrimSpace(newCfg.Pprof.Addr)))
|
||||||
|
}
|
||||||
if oldCfg.LoggingToFile != newCfg.LoggingToFile {
|
if oldCfg.LoggingToFile != newCfg.LoggingToFile {
|
||||||
changes = append(changes, fmt.Sprintf("logging-to-file: %t -> %t", oldCfg.LoggingToFile, newCfg.LoggingToFile))
|
changes = append(changes, fmt.Sprintf("logging-to-file: %t -> %t", oldCfg.LoggingToFile, newCfg.LoggingToFile))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type Watcher struct {
|
|||||||
reloadCallback func(*config.Config)
|
reloadCallback func(*config.Config)
|
||||||
watcher *fsnotify.Watcher
|
watcher *fsnotify.Watcher
|
||||||
lastAuthHashes map[string]string
|
lastAuthHashes map[string]string
|
||||||
|
lastAuthContents map[string]*coreauth.Auth
|
||||||
lastRemoveTimes map[string]time.Time
|
lastRemoveTimes map[string]time.Time
|
||||||
lastConfigHash string
|
lastConfigHash string
|
||||||
authQueue chan<- AuthUpdate
|
authQueue chan<- AuthUpdate
|
||||||
|
|||||||
@@ -155,20 +155,6 @@ func requestExecutionMetadata(ctx context.Context) map[string]any {
|
|||||||
return map[string]any{idempotencyKeyMetadataKey: key}
|
return map[string]any{idempotencyKeyMetadataKey: key}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeMetadata(base, overlay map[string]any) map[string]any {
|
|
||||||
if len(base) == 0 && len(overlay) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := make(map[string]any, len(base)+len(overlay))
|
|
||||||
for k, v := range base {
|
|
||||||
out[k] = v
|
|
||||||
}
|
|
||||||
for k, v := range overlay {
|
|
||||||
out[k] = v
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// BaseAPIHandler contains the handlers for API endpoints.
|
// BaseAPIHandler contains the handlers for API endpoints.
|
||||||
// It holds a pool of clients to interact with the backend service and manages
|
// It holds a pool of clients to interact with the backend service and manages
|
||||||
// load balancing, client selection, and configuration.
|
// load balancing, client selection, and configuration.
|
||||||
@@ -391,14 +377,18 @@ func (h *BaseAPIHandler) ExecuteWithAuthManager(ctx context.Context, handlerType
|
|||||||
}
|
}
|
||||||
reqMeta := requestExecutionMetadata(ctx)
|
reqMeta := requestExecutionMetadata(ctx)
|
||||||
reqMeta[coreexecutor.RequestedModelMetadataKey] = normalizedModel
|
reqMeta[coreexecutor.RequestedModelMetadataKey] = normalizedModel
|
||||||
|
payload := rawJSON
|
||||||
|
if len(payload) == 0 {
|
||||||
|
payload = nil
|
||||||
|
}
|
||||||
req := coreexecutor.Request{
|
req := coreexecutor.Request{
|
||||||
Model: normalizedModel,
|
Model: normalizedModel,
|
||||||
Payload: cloneBytes(rawJSON),
|
Payload: payload,
|
||||||
}
|
}
|
||||||
opts := coreexecutor.Options{
|
opts := coreexecutor.Options{
|
||||||
Stream: false,
|
Stream: false,
|
||||||
Alt: alt,
|
Alt: alt,
|
||||||
OriginalRequest: cloneBytes(rawJSON),
|
OriginalRequest: rawJSON,
|
||||||
SourceFormat: sdktranslator.FromString(handlerType),
|
SourceFormat: sdktranslator.FromString(handlerType),
|
||||||
}
|
}
|
||||||
opts.Metadata = reqMeta
|
opts.Metadata = reqMeta
|
||||||
@@ -418,7 +408,7 @@ func (h *BaseAPIHandler) ExecuteWithAuthManager(ctx context.Context, handlerType
|
|||||||
}
|
}
|
||||||
return nil, &interfaces.ErrorMessage{StatusCode: status, Error: err, Addon: addon}
|
return nil, &interfaces.ErrorMessage{StatusCode: status, Error: err, Addon: addon}
|
||||||
}
|
}
|
||||||
return cloneBytes(resp.Payload), nil
|
return resp.Payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteCountWithAuthManager executes a non-streaming request via the core auth manager.
|
// ExecuteCountWithAuthManager executes a non-streaming request via the core auth manager.
|
||||||
@@ -430,14 +420,18 @@ func (h *BaseAPIHandler) ExecuteCountWithAuthManager(ctx context.Context, handle
|
|||||||
}
|
}
|
||||||
reqMeta := requestExecutionMetadata(ctx)
|
reqMeta := requestExecutionMetadata(ctx)
|
||||||
reqMeta[coreexecutor.RequestedModelMetadataKey] = normalizedModel
|
reqMeta[coreexecutor.RequestedModelMetadataKey] = normalizedModel
|
||||||
|
payload := rawJSON
|
||||||
|
if len(payload) == 0 {
|
||||||
|
payload = nil
|
||||||
|
}
|
||||||
req := coreexecutor.Request{
|
req := coreexecutor.Request{
|
||||||
Model: normalizedModel,
|
Model: normalizedModel,
|
||||||
Payload: cloneBytes(rawJSON),
|
Payload: payload,
|
||||||
}
|
}
|
||||||
opts := coreexecutor.Options{
|
opts := coreexecutor.Options{
|
||||||
Stream: false,
|
Stream: false,
|
||||||
Alt: alt,
|
Alt: alt,
|
||||||
OriginalRequest: cloneBytes(rawJSON),
|
OriginalRequest: rawJSON,
|
||||||
SourceFormat: sdktranslator.FromString(handlerType),
|
SourceFormat: sdktranslator.FromString(handlerType),
|
||||||
}
|
}
|
||||||
opts.Metadata = reqMeta
|
opts.Metadata = reqMeta
|
||||||
@@ -457,7 +451,7 @@ func (h *BaseAPIHandler) ExecuteCountWithAuthManager(ctx context.Context, handle
|
|||||||
}
|
}
|
||||||
return nil, &interfaces.ErrorMessage{StatusCode: status, Error: err, Addon: addon}
|
return nil, &interfaces.ErrorMessage{StatusCode: status, Error: err, Addon: addon}
|
||||||
}
|
}
|
||||||
return cloneBytes(resp.Payload), nil
|
return resp.Payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteStreamWithAuthManager executes a streaming request via the core auth manager.
|
// ExecuteStreamWithAuthManager executes a streaming request via the core auth manager.
|
||||||
@@ -472,14 +466,18 @@ func (h *BaseAPIHandler) ExecuteStreamWithAuthManager(ctx context.Context, handl
|
|||||||
}
|
}
|
||||||
reqMeta := requestExecutionMetadata(ctx)
|
reqMeta := requestExecutionMetadata(ctx)
|
||||||
reqMeta[coreexecutor.RequestedModelMetadataKey] = normalizedModel
|
reqMeta[coreexecutor.RequestedModelMetadataKey] = normalizedModel
|
||||||
|
payload := rawJSON
|
||||||
|
if len(payload) == 0 {
|
||||||
|
payload = nil
|
||||||
|
}
|
||||||
req := coreexecutor.Request{
|
req := coreexecutor.Request{
|
||||||
Model: normalizedModel,
|
Model: normalizedModel,
|
||||||
Payload: cloneBytes(rawJSON),
|
Payload: payload,
|
||||||
}
|
}
|
||||||
opts := coreexecutor.Options{
|
opts := coreexecutor.Options{
|
||||||
Stream: true,
|
Stream: true,
|
||||||
Alt: alt,
|
Alt: alt,
|
||||||
OriginalRequest: cloneBytes(rawJSON),
|
OriginalRequest: rawJSON,
|
||||||
SourceFormat: sdktranslator.FromString(handlerType),
|
SourceFormat: sdktranslator.FromString(handlerType),
|
||||||
}
|
}
|
||||||
opts.Metadata = reqMeta
|
opts.Metadata = reqMeta
|
||||||
@@ -668,17 +666,6 @@ func cloneBytes(src []byte) []byte {
|
|||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneMetadata(src map[string]any) map[string]any {
|
|
||||||
if len(src) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dst := make(map[string]any, len(src))
|
|
||||||
for k, v := range src {
|
|
||||||
dst[k] = v
|
|
||||||
}
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteErrorResponse writes an error message to the response writer using the HTTP status embedded in the message.
|
// WriteErrorResponse writes an error message to the response writer using the HTTP status embedded in the message.
|
||||||
func (h *BaseAPIHandler) WriteErrorResponse(c *gin.Context, msg *interfaces.ErrorMessage) {
|
func (h *BaseAPIHandler) WriteErrorResponse(c *gin.Context, msg *interfaces.ErrorMessage) {
|
||||||
status := http.StatusInternalServerError
|
status := http.StatusInternalServerError
|
||||||
@@ -709,7 +696,7 @@ func (h *BaseAPIHandler) WriteErrorResponse(c *gin.Context, msg *interfaces.Erro
|
|||||||
var previous []byte
|
var previous []byte
|
||||||
if existing, exists := c.Get("API_RESPONSE"); exists {
|
if existing, exists := c.Get("API_RESPONSE"); exists {
|
||||||
if existingBytes, ok := existing.([]byte); ok && len(existingBytes) > 0 {
|
if existingBytes, ok := existing.([]byte); ok && len(existingBytes) > 0 {
|
||||||
previous = bytes.Clone(existingBytes)
|
previous = existingBytes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
appendAPIResponse(c, body)
|
appendAPIResponse(c, body)
|
||||||
|
|||||||
163
sdk/cliproxy/pprof_server.go
Normal file
163
sdk/cliproxy/pprof_server.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package cliproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/pprof"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pprofServer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
server *http.Server
|
||||||
|
addr string
|
||||||
|
enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPprofServer() *pprofServer {
|
||||||
|
return &pprofServer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) applyPprofConfig(cfg *config.Config) {
|
||||||
|
if s == nil || cfg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.pprofServer == nil {
|
||||||
|
s.pprofServer = newPprofServer()
|
||||||
|
}
|
||||||
|
s.pprofServer.Apply(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) shutdownPprof(ctx context.Context) error {
|
||||||
|
if s == nil || s.pprofServer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.pprofServer.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pprofServer) Apply(cfg *config.Config) {
|
||||||
|
if p == nil || cfg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addr := strings.TrimSpace(cfg.Pprof.Addr)
|
||||||
|
if addr == "" {
|
||||||
|
addr = config.DefaultPprofAddr
|
||||||
|
}
|
||||||
|
enabled := cfg.Pprof.Enable
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
currentServer := p.server
|
||||||
|
currentAddr := p.addr
|
||||||
|
p.addr = addr
|
||||||
|
p.enabled = enabled
|
||||||
|
if !enabled {
|
||||||
|
p.server = nil
|
||||||
|
p.mu.Unlock()
|
||||||
|
if currentServer != nil {
|
||||||
|
p.stopServer(currentServer, currentAddr, "disabled")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if currentServer != nil && currentAddr == addr {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.server = nil
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
if currentServer != nil {
|
||||||
|
p.stopServer(currentServer, currentAddr, "restarted")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.startServer(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pprofServer) Shutdown(ctx context.Context) error {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.mu.Lock()
|
||||||
|
currentServer := p.server
|
||||||
|
currentAddr := p.addr
|
||||||
|
p.server = nil
|
||||||
|
p.enabled = false
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
if currentServer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.stopServerWithContext(ctx, currentServer, currentAddr, "shutdown")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pprofServer) startServer(addr string) {
|
||||||
|
mux := newPprofMux()
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
if !p.enabled || p.addr != addr || p.server != nil {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.server = server
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
log.Infof("pprof server starting on %s", addr)
|
||||||
|
go func() {
|
||||||
|
if errServe := server.ListenAndServe(); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {
|
||||||
|
log.Errorf("pprof server failed on %s: %v", addr, errServe)
|
||||||
|
p.mu.Lock()
|
||||||
|
if p.server == server {
|
||||||
|
p.server = nil
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pprofServer) stopServer(server *http.Server, addr string, reason string) {
|
||||||
|
_ = p.stopServerWithContext(context.Background(), server, addr, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pprofServer) stopServerWithContext(ctx context.Context, server *http.Server, addr string, reason string) error {
|
||||||
|
if server == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
stopCtx := ctx
|
||||||
|
if stopCtx == nil {
|
||||||
|
stopCtx = context.Background()
|
||||||
|
}
|
||||||
|
stopCtx, cancel := context.WithTimeout(stopCtx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if errStop := server.Shutdown(stopCtx); errStop != nil {
|
||||||
|
log.Errorf("pprof server stop failed on %s: %v", addr, errStop)
|
||||||
|
return errStop
|
||||||
|
}
|
||||||
|
log.Infof("pprof server stopped on %s (%s)", addr, reason)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPprofMux() *http.ServeMux {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||||
|
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||||
|
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||||
|
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||||
|
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||||
|
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
|
||||||
|
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
|
||||||
|
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
|
||||||
|
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
|
||||||
|
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
|
||||||
|
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
|
||||||
|
return mux
|
||||||
|
}
|
||||||
@@ -57,6 +57,9 @@ type Service struct {
|
|||||||
// server is the HTTP API server instance.
|
// server is the HTTP API server instance.
|
||||||
server *api.Server
|
server *api.Server
|
||||||
|
|
||||||
|
// pprofServer manages the optional pprof HTTP debug server.
|
||||||
|
pprofServer *pprofServer
|
||||||
|
|
||||||
// serverErr channel for server startup/shutdown errors.
|
// serverErr channel for server startup/shutdown errors.
|
||||||
serverErr chan error
|
serverErr chan error
|
||||||
|
|
||||||
@@ -270,27 +273,42 @@ func (s *Service) wsOnDisconnected(channelID string, reason error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) applyCoreAuthAddOrUpdate(ctx context.Context, auth *coreauth.Auth) {
|
func (s *Service) applyCoreAuthAddOrUpdate(ctx context.Context, auth *coreauth.Auth) {
|
||||||
if s == nil || auth == nil || auth.ID == "" {
|
if s == nil || s.coreManager == nil || auth == nil || auth.ID == "" {
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.coreManager == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
auth = auth.Clone()
|
auth = auth.Clone()
|
||||||
s.ensureExecutorsForAuth(auth)
|
s.ensureExecutorsForAuth(auth)
|
||||||
s.registerModelsForAuth(auth)
|
|
||||||
if existing, ok := s.coreManager.GetByID(auth.ID); ok && existing != nil {
|
// IMPORTANT: Update coreManager FIRST, before model registration.
|
||||||
|
// This ensures that configuration changes (proxy_url, prefix, etc.) take effect
|
||||||
|
// immediately for API calls, rather than waiting for model registration to complete.
|
||||||
|
// Model registration may involve network calls (e.g., FetchAntigravityModels) that
|
||||||
|
// could timeout if the new proxy_url is unreachable.
|
||||||
|
op := "register"
|
||||||
|
var err error
|
||||||
|
if existing, ok := s.coreManager.GetByID(auth.ID); ok {
|
||||||
auth.CreatedAt = existing.CreatedAt
|
auth.CreatedAt = existing.CreatedAt
|
||||||
auth.LastRefreshedAt = existing.LastRefreshedAt
|
auth.LastRefreshedAt = existing.LastRefreshedAt
|
||||||
auth.NextRefreshAfter = existing.NextRefreshAfter
|
auth.NextRefreshAfter = existing.NextRefreshAfter
|
||||||
if _, err := s.coreManager.Update(ctx, auth); err != nil {
|
op = "update"
|
||||||
log.Errorf("failed to update auth %s: %v", auth.ID, err)
|
_, err = s.coreManager.Update(ctx, auth)
|
||||||
|
} else {
|
||||||
|
_, err = s.coreManager.Register(ctx, auth)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to %s auth %s: %v", op, auth.ID, err)
|
||||||
|
current, ok := s.coreManager.GetByID(auth.ID)
|
||||||
|
if !ok || current.Disabled {
|
||||||
|
GlobalModelRegistry().UnregisterClient(auth.ID)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return
|
auth = current
|
||||||
}
|
|
||||||
if _, err := s.coreManager.Register(ctx, auth); err != nil {
|
|
||||||
log.Errorf("failed to register auth %s: %v", auth.ID, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register models after auth is updated in coreManager.
|
||||||
|
// This operation may block on network calls, but the auth configuration
|
||||||
|
// is already effective at this point.
|
||||||
|
s.registerModelsForAuth(auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) applyCoreAuthRemoval(ctx context.Context, id string) {
|
func (s *Service) applyCoreAuthRemoval(ctx context.Context, id string) {
|
||||||
@@ -501,6 +519,8 @@ func (s *Service) Run(ctx context.Context) error {
|
|||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
fmt.Printf("API server started successfully on: %s:%d\n", s.cfg.Host, s.cfg.Port)
|
fmt.Printf("API server started successfully on: %s:%d\n", s.cfg.Host, s.cfg.Port)
|
||||||
|
|
||||||
|
s.applyPprofConfig(s.cfg)
|
||||||
|
|
||||||
if s.hooks.OnAfterStart != nil {
|
if s.hooks.OnAfterStart != nil {
|
||||||
s.hooks.OnAfterStart(s)
|
s.hooks.OnAfterStart(s)
|
||||||
}
|
}
|
||||||
@@ -546,6 +566,7 @@ func (s *Service) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.applyRetryConfig(newCfg)
|
s.applyRetryConfig(newCfg)
|
||||||
|
s.applyPprofConfig(newCfg)
|
||||||
if s.server != nil {
|
if s.server != nil {
|
||||||
s.server.UpdateClients(newCfg)
|
s.server.UpdateClients(newCfg)
|
||||||
}
|
}
|
||||||
@@ -639,6 +660,13 @@ func (s *Service) Shutdown(ctx context.Context) error {
|
|||||||
s.authQueueStop = nil
|
s.authQueueStop = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errShutdownPprof := s.shutdownPprof(ctx); errShutdownPprof != nil {
|
||||||
|
log.Errorf("failed to stop pprof server: %v", errShutdownPprof)
|
||||||
|
if shutdownErr == nil {
|
||||||
|
shutdownErr = errShutdownPprof
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// no legacy clients to persist
|
// no legacy clients to persist
|
||||||
|
|
||||||
if s.server != nil {
|
if s.server != nil {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func TestOpenAIToCodex_PreservesBuiltinTools(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOpenAIResponsesToOpenAI_PreservesBuiltinTools(t *testing.T) {
|
func TestOpenAIResponsesToOpenAI_IgnoresBuiltinTools(t *testing.T) {
|
||||||
in := []byte(`{
|
in := []byte(`{
|
||||||
"model":"gpt-5",
|
"model":"gpt-5",
|
||||||
"input":[{"role":"user","content":[{"type":"input_text","text":"hi"}]}],
|
"input":[{"role":"user","content":[{"type":"input_text","text":"hi"}]}],
|
||||||
@@ -42,13 +42,7 @@ func TestOpenAIResponsesToOpenAI_PreservesBuiltinTools(t *testing.T) {
|
|||||||
|
|
||||||
out := sdktranslator.TranslateRequest(sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAI, "gpt-5", in, false)
|
out := sdktranslator.TranslateRequest(sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAI, "gpt-5", in, false)
|
||||||
|
|
||||||
if got := gjson.GetBytes(out, "tools.#").Int(); got != 1 {
|
if got := gjson.GetBytes(out, "tools.#").Int(); got != 0 {
|
||||||
t.Fatalf("expected 1 tool, got %d: %s", got, string(out))
|
t.Fatalf("expected 0 tools (builtin tools not supported in Chat Completions), got %d: %s", got, string(out))
|
||||||
}
|
|
||||||
if got := gjson.GetBytes(out, "tools.0.type").String(); got != "web_search" {
|
|
||||||
t.Fatalf("expected tools[0].type=web_search, got %q: %s", got, string(out))
|
|
||||||
}
|
|
||||||
if got := gjson.GetBytes(out, "tools.0.search_context_size").String(); got != "low" {
|
|
||||||
t.Fatalf("expected tools[0].search_context_size=low, got %q: %s", got, string(out))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user