fix(windsurf): fix tool call for legacy-enum models + gateway logger

Three fixes:

1. Logger: windsurf_gateway_service used zap.L() (nop) instead of
   logger.L() — all gateway-level logs were silently dropped.

2. Tool mode routing: when tools are present in the request,
   force cascade mode even for legacy-enum models. Legacy mode
   ignores toolPreamble entirely, so tool calls were never injected.

3. Model enum hint: pass meta.EnumValue through to
   SendUserCascadeMessage/buildCascadeConfig as a fallback when
   modelUID-based enum resolution returns 0. Prevents 'neither
   PlanModel nor RequestedModel specified' gRPC errors.

Tested: claude-sonnet-4-6 with tool definitions returns proper
tool_use content blocks in both streaming and non-streaming modes.
Tool result round-trip verified.
This commit is contained in:
win 2026-04-23 23:04:02 +08:00
parent 9112645bf9
commit 8b446ffef8
4 changed files with 28 additions and 7 deletions

View File

@ -279,7 +279,7 @@ func main() {
// SendUserCascadeMessage
{
ctx, cancel := context.WithTimeout(context.Background(), f.timeout)
newCID, err := lsClient.SendUserCascadeMessage(ctx, f.jwt, cascadeID, f.prompt, pickedModel, "")
newCID, err := lsClient.SendUserCascadeMessage(ctx, f.jwt, cascadeID, f.prompt, pickedModel, "", 0)
if err == nil && newCID != "" {
cascadeID = newCID
}

View File

@ -159,8 +159,11 @@ func (l *LocalLSClient) StartCascade(ctx context.Context, token string) (string,
// SendUserCascadeMessage sends a message into an existing cascade session.
// Returns the (possibly new) cascadeID — it changes if panel-state retry triggers a new StartCascade.
// toolPreamble, if non-empty, is injected into the tool_calling_section override.
func (l *LocalLSClient) SendUserCascadeMessage(ctx context.Context, token, cascadeID, text, modelUID, toolPreamble string) (string, error) {
func (l *LocalLSClient) SendUserCascadeMessage(ctx context.Context, token, cascadeID, text, modelUID, toolPreamble string, modelEnumHint int) (string, error) {
modelEnum := resolveModelEnum(modelUID)
if modelEnum == 0 && modelEnumHint > 0 {
modelEnum = modelEnumHint
}
doSend := func(cid string) error {
body := encodeStringField(1, cid)
@ -369,7 +372,7 @@ func (e *CascadeModelError) Error() string { return e.Msg }
// StreamCascadeChat performs the full Cascade chat flow and returns accumulated text + thinking.
// Includes cold/warm stall detection, step error handling, and final sweep (aligned with JS v1.9).
// If reuseCascadeID is non-empty, skips StartCascade and reuses the existing cascade session.
func (l *LocalLSClient) StreamCascadeChat(ctx context.Context, token, modelUID, userText, toolPreamble, reuseCascadeID string) (*CascadeChatResult, error) {
func (l *LocalLSClient) StreamCascadeChat(ctx context.Context, token, modelUID, userText, toolPreamble, reuseCascadeID string, modelEnumHint int) (*CascadeChatResult, error) {
if err := l.WarmupCascade(ctx, token); err != nil {
return nil, fmt.Errorf("warmup: %w", err)
}
@ -385,7 +388,7 @@ func (l *LocalLSClient) StreamCascadeChat(ctx context.Context, token, modelUID,
}
}
cascadeID, err = l.SendUserCascadeMessage(ctx, token, cascadeID, userText, modelUID, toolPreamble)
cascadeID, err = l.SendUserCascadeMessage(ctx, token, cascadeID, userText, modelUID, toolPreamble, modelEnumHint)
if err != nil {
return nil, fmt.Errorf("SendUserCascadeMessage: %w", err)
}
@ -578,6 +581,17 @@ func (l *LocalLSClient) StreamCascadeChat(ctx context.Context, token, modelUID,
}
}
slog.Info("windsurf_cascade_poll_result",
"cascade_id", cascadeID[:min(8, len(cascadeID))],
"acc_text_len", len(accText),
"acc_thinking_len", len(accThinking),
"native_tool_calls", len(nativeToolCalls),
"saw_active", sawActive,
"saw_text", sawText,
"steps_seen", len(textCursors),
"idle_count", idleCount,
)
// Aggregate step usage
var aggUsage *StepUsage
for _, u := range usageByStep {

View File

@ -61,6 +61,10 @@ func (s *WindsurfChatService) Chat(ctx context.Context, req *WindsurfChatRequest
meta := windsurf.GetModelInfo(modelKey)
mode := s.resolveMode(meta)
// Tool emulation requires cascade mode for proto section injection
if mode == "legacy" && req.ToolPreamble != "" {
mode = "cascade"
}
var lease *windsurf.LSLease
if token.LSBinding.ContainerID != "" || token.LSBinding.ContainerName != "" {
@ -109,8 +113,10 @@ func (s *WindsurfChatService) resolveMode(meta *windsurf.ModelMeta) string {
func (s *WindsurfChatService) chatCascade(ctx context.Context, client *windsurf.LocalLSClient, apiKey string, meta *windsurf.ModelMeta, messages []windsurf.ChatMessage, toolPreamble string, modelKey string, lsEndpoint string) (*WindsurfChatResponse, error) {
modelUID := ""
modelEnumHint := 0
if meta != nil {
modelUID = meta.ModelUID
modelEnumHint = meta.EnumValue
}
fpBefore := windsurf.FingerprintBefore(messages, modelKey)
@ -125,11 +131,11 @@ func (s *WindsurfChatService) chatCascade(ctx context.Context, client *windsurf.
userText := buildCascadeText(messages, modelUID, isResume)
result, err := client.StreamCascadeChat(ctx, apiKey, modelUID, userText, toolPreamble, reuseCascadeID)
result, err := client.StreamCascadeChat(ctx, apiKey, modelUID, userText, toolPreamble, reuseCascadeID, modelEnumHint)
if err != nil && isResume {
slog.Warn("windsurf_cascade_reuse_failed", "error", err, "model", modelKey)
userText = buildCascadeText(messages, modelUID, false)
result, err = client.StreamCascadeChat(ctx, apiKey, modelUID, userText, toolPreamble, "")
result, err = client.StreamCascadeChat(ctx, apiKey, modelUID, userText, toolPreamble, "", modelEnumHint)
}
if err != nil {
return nil, err

View File

@ -10,6 +10,7 @@ import (
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/pkg/windsurf"
"github.com/gin-gonic/gin"
@ -674,7 +675,7 @@ func windsurfExtractContentTextFromRaw(raw json.RawMessage) string {
}
func windsurfLogger(c *gin.Context, component string, fields ...zap.Field) *zap.Logger {
l := zap.L().With(zap.String("component", component))
l := logger.L().With(zap.String("component", component))
if c != nil {
if reqID := c.GetHeader("X-Request-ID"); reqID != "" {
l = l.With(zap.String("request_id", reqID))