Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ spritesmith prompt --kind vector --format svg --operation-id emblem-v1 --out emb
spritesmith prompt --kind vector --format png --out emblem.png 'A golden shield emblem'
```

Use `--model haiku` (the default) or `--model opus` to choose the reasoning model. The API freezes this choice with the request, including retries. Both use medium reasoning effort.

Flags come before the prompt. Use the web app to attach reference images, work with several sprites in a session, and publish revisions.

The service creates and reviews vector animations through its agent workflow. This client downloads the default pose as SVG or PNG; the web app also exports animation sheets and frames.
Expand Down
4 changes: 4 additions & 0 deletions cmd/spritesmith/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var revision = "unknown"
const defaultAPIBase = "http://127.0.0.1:8080"

type commonFlags struct {
model *string
operationID *string
apiBase *string
out *string
Expand Down Expand Up @@ -64,6 +65,7 @@ func runPrompt(args []string) int {
// Folded into run* they were untestable, and losing a line there is invisible:
// dropping OutputPath makes --out do nothing, and the CLI still prints a path
// and still writes a PNG, just never the one that was asked for.

func parsePrompt(args []string) (apiBase string, req client.Request, ok bool) {
fs, flags := newCommandFlags("prompt")
terminated := promptFlagsTerminated(args, fs)
Expand All @@ -82,6 +84,7 @@ func parsePrompt(args []string) (apiBase string, req client.Request, ok bool) {

return *flags.apiBase, client.Request{
OperationID: *flags.operationID,
Model: *flags.model,
Mode: client.ModePrompt,
Prompt: prompt,
OutputPath: outputPath(flags),
Expand Down Expand Up @@ -145,6 +148,7 @@ func newCommandFlags(name string) (*flag.FlagSet, commonFlags) {
fs := flag.NewFlagSet(name, flag.ContinueOnError)
fs.SetOutput(os.Stderr)
flags := commonFlags{
model: fs.String("model", "haiku", "reasoning model: haiku (default) or opus"),
operationID: fs.String("operation-id", "", "reuse the same logical generation after an interrupted submission"),
apiBase: fs.String("api", envDefault("SPRITESMITH_API_BASE", defaultAPIBase), "spritesmith API base URL"),
out: fs.String("out", "", "output filename"),
Expand Down
13 changes: 13 additions & 0 deletions cmd/spritesmith/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,16 @@ func TestHelpSaysAnExistingFileIsNotOverwritten(t *testing.T) {
t.Errorf("--help does not say what happens when the output path is taken:\n%s", out.String())
}
}

func TestPromptModelChoice(t *testing.T) {
for _, model := range []string{"haiku", "opus"} {
_, req, ok := parsePrompt([]string{"--model", model, "a tiny slime"})
if !ok || req.Model != model {
t.Fatalf("model selection lost: %+v", req)
}
}
_, req, ok := parsePrompt([]string{"a tiny slime"})
if !ok || req.Model != "haiku" {
t.Fatal("default must be haiku")
}
}
16 changes: 16 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const (
const DefaultOutputPath = "spritesmith.png"

type Request struct {
Model string
Kind string
Format string
OperationID string
Expand All @@ -66,6 +67,17 @@ type Client struct {
}

func (c Client) Process(req Request) (Response, error) {
model := req.Model
switch model {
case "", "haiku":
model = "anthropic/claude-haiku-5.5"
case "opus":
model = "anthropic/claude-opus-5.5"
case "anthropic/claude-haiku-5.5", "anthropic/claude-opus-5.5":
default:
return Response{}, errors.New("model must be haiku or opus")
}

if req.Kind == "" {
req.Kind = "vector"
}
Expand Down Expand Up @@ -123,6 +135,10 @@ func (c Client) Process(req Request) (Response, error) {
return Response{}, err
}

if err := writer.WriteField("agent_model", model); err != nil {
return Response{}, err
}

endpoint := "/v1/prompt"
switch req.Mode {
case ModePrompt:
Expand Down
17 changes: 17 additions & 0 deletions internal/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -934,3 +934,20 @@ func TestTheDefaultOutputNameIsTheOneEverythingElseExpects(t *testing.T) {
t.Errorf("cli/.gitignore does not list %q, so `make cli` leaves a result in the source tree:\n%s", DefaultOutputPath, ignore)
}
}

func TestReasoningModelChoiceReachesAPI(t *testing.T) {
for _, tc := range []struct{ selected, want string }{{"", "anthropic/claude-haiku-5.5"}, {"haiku", "anthropic/claude-haiku-5.5"}, {"opus", "anthropic/claude-opus-5.5"}} {
api := &recordingAPI{}
srv := httptest.NewServer(api)
req := promptRequest(t)
req.Model = tc.selected
if _, err := (Client{BaseURL: srv.URL}).Process(req); err != nil {
srv.Close()
t.Fatal(err)
}
srv.Close()
if got := api.form.Get("agent_model"); got != tc.want {
t.Fatalf("selection %q sent %q, want %q", tc.selected, got, tc.want)
}
}
}