p2.2: cap embed inputs at 3000 runes, retry too-large 500

Link-heavy chunks estimated under EmbedMaxTok still overflow
Dixie's 2048-token embed slot. Cap every embed input at 3000
runes. A 500 whose body contains 'too large' halves inputs and
retries once. Fallback logs the truncated upstream body.
This commit is contained in:
2026-09-23 03:03:11 -07:00
parent 16a7d678d3
commit 5dbc7e4094
2 changed files with 103 additions and 13 deletions
+58 -13
View File
@@ -31,7 +31,7 @@ const (
SmallDoc = 6000
ChunkTokens = 1200
EmbedBatch = 16
EmbedMaxTok = 1536
EmbedMaxRunes = 3000
DefaultPermit = 2
DefaultQDepth = 8
DefaultQWait = 20 * time.Second
@@ -209,6 +209,29 @@ func (e *Engine) embedModel() string {
return "honcho-embed"
}
func (e *Engine) logFallback(reason string, err error) {
e.noteFallback(reason)
if e == nil || e.Log == nil {
return
}
if err != nil {
e.Log.Warn("digest fallback", "reason", reason, "err", truncateErr(err, 512))
return
}
e.Log.Warn("digest fallback", "reason", reason)
}
func truncateErr(err error, n int) string {
if err == nil {
return ""
}
s := err.Error()
if len(s) <= n {
return s
}
return s[:n]
}
func (e *Engine) noteFallback(reason string) {
if e == nil {
return
@@ -241,7 +264,7 @@ func (e *Engine) Run(ctx context.Context, cleaned, question, finalURL string) Re
}
ok, reason := e.Gate.Acquire(ctx)
if !ok {
e.noteFallback(reason)
e.logFallback(reason, nil)
r.Fallback = reason
r.DigestMS = time.Since(start).Milliseconds()
return r
@@ -260,7 +283,7 @@ func (e *Engine) Run(ctx context.Context, cleaned, question, finalURL string) Re
body, nUsed, nTotal, err := e.selectText(dctx, cleaned, question)
r.ChunksUsed, r.ChunksTotal = nUsed, nTotal
if err != nil {
e.noteFallback("embed")
e.logFallback("embed", err)
r.Fallback = "embed"
r.DigestMS = time.Since(start).Milliseconds()
return r
@@ -277,13 +300,13 @@ func (e *Engine) Run(ctx context.Context, cleaned, question, finalURL string) Re
if dctx.Err() != nil {
reason = "timeout"
}
e.noteFallback(reason)
e.logFallback(reason, err)
r.Fallback = reason
return r
}
text = strings.TrimSpace(text)
if text == "" {
e.noteFallback("empty")
e.logFallback("empty", nil)
r.Fallback = "empty"
return r
}
@@ -358,11 +381,7 @@ func (e *Engine) selectText(ctx context.Context, cleaned, question string) (stri
func capEmbed(chunks []string) []string {
out := make([]string, len(chunks))
for i, c := range chunks {
if EstTokens(c) > EmbedMaxTok {
out[i] = trimTokens(c, EmbedMaxTok)
} else {
out[i] = c
}
out[i] = trimRunes(c, EmbedMaxRunes)
}
return out
}
@@ -371,6 +390,7 @@ func (e *Engine) embed(ctx context.Context, inputs []string) ([][]float64, error
if e.EmbedURL == "" {
return nil, fmt.Errorf("no embed url")
}
inputs = capEmbed(inputs)
var all [][]float64
for i := 0; i < len(inputs); i += EmbedBatch {
j := i + EmbedBatch
@@ -387,6 +407,14 @@ func (e *Engine) embed(ctx context.Context, inputs []string) ([][]float64, error
}
func (e *Engine) embedBatch(ctx context.Context, inputs []string) ([][]float64, error) {
vecs, err := e.embedOnce(ctx, inputs)
if err != nil && strings.Contains(strings.ToLower(err.Error()), "too large") {
return e.embedOnce(ctx, halveInputs(inputs))
}
return vecs, err
}
func (e *Engine) embedOnce(ctx context.Context, inputs []string) ([][]float64, error) {
payload, _ := json.Marshal(map[string]any{"input": inputs, "model": e.embedModel()})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.EmbedURL, bytes.NewReader(payload))
if err != nil {
@@ -399,7 +427,8 @@ func (e *Engine) embedBatch(ctx context.Context, inputs []string) ([][]float64,
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("embed %d", resp.StatusCode)
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return nil, fmt.Errorf("embed %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
}
var env struct {
Data []struct {
@@ -455,7 +484,8 @@ func (e *Engine) chat(ctx context.Context, user string) (string, error) {
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("chat %d", resp.StatusCode)
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return "", fmt.Errorf("chat %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
}
return readSSE(ctx, resp.Body)
}
@@ -553,14 +583,29 @@ func countBudget(chunks []string, budget int) int {
func CapBudget(s string) string { return trimTokens(s, SmallDoc) }
func trimTokens(s string, max int) string {
return trimRunes(s, max*4)
}
func trimRunes(s string, n int) string {
r := []rune(s)
n := max * 4
if n >= len(r) {
return s
}
if n < 0 {
return ""
}
return string(r[:n])
}
func halveInputs(inputs []string) []string {
out := make([]string, len(inputs))
for i, s := range inputs {
n := utf8.RuneCountInString(s)
out[i] = trimRunes(s, n/2)
}
return out
}
func cosine(a, b []float64) float64 {
if len(a) == 0 || len(b) == 0 {
return 0