fix: address gadfly review — filename sanitization, truncation guard, shared plumbing
- Transcribe: sanitize the caller-supplied multipart filename (CR/LF would inject Content-Disposition headers; upload metadata is untrusted), always send the required model/response_format fields, parse MIME parameters before extension matching, and give audio/opus its own .opus extension. - doRaw: a response larger than maxResponseBytes is now an error, not a silent truncation. - Shared plumbing: requireBaseURL() + newRequest() helpers replace the 7x-duplicated guard/error string and the triplicated request building across doJSON/doRaw/Health. - Health: non-2xx now returns *llm.APIError (package convention, programmatically distinguishable from transport failure) instead of a one-off unexported error type. - Speak: reject negative Speed; speechMIME no longer accepts video/* Content-Types. - image.go: Generate/Edit share one sdWire validate+map helper. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AXQxVhXBw8PwFAtsVrXSmj
This commit is contained in:
+34
-46
@@ -16,8 +16,8 @@ import (
|
||||
// served by llama-swap (routed to a stable-diffusion.cpp upstream). The id is
|
||||
// passed through verbatim and selects which upstream llama-swap loads.
|
||||
func (p *Provider) ImageModel(id string, opts ...imagegen.ModelOption) (imagegen.Model, error) {
|
||||
if p.baseURL == "" {
|
||||
return nil, fmt.Errorf("llama-swap provider %q: no base URL configured (set one via WithBaseURL or an LLM_* env DSN)", p.name)
|
||||
if err := p.requireBaseURL(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = imagegen.ApplyModelOptions(opts)
|
||||
return &imageModel{p: p, id: id}, nil
|
||||
@@ -53,32 +53,39 @@ type txt2imgResponse struct {
|
||||
Images []string `json:"images"`
|
||||
}
|
||||
|
||||
// sdWire validates the generation knobs shared by Generate and Edit and
|
||||
// builds the common txt2img wire fields. verb labels validation errors.
|
||||
func (m *imageModel) sdWire(verb, prompt, negativePrompt, sampler, size string, seed *int64, steps *int, cfgScale *float64, n int) (txt2imgRequest, error) {
|
||||
if strings.TrimSpace(prompt) == "" {
|
||||
return txt2imgRequest{}, fmt.Errorf("%w: image %s requires a prompt", llm.ErrUnsupported, verb)
|
||||
}
|
||||
if n < 0 {
|
||||
return txt2imgRequest{}, fmt.Errorf("%w: image count N must be >= 0, got %d", llm.ErrUnsupported, n)
|
||||
}
|
||||
width, height, err := parseSize(size)
|
||||
if err != nil {
|
||||
return txt2imgRequest{}, fmt.Errorf("%w: %v", llm.ErrUnsupported, err)
|
||||
}
|
||||
return txt2imgRequest{
|
||||
Model: m.id,
|
||||
Prompt: prompt,
|
||||
NegativePrompt: negativePrompt,
|
||||
Seed: seed,
|
||||
Steps: steps,
|
||||
CFGScale: cfgScale,
|
||||
Width: width,
|
||||
Height: height,
|
||||
SampleMethod: sampler,
|
||||
BatchCount: n,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate implements imagegen.Model via POST {base}/sdapi/v1/txt2img.
|
||||
func (m *imageModel) Generate(ctx context.Context, req imagegen.Request, opts ...imagegen.Option) (*imagegen.Result, error) {
|
||||
req = req.Apply(opts...)
|
||||
if strings.TrimSpace(req.Prompt) == "" {
|
||||
return nil, fmt.Errorf("%w: image generation requires a prompt", llm.ErrUnsupported)
|
||||
}
|
||||
if req.N < 0 {
|
||||
return nil, fmt.Errorf("%w: image count N must be >= 0, got %d", llm.ErrUnsupported, req.N)
|
||||
}
|
||||
|
||||
width, height, err := parseSize(req.Size)
|
||||
wire, err := m.sdWire("generation", req.Prompt, req.NegativePrompt, req.Sampler, req.Size, req.Seed, req.Steps, req.CFGScale, req.N)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", llm.ErrUnsupported, err)
|
||||
}
|
||||
|
||||
wire := txt2imgRequest{
|
||||
Model: m.id,
|
||||
Prompt: req.Prompt,
|
||||
NegativePrompt: req.NegativePrompt,
|
||||
Seed: req.Seed,
|
||||
Steps: req.Steps,
|
||||
CFGScale: req.CFGScale,
|
||||
Width: width,
|
||||
Height: height,
|
||||
SampleMethod: req.Sampler,
|
||||
BatchCount: req.N,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp txt2imgResponse
|
||||
@@ -126,37 +133,18 @@ type img2imgRequest struct {
|
||||
// Edit implements imagegen.Editor via POST {base}/sdapi/v1/img2img.
|
||||
func (m *imageModel) Edit(ctx context.Context, req imagegen.EditRequest, opts ...imagegen.EditOption) (*imagegen.Result, error) {
|
||||
req = req.Apply(opts...)
|
||||
if strings.TrimSpace(req.Prompt) == "" {
|
||||
return nil, fmt.Errorf("%w: image edit requires a prompt", llm.ErrUnsupported)
|
||||
}
|
||||
if len(req.Init.Data) == 0 {
|
||||
return nil, fmt.Errorf("%w: image edit requires an init image", llm.ErrUnsupported)
|
||||
}
|
||||
if req.N < 0 {
|
||||
return nil, fmt.Errorf("%w: image count N must be >= 0, got %d", llm.ErrUnsupported, req.N)
|
||||
}
|
||||
if req.Strength != nil && (*req.Strength < 0 || *req.Strength > 1) {
|
||||
return nil, fmt.Errorf("%w: edit strength must be in [0,1], got %g", llm.ErrUnsupported, *req.Strength)
|
||||
}
|
||||
|
||||
width, height, err := parseSize(req.Size)
|
||||
base, err := m.sdWire("edit", req.Prompt, req.NegativePrompt, req.Sampler, req.Size, req.Seed, req.Steps, req.CFGScale, req.N)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", llm.ErrUnsupported, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wire := img2imgRequest{
|
||||
txt2imgRequest: txt2imgRequest{
|
||||
Model: m.id,
|
||||
Prompt: req.Prompt,
|
||||
NegativePrompt: req.NegativePrompt,
|
||||
Seed: req.Seed,
|
||||
Steps: req.Steps,
|
||||
CFGScale: req.CFGScale,
|
||||
Width: width,
|
||||
Height: height,
|
||||
SampleMethod: req.Sampler,
|
||||
BatchCount: req.N,
|
||||
},
|
||||
txt2imgRequest: base,
|
||||
InitImages: []string{base64.StdEncoding.EncodeToString(req.Init.Data)},
|
||||
DenoisingStrength: req.Strength,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user