Files
majordomo/provider/llamaswap/upstream.go
T
steveandClaude Fable 5 966ea16166
CI / Tidy (pull_request) Successful in 9m31s
CI / Build & Test (pull_request) Successful in 9m48s
fix: review findings — NaN threshold guard, percent-escape rejection in upstream model ids
- Segment: reject NaN thresholds (NaN fails every comparison, so it
  passed the [0,1] range check and reached the shim as the literal
  string "NaN"); ±Inf were already caught by the range comparisons,
  now covered by tests too.
- upstreamPath: reject '%' in model ids — %2F/%2E%2E percent-escapes
  decode back into path structure server-side, bypassing the literal
  /?#/.. rejection. Ids never legitimately contain '%'.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WWCQcYStWXBYUy5sZWnbLT
2026-07-16 19:00:11 -04:00

71 lines
2.7 KiB
Go

package llamaswap
import (
"bytes"
"fmt"
"mime/multipart"
"strings"
)
// upstreamPath builds a path through llama-swap's generic /upstream/<model>/
// passthrough, which pins the model (triggering the normal load/swap queue)
// and forwards the remaining path to the upstream verbatim. This is how the
// provider reaches upstreams whose native APIs carry no routable `model`
// field (rembg, mediautils, WhisperX, ACE-Step, ...) without needing a
// llama-swap route per endpoint (ADR-0020).
//
// Why reject rather than escape: same rationale as Unload — model ids
// legitimately contain ":" but never path-structure characters, and escaping
// would mask a config error instead of surfacing it. '%' is rejected too:
// ids never legitimately carry percent-escapes, and %2F/%2E%2E would decode
// back into path structure on the server side.
func upstreamPath(model, rest string) (string, error) {
if strings.TrimSpace(model) == "" {
return "", fmt.Errorf("llama-swap: upstream call requires a model id")
}
if strings.ContainsAny(model, "/?#%") || strings.Contains(model, "..") {
return "", fmt.Errorf("llama-swap: invalid model id %q for upstream call (contains a path separator)", model)
}
if !strings.HasPrefix(rest, "/") {
rest = "/" + rest
}
// rest may embed SERVER-SUPPLIED components (e.g. ACE-Step's result
// file URL) — refuse dot-dot segments and absolute-URL smuggling so a
// hostile/buggy upstream cannot redirect the follow-up request at
// another proxy endpoint (/api/models/unload, ...).
if strings.Contains(rest, "..") || strings.Contains(rest, "://") {
return "", fmt.Errorf("llama-swap: invalid upstream path %q (dot-dot or scheme)", rest)
}
return "/upstream/" + model + rest, nil
}
// filePart is the single file entry of a media multipart form.
type filePart struct {
field string // form field name ("file", "audio_file", ...)
filename string // already sanitized
data []byte
}
// buildMultipart assembles a one-file multipart body: the file part first,
// then the given fields (optional fields skipped when empty, matching
// writeFormFields). wrap labels errors. Returns the body and its content
// type.
func buildMultipart(wrap string, file filePart, fields []formField) (*bytes.Buffer, string, error) {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
fw, err := w.CreateFormFile(file.field, file.filename)
if err != nil {
return nil, "", fmt.Errorf("llama-swap: %s: %w", wrap, err)
}
if _, err := fw.Write(file.data); err != nil {
return nil, "", fmt.Errorf("llama-swap: %s: %w", wrap, err)
}
if err := writeFormFields(w, wrap, fields); err != nil {
return nil, "", err
}
if err := w.Close(); err != nil {
return nil, "", fmt.Errorf("llama-swap: %s: %w", wrap, err)
}
return &buf, w.FormDataContentType(), nil
}