Build1 publisher3 min readPublished
Force the tool call, then hand Lightsail a long-lived key
A dev.to build note gets two things right about Bedrock: toolChoice turns model output into a typed schema, and Lightsail container services leave you no task role to attach.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- A dev.to write-up describes a webcam scanner app that reports whether the subject held up to the camera is a dog, built as one HTML page with a single POST /api/scan route and one model call.
- The app runs FastAPI on Python 3.13, deployed to an Amazon Lightsail container service (nano, scale 1) in us-east-1, calling us.amazon.nova-lite-v1:0 via the Bedrock Converse API.
- The Converse API's toolConfig plus toolChoice forces a named function call, so is_dog arrives as a boolean because it was declared as one.
- 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 tool is named report_verdict, described as reporting whether the subject presented to the scanner is a dog and to be called exactly once for every image, and toolChoice is set to {"tool": {"name": "report_verdict"}}.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A build write-up on dev.to describes a webcam scanner that decides whether the thing in front of the camera is a dog, running FastAPI on Python 3.13 in an Amazon Lightsail container service and calling Amazon Nova Lite through the Bedrock Converse API [1][2]. Two of its decisions generalise past the toy: the model is required to fill in a declared schema instead of writing a sentence, and the deployment target has no IAM task role, so credentials sit in the environment [3][4].
The typed-output half is straightforward. Converse's `toolConfig` declares a tool named `report_verdict`, and `toolChoice` pins the model to that tool, so the reply arrives as a function call rather than prose [5]. `is_dog` comes back as a boolean because it was declared as one [3]. The schema marks four fields required: `is_dog`, `confidence` as an integer 0-100, `subject` as a string of three words or fewer, and `is_cat` as a separate boolean, on the stated grounds that a cat is not merely a non-dog [6][1]. According to the author, the field descriptions do more work than the system prompt, and that is where the classification rule actually lives: true only for a living domestic dog, false for a wolf, coyote, fox, plush toy, statue, drawing, cartoon or costume [7][8]. A second rule judges the subject depicted, not the medium carrying it, because people test the thing by holding a photo up on a phone [9].
The payoff is in the ambiguous cases. Every image returns in the same shape, including the ones where free text gets creative and a string-matching parser gets it wrong [10]. A code comment records why `toolChoice` is not optional: without it, Nova sometimes narrates instead of calling the tool [11].
Then the cost of the hosting choice. Lightsail container services have no IAM task role, so there is nothing to attach a policy to and the container needs a real access key as an environment variable [4]. The author's mitigation is scope, not secrecy [12]. Scope has a sharp edge here: a cross-region inference profile is authorized against every region it routes to, and with the policy pinned to `us-east-1`, a call made to `us-east-1` was denied naming `us-west-2`, which the write-up says was measured rather than inferred [13]. The `us.` prefix on the model id is that profile, and in several regions Nova is only served through one, where invoking the bare `amazon.nova-lite-v1:0` fails with a ValidationException that never mentions profiles [14].
One more operational trap, cheap to avoid and expensive to diagnose: `--platform linux/amd64`. An arm64 image builds, pushes and deploys cleanly, then crash-loops with an exec format error that never mentions architecture [15].
The numbers are small and stated plainly. Nano instance, scale 1, `us-east-1`, build dated 2026-08-15, a 285-line backend, one HTML page and one POST `/api/scan` [2][16][17]. It scored 20/20 on the fixture set against the live deployment rather than localhost, median 880 ms per scan [18] - a 100 percent pass rate on a fixture set the same author wrote, which is the caveat worth carrying [2]. Total surface is two services, one container, one model, one IAM user, with no load balancer, bucket, API Gateway or CDN [19]. A mock mode that answers every scan locally let the frontend be built with no credentials, no model access and no bill [20].
What to watch is whether that pinned-region policy stays correct. The stated failure mode is a policy that was right when written and then denies a region the inference profile began routing to [13]. The access key is the other one: one IAM user, and the defence on offer is narrow permissions rather than rotation [4][12].