fix: review findings — chain NaN/Inf + id hygiene, percent-escape jobPath, shared singleVideoResult
- SubmitChain rejects NaN/±Inf segment seconds with ErrUnsupported (previously an obscure json.Marshal error; NaN fails every comparison and +Inf passed the >= 0 check). - ChainStatus skips segment entries with no usable id — JSON null (which no-op-unmarshals into a string, previously appending ""), empty strings, and id-less objects; the unfiltered list survives in Raw. ChainJob.SegmentIDs doc now also says ChainSegmentResult takes the segment index, not an id string. - jobPath rejects '%' in job ids — %2F/%2E%2E percent-escapes decode back into path structure server-side, bypassing the literal check on this upstream-echoed value. - singleVideoResult moves to video.go next to videoMIME, and the two remaining hand-rolled copies of the video-result validation (videoModel.Generate, Interpolate) now use it — one validation, one message shape. - videogen.LipSyncer renamed to videogen.Lipsyncer for consistency with the rest of the surface's Lipsync* naming (LipsyncProvider, LipsyncModel, LipsyncRequest); not yet consumed downstream, so the rename is free now and never again. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WWCQcYStWXBYUy5sZWnbLT
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -71,8 +72,10 @@ func (m *chainerModel) SubmitChain(ctx context.Context, req videogen.ChainReques
|
||||
if strings.TrimSpace(seg.Prompt) == "" {
|
||||
return "", fmt.Errorf("%w: video chain segment %d requires a prompt", llm.ErrUnsupported, i)
|
||||
}
|
||||
if seg.Seconds < 0 {
|
||||
return "", fmt.Errorf("%w: video chain segment %d seconds must be >= 0, got %g", llm.ErrUnsupported, i, seg.Seconds)
|
||||
// NaN/±Inf would otherwise surface as an obscure json.Marshal error
|
||||
// (NaN fails every comparison; +Inf passes the >= 0 check).
|
||||
if seg.Seconds < 0 || math.IsNaN(seg.Seconds) || math.IsInf(seg.Seconds, 0) {
|
||||
return "", fmt.Errorf("%w: video chain segment %d seconds must be a finite value >= 0, got %g", llm.ErrUnsupported, i, seg.Seconds)
|
||||
}
|
||||
wire.Segments = append(wire.Segments, chainSegmentWire{Prompt: seg.Prompt, Seconds: seg.Seconds})
|
||||
}
|
||||
@@ -133,10 +136,17 @@ func (m *chainerModel) ChainStatus(ctx context.Context, jobID string) (*videogen
|
||||
Total: out.Total,
|
||||
Raw: raw,
|
||||
}
|
||||
// Entries that carry no usable id — JSON null (which unmarshals into a
|
||||
// string as a no-op, leaving ""), an empty string, or an object with
|
||||
// neither key — are SKIPPED, never appended as "": SegmentIDs promises
|
||||
// fetchable artifacts, and the full payload stays in Raw for callers
|
||||
// that want the unfiltered list.
|
||||
for _, entry := range out.Segments {
|
||||
var s string
|
||||
if json.Unmarshal(entry, &s) == nil {
|
||||
job.SegmentIDs = append(job.SegmentIDs, s)
|
||||
if s != "" {
|
||||
job.SegmentIDs = append(job.SegmentIDs, s)
|
||||
}
|
||||
continue
|
||||
}
|
||||
var obj struct {
|
||||
@@ -197,7 +207,10 @@ func (m *chainerModel) jobPath(jobID, suffix string) (string, error) {
|
||||
if strings.TrimSpace(jobID) == "" {
|
||||
return "", fmt.Errorf("llama-swap: chain job call requires a job id")
|
||||
}
|
||||
if strings.ContainsAny(jobID, "/?#") || strings.Contains(jobID, "..") {
|
||||
// '%' is rejected alongside the literal path characters: job ids never
|
||||
// legitimately carry percent-escapes, and %2F/%2E%2E would decode back
|
||||
// into path structure server-side.
|
||||
if strings.ContainsAny(jobID, "/?#%") || strings.Contains(jobID, "..") {
|
||||
return "", fmt.Errorf("llama-swap: invalid chain job id %q (contains path structure)", jobID)
|
||||
}
|
||||
return upstreamPath(m.id, "/v1/jobs/"+jobID+suffix)
|
||||
|
||||
Reference in New Issue
Block a user