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
57 changes: 44 additions & 13 deletions internal/providers/workbuddy/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,14 +417,6 @@ func (c *Client) Models(ctx context.Context, accountID string) ([]providers.Mode
}

func (c *Client) chatRequest(ctx context.Context, accountID string, credential Credential, req translate.ChatRequest) (*http.Request, error) {
body := map[string]any{
"model": upstreamModelID(req.Model),
"messages": req.Messages,
"max_tokens": req.MaxTokens,
"temperature": req.Temperature,
"tools": req.Tools,
"tool_choice": req.ToolChoice,
}
caps := c.capsFor(req.Model)
storedLevel := ""
if setter, ok := c.store.(interface {
Expand All @@ -436,10 +428,21 @@ func (c *Client) chatRequest(ctx context.Context, accountID string, credential C
storedLevel = stored.ReasoningEffort
}
}
if len(caps.ReasoningOptions) == 0 && accountID != "" {
// Resolve the upstream model after the live catalog is warm. WorkBuddy has
// renamed deepseek-v4.1-flash from the old deep-model alias; sending the
// stale alias lets upstream silently fall back to another model.
if (!c.hasCatalogEntry(req.Model) || len(caps.ReasoningOptions) == 0) && accountID != "" {
_, _ = c.Models(ctx, accountID)
caps = c.capsFor(req.Model)
}
body := map[string]any{
"model": c.upstreamModelID(req.Model),
"messages": req.Messages,
"max_tokens": req.MaxTokens,
"temperature": req.Temperature,
"tools": req.Tools,
"tool_choice": req.ToolChoice,
}
applyChatReasoning(body, req, storedLevel, caps)
payload, err := json.Marshal(body)
if err != nil {
Expand Down Expand Up @@ -1012,11 +1015,39 @@ var workbuddyModelAliases = map[string]string{
"deepseek-v4.1-flash": "deep-model",
}

func upstreamModelID(model string) string {
func (c *Client) hasCatalogEntry(model string) bool {
model = strings.TrimSpace(model)
if model == "" || c == nil {
return false
}
canonical := accounts.CanonicalModelID(model)
for alias, nativeModel := range workbuddyModelAliases {
if accounts.CanonicalModelID(alias) == canonical {
return nativeModel
c.mu.Lock()
defer c.mu.Unlock()
if _, ok := c.catalog[model]; ok {
return true
}
_, ok := c.catalog[canonical]
return ok
}

// upstreamModelID returns the native ID to send upstream. For current
// WorkBuddy catalogs that already expose deepseek-v4.1-flash natively, that
// ID is used as-is. Older catalogs that only expose deep-model still work
// because appendAliasModels indexes the public alias onto a ModelInfo whose
// NativeModel is deep-model. There is intentionally no hardcoded rewrite
// here: blindly mapping to deep-model on a cold/missing catalog is what made
// production silently fall back to kimi/glm.
func (c *Client) upstreamModelID(model string) string {
canonical := accounts.CanonicalModelID(model)
if c != nil {
c.mu.Lock()
info, ok := c.catalog[model]
if !ok {
info, ok = c.catalog[canonical]
}
c.mu.Unlock()
if ok && strings.TrimSpace(info.NativeModel) != "" {
return info.NativeModel
}
}
return model
Expand Down
65 changes: 65 additions & 0 deletions internal/providers/workbuddy/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,9 @@ func TestChatRequestSendsOfficialReasoningFieldsForDeepseekFlash(t *testing.T) {
}); err != nil {
t.Fatal(err)
}
if got["model"] != "deepseek-v4.1-flash" {
t.Fatalf("upstream model=%v body=%v", got["model"], got)
}
if _, ok := got["reasoning"]; ok {
t.Fatalf("deepseek kept nested reasoning: %v", got["reasoning"])
}
Expand Down Expand Up @@ -679,6 +682,68 @@ func TestChatRequestMapsDeepseekAliasToNativeModel(t *testing.T) {
}
}

// Regression: once WorkBuddy publishes deepseek-v4.1-flash as a native CLI
// model, chat must send that ID instead of the stale deep-model alias.
func TestChatRequestKeepsNativeDeepseekWhenCatalogHasIt(t *testing.T) {
payload, _ := Credential{AccessToken: "at", UID: "u1", Domain: "codebuddy.cn", ExpiresAt: 4102444800}.Encode()
store := &memStore{items: map[string][]byte{"acc1": payload}}
var got map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == pathModelsCN {
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{
"models": []map[string]any{{
"id": "deepseek-v4.1-flash", "name": "Deepseek-V4.1-Flash", "maxInputTokens": 1000000,
"supportsReasoning": true, "onlyReasoning": true,
"reasoning": map[string]any{"defaultEffort": "high", "supportedEfforts": []string{"high"}},
}},
"agents": []map[string]any{{"name": "cli", "models": []string{"deepseek-v4.1-flash"}}},
}})
return
}
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
t.Fatalf("decode chat body: %v", err)
}
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte(chatSSE))
}))
defer server.Close()
client := NewClient(store)
client.http = server.Client()
client.http.Transport = rewriteTransport{server: server.URL, round: server.Client().Transport}

if _, err := client.ChatNonStream(context.Background(), "acc1", translate.ChatRequest{
Model: "deepseek-v4.1-flash", Messages: []translate.ChatMessage{{Role: "user", Content: "hi"}},
}); err != nil {
t.Fatal(err)
}
if got["model"] != "deepseek-v4.1-flash" {
t.Fatalf("upstream model=%v body=%v", got["model"], got)
}
}

func TestUpstreamModelIDDoesNotHardcodeStaleDeepModel(t *testing.T) {
client := NewClient(&memStore{items: map[string][]byte{}})
if got := client.upstreamModelID("deepseek-v4.1-flash"); got != "deepseek-v4.1-flash" {
t.Fatalf("cold catalog rewrite = %q", got)
}
client.rememberCatalog([]providers.ModelInfo{{
NativeModel: "deepseek-v4.1-flash",
PublicModel: "deepseek-v4.1-flash",
DisplayName: "Deepseek-V4.1-Flash",
}})
if got := client.upstreamModelID("deepseek-v4.1-flash"); got != "deepseek-v4.1-flash" {
t.Fatalf("native catalog rewrite = %q", got)
}
client.rememberCatalog([]providers.ModelInfo{{
NativeModel: "deep-model",
PublicModel: "deepseek-v4.1-flash",
DisplayName: "Deepseek-V4.1-Flash",
}})
if got := client.upstreamModelID("deepseek-v4.1-flash"); got != "deep-model" {
t.Fatalf("legacy alias rewrite = %q", got)
}
}

func TestModelsAcceptsGlobalCLIAgentNamesAndUsesAccountRegion(t *testing.T) {
payload, _ := Credential{AccessToken: "at", UID: "u1", Domain: "codebuddy.cn", ExpiresAt: 4102444800}.Encode()
store := &memStore{items: map[string][]byte{"acc1": payload}, region: "global"}
Expand Down
Loading