Files
majordomo/provider/llamaswap/mesh_convert.go
T
steveandClaude Fable 5 fd8d56c3c1
Gadfly review (reusable) / review (pull_request) Successful in 15s
Adversarial Review (Gadfly) / review (pull_request) Successful in 15s
CI / Tidy (pull_request) Successful in 9m26s
CI / Build & Test (pull_request) Successful in 10m0s
fix: live-API corrections — music result shapes, mesh format honesty, mesh conversion
Round 2 from live smokes on netherstorm (2026-07-14):

- ACE-Step result blob is an ARRAY of objects and carries RAW control
  characters inside string values (literal newlines) — strict JSON
  rejected it. parseMusicResult sanitizes control chars (only legal
  inside string values in the double-encoded blob) and accepts array or
  object shapes. Regression test uses the live payload shape.
- Hunyuan3D GenerationRequest has NO output-format field (the documented
  type param is fiction) — it always returns GLB. Results are now
  labelled by sniffed magic bytes, never by the requested format.
- NEW meshgen.Converter/ConverterProvider optional surface + llamaswap
  impl over the mediautils shim POST /v1/convert_mesh — the STL hop for
  the printer pipeline.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-14 12:22:06 -04:00

65 lines
2.1 KiB
Go

// mesh_convert.go implements meshgen.ConverterProvider against the
// mediautils shim's POST /v1/convert_mesh (multipart file + target),
// reached through the /upstream passthrough (ADR-0020). Exists because
// Hunyuan3D's api_server always returns GLB — STL for the printer
// pipeline is produced by this conversion hop.
package llamaswap
import (
"context"
"fmt"
"net/http"
"strings"
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
"gitea.stevedudenhoeffer.com/steve/majordomo/meshgen"
)
// MeshConverter implements meshgen.ConverterProvider.
func (p *Provider) MeshConverter(id string) (meshgen.Converter, error) {
if err := p.requireBaseURL(); err != nil {
return nil, err
}
return &meshConverter{p: p, id: id}, nil
}
type meshConverter struct {
p *Provider
id string
}
// Convert implements meshgen.Converter.
func (m *meshConverter) Convert(ctx context.Context, mesh meshgen.Mesh, format string) (*meshgen.Result, error) {
if len(mesh.Data) == 0 {
return nil, fmt.Errorf("%w: mesh conversion requires mesh bytes", llm.ErrUnsupported)
}
format = strings.ToLower(strings.TrimSpace(format))
mimeType, ok := meshFormats[format]
if !ok {
return nil, fmt.Errorf("%w: unsupported mesh format %q (want glb, stl, or obj)", llm.ErrUnsupported, format)
}
path, err := upstreamPath(m.id, "/v1/convert_mesh")
if err != nil {
return nil, err
}
filename := "mesh." + mesh.Format
if mesh.Format == "" {
filename = "mesh"
}
body, contentType, err := buildMultipart("build mesh-convert form",
filePart{field: "file", filename: filename, data: mesh.Data},
[]formField{{"target", format, true}})
if err != nil {
return nil, err
}
raw, _, err := m.p.doRaw(ctx, http.MethodPost, path, m.id, contentType, body, maxMeshResponseBytes)
if err != nil {
return nil, err
}
if len(raw) == 0 {
return nil, &llm.APIError{Provider: m.p.name, Model: m.id, Message: "mesh conversion returned no data"}
}
actualFormat, actualMIME := sniffMeshFormat(raw, format, mimeType)
return &meshgen.Result{Mesh: meshgen.Mesh{Data: raw, Format: actualFormat, MIME: actualMIME}}, nil
}