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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
cli-proxy-api:
image: ${CLI_PROXY_IMAGE:-eceasy/cli-proxy-api-plus:latest}
image: rensumo/cli-proxy-api-plus:latest
pull_policy: always
build:
context: .
Expand Down
2 changes: 1 addition & 1 deletion internal/auth/codebuddy/codebuddy_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
const (
BaseURL = "https://copilot.tencent.com"
DefaultDomain = "www.codebuddy.cn"
UserAgent = "CLI/2.63.2 CodeBuddy/2.63.2"
UserAgent = "CodeBuddyIDE/4.9.7 CodeBuddy/4.9.7"

codeBuddyStatePath = "/v2/plugin/auth/state"
codeBuddyTokenPath = "/v2/plugin/auth/token"
Expand Down
4 changes: 2 additions & 2 deletions internal/auth/qoder/uri_handler_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ End Function
regCmds := [][]string{
{"reg", "add", `HKCU\Software\Classes\qoder`, "/ve", "/t", "REG_SZ", "/d", "URL:QoderLogin", "/f"},
{"reg", "add", `HKCU\Software\Classes\qoder`, "/v", "URL Protocol", "/t", "REG_SZ", "/d", "", "/f"},
{"reg", "add", `HKCU\Software\Classes\qoder\shell`},
{"reg", "add", `HKCU\Software\Classes\qoder\shell\open`},
{"reg", "add", `HKCU\Software\Classes\qoder\shell`, "/f"},
{"reg", "add", `HKCU\Software\Classes\qoder\shell\open`, "/f"},
{"reg", "add", `HKCU\Software\Classes\qoder\shell\open\command`,
"/ve", "/t", "REG_SZ", "/d", fmt.Sprintf(`wscript.exe "%s" %%1`, vbsPath), "/f"},
}
Expand Down
147 changes: 66 additions & 81 deletions internal/registry/model_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,103 +185,88 @@ func GetQoderModels() []*ModelInfo {
// GetCodeBuddyModels returns the available models for CodeBuddy (Tencent).
// These models are served through the copilot.tencent.com API.
func GetCodeBuddyModels() []*ModelInfo {
now := int64(1748044800) // 2025-05-24
now := int64(1748044800)
return []*ModelInfo{
{
ID: "auto",
Object: "model",
Created: now,
OwnedBy: "tencent",
Type: "codebuddy",
DisplayName: "Auto",
Description: "Automatic model selection via CodeBuddy",
ContextLength: 128000,
MaxCompletionTokens: 32768,
SupportedEndpoints: []string{"/chat/completions"},
ID: "auto", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "Auto", Description: "Automatic model selection via CodeBuddy",
ContextLength: 168000, MaxCompletionTokens: 32000, SupportedEndpoints: []string{"/chat/completions"},
SupportedInputModalities: []string{"TEXT", "IMAGE"},
},
{
ID: "glm-5v-turbo",
Object: "model",
Created: now,
OwnedBy: "tencent",
Type: "codebuddy",
DisplayName: "GLM-5v Turbo",
Description: "GLM-5v Turbo via CodeBuddy",
ContextLength: 200000,
MaxCompletionTokens: 32768,
SupportedEndpoints: []string{"/chat/completions"},
ID: "hy3-preview", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "Hy3 Preview", Description: "Hunyuan thinking model with enhanced reasoning capabilities via CodeBuddy",
ContextLength: 192000, MaxCompletionTokens: 64000, SupportedEndpoints: []string{"/chat/completions"},
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}},
},
{
ID: "glm-5.1",
Object: "model",
Created: now,
OwnedBy: "tencent",
Type: "codebuddy",
DisplayName: "GLM-5.1",
Description: "GLM-5.1 via CodeBuddy",
ContextLength: 200000,
MaxCompletionTokens: 32768,
SupportedEndpoints: []string{"/chat/completions"},
ID: "glm-5v-turbo", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "GLM-5v Turbo", Description: "Native multimodal model via CodeBuddy",
ContextLength: 200000, MaxCompletionTokens: 38000, SupportedEndpoints: []string{"/chat/completions"},
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}},
SupportedInputModalities: []string{"TEXT", "IMAGE"},
},
{
ID: "glm-5.0-turbo",
Object: "model",
Created: now,
OwnedBy: "tencent",
Type: "codebuddy",
DisplayName: "GLM-5.0 Turbo",
Description: "GLM-5.0 Turbo via CodeBuddy",
ContextLength: 200000,
MaxCompletionTokens: 32768,
SupportedEndpoints: []string{"/chat/completions"},
ID: "glm-5.1", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "GLM-5.1", Description: "GLM-5.1 via CodeBuddy",
ContextLength: 200000, MaxCompletionTokens: 48000, SupportedEndpoints: []string{"/chat/completions"},
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}},
},
{
ID: "kimi-k2.6",
Object: "model",
Created: now,
OwnedBy: "tencent",
Type: "codebuddy",
DisplayName: "Kimi K2.6",
Description: "Kimi K2.6 via CodeBuddy",
ContextLength: 256000,
MaxCompletionTokens: 32768,
SupportedEndpoints: []string{"/chat/completions"},
ID: "glm-5.0-turbo", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "GLM-5.0 Turbo", Description: "GLM-5.0 Turbo via CodeBuddy",
ContextLength: 200000, MaxCompletionTokens: 48000, SupportedEndpoints: []string{"/chat/completions"},
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}},
},
{
ID: "kimi-k2.5",
Object: "model",
Created: now,
OwnedBy: "tencent",
Type: "codebuddy",
DisplayName: "Kimi K2.5",
Description: "Kimi K2.5 via CodeBuddy",
ContextLength: 256000,
MaxCompletionTokens: 32768,
SupportedEndpoints: []string{"/chat/completions"},
ID: "kimi-k2.6", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "Kimi K2.6", Description: "Kimi K2.6 via CodeBuddy",
ContextLength: 256000, MaxCompletionTokens: 32000, SupportedEndpoints: []string{"/chat/completions"},
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}},
SupportedInputModalities: []string{"TEXT", "IMAGE"},
},
{
ID: "minimax-m2.7",
Object: "model",
Created: now,
OwnedBy: "tencent",
Type: "codebuddy",
DisplayName: "MiniMax M2.7",
Description: "MiniMax M2.7 via CodeBuddy",
ContextLength: 200000,
MaxCompletionTokens: 32768,
SupportedEndpoints: []string{"/chat/completions"},
ID: "kimi-k2.5", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "Kimi K2.5", Description: "Kimi K2.5 via CodeBuddy",
ContextLength: 256000, MaxCompletionTokens: 32000, SupportedEndpoints: []string{"/chat/completions"},
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}},
SupportedInputModalities: []string{"TEXT", "IMAGE"},
},
{
ID: "deepseek-v3-2-volc",
Object: "model",
Created: now,
OwnedBy: "tencent",
Type: "codebuddy",
DisplayName: "DeepSeek V3.2 (Volc)",
Description: "DeepSeek V3.2 via CodeBuddy",
ContextLength: 128000,
MaxCompletionTokens: 32768,
SupportedEndpoints: []string{"/chat/completions"},
ID: "minimax-m2.7", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "MiniMax M2.7", Description: "MiniMax M2.7 via CodeBuddy",
ContextLength: 200000, MaxCompletionTokens: 48000, SupportedEndpoints: []string{"/chat/completions"},
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}},
SupportedInputModalities: []string{"TEXT", "IMAGE"},
},
{
ID: "deepseek-v4-flash", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "DeepSeek V4 Flash", Description: "DeepSeek V4 Flash via CodeBuddy",
ContextLength: 1000000, MaxCompletionTokens: 50000, SupportedEndpoints: []string{"/chat/completions"},
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}},
SupportedInputModalities: []string{"TEXT", "IMAGE"},
},
{
ID: "deepseek-v3-2-volc", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "DeepSeek V3.2", Description: "DeepSeek V3.2 via CodeBuddy",
ContextLength: 96000, MaxCompletionTokens: 32000, SupportedEndpoints: []string{"/chat/completions"},
Thinking: &ThinkingSupport{Levels: []string{"low", "medium", "high"}},
SupportedInputModalities: []string{"TEXT", "IMAGE"},
},
{
ID: "deepseek-v3-1-volc", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "DeepSeek V3.1 Terminus", Description: "DeepSeek V3.1 Terminus via CodeBuddy",
ContextLength: 128000, MaxCompletionTokens: 32000, SupportedEndpoints: []string{"/chat/completions"},
},
{
ID: "deepseek-r1-0528-lkeap", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "DeepSeek R1 0528", Description: "DeepSeek R1 0528 via CodeBuddy",
ContextLength: 112000, MaxCompletionTokens: 16000, SupportedEndpoints: []string{"/chat/completions"},
},
{
ID: "hunyuan-chat", Object: "model", Created: now, OwnedBy: "tencent",
Type: "codebuddy", DisplayName: "Hunyuan Turbos", Description: "Tencent Hunyuan Turbos via CodeBuddy",
ContextLength: 128000, MaxCompletionTokens: 8192, SupportedEndpoints: []string{"/chat/completions"},
},
}
}
Expand Down
79 changes: 72 additions & 7 deletions internal/runtime/executor/codebuddy_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,14 @@ func (e *CodeBuddyExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
if !bytes.HasPrefix(line, []byte("data:")) {
continue
}
raw := bytes.TrimSpace(line[5:])
if len(raw) > 0 && !bytes.Equal(raw, []byte("[DONE]")) {
if cleaned := cleanDeltaChunk(raw); cleaned == nil {
continue
} else if !bytes.Equal(cleaned, raw) {
line = append([]byte("data: "), cleaned...)
}
}
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(line), &param)
for i := range chunks {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
Expand Down Expand Up @@ -352,9 +360,10 @@ func (e *CodeBuddyExecutor) applyHeaders(req *http.Request, accessToken, userID,
req.Header.Set("X-User-Id", userID)
req.Header.Set("X-Domain", domain)
req.Header.Set("X-Product", "SaaS")
req.Header.Set("X-IDE-Type", "CLI")
req.Header.Set("X-IDE-Name", "CLI")
req.Header.Set("X-IDE-Version", "2.63.2")
req.Header.Set("X-IDE-Type", "CodeBuddyIDE")
req.Header.Set("X-IDE-Name", "CodeBuddyIDE")
req.Header.Set("X-IDE-Version", "4.9.7")
req.Header.Set("X-Product-Version", "4.9.7")
req.Header.Set("X-Requested-With", "XMLHttpRequest")
}

Expand Down Expand Up @@ -552,27 +561,77 @@ func aggregateOpenAIChatCompletionStream(raw []byte) ([]byte, usage.Detail, erro
return out, usageDetail, nil
}

// filterEmptyDelta returns nil if the SSE JSON chunk contains a delta with no
// meaningful content (both content and reasoning_content are empty/null).
// This prevents clients from interpreting empty reasoning_content deltas as
// interruptions in the thinking chain. Non-delta chunks (usage, finish_reason)
// are passed through.
// cleanDeltaChunk processes a single SSE JSON chunk for CodeBuddy streaming.
// It returns:
// - nil: chunk should be dropped (no meaningful content)
// - modified bytes: chunk cleaned up (e.g. empty reasoning_content removed)
// - original bytes: chunk passed through as-is
//
// The CodeBuddy upstream sends reasoning_content:"" alongside non-empty content
// during the thinking-to-content transition. Many clients interpret
// reasoning_content:"" as "thinking ended", then see the next chunk's
// reasoning_content:"" again and think "thinking restarted". By stripping the
// empty reasoning_content field, the client never sees spurious thinking
// transitions.
func cleanDeltaChunk(raw []byte) []byte {
delta := gjson.GetBytes(raw, "choices.0.delta")
if !delta.Exists() {
return raw
}
finishReason := gjson.GetBytes(raw, "choices.0.finish_reason").String()
if finishReason == "stop" || finishReason == "tool_calls" {
return raw
}
content := delta.Get("content").String()
reasoning := delta.Get("reasoning_content").String()
hasRole := delta.Get("role").Exists()
toolCalls := delta.Get("tool_calls")
hasToolCalls := toolCalls.Exists() && len(toolCalls.Array()) > 0
if content == "" && reasoning == "" && !hasRole && !hasToolCalls {
return nil
}
if reasoning == "" && content != "" {
if cleaned, err := sjson.DeleteBytes(raw, "choices.0.delta.reasoning_content"); err == nil {
return cleaned
}
}
return raw
}

var codeBuddyInternalModelPrefixes = []string{
"completion-",
"codewise-",
"hunyuan-3b",
"hunyuan-7b",
"nes-",
"default-",
"deepseek-r1-0528",
"deepseek-v3-0324-taco-",
"chat-",
"glm-4.6",
"glm-4.6v",
"hunyuan-image-",
}

var codeBuddyAllowedInternalModels = map[string]bool{
"deepseek-r1-0528": true,
"deepseek-r1-0528-lkeap": true,
"deepseek-v3-0324": true,
"deepseek-v3-0324-lkeap": true,
"deepseek-v3-0324-taco-completion": true,
"hunyuan-2.0-instruct": true,
"hunyuan-chat": true,
"glm-4.6": true,
"glm-4.6v": true,
"glm-4.7": true,
"glm-5.0": true,
"deepseek-v3-1": true,
"deepseek-v3-1-lkeap": true,
"deepseek-v3-1-volc": true,
"kimi-k2-instruct-taiji": true,
"kimi-k2-thinking": true,
"minimax-m2.5": true,
}

func isCodeBuddyInternalModel(id string) bool {
Expand Down Expand Up @@ -603,10 +662,16 @@ func FetchCodeBuddyModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *con
req.Header.Set("User-Agent", codebuddy.UserAgent)
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("X-Requested-With", "XMLHttpRequest")
req.Header.Set("X-IDE-Type", "CodeBuddyIDE")
req.Header.Set("X-IDE-Name", "CodeBuddyIDE")
req.Header.Set("X-IDE-Version", "4.9.7")
req.Header.Set("X-Product-Version", "4.9.7")
req.Header.Set("X-Env-ID", "production")
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Id", userID)
req.Header.Set("X-Domain", domain)
req.Header.Set("X-Product", "SaaS")
req.Header.Set("Connection", "close")

resp, err := httpClient.Do(req)
if err != nil {
Expand Down
Loading