feat!: add Anthropic Message Batches API#5577
Conversation
✱ Stainless preview buildsThis PR will update the
|
7f7488d to
0ed9397
Compare
…ranslation (ogx-ai#5587) ## Summary - Adds a new `messages-openai` integration test suite that reuses `tests/integration/messages` with the `gpt` setup, so requests flow through `_anthropic_to_openai` / `_openai_to_anthropic` in `providers/inline/messages/impl.py` instead of the native passthrough branch that the existing `messages` (ollama-reasoning) suite hits. - Registers the suite in `tests/integration/ci_matrix.json` so replay-mode CI runs it, and adds a `gpt` / `messages-openai` row to `.github/workflows/record-integration-tests.yml` so future rerecord runs refresh it. - Includes 12 recordings captured against `openai/gpt-4o` so replay mode works out-of-the-box (no API key required in CI). This is a precursor to ogx-ai#5577 — the batches tests there need a suite that targets OpenAI, because batches go through `create_message()` which only exercises translation when the provider is not in `_NATIVE_MESSAGES_MODULES`. ## Test plan Recorded locally and verified against `openai/gpt-4o`: ```bash ./scripts/integration-tests.sh \ --stack-config server:ci-tests \ --suite messages-openai \ --inference-mode record-if-missing ``` Result: 13/13 tests pass. Recording URLs point at `https://api.openai.com/v1/chat/completions`, confirming the translation path is being exercised (not the Anthropic-native `/v1/messages` endpoint the ollama suite hits). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Charlie Doern <[email protected]>
70091ac to
25dae2f
Compare
|
not sure why backwards compatibility test is failing, I added |
25dae2f to
f9f0570
Compare
Add the 5 Anthropic Message Batches endpoints under /v1/messages:
- POST /v1/messages/batches (create batch)
- GET /v1/messages/batches (list batches)
- GET /v1/messages/batches/{id} (retrieve batch)
- POST /v1/messages/batches/{id}/cancel (cancel batch)
- GET /v1/messages/batches/{id}/results (stream JSONL results)
The implementation follows the same pattern as the existing OpenAI Batches
provider: KVStore for persistence, background asyncio tasks with
semaphore-based concurrency control, and reuse of the existing
create_message() flow for processing individual batch requests. The
MessagesConfig now requires a kvstore backend, which is a breaking change
to the provider configuration.
Integration tests run under the messages-openai suite and cover create /
retrieve / list / cancel / results / duplicate-custom_id / 404 paths against
openai/gpt-4o, with recordings committed for replay-mode CI.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Charlie Doern <[email protected]>
f9f0570 to
d63702b
Compare
leseb
left a comment
There was a problem hiding this comment.
Looks good, AI caught this one too:
Cancel during processing can misreport completed requests as canceled
Results are only persisted once after asyncio.gather completes, but cancellation finalization depends on previously persisted results (existing_data). If cancellation happens mid-run, completed requests are not yet saved, so _finalize_batch_canceled marks all not-yet-recorded requests as canceled, losing true
succeeded/errored outcomes and producing incorrect counts.
| limit: int = 20, | ||
| before_id: str | None = None, | ||
| after_id: str | None = None, |
|
|
||
| async def retrieve_message_batch_results( | ||
| self, | ||
| batch_id: str, |
|
|
||
| async def _process_message_batch( | ||
| self, | ||
| batch_id: str, |
There was a problem hiding this comment.
why isn't batch_id part of the request class?
|
|
||
| async def _process_message_batch_impl( | ||
| self, | ||
| batch_id: str, |
|
|
||
| async def _finalize_batch_canceled( | ||
| self, | ||
| batch_id: str, |
| requests: list[MessageBatchRequestParams] = Field( | ||
| ..., | ||
| min_length=1, | ||
| max_length=100000, |
There was a problem hiding this comment.
isn't 100000 a little too much for asyncio, let's do 1000 instead?
| if batch.processing_status == "ended": | ||
| raise ValueError(f"Cannot cancel batch '{batch_id}' that has already ended") | ||
|
|
||
| async with self._update_lock: |
There was a problem hiding this comment.
let's acquire the lock earlier so we avoid the read-check-write race? we need to get the batch in the lock too, then cancel the task after releasing the lock.
- Introduce request classes for list/retrieve/cancel/results endpoints and thread them through the Messages protocol, FastAPI routes, and inline impl. - Lower CreateMessageBatchRequest.requests max_length from 100000 to 1000 to keep batch size reasonable for asyncio scheduling. - Bundle batch_id with its original request via an internal _BatchContext so private helpers take a single argument. - Fix cancel_message_batch race by reading and updating the batch under the update lock, and cancelling the processing task only after the lock is released so the task can acquire it to finalize. - Track partial batch results in memory as each request completes so a mid-run cancellation finalizes with the true succeeded/errored counts rather than marking already-completed requests as canceled. Signed-off-by: Charlie Doern <[email protected]>
Signed-off-by: Charlie Doern <[email protected]>
The messages suite defaults to the ollama setup which uses ollama/llama3.2:3b-instruct-fp16, but the recorded HTTP responses for the batch tests only existed for ollama/gpt-oss:20b. The ollama suite batch tests therefore had no matching recording and fell through to the live container which returned 400. Add llama3.2-keyed recordings cloned from the existing gpt-oss responses so replay covers both parameter variants. Signed-off-by: Charlie Doern <[email protected]>
The messages suite was switched from ollama-reasoning (gpt-oss:20b) to ollama (llama3.2:3b-instruct-fp16) in ogx-ai#5746, so the gpt-oss-keyed batch recordings are never consulted in replay. Flagged by the unused-recordings check. Signed-off-by: Charlie Doern <[email protected]>
| limit: int = 20, | ||
| before_id: str | None = None, | ||
| after_id: str | None = None, |
There was a problem hiding this comment.
should be ListMessageBatchesRequest?
…ssageBatchesRequest Addresses review feedback on PR ogx-ai#5577: the GET /messages/batches route now takes a ListMessageBatchesRequest directly via Annotated[..., Query()] instead of unpacking the individual query parameters and reconstructing the request class inside the handler. Signed-off-by: Charlie Doern <[email protected]>
Summary
Adds all 5 Anthropic Message Batches API endpoints under
/v1/messages:POST /v1/messages/batches— create a batch of message requestsGET /v1/messages/batches— list batches with paginationGET /v1/messages/batches/{id}— retrieve batch statusPOST /v1/messages/batches/{id}/cancel— cancel an in-progress batchGET /v1/messages/batches/{id}/results— stream results as JSONLThe implementation follows the same architecture as the existing OpenAI Batches provider: KVStore persistence, background asyncio tasks with semaphore-based concurrency control, and reuse of the existing
create_message()flow for processing individual batch requests.Breaking change
MessagesConfignow requires akvstorebackend (KVStoreReference). Existing distributions are updated via codegen; out-of-tree configs will need to add this field.Test plan
tests/integration/messages/test_message_batches.pycovering create / retrieve / list / cancel / results / duplicate-custom_id / 404, running under themessages-openaisuite (already wired intotests/integration/ci_matrix.json)openai/gpt-4oso replay-mode CI works without an API keyLocally verified:
./scripts/integration-tests.sh \ --stack-config server:ci-tests --setup gpt \ --file tests/integration/messages/test_message_batches.py # 6 passed in 1.76s🤖 Generated with Claude Code