From 1c9e8db8e45b292b834d395bb000d94aa973fc99 Mon Sep 17 00:00:00 2001 From: Luvu182 <208665161+Luvu182@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:38:38 +0700 Subject: [PATCH] fix: OpenAI-compat SSE parsing, scanner error check, reasoning passback, and env var parity - SSE: handle "data:" without space (Kimi and other providers) - Add scanner.Err() check to detect stream read failures - Echo reasoning_content for thinking models (Kimi, DeepSeek) - Add Thinking field to Message struct for reasoning passback - Add GOCLAW_OPENAI_BASE_URL env var override (parity with Anthropic) --- docker-compose.yml | 1 + internal/config/config_load.go | 1 + internal/providers/openai.go | 17 +++++++++++++++-- internal/providers/types.go | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 191cc388..7d4770ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,7 @@ services: - GOCLAW_ANTHROPIC_API_KEY=${GOCLAW_ANTHROPIC_API_KEY:-} - GOCLAW_ANTHROPIC_BASE_URL=${GOCLAW_ANTHROPIC_BASE_URL:-} - GOCLAW_OPENAI_API_KEY=${GOCLAW_OPENAI_API_KEY:-} + - GOCLAW_OPENAI_BASE_URL=${GOCLAW_OPENAI_BASE_URL:-} - GOCLAW_MINIMAX_API_KEY=${GOCLAW_MINIMAX_API_KEY:-} - GOCLAW_GROQ_API_KEY=${GOCLAW_GROQ_API_KEY:-} - GOCLAW_DEEPSEEK_API_KEY=${GOCLAW_DEEPSEEK_API_KEY:-} diff --git a/internal/config/config_load.go b/internal/config/config_load.go index 02729206..989b48b8 100644 --- a/internal/config/config_load.go +++ b/internal/config/config_load.go @@ -95,6 +95,7 @@ func (c *Config) applyEnvOverrides() { envStr("GOCLAW_ANTHROPIC_API_KEY", &c.Providers.Anthropic.APIKey) envStr("GOCLAW_ANTHROPIC_BASE_URL", &c.Providers.Anthropic.APIBase) envStr("GOCLAW_OPENAI_API_KEY", &c.Providers.OpenAI.APIKey) + envStr("GOCLAW_OPENAI_BASE_URL", &c.Providers.OpenAI.APIBase) envStr("GOCLAW_OPENROUTER_API_KEY", &c.Providers.OpenRouter.APIKey) envStr("GOCLAW_GROQ_API_KEY", &c.Providers.Groq.APIKey) envStr("GOCLAW_DEEPSEEK_API_KEY", &c.Providers.DeepSeek.APIKey) diff --git a/internal/providers/openai.go b/internal/providers/openai.go index 7b903808..18d5c174 100644 --- a/internal/providers/openai.go +++ b/internal/providers/openai.go @@ -105,10 +105,13 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ChatRequest, onChun scanner := bufio.NewScanner(respBody) for scanner.Scan() { line := scanner.Text() - if !strings.HasPrefix(line, "data: ") { + if !strings.HasPrefix(line, "data:") { continue } - data := strings.TrimPrefix(line, "data: ") + // SSE spec allows both "data: value" and "data:value" (space is optional). + // Some providers (e.g. Kimi) omit the space after the colon. + data := strings.TrimPrefix(line, "data:") + data = strings.TrimPrefix(data, " ") if data == "[DONE]" { break } @@ -174,6 +177,11 @@ func (p *OpenAIProvider) ChatStream(ctx context.Context, req ChatRequest, onChun } + // Check for scanner errors (timeout, connection reset, etc.) + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("%s: stream read error: %w", p.name, err) + } + // Parse accumulated tool call arguments for i := 0; i < len(accumulators); i++ { acc := accumulators[i] @@ -217,6 +225,11 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream "role": m.Role, } + // Echo reasoning_content for assistant messages (required by Kimi, DeepSeek when thinking is enabled) + if m.Thinking != "" && m.Role == "assistant" { + msg["reasoning_content"] = m.Thinking + } + // Include content; omit empty content for assistant messages with tool_calls // (Gemini rejects empty content → "must include at least one parts field"). if m.Role == "user" && len(m.Images) > 0 { diff --git a/internal/providers/types.go b/internal/providers/types.go index 929b9519..4450a601 100644 --- a/internal/providers/types.go +++ b/internal/providers/types.go @@ -76,6 +76,7 @@ type ImageContent struct { type Message struct { Role string `json:"role"` // "system", "user", "assistant", "tool" Content string `json:"content"` + Thinking string `json:"thinking,omitempty"` // reasoning_content for thinking models (Kimi, DeepSeek, etc.) Images []ImageContent `json:"images,omitempty"` // vision: base64 images ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` // for role="tool" responses