返回 DeepSeek-Reasonix
context_limit.go
根目录 / internal / provider / context_limit.go
1 package provider
2
3 import (
4 "encoding/json"
5 "errors"
6 "net/http"
7 "regexp"
8 "strconv"
9 "strings"
10 )
11
12 // ContextLimitError is a trusted shared-window overflow from a provider HTTP
13 // 400/413/422. Unwrap returns the original APIError so localization, trace IDs,
14 // and telemetry keep working. The body is never persisted or replayed.
15 type ContextLimitError struct {
16 APIError *APIError
17 WindowTokens int
18 RequestedTokens int
19 PromptTokens int
20 CompletionTokens int
21 }
22
23 // OutputLimitError is a provider-reported completion-token ceiling. It is
24 // separate from ContextLimitError because the request may fit the model
25 // context window while exceeding the route's output-only limit.
26 type OutputLimitError struct {
27 APIError *APIError
28 RequestedTokens int
29 MaxOutputTokens int
30 }
31
32 func (e *OutputLimitError) Error() string {
33 if e == nil {
34 return "output token limit exceeded"
35 }
36 if e.APIError != nil {
37 return e.APIError.Error()
38 }
39 return "output token limit exceeded"
40 }
41
42 func (e *OutputLimitError) Unwrap() error {
43 if e == nil {
44 return nil
45 }
46 return e.APIError
47 }
48
49 func (e *ContextLimitError) Error() string {
50 if e == nil {
51 return "context limit exceeded"
52 }
53 if e.APIError != nil {
54 return e.APIError.Error()
55 }
56 return "context limit exceeded"
57 }
58
59 func (e *ContextLimitError) Unwrap() error {
60 if e == nil {
61 return nil
62 }
63 return e.APIError
64 }
65
66 var (
67 contextLimitEnglishRe = regexp.MustCompile(`(?i)maximum context length is (\d+) tokens?\.?\s*however,\s*you requested (\d+) tokens? \((\d+) in the (?:messages|prompt), (\d+) in the completion\)`)
68 contextLimitPromptRe = regexp.MustCompile(`(?i)prompt is too long:\s*(\d+) tokens? > (\d+) maximum`)
69 contextLimitSumRe = regexp.MustCompile("(?i)input length and [`']?max_tokens[`']? exceed context limit:\\s*(\\d+)\\s*\\+\\s*(\\d+)\\s*>\\s*(\\d+)")
70 outputLimitRe = regexp.MustCompile(`(?i)max_tokens\s*(?:is\s+too\s+large|too\s+large)\s*[:=]?\s*(\d+).*?(?:supports?|maximum|at\s+most)[^\d]*(\d+)`)
71 )
72
73 func contextLimitStatusOK(status int) bool {
74 return status == http.StatusBadRequest || status == http.StatusRequestEntityTooLarge || status == http.StatusUnprocessableEntity
75 }
76
77 func positiveToken(n int) bool { return n > 0 }
78
79 func contextLimitInvariant(window, requested, prompt, completion int) bool {
80 if !positiveToken(window) {
81 return false
82 }
83 if positiveToken(prompt) && positiveToken(completion) {
84 if prompt+completion <= window {
85 return false
86 }
87 if requested > 0 && requested != prompt+completion {
88 return false
89 }
90 return true
91 }
92 if requested > window && (prompt > 0 || completion > 0 || requested > 0) {
93 return requested > window
94 }
95 return false
96 }
97
98 func completeContextLimit(window, requested, prompt, completion int) (int, int, int, int, bool) {
99 if window <= 0 {
100 return 0, 0, 0, 0, false
101 }
102 if prompt > 0 && completion > 0 && requested <= 0 {
103 requested = prompt + completion
104 }
105 if requested > 0 && prompt > 0 && completion <= 0 && requested > prompt {
106 completion = requested - prompt
107 }
108 if requested > 0 && completion > 0 && prompt <= 0 && requested > completion {
109 prompt = requested - completion
110 }
111 if !contextLimitInvariant(window, requested, prompt, completion) {
112 return 0, 0, 0, 0, false
113 }
114 if requested <= 0 {
115 requested = prompt + completion
116 }
117 return window, requested, prompt, completion, true
118 }
119
120 type contextLimitJSON struct {
121 Error *struct {
122 Message string `json:"message"`
123 ContextLength int `json:"context_length"`
124 MaxContextLength int `json:"max_context_length"`
125 MaxTokens int `json:"max_tokens"`
126 RequestedTokens int `json:"requested_tokens"`
127 Requested int `json:"requested"`
128 PromptTokens int `json:"prompt_tokens"`
129 InputTokens int `json:"input_tokens"`
130 CompletionTokens int `json:"completion_tokens"`
131 OutputTokens int `json:"output_tokens"`
132 } `json:"error"`
133 ContextLength int `json:"context_length"`
134 MaxContextLength int `json:"max_context_length"`
135 RequestedTokens int `json:"requested_tokens"`
136 PromptTokens int `json:"prompt_tokens"`
137 InputTokens int `json:"input_tokens"`
138 CompletionTokens int `json:"completion_tokens"`
139 OutputTokens int `json:"output_tokens"`
140 Usage *struct {
141 InputTokens int `json:"input_tokens"`
142 OutputTokens int `json:"output_tokens"`
143 PromptTokens int `json:"prompt_tokens"`
144 CompletionTokens int `json:"completion_tokens"`
145 } `json:"usage"`
146 }
147
148 func firstPositive(values ...int) int {
149 for _, n := range values {
150 if n > 0 {
151 return n
152 }
153 }
154 return 0
155 }
156
157 func parseContextLimitJSON(body string) (window, requested, prompt, completion int, message string, ok bool) {
158 var parsed contextLimitJSON
159 if json.Unmarshal([]byte(body), &parsed) != nil {
160 return 0, 0, 0, 0, "", false
161 }
162 if parsed.Error != nil {
163 message = parsed.Error.Message
164 window = firstPositive(parsed.Error.ContextLength, parsed.Error.MaxContextLength)
165 requested = firstPositive(parsed.Error.RequestedTokens, parsed.Error.Requested)
166 prompt = firstPositive(parsed.Error.PromptTokens, parsed.Error.InputTokens)
167 completion = firstPositive(parsed.Error.CompletionTokens, parsed.Error.OutputTokens)
168 }
169 window = firstPositive(window, parsed.ContextLength, parsed.MaxContextLength)
170 requested = firstPositive(requested, parsed.RequestedTokens)
171 prompt = firstPositive(prompt, parsed.PromptTokens, parsed.InputTokens)
172 completion = firstPositive(completion, parsed.CompletionTokens, parsed.OutputTokens)
173 if parsed.Usage != nil {
174 prompt = firstPositive(prompt, parsed.Usage.PromptTokens, parsed.Usage.InputTokens)
175 completion = firstPositive(completion, parsed.Usage.CompletionTokens, parsed.Usage.OutputTokens)
176 }
177 if window, requested, prompt, completion, ok = completeContextLimit(window, requested, prompt, completion); ok {
178 return window, requested, prompt, completion, message, true
179 }
180 return 0, 0, 0, 0, message, false
181 }
182
183 func parseContextLimitText(text string) (window, requested, prompt, completion int, ok bool) {
184 text = strings.TrimSpace(text)
185 if text == "" {
186 return 0, 0, 0, 0, false
187 }
188 if m := contextLimitEnglishRe.FindStringSubmatch(text); len(m) == 5 {
189 return completeContextLimit(atoiStrict(m[1]), atoiStrict(m[2]), atoiStrict(m[3]), atoiStrict(m[4]))
190 }
191 if m := contextLimitSumRe.FindStringSubmatch(text); len(m) == 4 {
192 return completeContextLimit(atoiStrict(m[3]), 0, atoiStrict(m[1]), atoiStrict(m[2]))
193 }
194 if m := contextLimitPromptRe.FindStringSubmatch(text); len(m) == 3 {
195 prompt = atoiStrict(m[1])
196 window = atoiStrict(m[2])
197 if prompt > 0 && window > 0 && prompt > window {
198 return window, prompt, prompt, 0, true
199 }
200 }
201 return 0, 0, 0, 0, false
202 }
203
204 func atoiStrict(s string) int {
205 n, err := strconv.Atoi(strings.TrimSpace(s))
206 if err != nil || n <= 0 {
207 return 0
208 }
209 return n
210 }
211
212 // ParseContextLimitError extracts a trusted overflow from an APIError.
213 // Unparseable, non-context, or invariant-breaking bodies return nil.
214 func ParseContextLimitError(apiErr *APIError) *ContextLimitError {
215 if apiErr == nil || !contextLimitStatusOK(apiErr.Status) {
216 return nil
217 }
218 window, requested, prompt, completion, message, jsonOK := parseContextLimitJSON(apiErr.Body)
219 if !jsonOK {
220 if w, r, p, c, ok := parseContextLimitText(apiErr.Body); ok {
221 window, requested, prompt, completion = w, r, p, c
222 } else if w, r, p, c, ok := parseContextLimitText(message); ok {
223 window, requested, prompt, completion = w, r, p, c
224 } else {
225 // A bare overflow with no token numbers (Zhipu GLM 1261) is still
226 // provider-confirmed: trust it with an unknown window so consumers
227 // fall back to the configured window instead of resending as-is.
228 if isUnnumberedPromptTooLong(message, apiErr.Body) {
229 return &ContextLimitError{APIError: apiErr}
230 }
231 return nil
232 }
233 }
234 if !contextLimitInvariant(window, requested, prompt, completion) &&
235 !(window > 0 && requested > window && prompt > 0) {
236 if isUnnumberedPromptTooLong(message, apiErr.Body) {
237 return &ContextLimitError{APIError: apiErr}
238 }
239 return nil
240 }
241 if requested <= 0 {
242 requested = prompt + completion
243 }
244 return &ContextLimitError{
245 APIError: apiErr,
246 WindowTokens: window,
247 RequestedTokens: requested,
248 PromptTokens: prompt,
249 CompletionTokens: completion,
250 }
251 }
252
253 // isUnnumberedPromptTooLong matches provider overflow errors that carry no
254 // token numbers at all. Canonical shape — Zhipu GLM 1261:
255 //
256 // {"error":{"code":"1261","message":"Prompt exceeds max length"}}
257 //
258 // The message (or the whole body, when the JSON shape differs) is matched
259 // case-insensitively; code 1261 is not matched directly so sibling GLM codes
260 // that reuse the message stay covered and numeric codes never false-positive.
261 func isUnnumberedPromptTooLong(message, body string) bool {
262 for _, s := range []string{message, body} {
263 if s == "" {
264 continue
265 }
266 if strings.Contains(strings.ToLower(s), "prompt exceeds max length") {
267 return true
268 }
269 }
270 return false
271 }
272
273 // AsContextLimitError unwraps err to a trusted overflow, if any.
274 func AsContextLimitError(err error) *ContextLimitError {
275 var limit *ContextLimitError
276 if err != nil && errors.As(err, &limit) {
277 return limit
278 }
279 return nil
280 }
281
282 // ParseOutputLimitError extracts a completion-only ceiling from a 400/413/422
283 // API error. The parser is intentionally conservative: it only accepts text
284 // that names both the requested max_tokens and a smaller supported maximum.
285 func ParseOutputLimitError(apiErr *APIError) *OutputLimitError {
286 if apiErr == nil || !contextLimitStatusOK(apiErr.Status) {
287 return nil
288 }
289 text := strings.TrimSpace(apiErr.Body)
290 if text == "" {
291 return nil
292 }
293 m := outputLimitRe.FindStringSubmatch(text)
294 if len(m) != 3 {
295 return nil
296 }
297 requested, maxOutput := atoiStrict(m[1]), atoiStrict(m[2])
298 if requested <= 0 || maxOutput <= 0 || requested <= maxOutput {
299 return nil
300 }
301 return &OutputLimitError{APIError: apiErr, RequestedTokens: requested, MaxOutputTokens: maxOutput}
302 }
303
304 // AsOutputLimitError unwraps err to a trusted output ceiling, if any.
305 func AsOutputLimitError(err error) *OutputLimitError {
306 var limit *OutputLimitError
307 if err != nil && errors.As(err, &limit) {
308 return limit
309 }
310 return nil
311 }
312
312 lines GO