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
7 changes: 7 additions & 0 deletions changelog/unreleased/workbuddy-cli-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### English

- Update the WorkBuddy chat identity to CodeBuddy CLI 2.168.2. Chat requests now send that version, the official CLI's OpenAI SDK fingerprint, and `cache_retention: long` when the caller did not set one. The desktop catalog identity is unchanged.

### 中文

- WorkBuddy 对话身份对齐到 CodeBuddy CLI 2.168.2。对话请求现在发送该版本号、官方 CLI 的 OpenAI SDK 指纹,并在调用方未指定时补上 `cache_retention: long`。桌面端目录身份不变。
31 changes: 26 additions & 5 deletions internal/providers/workbuddy/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1087,9 +1087,17 @@ func TestPrepareBodyForcesStreamAndStringToolChoice(t *testing.T) {
if err := json.Unmarshal(out, &body); err != nil {
t.Fatal(err)
}
if body["stream"] != true || body["tool_choice"] != "get_time" {
if body["stream"] != true || body["tool_choice"] != "get_time" || body["cache_retention"] != "long" {
t.Fatalf("body=%v", body)
}
kept := PrepareBody([]byte(`{"model":"m","cache_retention":"short"}`))
var explicit map[string]any
if err := json.Unmarshal(kept, &explicit); err != nil {
t.Fatal(err)
}
if explicit["cache_retention"] != "short" {
t.Fatalf("explicit cache_retention overwritten: %v", explicit["cache_retention"])
}
}

// Image parts must reach upstream unchanged. The catalog advertises
Expand Down Expand Up @@ -1431,7 +1439,7 @@ func TestCatalogHeadersUseDesktopUAOnlyForGlobal(t *testing.T) {
if global.Get("User-Agent") != DesktopUserAgent {
t.Fatalf("global catalog UA=%q", global.Get("User-Agent"))
}
if global.Get("X-IDE-Type") != "" || global.Get("X-Agent-Intent") != "" || global.Get("X-Request-ID") != "" {
if global.Get("X-IDE-Type") != "" || global.Get("X-Agent-Intent") != "" || global.Get("X-Request-ID") != "" || global.Get("X-Stainless-Lang") != "" {
t.Fatalf("catalog must not carry chat CLI channel headers: %+v", global)
}
if global.Get("X-Product") != "SaaS" || global.Get("Authorization") != "Bearer at" {
Expand All @@ -1454,13 +1462,26 @@ func TestChatHeadersStayRegionSpecificAndIncludeCLIChannel(t *testing.T) {
if cn.Get("Origin") != "https://www.codebuddy.cn" || cn.Get("Referer") != "https://www.codebuddy.cn/" {
t.Fatalf("CN origin/referer mixed: %+v", cn)
}
if got := cn.Get("User-Agent"); got != UserAgent || !strings.Contains(got, "2.161.0") {
if got := cn.Get("User-Agent"); got != UserAgent || !strings.Contains(got, CLIVersion) {
t.Fatalf("CN UA=%q", got)
}
if cn.Get("X-Product") != "SaaS" || cn.Get("X-IDE-Type") != "CLI" || cn.Get("X-Agent-Intent") != "craft" ||
cn.Get("X-Agent-Type") != "main" || cn.Get("X-Private-Data") != "false" || cn.Get("X-Request-ID") == "" {
if cn.Get("X-Product") != "SaaS" || cn.Get("X-IDE-Type") != "CLI" || cn.Get("X-IDE-Version") != CLIVersion ||
cn.Get("X-Agent-Intent") != "craft" || cn.Get("X-Agent-Purpose") != "conversation" ||
cn.Get("X-Agent-Runtime") != "other" || cn.Get("X-Agent-Type") != "main" ||
cn.Get("X-Private-Data") != "false" || cn.Get("X-Request-ID") == "" {
t.Fatalf("CN missing CLI channel headers: %+v", cn)
}
if cn.Get("X-Product-Version") != "" || cn.Get("X-Session-ID") != "" {
t.Fatalf("CLI chat must not carry the old product/session headers: %+v", cn)
}
if cn.Get("X-Stainless-Lang") != "js" || cn.Get("X-Stainless-Package-Version") != CLIStainlessPackageVersion ||
cn.Get("X-Stainless-Runtime") != "node" || cn.Get("X-Stainless-Runtime-Version") != CLIStainlessRuntimeVersion ||
cn.Get("X-Stainless-Timeout") != "1200" {
t.Fatalf("CN missing CLI SDK fingerprint: %+v", cn)
}
if cn.Get("X-Request-ID") == cn.Get("X-Conversation-ID") || cn.Get("X-Root-Request-ID") != cn.Get("X-Conversation-Request-ID") {
t.Fatalf("CLI request ids must split message and turn: %+v", cn)
}
if cn.Get("X-Refresh-Token") != "" || cn.Get("X-API-Key") != "" {
t.Fatal("chat must not carry refresh token or API key")
}
Expand Down
35 changes: 28 additions & 7 deletions internal/providers/workbuddy/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,36 @@ const (
// UserAgent matches official @tencent-ai/codebuddy-code in the dual-token
// form WorkBuddy chat still accepts. Origin/Referer stay region-specific
// and must never mix CN with Global. All client versions live here.
CLIVersion = "2.161.0"
// CLIVersion is the unpacked @tencent-ai/codebuddy-code package version
// (hub identity audit, 2.168.2). Chat sends it as both UA segments and
// X-IDE-Version.
CLIVersion = "2.168.2"
UserAgent = "CLI/" + CLIVersion + " CodeBuddy/" + CLIVersion

// OpenAI Node SDK fingerprint the official CLI attaches to chat.
// Values are the SDK 6.25.0 defaults shipped with that CLI package.
// The host OS and architecture stay fixed: a gateway must not leak
// its own platform into the upstream fingerprint.
CLIStainlessLang = "js"
CLIStainlessPackageVersion = "6.25.0"
CLIStainlessOS = "MacOS"
CLIStainlessArch = "arm64"
CLIStainlessRuntime = "node"
CLIStainlessRuntimeVersion = "v22.21.1"
CLIStainlessRetryCount = "0"
CLIStainlessTimeout = "1200"

// Desktop clients ship separately per edition. Their UA is three-part:
// app token, edition token, then the bundled CLI version.
// International desktop embeds its own CLI (not the official CLI chat UA).
// app token, edition token, then the CLI bundled inside that desktop
// build. Those bundled CLIs are not CLIVersion. International desktop
// 5.9.4 embeds 2.165.0; the CN desktop build was not re-verified and
// stays on its previous bundled CLI.
DesktopVersionGlobal = "5.9.4"
DesktopCLIVersionGlobal = "2.165.0"
DesktopVersionCN = "5.8.3"
DesktopCLIVersionCN = "2.161.0"
DesktopUserAgentGlobal = "WorkBuddy/" + DesktopVersionGlobal + " workbuddy-ai/" + DesktopVersionGlobal + " CLI/" + DesktopCLIVersionGlobal
DesktopUserAgentCN = "WorkBuddy/" + DesktopVersionCN + " WorkBuddy/" + DesktopVersionCN + " CLI/" + CLIVersion
DesktopUserAgentCN = "WorkBuddy/" + DesktopVersionCN + " WorkBuddy/" + DesktopVersionCN + " CLI/" + DesktopCLIVersionCN

// IDEVersion is the CodeBuddy IDE plugin release, kept with the others.
IDEVersion = "2.168.2"
Expand All @@ -46,9 +65,11 @@ const (
DesktopVersion = DesktopVersionGlobal
DesktopUserAgent = DesktopUserAgentGlobal

productTypeCLI = "CLI"
agentIntentDefault = "craft"
agentTypeMain = "main"
productTypeCLI = "CLI"
agentIntentDefault = "craft"
agentPurposeConversation = "conversation"
agentRuntimeOther = "other"
agentTypeMain = "main"

pathAuthState = "/v2/plugin/auth/state"
pathAuthToken = "/v2/plugin/auth/token"
Expand Down
29 changes: 23 additions & 6 deletions internal/providers/workbuddy/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,25 @@ func setCommonHeaders(header http.Header, global bool) {
header.Set("User-Agent", UserAgent)
}

// setCLIChannelHeaders is chat-only. Values come from CodeBuddy CLI 2.139.0
// (craft / CLI / SaaS). CN and Global share the names; Origin/host stay split.
// setCLIChannelHeaders is chat-only. Values come from the official
// CodeBuddy CLI (craft / CLI / SaaS, plus the OpenAI Node SDK fingerprint
// that package sends). CN and Global share the names; Origin/host stay split.
func setCLIChannelHeaders(header http.Header, credential Credential) {
requestID := newCLIRequestID()
header.Set("X-Request-ID", requestID)
messageID := newCLIRequestID()
header.Set("X-Request-ID", messageID)
header.Set("X-Conversation-ID", requestID)
header.Set("X-Session-ID", requestID)
header.Set("X-Conversation-Request-ID", requestID)
header.Set("X-Conversation-Message-ID", requestID)
header.Set("X-Root-Request-ID", requestID)
header.Set("X-Conversation-Message-ID", messageID)
header.Set("X-Agent-Type", agentTypeMain)
header.Set("X-Agent-Intent", agentIntentDefault)
header.Set("X-Agent-Purpose", agentPurposeConversation)
header.Set("X-Agent-Runtime", agentRuntimeOther)
header.Set("X-IDE-Type", productTypeCLI)
header.Set("X-IDE-Name", productTypeCLI)
header.Set("X-IDE-Version", CLIVersion)
header.Set("X-Product-Version", CLIVersion)
setCLIStainlessHeaders(header)
if value, ok := privateDataHeader(identityCLI, privateDataModeFromEnv(), credential); ok {
header.Set("X-Private-Data", value)
} else {
Expand Down Expand Up @@ -97,6 +101,19 @@ func privateDataHeader(identity, mode string, credential Credential) (string, bo
}
}

// setCLIStainlessHeaders adds the OpenAI Node SDK fingerprint the official
// CLI attaches to chat. Names are lowercase, matching the SDK's wire form.
func setCLIStainlessHeaders(header http.Header) {
header.Set("X-Stainless-Lang", CLIStainlessLang)
header.Set("X-Stainless-Package-Version", CLIStainlessPackageVersion)
header.Set("X-Stainless-OS", CLIStainlessOS)
header.Set("X-Stainless-Arch", CLIStainlessArch)
header.Set("X-Stainless-Runtime", CLIStainlessRuntime)
header.Set("X-Stainless-Runtime-Version", CLIStainlessRuntimeVersion)
header.Set("X-Stainless-Retry-Count", CLIStainlessRetryCount)
header.Set("X-Stainless-Timeout", CLIStainlessTimeout)
}

func newCLIRequestID() string {
var raw [16]byte
if _, err := rand.Read(raw[:]); err != nil {
Expand Down
13 changes: 13 additions & 0 deletions internal/providers/workbuddy/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func PrepareBody(src []byte) []byte {
return src
}
body["stream"] = true
ensureCacheRetention(body)
normalizeToolChoice(body)
normalizeTools(body)
dropEmptyTools(body)
Expand All @@ -33,6 +34,18 @@ func PrepareBody(src []byte) []byte {
return out
}

// ensureCacheRetention matches the official CLI: a chat body without
// cache_retention gets "long". An explicit caller value is kept.
func ensureCacheRetention(body map[string]any) {
raw, ok := body["cache_retention"]
if ok {
if value, isString := raw.(string); isString && strings.TrimSpace(value) != "" {
return
}
}
body["cache_retention"] = "long"
}

func normalizeTools(body map[string]any) {
raw, ok := body["tools"]
if !ok || raw == nil {
Expand Down
7 changes: 5 additions & 2 deletions internal/providers/workbuddy/versions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
)

func TestClientVersionsAndDesktopUserAgents(t *testing.T) {
if CLIVersion != "2.161.0" || IDEVersion != "2.168.2" {
if CLIVersion != "2.168.2" || IDEVersion != "2.168.2" {
t.Fatalf("CLI=%q IDE=%q", CLIVersion, IDEVersion)
}
if UserAgent != "CLI/2.161.0 CodeBuddy/2.161.0" {
if UserAgent != "CLI/2.168.2 CodeBuddy/2.168.2" {
t.Fatalf("CLI UA=%q", UserAgent)
}
if CLIStainlessPackageVersion != "6.25.0" || CLIStainlessRuntimeVersion != "v22.21.1" {
t.Fatalf("SDK fingerprint package=%q runtime=%q", CLIStainlessPackageVersion, CLIStainlessRuntimeVersion)
}
if DesktopVersionGlobal != "5.9.4" || DesktopCLIVersionGlobal != "2.165.0" {
t.Fatalf("global desktop=%q cli=%q", DesktopVersionGlobal, DesktopCLIVersionGlobal)
}
Expand Down
Loading