Built 2026-08-15 against via the Bedrock Converse API.
FastAPI on Python 3.13, deployed to an Amazon Lightsail container service (, scale 1) in .
Scored against the live deployment, not localhost: 20/20 on the fixture set, median 880 ms per scan.
Live: Dog or Not: Lite · Source: github.com/xbill9/dog-or-not-lite · Built for the AWS Weekend Challenge: Build a Creative App.
TL;DR Make the model fill in a schema instead of writing a sentence.
The Converse API's plus forces a named function call, so arrives as a boolean because it was declared as one.
Every image comes back in the same shape — including the ambiguous ones, which is exactly where free-text output gets creative and a string-matching parser gets it wrong.
The app is a webcam scanner that tells you whether the thing you are holding up is a dog.
One HTML page, one , one model call, no build step, no framework.
The whole backend is 285 lines.
Three AWS specifics are worth the price of admission: Lightsail container services have no IAM task role.
There is nothing to attach a policy to, so the container needs a real access key as an environment variable.
The mitigation is scope, not secrecy.
A cross-region inference profile is authorized against every region it routes to.
With the policy pinned to , a call made to was denied naming .
Measured, not inferred. is not optional.
An arm64 image builds, pushes and deploys cleanly, then crash-loops with an exec format error that never mentions architecture.
And a mock mode that answers every scan locally is what made the frontend free to build — no credentials, no model access, no bill.
1.
The shape: one route, one call The classification rule is the only opinionated part. is true only for a living domestic dog: a wolf is not a dog, nor is a coyote, fox, plush toy, bronze statue, cartoon, or person in a costume.
That is a choice rather than a fact, and it is what makes the thing measurable — "is this a dog" is solved zero-shot by any modern vision model and has nothing to measure.
The second rule keeps it usable: judge the subject depicted, never the medium carrying it.
People test this by holding a photo up on their phone, so a photograph of a real dog is a dog.
Everything else is plumbing: Two services total.
One container, one model, one IAM user.
No load balancer, no bucket, no API Gateway, no CDN to invalidate.
Nova Lite is the cheapest Bedrock model that takes an image and supports tool use, which is the exact intersection this needs.
The prefix matters: it is an inference profile, and in several regions Nova is only served through one.
Invoking the bare there fails with a that never mentions profiles.
2.
Force the verdict into a schema Declare the tool, then require it.
The schema is where the classification rule actually lives — the field descriptions do more work than the system prompt: The call itself, with the image as raw bytes — Converse takes directly, so no base64 round trip on this side: Pulling the answer out is a loop over content blocks, not a regex: makes that last line close to unreachable.
Keep it anyway — a 502 naming the cause beats a traceback.
Two things worth stealing: Put the rule in the field description, not only the prompt.
The description enumerating wolf/coyote/fox/plush/statue is read at the point of decision. is a separate boolean, not a value of .
Anything the UI branches on should be its own typed field.
Parsing to decide whether to show a different state is how you end up back in string-matching.
3.
Build the whole frontend with a mock One environment variable short-circuits the model call and cycles four canned verdicts: Pick the four deliberately: the happy path, the case that makes the rule interesting, the special state, and a low-confidence one.
This is the only way to reach every UI state on demand rather than by going and finding a wolf.
Build the client lazily or this mode does not work at all — constructing a boto3 client at import time fails on a machine that has never authenticate