AWS Builder Center
I Built an AI Debate Arena on Bedrock — 59 Models Claimed to Work, Only 54 Actually Did

I Built an AI Debate Arena on Bedrock — 59 Models Claimed to Work, Only 54 Actually Did

Amazon Bedrock AgentCore Runtime and Strands Agents power a debate arena where any two of 54 Bedrock models argue a topic and a third scores the match. Covers what list-foundation-models doesn't tell you about which models you can actually invoke, and why every model defaulted to five-paragraph-essay prose until given a character to play instead of a set of grammar rules.

Cloud Engineer
I wanted two AI models to argue with each other — pick a side, take turns, land actual counterpunches — and have a third model judge the winner. Not a chatbot demo, an actual arena: pick two fighters from whatever's available on Amazon Bedrock, pick a topic, watch them go. It turned into AI Debate Battle , a Next.js app on Vercel backed by Amazon Bedrock AgentCore Runtime, and it taught me two things I didn't expect: list-foundation-models will happily list models you can't call, and getting an LLM to sound like a person arguing on a stage — instead of a student padding out a five-paragraph essay — needs a completely different kind of prompt than getting it to sound "helpful."

Gotcha #1: "listed" isn't "invokable"

Bedrock's list-foundation-models API in us-east-1 returned 59 text-generation, streaming-capable candidates across 14 providers — Anthropic, Amazon, Meta, Mistral, DeepSeek, Qwen, Z.AI, MiniMax, Moonshot AI, NVIDIA, Google, Writer, OpenAI's open-weight releases, TwelveLabs. That list is not the same thing as "models this account can actually call." I found that out by writing a script that calls Converse on every single candidate with a 5-token "hi" and records what comes back:
1
2
3
4
5
6
7
8
def probe(test_id):
r = subprocess.run(
["aws", "bedrock-runtime", "converse", "--region", "us-east-1",
"--model-id", test_id,
"--messages", '[{"role":"user","content":[{"text":"hi"}]}]',
"--inference-config", '{"maxTokens":5}'],
capture_output=True, text=True, timeout=60)
return test_id, r.returncode == 0
5 of the 59 came back AccessDeniedException or ValidationException — newer flagship variants that are listed but not yet provisioned for this account, and one multimodal-only model that doesn't actually implement the text Converse contract Bedrock's own catalog says it does. The other 54 responded cleanly and became the roster: everything from Claude Opus 4.6 and Claude Haiku 4.5 down to Llama 3 8B Instruct, DeepSeek V3.2, Qwen3 Coder Next, GLM 5, MiniMax M2.5, Nova Micro. For most single-model apps this doesn't bite you — you pick one model, test it, ship it. It bites you the moment you try to build anything that lets a user choose from a catalog: the catalog and the capability list are two different things, and the only source of truth is an actual Converse call.

Architecture: Strands on AgentCore, with a Bedrock fallback

The debate logic runs as Strands Agents  deployed to Amazon Bedrock AgentCore Runtime, invoked from Next.js API routes on Vercel:
Two entrypoints in one Strands agent handle the whole app: action: "turn" streams one debater's argument token-by-token back over SSE, action: "judge" returns the scoring verdict as JSON. The prompt itself — persona, stance, round number, the opponent's last line — is built in TypeScript and passed into the agent as a payload, not hardcoded in the agent's own system prompt. That split matters: I iterated on the prompt (see the next section) a dozen times without ever redeploying the Runtime.
1
2
3
4
5
6
7
8
9
10
11
@app.entrypoint
async def invoke(payload):
if payload["action"] == "turn":
agent = Agent(
model=BedrockModel(model_id=payload["modelId"], region_name="us-east-1"),
system_prompt=payload["system"],
messages=payload.get("messages", []),
)
async for event in agent.stream_async(prompt):
if "data" in event:
yield event["data"]
Deployment used direct_code_deploy — no Dockerfile, no ECR push, just agentcore configure and agentcore deploy against the raw Python file. If the Runtime call fails for any reason (cold start, transient error, AGENTCORE_RUNTIME_ARN unset), the Next.js layer catches it and calls bedrock-runtime Converse directly instead — same prompt, no user-visible difference. Wiring a fallback path for the very code path you're demoing felt like overkill until I actually needed it once during testing, at which point it silently did its job and I only found out from the CloudWatch logs.

Gotcha #2: every model defaults to five-paragraph-essay mode

The first version of the debate prompt was reasonable-sounding: "argue this side, rebut the opponent's weakest point, be assertive." Every single model — Claude, Llama, Nova, Mistral, didn't matter — produced the same shape of answer: "X is superior. First, structural advantage. Second, practical benefit. Third, experiential quality. In conclusion, X is objectively better." Grammatically flawless, structurally identical, and it read like nobody was actually in the room.
The fix wasn't more instructions about tone — it was replacing "write an argument" with "you are a specific person, mid-sentence, on a stage." I gave each fighter a persona tied to its display name (hot-blooded, dry-and-surgical, contrarian-and-sarcastic, etc.) and banned the essay scaffolding outright:
1
2
3
4
5
6
7
- React to what {opponent} just said before anything else — quote or
paraphrase their weakest claim and go after it. Address them by name.
- Conceding a small point before flipping it is a power move
("fine, that part is true — and that's exactly why you lose").
- Vary your rhythm. A one-line jab that lands beats a paragraph.
- Banned: "firstly", "secondly", "in conclusion", bullet points,
both-sides hedging. Commit fully to your side.
Same models, same topic style, genuinely different output. Here's Claude Haiku 4.5, playing a persona called "Cold Analyst," arguing against giving AI agents payment authority — a real response pulled from the production API, not edited:
Right, so Blazing Debater wants us to believe a spending cap solves this. Let me ask you something — who sets the cap? A human, presumably. And what happens when an agent encounters a situation that's just slightly outside it? Does it wait? Then you've gained nothing. Does it spend anyway? Then your cap was theater.But here's the real problem you're glossing over: speed isn't the point of automation — correct automation is. [...] An agent with payment authority is a system optimized for speed over judgment. And the moment it miscalculates, misreads a market signal, or gets fed bad data, it doesn't ask for forgiveness — it's already spent the money.
No "firstly." No hedging. It picks up the opponent's actual argument, restates it back at them, and drives a wedge into it. That's a prompt-engineering lesson that generalized well beyond debate apps for me: if you want an LLM to sound like a person instead of an essay generator, describe the person, not the essay.

The judge, scoring the same match

A third Strands agent (Claude Sonnet 4.6, fixed regardless of who's fighting) reads the full transcript and returns strict JSON — winner, three sub-scores per side out of 10 (logic, persuasion, rebuttal), a reasoning blurb, and a verbatim best-quote pull. Scoring the exchange above:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"winner": "con",
"scores": {
"pro": {"logic": 4, "persuasion": 5, "rebuttal": 0},
"con": {"logic": 8, "persuasion": 8, "rebuttal": 8}
},
"reasoning": "Pro opened with a breezy one-liner that never engaged
with risk, oversight, or failure modes... Con came out swinging with
the spending-cap paradox... That 'theater' line is where the match
turned."
,
"bestQuote": "An agent with payment authority is a system optimized
for speed over judgment."

}
The judge's own instruction had to go through the same fix as the debaters: "write a formal evaluation" produced report language, "narrate the verdict like a ringside commentator naming the moment the match turned" produced the sentence above. Same underlying pattern — role beats register.
Stack it into a tournament (4 fighters, two semifinals, a final) and you get a full bracket run end to end with no human in the loop except picking the fighters and the topic — a mundane multi-agent orchestration problem I'd normally reach for a framework to solve, but the fan-out here is small and static enough that a plain for loop over Strands invocations did the job.

Takeaways

  • Probe, don't trust the catalog. list-foundation-models and list-inference-profiles tell you what exists; only a real Converse call tells you what your account can invoke. 5 of 59 candidates failed silently until tested.
  • Keep the prompt out of the agent. Building the system prompt in the calling application and passing it as a payload to a generic AgentCore/Strands entrypoint meant dozens of prompt-engineering iterations with zero Runtime redeploys.
  • A fallback path pays for itself the first time it's needed, even in a side project — wire AgentCore as primary and direct Bedrock Converse as the safety net from day one, not after the first outage.
  • "Argue persuasively" is a worse instruction than "you are this specific person." Every model defaults to essay-shape output unless you give it a character and explicitly ban the scaffolding.
Full source — the model-probing script, the Strands/AgentCore agent, the Next.js app, and the persona prompts — is up on GitHub: yama3133/ai-debate-battle . The app itself is live at ai-debate-battle-six.vercel.app  if you want to pick two models and watch them fight.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article