Files
llmgw/internal/router/context_limit_test.go
T
Ilker ManapandClaude Opus 5.5 3858114ada
CI / test (push) Successful in 10m43s
A picture in a prompt counts as what a model makes of it, not its base64
A 1280x720 PNG sent to qwen3-vl-8b was estimated at 138 thousand prompt
tokens and refused against its 8k context before reaching the engine, which
makes about a thousand of it. Parts are counted by kind: text by its text,
a picture or a sound as a thousand tokens.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 20:45:06 +02:00

124 lines
4.6 KiB
Go

package router
import (
"encoding/json"
"strings"
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
)
func TestAModelWithNoDeclaredContextRefusesNothing(t *testing.T) {
// Every model was in this state until now, and guessing a limit would turn
// working requests into errors.
m := catalog.Model{Name: "m"}
if why := checkContextFits(m, 1_000_000, 1_000_000); why != "" {
t.Errorf("refused a request against an unknown context: %q", why)
}
}
func TestAPromptOverTheContextIsRefused(t *testing.T) {
m := catalog.Model{Name: "coder", ContextLength: 2048}
why := checkContextFits(m, 5000, 0)
if why == "" {
t.Fatal("a prompt well over the context was allowed")
}
for _, want := range []string{"5000", "2048", "coder"} {
if !strings.Contains(why, want) {
t.Errorf("the message does not mention %s: %q", want, why)
}
}
}
func TestPromptPlusMaxTokensMustFitTogether(t *testing.T) {
// The failure people actually hit: a prompt that fits, asking for a
// completion that cannot follow it.
m := catalog.Model{Name: "coder", ContextLength: 2048}
if why := checkContextFits(m, 1800, 1000); why == "" {
t.Error("prompt plus max_tokens over the window was allowed")
}
if why := checkContextFits(m, 1000, 500); why != "" {
t.Errorf("a request that fits was refused: %q", why)
}
}
func TestTheEstimateIsGivenRoomToBeWrong(t *testing.T) {
// Bytes over four is wrong in both directions — code and non-Latin scripts
// tokenize nothing like English prose — and being a few percent out must
// not turn a valid request into a refusal.
m := catalog.Model{Name: "m", ContextLength: 1000}
if why := checkContextFits(m, 1050, 0); why != "" {
t.Errorf("a request just over the estimate was refused: %q", why)
}
if why := checkContextFits(m, 2000, 0); why == "" {
t.Error("a prompt at twice the context was allowed; the margin is too wide")
}
}
func TestMaxOutputIsCheckedOnItsOwn(t *testing.T) {
// Asking for more output than the model can produce is worth catching even
// when the total would fit.
m := catalog.Model{Name: "m", ContextLength: 32768, MaxOutput: 4096}
why := checkContextFits(m, 10, 8000)
if why == "" {
t.Fatal("max_tokens over max_output was allowed")
}
if !strings.Contains(why, "4096") {
t.Errorf("the message does not say the limit: %q", why)
}
}
func TestTheTokenEstimateCoversTheWholePrompt(t *testing.T) {
// It used to be taken from the routing prefix, which stops at prefixBytes,
// so every prompt over 2 KB estimated as 512 tokens however long it was.
// TPM limits therefore undercounted exactly the requests that consume most,
// and no context check could work.
long := strings.Repeat("x", 40_000)
b := &inferenceBody{Messages: []struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
}{{Role: "user", Content: json.RawMessage(`"` + long + `"`)}}}
got := promptBytes(b)
if got < 40_000 {
t.Fatalf("promptBytes = %d for a 40,000 byte prompt", got)
}
if prefix := len(promptPrefix(b)); prefix > prefixBytes {
t.Errorf("the routing prefix grew past its bound: %d", prefix)
}
// The two must stay different things: bounded for routing, whole for limits.
if got <= prefixBytes {
t.Error("the estimate is still bounded by the routing prefix")
}
}
func TestStructuredContentIsCounted(t *testing.T) {
// Multimodal messages carry an array of parts rather than a string. Counting
// zero for those would let a large request through unmeasured.
b := &inferenceBody{Messages: []struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
}{{Role: "user", Content: json.RawMessage(`[{"type":"text","text":"` + strings.Repeat("y", 5000) + `"}]`)}}}
if got := promptBytes(b); got < 5000 {
t.Errorf("structured content counted as %d bytes", got)
}
}
func TestAPictureIsCountedAsWhatAModelMakesOfItNotItsBase64(t *testing.T) {
// A 1280x720 PNG was counted as 138 thousand tokens of base64 and an 8k
// vision model refused it; its projector makes about a thousand of it.
pic := `{"type":"image_url","image_url":{"url":"data:image/png;base64,` + strings.Repeat("A", 550_000) + `"}}`
b := &inferenceBody{Messages: []struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
}{{Role: "user", Content: json.RawMessage(`[{"type":"text","text":"check it"},` + pic + `,` + pic + `]`)}}}
got := promptBytes(b) / 4
if got < 2000 || got > 2200 {
t.Errorf("two pictures and a line estimated as %d tokens", got)
}
m := catalog.Model{Name: "vlm", ContextLength: 8192}
if why := checkContextFits(m, got, 300); why != "" {
t.Errorf("refused: %s", why)
}
}