Skip to content

feat(llm): add OpenAI Responses and Anthropic Messages API support - #88

Open
garfield-oo7 wants to merge 5 commits into
coder:mainfrom
garfield-oo7:feat/responses-api
Open

garfield-oo7 wants to merge 5 commits into
coder:mainfrom
garfield-oo7:feat/responses-api

Conversation

@garfield-oo7

Copy link
Copy Markdown

Why

Newer OpenAI models (gpt-6.1-sol, gpt-6-astra) can't be benchmarked with BalatroLLM today. On /v1/chat/completions they reject function tools:

Function tools with reasoning_effort are not supported for gpt-6.1-sol in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.

and reasoning_effort: "none" isn't allowed for these models either. On /v1/responses both models call tools correctly. This PR also adds the Anthropic Messages API, so Claude models can be run directly with an Anthropic key.

What

A new api option: chat (default, unchanged), responses, or anthropic, set with --api, BALATROLLM_API, or api: in YAML.

LLMClient translates the existing Chat Completions request (messages, TOOLS.json tools, model_config) for the chosen API, then converts the reply back into a ChatCompletion. Strategies, bot.py and the run output format don't change, so BalatroBench reads these runs the same way it reads existing ones:

What BalatroBench reads responses anthropic
runs/.../{vendor}/{model}/ openai/... (prefix stripped before calling api.openai.com) anthropic/... (prefix stripped before calling the API)
body.provider OpenAI Anthropic
requests.jsonl body.messages unchanged (chat-shaped) unchanged
tool_calls, message.reasoning ✅ ✅ (adaptive-thinking summaries)
usage.prompt_tokens / completion_tokens ✅ ✅ (counts cache reads and writes)
usage.cost / cost_details estimated* estimated*

*Neither API reports cost. It's estimated from per-model prices in pricing.py (sourced from the providers' pricing pages, with long-context tiers) and marked cost_details.estimated: true. Models without a listed price get no cost. A cost that's already reported (e.g. OpenRouter's) is kept.

Robustness:

  • Truncated or incomplete tool calls are never executed.
  • Failed Responses API results are retried.
  • Refusals are kept.
  • Chat-only params (e.g. seed) are rejected with a clear error.
  • Calls on the Responses API are sent with store: false.

Also:

  • base_url now defaults to None: OpenRouter for chat/responses, the Anthropic API for anthropic.
  • fix(collector): handle usage: null in responses, which previously crashed stats aggregation.

Usage

balatrollm --api responses --base-url https://api.openai.com/v1 --model openai/gpt-6.1-sol
balatrollm --api anthropic --model anthropic/claude-haiku-5-5   # reads ANTHROPIC_API_KEY

Testing

  • make quality passes.
  • 44 new unit tests in test_llm.py, test_config.py and test_pricing.py. They're built from real OpenAI/Anthropic SDK response objects (Response, Message), not mocks.
  • Live API checks used the real default strategy prompt (~12k tokens) and tools with a hand-built SELECTING_HAND gamestate. Two decisions per model went through the real Collector, and the output files were parsed with BalatroBench's extractor.py: prompts, tool calls, reasoning, tokens, provider, vendor/model and cost all came out correctly. Models checked:
    • gpt-6.1-sol and gpt-6-astra (Responses API)
    • claude-haiku-5-5 (Anthropic). Prompt caching works: 17.5k tokens are read from cache on the second call.
  • Not run:
    • Full games and the integration tests: I don't have a Balatro install.
    • Unit tests: 13 fail identically on main. 11 need game-generated fixtures (tests/unit/fixtures/), and 2 are stale seed assertions in TestGetModelConfig left over from d642f7d. None are touched by this PR.

Needed for the Anthropic Messages API support in LLMClient.
body.get("usage", {}) returns None when the key is present with a null
value, which crashed token/cost aggregation. Fall back to {} instead.
Newer OpenAI models (e.g. gpt-6.1-sol, gpt-6-astra) reject function tools
with reasoning on /v1/chat/completions; they only support them on
/v1/responses. Add an `api` option (chat | responses | anthropic; default
chat, so OpenRouter usage is unchanged), settable via YAML, BALATROLLM_API
or --api.

LLMClient translates the existing Chat Completions request (messages,
TOOLS.json tools, model_config) for the chosen API and converts the reply
back into a ChatCompletion, so the bot, strategies and run output format
are unchanged and BalatroBench keeps reading runs as before:

- responses: input_text parts, flat function tools, reasoning.effort,
  store=false; truncated/incomplete tool calls are dropped, failed
  responses are retried, refusals are kept, chat-only params (e.g. seed)
  are rejected with a clear error.
- anthropic: official anthropic SDK (reads ANTHROPIC_API_KEY when no key
  is configured); cache_control is kept, adaptive thinking summaries are
  stored as message.reasoning, parallel_tool_calls maps to
  disable_parallel_tool_use.
- "openai/" (on api.openai.com) and "anthropic/" model prefixes are
  stripped before the call, so runs are stored under the same
  runs/.../{vendor}/{model}/ layout BalatroBench uses, and body.provider
  is set (OpenAI / Anthropic).
- Neither API reports cost, so it is estimated from per-model prices
  (pricing.py, sourced from the providers' pricing pages) into the
  usage.cost / cost_details fields BalatroBench reads, marked estimated.

base_url now defaults to None: OpenRouter for chat/responses, the
Anthropic API for anthropic.
@garfield-oo7

Copy link
Copy Markdown
Author

Open items for this PR:

  • Not yet run in real games. The live checks used a hand-built SELECTING_HAND gamestate. Full games and the integration tests (tests/integration/) still need a Balatro + BalatroBot install, which I don't have yet. I plan to post results for openai/gpt-6.1-sol and anthropic/claude-haiku-5-5 once I've run them.
  • Prices in pricing.py are updated by hand. They're estimates taken from the providers' pricing pages on 2026-10-20 (PRICING_AS_OF), and they need updating when OpenAI or Anthropic change prices. Models without a listed price get no cost rather than a wrong one. If you'd rather not keep a price table in the repo, I can drop the cost estimation from this PR.
  • CI hasn't run yet. It's waiting for a maintainer to approve workflows for a first-time contributor's fork PR. make quality passes locally.

OpenAI's implicit caching on GPT-5.6+ only places a breakpoint at the end
of the prompt, so the static strategy prefix was never reused when the
game state changed (0 cached tokens, full cache-write cost on every call).
Marking the strategy part as an explicit breakpoint makes later calls read
it from cache (~10.6k of ~10.6k prompt tokens cached in testing).
garfield-oo7 added a commit to garfield-oo7/balatrobench that referenced this pull request Oct 21, 2026
5 real Balatro games (default strategy, RED/WHITE, seeds AAAAAAA-EEEEEEE
x1, medium reasoning) via the Anthropic Messages API (coder/balatrollm#88
at deb2d1a; the AAAAAAA run also had the transport-error loop fix).
Average final round 21.0, 3 wins (AAAAAAA, BBBBBBB, CCCCCCC), est. cost
$23.86 for the 5 counted runs.

AAAAAAA froze the game twice at ante 7 round 20 when a Celestial pack
opened with Black Hole first (coder/balatrobot#199); the counted run used
BalatroBot v1.5.2 with that fix applied to buy.lua and pack.lua. The About
page notes this and the smaller run count.

Leaderboard regenerated from all v1.1.1 runs (other models unchanged).
Data compacted with compact_benchmarks.py (116 MB -> 17 MB).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant