распознавание: большие раздачи размечаются целиком, файлы вне плана видны

- модель адресует файл номером строки нашего списка вместо копии пути: ответ
  на 180 файлов вместо ~15k токенов стоит ~2.5k, усечение сотней снято,
  max_files и max_tokens ушли в [recognition], correction-ретрай больше не
  переприсылает список
- негодный элемент ответа отбрасывается поимённой причиной, обрыв генерации и
  отказ по размеру запроса названы своими причинами, покрытие плана блокирует
  авто только при непокрытом видеофайле
- раскладка показывает все файлы раздачи со строками «не в плане» и полным
  порядком сортировки; снимок списка файлов лёг рядом с планом (миграция 0012)
This commit is contained in:
av
2026-09-02 08:54:58 +03:00
parent 01ac0430a9
commit fc9a3b4066
26 changed files with 1978 additions and 201 deletions
+38 -4
View File
@@ -193,8 +193,11 @@ func (c *openAICompat) do(ctx context.Context, body []byte) (Response, bool, err
if resp.StatusCode != http.StatusOK {
retryable := resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500
return Response{}, retryable, fmt.Errorf("llm: status %d: %s",
resp.StatusCode, snippet(raw))
err := fmt.Errorf("llm: status %d: %s", resp.StatusCode, snippet(raw))
if requestTooLarge(resp.StatusCode, raw) {
err = fmt.Errorf("%w: %w", ErrRequestTooLarge, err)
}
return Response{}, retryable, err
}
var cr chatResponse
@@ -209,8 +212,9 @@ func (c *openAICompat) do(ctx context.Context, body []byte) (Response, bool, err
}
return Response{
Content: cr.Choices[0].Message.Content,
Model: cr.Model,
Content: cr.Choices[0].Message.Content,
Model: cr.Model,
FinishReason: cr.Choices[0].FinishReason,
Usage: Usage{
PromptTokens: cr.Usage.PromptTokens,
CompletionTokens: cr.Usage.CompletionTokens,
@@ -236,6 +240,36 @@ func (c *openAICompat) wait(ctx context.Context, attempt int) error {
}
}
// tooLargeMarkers — текстовые признаки переполнения контекста в теле отказа.
// Единого машинного кода у OpenAI-совместимых шлюзов нет: часть отдаёт 413,
// остальные кладут причину в текст при обычном 400.
var tooLargeMarkers = []string{
"context length", "context_length", "context window", "maximum context",
"too many tokens", "prompt is too long", "request too large",
"reduce the length",
}
// requestTooLarge — отказал ли провайдер из-за размера запроса. 413 —
// безусловно, без разбора текста. 400 — только по маркерам, и смотрим их в
// ПОЛНОМ теле: обрезка тела (snippet) выбрасывает причину ровно у тех шлюзов,
// которые сперва echo'ят запрос. Ложное срабатывание безопасно: задача уходит
// в review, а не в повтор.
func requestTooLarge(status int, body []byte) bool {
if status == http.StatusRequestEntityTooLarge {
return true
}
if status != http.StatusBadRequest {
return false
}
lower := strings.ToLower(string(body))
for _, marker := range tooLargeMarkers {
if strings.Contains(lower, marker) {
return true
}
}
return false
}
// snippet обрезает тело для сообщения об ошибке.
func snippet(b []byte) string {
const max = 300