JuliusBrussee/caveman · error

openai compress: replacements for segments

Error message

openai compress: %d replacements for %d segments

What it means

spliceStringReplacements splices string replacements into an OpenAI payload and, like jsonsplice.Replace, requires len(reps) == len(candidates) so each candidate maps to exactly one replacement. Mismatched lengths abort the splice.

Solutions

  1. Build candidates and replacements together in one loop; never filter one without the other
  2. Filter with a paired struct then split into the two slices immediately before the call
  3. When no segment changes, return the original body instead of invoking the splice
  4. Add a length assertion/log upstream to catch divergent filters in CI

Example fix

// before: unfiltered reps
out, err := spliceStringReplacements(body, cands, reps)
// after: pair and filter together
type seg struct{ c spliceCandidate; r []byte }
var kept []seg
for i := range cands {
    if !bytes.Equal(reps[i], body[cands[i].start:cands[i].end]) {
        kept = append(kept, seg{cands[i], reps[i]})
    }
}
c := make([]spliceCandidate, len(kept)); r := make([][]byte, len(kept))
for i, s := range kept { c[i], r[i] = s.c, s.r }
out, err := spliceStringReplacements(body, c, r)
Defensive patterns

Strategy: validation

Validate before calling

if len(cands) != len(reps) { return fmt.Errorf("segment/replacement mismatch: %d vs %d", len(cands), len(reps)) }

Try / catch

out, err := spliceStringReplacements(body, cands, reps)
if err != nil {
    if strings.Contains(err.Error(), "replacements for") {
        log.Printf("compression splice mismatch: cands=%d reps=%d", len(cands), len(reps))
    }
    return nil, err
}

Prevention

When it happens

Trigger: Content compression pipelines that drop unchanged segments (replacement equal to original) without dropping the matching candidate, or vice versa; parallel builders appending to the two slices unevenly on error paths.

Common situations: A compression pass filtering candidates for nil content while keeping all replacement bytes; concurrency races appending to reps; early-return error paths leaving reps shorter than candidates.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/aab0b4083d4d4ff2. Report an issue: GitHub.

Appendix: source

Thrown at proxy/providers/openai/content_compress.go:644

}

func isJSONString(body []byte, span jsonSpan) bool {
	return span.start < span.end && span.start >= 0 && span.end <= len(body) && body[span.start] == '"'
}

func quoteJSONStringNoHTML(s string) ([]byte, error) {
	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	if err := enc.Encode(s); err != nil {
		return nil, err
	}
	return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil
}

func spliceStringReplacements(body []byte, candidates []spliceCandidate, reps [][]byte) ([]byte, error) {
	if len(reps) != len(candidates) {
		return nil, fmt.Errorf("openai compress: %d replacements for %d segments", len(reps), len(candidates))
	}
	var out []byte
	last := 0
	changed := false
	for i, c := range candidates {
		if c.start < last || c.end > len(body) || c.start >= c.end {
			return nil, fmt.Errorf("openai compress: invalid splice range")
		}
		rep := reps[i]
		if rep == nil || bytes.Equal(rep, c.original) {
			continue
		}
		quoted, err := quoteJSONStringNoHTML(string(rep))
		if err != nil {
			return nil, err
		}
		if !changed {
			out = make([]byte, 0, len(body)-len(c.original)+len(rep))

View on GitHub (pinned to 3ee70a1026)