| 1 | package provider |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "net/http" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | // ReasoningReplayError is a trusted provider rejection of replayed |
| 10 | // thinking/reasoning history: HTTP 400 whose body says the request's |
| 11 | // thinking/reasoning blocks must be passed back. Unwrap returns the original |
| 12 | // APIError so localization, trace IDs, and telemetry keep working. The body is |
| 13 | // never persisted or replayed. |
| 14 | type ReasoningReplayError struct { |
| 15 | APIError *APIError |
| 16 | } |
| 17 | |
| 18 | func (e *ReasoningReplayError) Error() string { |
| 19 | if e == nil || e.APIError == nil { |
| 20 | return "reasoning replay rejected" |
| 21 | } |
| 22 | return e.APIError.Error() |
| 23 | } |
| 24 | |
| 25 | func (e *ReasoningReplayError) Unwrap() error { |
| 26 | if e == nil { |
| 27 | return nil |
| 28 | } |
| 29 | return e.APIError |
| 30 | } |
| 31 | |
| 32 | // ParseReasoningReplayError extracts a trusted thinking-replay rejection from |
| 33 | // an APIError. The match is deliberately exact — DeepSeek's documented shape is |
| 34 | // "The `content[].thinking` in the thinking mode must be passed back to the |
| 35 | // API" — so only a 400 naming thinking/reasoning content AND the pass-back |
| 36 | // obligation qualifies. Any other 400 (and every other status) returns nil so |
| 37 | // the retry budget can never swallow an unrelated client error. |
| 38 | func ParseReasoningReplayError(apiErr *APIError) *ReasoningReplayError { |
| 39 | if apiErr == nil || apiErr.Status != http.StatusBadRequest { |
| 40 | return nil |
| 41 | } |
| 42 | body := strings.ToLower(apiErr.Body) |
| 43 | namesReasoning := strings.Contains(body, "content[].thinking") || strings.Contains(body, "reasoning_content") || strings.Contains(body, "reasoning_text") |
| 44 | if !namesReasoning || !strings.Contains(body, "must be passed back") { |
| 45 | return nil |
| 46 | } |
| 47 | return &ReasoningReplayError{APIError: apiErr} |
| 48 | } |
| 49 | |
| 50 | // AsReasoningReplayError unwraps err to a trusted thinking-replay rejection, |
| 51 | // if any. |
| 52 | func AsReasoningReplayError(err error) *ReasoningReplayError { |
| 53 | var replay *ReasoningReplayError |
| 54 | if err != nil && errors.As(err, &replay) { |
| 55 | return replay |
| 56 | } |
| 57 | return nil |
| 58 | } |
| 59 |