Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56a05d2cce | ||
|
|
3e09bc9470 | ||
|
|
5ed79e5aa3 | ||
|
|
f38b78dbe6 | ||
|
|
f1d6f01585 | ||
|
|
9b627a93ac | ||
|
|
d4709ffcf9 |
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Git and GitHub folders
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
|
||||||
|
# Docker and CI/CD related files
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
.gitignore
|
||||||
|
.goreleaser.yml
|
||||||
|
Dockerfile
|
||||||
|
|
||||||
|
# Documentation and license
|
||||||
|
README.md
|
||||||
|
README_CN.md
|
||||||
|
MANAGEMENT_API.md
|
||||||
|
MANAGEMENT_API_CN.md
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
# Example configuration
|
||||||
|
config.example.yaml
|
||||||
|
|
||||||
|
# Runtime data folders (should be mounted as volumes)
|
||||||
|
auths
|
||||||
|
logs
|
||||||
|
config.yaml
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
config.yaml
|
config.yaml
|
||||||
docs/
|
docs/*
|
||||||
logs/
|
logs/*
|
||||||
|
auths/*
|
||||||
|
!auths/.gitkeep
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -486,6 +486,31 @@ Run the following command to start the server:
|
|||||||
docker run --rm -p 8317:8317 -v /path/to/your/config.yaml:/CLIProxyAPI/config.yaml -v /path/to/your/auth-dir:/root/.cli-proxy-api eceasy/cli-proxy-api:latest
|
docker run --rm -p 8317:8317 -v /path/to/your/config.yaml:/CLIProxyAPI/config.yaml -v /path/to/your/auth-dir:/root/.cli-proxy-api eceasy/cli-proxy-api:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Run with Docker Compose
|
||||||
|
|
||||||
|
1. Create a `config.yaml` from `config.example.yaml` and customize it.
|
||||||
|
|
||||||
|
2. Build and start the services using Docker Compose:
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. To authenticate with providers, run the login command inside the container:
|
||||||
|
- **Gemini**: `docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --login`
|
||||||
|
- **OpenAI (Codex)**: `docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --codex-login`
|
||||||
|
- **Claude**: `docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --claude-login`
|
||||||
|
- **Qwen**: `docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --qwen-login`
|
||||||
|
|
||||||
|
4. To view the server logs:
|
||||||
|
```bash
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
5. To stop the application:
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
## Management API
|
## Management API
|
||||||
|
|
||||||
see [MANAGEMENT_API.md](MANAGEMENT_API.md)
|
see [MANAGEMENT_API.md](MANAGEMENT_API.md)
|
||||||
|
|||||||
25
README_CN.md
25
README_CN.md
@@ -499,6 +499,31 @@ docker run -it -rm -v /path/to/your/config.yaml:/CLIProxyAPI/config.yaml -v /pat
|
|||||||
docker run --rm -p 8317:8317 -v /path/to/your/config.yaml:/CLIProxyAPI/config.yaml -v /path/to/your/auth-dir:/root/.cli-proxy-api eceasy/cli-proxy-api:latest
|
docker run --rm -p 8317:8317 -v /path/to/your/config.yaml:/CLIProxyAPI/config.yaml -v /path/to/your/auth-dir:/root/.cli-proxy-api eceasy/cli-proxy-api:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 使用 Docker Compose 运行
|
||||||
|
|
||||||
|
1. 从 `config.example.yaml` 创建一个 `config.yaml` 文件并进行自定义。
|
||||||
|
|
||||||
|
2. 使用 Docker Compose 构建并启动服务:
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 要在容器内运行登录命令进行身份验证:
|
||||||
|
- **Gemini**: `docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --login`
|
||||||
|
- **OpenAI (Codex)**: `docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --codex-login`
|
||||||
|
- **Claude**: `docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --claude-login`
|
||||||
|
- **Qwen**: `docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --qwen-login`
|
||||||
|
|
||||||
|
4. 查看服务器日志:
|
||||||
|
```bash
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 停止应用程序:
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
## 管理 API 文档
|
## 管理 API 文档
|
||||||
|
|
||||||
请参见 [MANAGEMENT_API_CN.md](MANAGEMENT_API_CN.md)
|
请参见 [MANAGEMENT_API_CN.md](MANAGEMENT_API_CN.md)
|
||||||
|
|||||||
0
auths/.gitkeep
Normal file
0
auths/.gitkeep
Normal file
@@ -8,7 +8,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/luispater/CLIProxyAPI/internal/cmd"
|
"github.com/luispater/CLIProxyAPI/internal/cmd"
|
||||||
@@ -36,7 +36,7 @@ func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
|
|||||||
timestamp := entry.Time.Format("2006-01-02 15:04:05")
|
timestamp := entry.Time.Format("2006-01-02 15:04:05")
|
||||||
var newLog string
|
var newLog string
|
||||||
// Customize the log format to include timestamp, level, caller file/line, and message.
|
// Customize the log format to include timestamp, level, caller file/line, and message.
|
||||||
newLog = fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, path.Base(entry.Caller.File), entry.Caller.Line, entry.Message)
|
newLog = fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, filepath.Base(entry.Caller.File), entry.Caller.Line, entry.Message)
|
||||||
|
|
||||||
b.WriteString(newLog)
|
b.WriteString(newLog)
|
||||||
return b.Bytes(), nil
|
return b.Bytes(), nil
|
||||||
@@ -96,7 +96,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to get working directory: %v", err)
|
log.Fatalf("failed to get working directory: %v", err)
|
||||||
}
|
}
|
||||||
configFilePath = path.Join(wd, "config.yaml")
|
configFilePath = filepath.Join(wd, "config.yaml")
|
||||||
cfg, err = config.LoadConfig(configFilePath)
|
cfg, err = config.LoadConfig(configFilePath)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,7 +120,7 @@ func main() {
|
|||||||
parts := strings.Split(cfg.AuthDir, string(os.PathSeparator))
|
parts := strings.Split(cfg.AuthDir, string(os.PathSeparator))
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
parts[0] = home
|
parts[0] = home
|
||||||
cfg.AuthDir = path.Join(parts...)
|
cfg.AuthDir = filepath.Join(parts...)
|
||||||
} else {
|
} else {
|
||||||
// If the path is just "~", set it to the home directory.
|
// If the path is just "~", set it to the home directory.
|
||||||
cfg.AuthDir = home
|
cfg.AuthDir = home
|
||||||
|
|||||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
cli-proxy-api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: cli-proxy-api:latest
|
||||||
|
container_name: cli-proxy-api
|
||||||
|
ports:
|
||||||
|
- "8317:8317"
|
||||||
|
- "8085:8085"
|
||||||
|
- "1455:1455"
|
||||||
|
- "54545:54545"
|
||||||
|
volumes:
|
||||||
|
- ./config.yaml:/CLIProxyAPI/config.yaml
|
||||||
|
- ./auths:/root/.cli-proxy-api
|
||||||
|
- ./logs:/CLIProxyAPI/logs
|
||||||
|
restart: unless-stopped
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClaudeTokenStorage stores OAuth2 token information for Anthropic Claude API authentication.
|
// ClaudeTokenStorage stores OAuth2 token information for Anthropic Claude API authentication.
|
||||||
@@ -49,7 +49,7 @@ func (ts *ClaudeTokenStorage) SaveTokenToFile(authFilePath string) error {
|
|||||||
ts.Type = "claude"
|
ts.Type = "claude"
|
||||||
|
|
||||||
// Create directory structure if it doesn't exist
|
// Create directory structure if it doesn't exist
|
||||||
if err := os.MkdirAll(path.Dir(authFilePath), 0700); err != nil {
|
if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil {
|
||||||
return fmt.Errorf("failed to create directory: %v", err)
|
return fmt.Errorf("failed to create directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CodexTokenStorage stores OAuth2 token information for OpenAI Codex API authentication.
|
// CodexTokenStorage stores OAuth2 token information for OpenAI Codex API authentication.
|
||||||
@@ -43,7 +43,7 @@ type CodexTokenStorage struct {
|
|||||||
// - error: An error if the operation fails, nil otherwise
|
// - error: An error if the operation fails, nil otherwise
|
||||||
func (ts *CodexTokenStorage) SaveTokenToFile(authFilePath string) error {
|
func (ts *CodexTokenStorage) SaveTokenToFile(authFilePath string) error {
|
||||||
ts.Type = "codex"
|
ts.Type = "codex"
|
||||||
if err := os.MkdirAll(path.Dir(authFilePath), 0700); err != nil {
|
if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil {
|
||||||
return fmt.Errorf("failed to create directory: %v", err)
|
return fmt.Errorf("failed to create directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -46,7 +46,7 @@ type GeminiTokenStorage struct {
|
|||||||
// - error: An error if the operation fails, nil otherwise
|
// - error: An error if the operation fails, nil otherwise
|
||||||
func (ts *GeminiTokenStorage) SaveTokenToFile(authFilePath string) error {
|
func (ts *GeminiTokenStorage) SaveTokenToFile(authFilePath string) error {
|
||||||
ts.Type = "gemini"
|
ts.Type = "gemini"
|
||||||
if err := os.MkdirAll(path.Dir(authFilePath), 0700); err != nil {
|
if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil {
|
||||||
return fmt.Errorf("failed to create directory: %v", err)
|
return fmt.Errorf("failed to create directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// QwenTokenStorage stores OAuth2 token information for Alibaba Qwen API authentication.
|
// QwenTokenStorage stores OAuth2 token information for Alibaba Qwen API authentication.
|
||||||
@@ -41,7 +41,7 @@ type QwenTokenStorage struct {
|
|||||||
// - error: An error if the operation fails, nil otherwise
|
// - error: An error if the operation fails, nil otherwise
|
||||||
func (ts *QwenTokenStorage) SaveTokenToFile(authFilePath string) error {
|
func (ts *QwenTokenStorage) SaveTokenToFile(authFilePath string) error {
|
||||||
ts.Type = "qwen"
|
ts.Type = "qwen"
|
||||||
if err := os.MkdirAll(path.Dir(authFilePath), 0700); err != nil {
|
if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil {
|
||||||
return fmt.Errorf("failed to create directory: %v", err)
|
return fmt.Errorf("failed to create directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,9 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
previewModels = map[string][]string{
|
previewModels = map[string][]string{
|
||||||
"gemini-2.5-pro": {"gemini-2.5-pro-preview-05-06", "gemini-2.5-pro-preview-06-05"},
|
"gemini-2.5-pro": {"gemini-2.5-pro-preview-05-06", "gemini-2.5-pro-preview-06-05"},
|
||||||
"gemini-2.5-flash": {"gemini-2.5-flash-preview-04-17", "gemini-2.5-flash-preview-05-20"},
|
"gemini-2.5-flash": {"gemini-2.5-flash-preview-04-17", "gemini-2.5-flash-preview-05-20"},
|
||||||
|
"gemini-2.5-flash-lite": {"gemini-2.5-flash-lite-preview-06-17"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,6 +100,7 @@ func (c *GeminiCLIClient) CanProvideModel(modelName string) bool {
|
|||||||
models := []string{
|
models := []string{
|
||||||
"gemini-2.5-pro",
|
"gemini-2.5-pro",
|
||||||
"gemini-2.5-flash",
|
"gemini-2.5-flash",
|
||||||
|
"gemini-2.5-flash-lite",
|
||||||
}
|
}
|
||||||
return util.InArray(models, modelName)
|
return util.InArray(models, modelName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,20 @@ func GetGeminiCLIModels() []*ModelInfo {
|
|||||||
OutputTokenLimit: 65536,
|
OutputTokenLimit: 65536,
|
||||||
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "gemini-2.5-flash-lite",
|
||||||
|
Object: "model",
|
||||||
|
Created: time.Now().Unix(),
|
||||||
|
OwnedBy: "google",
|
||||||
|
Type: "gemini",
|
||||||
|
Name: "models/gemini-2.5-flash-lite",
|
||||||
|
Version: "2.5",
|
||||||
|
DisplayName: "Gemini 2.5 Flash Lite",
|
||||||
|
Description: "Our smallest and most cost effective model, built for at scale usage.",
|
||||||
|
InputTokenLimit: 1048576,
|
||||||
|
OutputTokenLimit: 65536,
|
||||||
|
SupportedGenerationMethods: []string{"generateContent", "countTokens", "createCachedContent", "batchGenerateContent"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,17 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
out, _ = sjson.Set(out, "stop_sequences", stopSequences)
|
out, _ = sjson.Set(out, "stop_sequences", stopSequences)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Include thoughts configuration for reasoning process visibility
|
||||||
|
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
||||||
|
if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() {
|
||||||
|
if includeThoughts.Type == gjson.True {
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() {
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", thinkingBudget.Int())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// System instruction conversion to Claude Code format
|
// System instruction conversion to Claude Code format
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original
|
|||||||
}
|
}
|
||||||
case "thinking_delta":
|
case "thinking_delta":
|
||||||
// Thinking/reasoning content delta for models with reasoning capabilities
|
// Thinking/reasoning content delta for models with reasoning capabilities
|
||||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
if text := delta.Get("thinking"); text.Exists() && text.String() != "" {
|
||||||
thinkingPart := `{"thought":true,"text":""}`
|
thinkingPart := `{"thought":true,"text":""}`
|
||||||
thinkingPart, _ = sjson.Set(thinkingPart, "text", text.String())
|
thinkingPart, _ = sjson.Set(thinkingPart, "text", text.String())
|
||||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", thinkingPart)
|
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", thinkingPart)
|
||||||
@@ -411,7 +411,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
}
|
}
|
||||||
case "thinking_delta":
|
case "thinking_delta":
|
||||||
// Process reasoning/thinking content
|
// Process reasoning/thinking content
|
||||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
if text := delta.Get("thinking"); text.Exists() && text.String() != "" {
|
||||||
partJSON := `{"thought":true,"text":""}`
|
partJSON := `{"thought":true,"text":""}`
|
||||||
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
||||||
part := gjson.Parse(partJSON).Value().(map[string]interface{})
|
part := gjson.Parse(partJSON).Value().(map[string]interface{})
|
||||||
|
|||||||
@@ -41,6 +41,21 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
|
if v := root.Get("reasoning_effort"); v.Exists() {
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
|
||||||
|
switch v.String() {
|
||||||
|
case "none":
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
||||||
|
case "low":
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 1024)
|
||||||
|
case "medium":
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 8192)
|
||||||
|
case "high":
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 24576)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper for generating tool call IDs in the form: toolu_<alphanum>
|
// Helper for generating tool call IDs in the form: toolu_<alphanum>
|
||||||
// This ensures unique identifiers for tool calls in the Claude Code format
|
// This ensures unique identifiers for tool calls in the Claude Code format
|
||||||
genToolCallID := func() string {
|
genToolCallID := func() string {
|
||||||
|
|||||||
@@ -128,10 +128,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []string{template}
|
return []string{}
|
||||||
|
|
||||||
case "content_block_delta":
|
case "content_block_delta":
|
||||||
// Handle content delta (text, tool use arguments, or reasoning content)
|
// Handle content delta (text, tool use arguments, or reasoning content)
|
||||||
|
hasContent := false
|
||||||
if delta := root.Get("delta"); delta.Exists() {
|
if delta := root.Get("delta"); delta.Exists() {
|
||||||
deltaType := delta.Get("type").String()
|
deltaType := delta.Get("type").String()
|
||||||
|
|
||||||
@@ -140,8 +141,14 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
// Text content delta - send incremental text updates
|
// Text content delta - send incremental text updates
|
||||||
if text := delta.Get("text"); text.Exists() {
|
if text := delta.Get("text"); text.Exists() {
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.content", text.String())
|
template, _ = sjson.Set(template, "choices.0.delta.content", text.String())
|
||||||
|
hasContent = true
|
||||||
|
}
|
||||||
|
case "thinking_delta":
|
||||||
|
// Accumulate reasoning/thinking content
|
||||||
|
if thinking := delta.Get("thinking"); thinking.Exists() {
|
||||||
|
template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", thinking.String())
|
||||||
|
hasContent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
case "input_json_delta":
|
case "input_json_delta":
|
||||||
// Tool use input delta - accumulate arguments for tool calls
|
// Tool use input delta - accumulate arguments for tool calls
|
||||||
if partialJSON := delta.Get("partial_json"); partialJSON.Exists() {
|
if partialJSON := delta.Get("partial_json"); partialJSON.Exists() {
|
||||||
@@ -156,7 +163,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []string{template}
|
if hasContent {
|
||||||
|
return []string{template}
|
||||||
|
} else {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
case "content_block_stop":
|
case "content_block_stop":
|
||||||
// End of content block - output complete tool call if it's a tool_use block
|
// End of content block - output complete tool call if it's a tool_use block
|
||||||
|
|||||||
@@ -28,6 +28,23 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
|
if v := root.Get("reasoning.effort"); v.Exists() {
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
||||||
|
|
||||||
|
switch v.String() {
|
||||||
|
case "none":
|
||||||
|
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
||||||
|
case "minimal":
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 1024)
|
||||||
|
case "low":
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 4096)
|
||||||
|
case "medium":
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 8192)
|
||||||
|
case "high":
|
||||||
|
out, _ = sjson.Set(out, "thinking.budget_tokens", 24576)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper for generating tool call IDs when missing
|
// Helper for generating tool call IDs when missing
|
||||||
genToolCallID := func() string {
|
genToolCallID := func() string {
|
||||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ type Watcher struct {
|
|||||||
clientsMutex sync.RWMutex
|
clientsMutex sync.RWMutex
|
||||||
reloadCallback func(map[string]interfaces.Client, *config.Config)
|
reloadCallback func(map[string]interfaces.Client, *config.Config)
|
||||||
watcher *fsnotify.Watcher
|
watcher *fsnotify.Watcher
|
||||||
|
eventTimes map[string]time.Time
|
||||||
|
eventMutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWatcher creates a new file watcher instance
|
// NewWatcher creates a new file watcher instance
|
||||||
@@ -54,6 +56,7 @@ func NewWatcher(configPath, authDir string, reloadCallback func(map[string]inter
|
|||||||
reloadCallback: reloadCallback,
|
reloadCallback: reloadCallback,
|
||||||
watcher: watcher,
|
watcher: watcher,
|
||||||
clients: make(map[string]interfaces.Client),
|
clients: make(map[string]interfaces.Client),
|
||||||
|
eventTimes: make(map[string]time.Time),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +126,16 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
log.Debugf("file system event detected: %s %s", event.Op.String(), event.Name)
|
log.Debugf("file system event detected: %s %s", event.Op.String(), event.Name)
|
||||||
|
|
||||||
|
// Debounce logic to prevent rapid reloads
|
||||||
|
w.eventMutex.Lock()
|
||||||
|
if lastTime, ok := w.eventTimes[event.Name]; ok && now.Sub(lastTime) < 500*time.Millisecond {
|
||||||
|
log.Debugf("debouncing event for %s", event.Name)
|
||||||
|
w.eventMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.eventTimes[event.Name] = now
|
||||||
|
w.eventMutex.Unlock()
|
||||||
|
|
||||||
// Handle config file changes
|
// Handle config file changes
|
||||||
if event.Name == w.configPath && (event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create) {
|
if event.Name == w.configPath && (event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create) {
|
||||||
log.Infof("config file changed, reloading: %s", w.configPath)
|
log.Infof("config file changed, reloading: %s", w.configPath)
|
||||||
|
|||||||
Reference in New Issue
Block a user