sipeed/picoclaw · error

parse response: %w

Error message

parse response: %w

What it means

json.Unmarshal of the HTTP response body into chatResponse failed (llm_client.go:180). The server returned 200 but the body is not valid JSON for this struct — an HTML error page from a reverse proxy, an empty or truncated body, a differently-shaped JSON (missing fields is fine, wrong types are not), or a BOM/whitespace prefix. The error text names byte offset, which localizes the mismatch.

Source

Thrown at cmd/membench/llm_client.go:180

		if resp.StatusCode == 429 || resp.StatusCode >= 500 {
			lastErr = fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBody))
			continue // rate limit or server error → retry
		}
		if resp.StatusCode != 200 {
			return "", fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBody))
		}

		lastErr = nil
		break
	}
	if lastErr != nil {
		return "", fmt.Errorf("after %d retries: %w", c.MaxRetries, lastErr)
	}

	var chatResp chatResponse
	if err := json.Unmarshal(respBody, &chatResp); err != nil {
		return "", fmt.Errorf("parse response: %w", err)
	}
	if len(chatResp.Choices) == 0 {
		return "", fmt.Errorf("no choices in response")
	}
	content := strings.TrimSpace(chatResp.Choices[0].Message.Content)
	// Strip any residual <think>...</think> blocks
	if idx := strings.Index(content, "</think>"); idx >= 0 {
		content = strings.TrimSpace(content[idx+len("</think>"):])
	}
	// Fallback: GLM/DeepSeek put thinking output in reasoning_content when thinking is enabled
	if content == "" && chatResp.Choices[0].Message.ReasoningContent != "" {
		content = strings.TrimSpace(chatResp.Choices[0].Message.ReasoningContent)
	}
	if content == "" {
		return "", fmt.Errorf("empty LLM response")
	}
	return content, nil
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. curl -sS $BASE/chat/completions -H 'Content-Type: application/json' -d @req.json and eyeball the actual body
  2. Fix BaseURL to the exact OpenAI-compatible path (commonly http://host:port/v1)
  3. Log the first ~512 bytes of respBody on parse failure to see what arrived
  4. If fields differ, extend chatResponse with json.RawMessage or custom UnmarshalJSON instead of failing

Example fix

// before
if err := json.Unmarshal(respBody, &chatResp); err != nil {
    return "", fmt.Errorf("parse response: %w", err)
}

// after
if err := json.Unmarshal(respBody, &chatResp); err != nil {
    snippet := string(respBody)
    if len(snippet) > 512 { snippet = snippet[:512] }
    return "", fmt.Errorf("parse response (status 200, body starts with %q): %w", snippet, err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

ct := resp.Header.Get("Content-Type")
if ct != "" && !strings.Contains(ct, "json") {
    return "", fmt.Errorf("non-JSON response (%s) from %s — wrong endpoint?", ct, endpoint)
}

Try / catch

var chatResp chatResponse
if err := json.Unmarshal(respBody, &chatResp); err != nil {
    head := string(respBody)
    if len(head) > 256 { head = head[:256] }
    return "", fmt.Errorf("parse response (status %d, body %q): %w", resp.StatusCode, head, err)
}

Prevention

When it happens

Trigger: BaseURL points at a web UI or gateway that answers 200 with HTML; proxy (nginx, Cloudflare) intercepting with a status page; response field types differing (e.g. usage as string vs object); chunked body cut off mid-transfer; Ollama version returning a non-OpenAI schema at the wrong path.

Common situations: Using an OpenAI-compatible client against a non-compatible endpoint; BaseURL missing /v1 so the root handler responds 200 HTML; older/newer provider schema drift; corporate proxy rewriting responses.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/a85aa0d5db74f34f. Report an issue: GitHub.