JuliusBrussee/caveman · error

json splice: replacements for candidates

Error message

json splice: %d replacements for %d candidates

What it means

jsonsplice.Replace splices candidate string replacements into a JSON body byte-for-byte and requires a 1:1 mapping between candidates and replacement values. If the lengths differ it refuses to proceed because it cannot determine which candidate each replacement belongs to.

Solutions

  1. Build candidates and replacements in a single loop so their lengths always match
  2. Filter both slices together (e.g. pair them in a temp slice of structs) instead of independently
  3. When everything is unchanged, pass empty (not nil-mismatched) slices or skip the call
  4. Add an assertion/log before the call when lengths diverge to find the filtering bug

Example fix

// before: independent filtering
var cands []Candidate; var reps [][]byte
for _, c := range all { if !bytes.Equal(c.Rep, c.Original) { cands = append(cands, c) } }
reps = allReps
out, err := Replace(body, cands, reps)
// after: paired filtering
type pair struct{ c Candidate; r []byte }
var kept []pair
for _, p := range pairs { if !bytes.Equal(p.r, p.c.Original) { kept = append(kept, p) } }
cands := make([]Candidate, len(kept)); reps := make([][]byte, len(kept))
for i, p := range kept { cands[i], reps[i] = p.c, p.r }
out, err := Replace(body, cands, reps)
Defensive patterns

Strategy: validation

Validate before calling

if len(candidates) != len(replacements) { return fmt.Errorf("candidate/replacement mismatch: %d vs %d", len(candidates), len(replacements)) }

Try / catch

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

Prevention

When it happens

Trigger: Calling Replace(body, candidates, replacements) where len(replacements) != len(candidates), typically after filtering candidates (skipping unchanged strings) without filtering replacements in parallel.

Common situations: OpenAI-style compression passes that drop candidates equal to their original but forget to drop matching replacements; off-by-one when building replacement slices; nil-vs-empty slice handling when no strings changed.

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/56efc83686765bd7. Report an issue: GitHub.

Appendix: source

Thrown at proxy/providers/jsonsplice/jsonsplice.go:105

	}
	var out string
	if json.Unmarshal(body[span.Start:span.End], &out) != nil {
		return "", false
	}
	return out, true
}

func StringField(body []byte, object Span, name string) (string, bool) {
	span, ok := Field(body, object, name)
	if !ok {
		return "", false
	}
	return String(body, span)
}

func Replace(body []byte, candidates []Candidate, replacements [][]byte) ([]byte, error) {
	if len(candidates) != len(replacements) {
		return nil, fmt.Errorf("json splice: %d replacements for %d candidates", len(replacements), len(candidates))
	}
	var out []byte
	last := 0
	for i, candidate := range candidates {
		if candidate.Start < last || candidate.End > len(body) || candidate.Start >= candidate.End {
			return nil, fmt.Errorf("json splice: invalid range")
		}
		replacement := replacements[i]
		if replacement == nil || bytes.Equal(replacement, candidate.Original) {
			continue
		}
		quoted, err := quote(string(replacement))
		if err != nil {
			return nil, err
		}
		if out == nil {
			out = make([]byte, 0, len(body)-len(candidate.Original)+len(replacement))
		}

View on GitHub (pinned to 3ee70a1026)