From aee2e3842604f05f8695fed6dd7a30a012155262 Mon Sep 17 00:00:00 2001 From: nicodes Date: Fri, 16 Oct 2026 15:17:47 -0600 Subject: [PATCH 1/2] Allow Haiku and Opus prompt selection with Haiku default --- README.md | 2 ++ cmd/spritesmith/main.go | 4 ++++ cmd/spritesmith/main_test.go | 13 +++++++++++++ internal/client/client.go | 16 ++++++++++++++++ 4 files changed, 35 insertions(+) diff --git a/README.md b/README.md index 59b3129..0c57b1e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cmd/spritesmith/main.go b/cmd/spritesmith/main.go index 2769b06..dcbd5f3 100644 --- a/cmd/spritesmith/main.go +++ b/cmd/spritesmith/main.go @@ -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 @@ -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) @@ -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), @@ -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"), diff --git a/cmd/spritesmith/main_test.go b/cmd/spritesmith/main_test.go index d1099ed..0429636 100644 --- a/cmd/spritesmith/main_test.go +++ b/cmd/spritesmith/main_test.go @@ -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") + } +} diff --git a/internal/client/client.go b/internal/client/client.go index 9ca8f54..8094c48 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -42,6 +42,7 @@ const ( const DefaultOutputPath = "spritesmith.png" type Request struct { + Model string Kind string Format string OperationID string @@ -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" } @@ -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: From 5c99593a45ed039dc3ab6674461d14e4e0c587f3 Mon Sep 17 00:00:00 2001 From: nicodes Date: Fri, 16 Oct 2026 15:22:14 -0600 Subject: [PATCH 2/2] Cover default and explicit reasoning-model transport --- internal/client/client_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/client/client_test.go b/internal/client/client_test.go index 7a196e8..4f244f6 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -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) + } + } +}