распознавание: большие раздачи размечаются целиком, файлы вне плана видны
- модель адресует файл номером строки нашего списка вместо копии пути: ответ на 180 файлов вместо ~15k токенов стоит ~2.5k, усечение сотней снято, max_files и max_tokens ушли в [recognition], correction-ретрай больше не переприсылает список - негодный элемент ответа отбрасывается поимённой причиной, обрыв генерации и отказ по размеру запроса названы своими причинами, покрытие плана блокирует авто только при непокрытом видеофайле - раскладка показывает все файлы раздачи со строками «не в плане» и полным порядком сортировки; снимок списка файлов лёг рядом с планом (миграция 0012)
This commit is contained in:
@@ -3,6 +3,7 @@ package llm
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -234,3 +235,72 @@ func TestNew_OpenAICompatValidation(t *testing.T) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Признак обрыва генерации по длине доходит до вызывающего: по одному телу
|
||||
// ответа обрыв неотличим от мусора, а повторять такой запрос бессмысленно.
|
||||
func TestComplete_FinishReasonReachesCaller(t *testing.T) {
|
||||
for _, want := range []string{"length", "stop"} {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, `{"model":"m","choices":[{"message":{"content":"{\"type\":"},`+
|
||||
`"finish_reason":"`+want+`"}]}`)
|
||||
}))
|
||||
|
||||
p := newTestProvider(t, srv.URL, "")
|
||||
resp, err := p.Complete(context.Background(), Request{
|
||||
Messages: []Message{{Role: RoleUser, Content: "hi"}},
|
||||
})
|
||||
srv.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Complete: %v", err)
|
||||
}
|
||||
if resp.FinishReason != want {
|
||||
t.Errorf("finish_reason = %q, want %q", resp.FinishReason, want)
|
||||
}
|
||||
}
|
||||
if FinishLength != "length" {
|
||||
t.Errorf("FinishLength = %q, want length", FinishLength)
|
||||
}
|
||||
}
|
||||
|
||||
// Отказ по размеру запроса помечается sentinel'ом там, где ещё целы
|
||||
// HTTP-статус и ПОЛНОЕ тело: вызывающему нечего матчить по тексту, а текст
|
||||
// итоговой ошибки обрезан (snippet) и несёт эхо запроса.
|
||||
func TestComplete_RequestTooLargeSentinel(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
status int
|
||||
body string
|
||||
want bool
|
||||
}{
|
||||
{"413 без разбора текста", http.StatusRequestEntityTooLarge, `payload too big`, true},
|
||||
{"400 с маркером в теле", http.StatusBadRequest,
|
||||
`{"error":{"message":"This model's maximum context length is 128000 tokens"}}`, true},
|
||||
{"400 с маркером за пределом snippet", http.StatusBadRequest,
|
||||
`{"echo":"` + strings.Repeat("x", 400) + `","error":{"message":"prompt is too long"}}`, true},
|
||||
{"400 по другой причине", http.StatusBadRequest,
|
||||
`{"error":{"message":"invalid api key"}}`, false},
|
||||
{"500 — транзиентный сбой, не размер", http.StatusInternalServerError,
|
||||
`context length exceeded`, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(tc.status)
|
||||
_, _ = io.WriteString(w, tc.body)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := newTestProvider(t, srv.URL, "")
|
||||
_, err := p.Complete(context.Background(), Request{
|
||||
Messages: []Message{{Role: RoleUser, Content: "hi"}},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("ожидалась ошибка")
|
||||
}
|
||||
if got := errors.Is(err, ErrRequestTooLarge); got != tc.want {
|
||||
t.Errorf("errors.Is(err, ErrRequestTooLarge) = %v, want %v (err = %v)",
|
||||
got, tc.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user