Skip to content

Commit 1a23a9f

Browse files
test(e2e): updated scenarios with anthropic batch API tests and more
1 parent 0d12fc8 commit 1a23a9f

1 file changed

Lines changed: 335 additions & 0 deletions

File tree

tests/e2e/release-e2e-scenarios.md

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ Stateful note:
9393
managed read-only, env-over-YAML, startup validation) needs gateways launched
9494
with custom config, so it is covered by a standalone script rather than this
9595
running-stack matrix
96+
- `S173`-`S182` exercise the Anthropic Messages drop-in compatibility fixes
97+
(`x-api-key` auth fallback, `stop_sequence`, seeded stream usage,
98+
dialect-aware `/v1/models` and 404s); `S173`-`S175` need the auth-enabled
99+
gateway (`$AUTH_BASE_URL`), the rest are read-mostly on `$BASE_URL` and
100+
rerunnable in any order
101+
- `S183`-`S191` exercise the Anthropic Message Batches API
102+
(`/v1/messages/batches*`); each creates its own `msgbatch_`-scoped batch and
103+
is rerunnable in any order, but (like `S47`-`S48`) they leave
104+
`in_progress`/`canceling` batches behind since the scenarios do not wait for
105+
a real provider batch to end. Bedrock Mantle (`internal/providers/bedrockmantle`)
106+
has no running-stack coverage here: no `BEDROCK_MANTLE_API_KEY`/AWS
107+
credentials are available in `.env`, so it is covered by its unit test suite
108+
(`config_test.go`, `bedrock_mantle_test.go`) plus a one-off manual check that
109+
a `BEDROCK_MANTLE_*`-prefixed provider registers distinctly from `BEDROCK_*`
110+
at startup without colliding or crashing
96111
- For stateful partial reruns, prefer a contiguous range that includes the
97112
prerequisite setup scenarios, or rerun with the same `--qa-suffix` and
98113
`--keep-artifacts`
@@ -4236,3 +4251,323 @@ mcp_post "$BASE_URL/mcp" "$SID" \
42364251
'{"jsonrpc":"2.0","id":5,"method":"resources/read","params":{"uri":"mock://alpha/info"}}' \
42374252
| jq -e 'any(.result.contents[]?; .text == "MOCKMCP_ALPHA_RESOURCE_OK")' >/dev/null
42384253
```
4254+
4255+
## 24. Anthropic Messages drop-in compatibility
4256+
4257+
These scenarios cover the gaps closed by the Anthropic-SDK drop-in fix pass:
4258+
`x-api-key` auth fallback, `stop_sequence` surfaced as a typed field, seeded
4259+
`message_start` usage on streams, dialect-aware `/v1/models`, and a canonical
4260+
404 envelope that no longer swallows 405s. `S173`-`S175` run on the
4261+
auth-enabled gateway (`$AUTH_BASE_URL`) since the auth fallback needs a
4262+
gateway with a master key configured; the rest run on `$BASE_URL`, which runs
4263+
in unsafe mode. All are read-mostly and rerunnable in any order.
4264+
4265+
### S173 `x-api-key` header authenticates like `Authorization: Bearer`
4266+
4267+
Checks the Anthropic-native credential header works unchanged, matching
4268+
`Anthropic(api_key=...)` SDK defaults.
4269+
4270+
```bash
4271+
RESP_FILE="$QA_RUN_DIR/s173.chat.json"
4272+
curl -fsS "$AUTH_BASE_URL/v1/chat/completions" \
4273+
-H 'Content-Type: application/json' \
4274+
-H "x-api-key: $GOMODEL_MASTER_KEY" \
4275+
-d '{"model":"gpt-4.1-nano","messages":[{"role":"user","content":"Reply with exactly QA_XAPIKEY_OK"}],"max_tokens":20}' \
4276+
> "$RESP_FILE"
4277+
assert_chat_response_contains "$RESP_FILE" "openai" "QA_XAPIKEY_OK"
4278+
```
4279+
4280+
### S174 Missing credentials names both accepted schemes (negative)
4281+
4282+
Checks the combined error message added when neither header is present.
4283+
4284+
```bash
4285+
HEADERS_FILE="$QA_RUN_DIR/s174.headers"
4286+
BODY_FILE="$QA_RUN_DIR/s174.body"
4287+
curl -sS -D "$HEADERS_FILE" -o "$BODY_FILE" "$AUTH_BASE_URL/v1/chat/completions" \
4288+
-H 'Content-Type: application/json' \
4289+
-d '{"model":"gpt-4.1-nano","messages":[{"role":"user","content":"hi"}]}'
4290+
grep -Eiq '^HTTP/.* 401 ' "$HEADERS_FILE"
4291+
jq -e '.error.type == "authentication_error" and (.error.message | test("Authorization: Bearer") and test("x-api-key"))' "$BODY_FILE" >/dev/null
4292+
```
4293+
4294+
### S175 `Authorization` takes precedence over `x-api-key` when both are sent
4295+
4296+
A wrong bearer token is still rejected even when a valid `x-api-key` is also
4297+
present, confirming the fallback only applies when `Authorization` is absent.
4298+
4299+
```bash
4300+
HEADERS_FILE="$QA_RUN_DIR/s175.headers"
4301+
BODY_FILE="$QA_RUN_DIR/s175.body"
4302+
curl -sS -D "$HEADERS_FILE" -o "$BODY_FILE" "$AUTH_BASE_URL/v1/chat/completions" \
4303+
-H 'Content-Type: application/json' \
4304+
-H 'Authorization: Bearer totally-wrong-key' \
4305+
-H "x-api-key: $GOMODEL_MASTER_KEY" \
4306+
-d '{"model":"gpt-4.1-nano","messages":[{"role":"user","content":"hi"}]}'
4307+
grep -Eiq '^HTTP/.* 401 ' "$HEADERS_FILE"
4308+
```
4309+
4310+
### S176 `stop_sequence` surfaces as a typed field (non-streaming, Anthropic backend)
4311+
4312+
Checks the natively reported matched sequence round-trips through
4313+
`/v1/messages` as `stop_reason: "stop_sequence"` plus `stop_sequence`.
4314+
4315+
```bash
4316+
RESP_FILE="$QA_RUN_DIR/s176.messages.json"
4317+
curl -fsS "$BASE_URL/v1/messages" \
4318+
-H 'Content-Type: application/json' \
4319+
-d '{"model":"claude-sonnet-4-6","max_tokens":64,"stop_sequences":["QA_STOP_HERE"],"messages":[{"role":"user","content":"Count from 1 to 10, one number per line. After the number 3 write the exact text QA_STOP_HERE then continue."}]}' \
4320+
> "$RESP_FILE"
4321+
jq '{stop_reason,stop_sequence}' "$RESP_FILE"
4322+
jq -e '.stop_reason == "stop_sequence" and .stop_sequence == "QA_STOP_HERE"' "$RESP_FILE" >/dev/null
4323+
```
4324+
4325+
### S177 `stop_sequence` in streaming `message_delta` plus seeded `message_start` usage
4326+
4327+
Checks the streaming counterpart of `S176` and that `message_start` no longer
4328+
reports a hardcoded zero for `usage.input_tokens`.
4329+
4330+
```bash
4331+
SSE_FILE="$QA_RUN_DIR/s177.messages.sse"
4332+
curl -fsS --no-buffer "$BASE_URL/v1/messages" \
4333+
-H 'Content-Type: application/json' \
4334+
-d '{"model":"claude-sonnet-4-6","max_tokens":64,"stream":true,"stop_sequences":["QA_STOP_HERE"],"messages":[{"role":"user","content":"Count from 1 to 10, one number per line. After the number 3 write the exact text QA_STOP_HERE then continue."}]}' \
4335+
> "$SSE_FILE"
4336+
grep -A1 '^event: message_start' "$SSE_FILE" | sed -n '2p' | sed 's/^data: //' \
4337+
| jq -e '.message.usage.input_tokens > 0' >/dev/null
4338+
grep -A1 '^event: message_delta' "$SSE_FILE" | sed -n '2p' | sed 's/^data: //' \
4339+
| jq -e '.delta.stop_reason == "stop_sequence" and .delta.stop_sequence == "QA_STOP_HERE"' >/dev/null
4340+
```
4341+
4342+
### S178 OpenAI-family backend keeps `end_turn` through `/v1/messages` (documented limitation)
4343+
4344+
`finish_reason: "stop"` conflates a natural stop with a stop-sequence hit, so
4345+
OpenAI-family providers structurally cannot report `stop_sequence`; checks the
4346+
gateway keeps `end_turn` rather than fabricating a value.
4347+
4348+
```bash
4349+
RESP_FILE="$QA_RUN_DIR/s178.messages.json"
4350+
curl -fsS "$BASE_URL/v1/messages" \
4351+
-H 'Content-Type: application/json' \
4352+
-d '{"model":"gpt-4.1-nano","max_tokens":64,"stop_sequences":["QA_STOP_HERE"],"messages":[{"role":"user","content":"Count from 1 to 10, one number per line. After the number 3 write the exact text QA_STOP_HERE then continue."}]}' \
4353+
> "$RESP_FILE"
4354+
jq '{stop_reason,stop_sequence}' "$RESP_FILE"
4355+
jq -e '.stop_reason == "end_turn" and .stop_sequence == null' "$RESP_FILE" >/dev/null
4356+
```
4357+
4358+
### S179 `GET /v1/models` renders the Anthropic shape for Anthropic SDK clients
4359+
4360+
The Anthropic SDK always sends `anthropic-version`; checks the response takes
4361+
the Anthropic list shape (`type`, `display_name`, `created_at`,
4362+
`has_more`/`first_id`/`last_id`) instead of the OpenAI shape.
4363+
4364+
```bash
4365+
RESP_FILE="$QA_RUN_DIR/s179.models.json"
4366+
curl -fsS "$BASE_URL/v1/models" -H 'anthropic-version: 2023-06-01' > "$RESP_FILE"
4367+
jq '{sample: .data[0], has_more, first_id, last_id}' "$RESP_FILE"
4368+
jq -e '
4369+
(.data | length) > 0
4370+
and .data[0].type == "model"
4371+
and (.data[0].display_name | type == "string")
4372+
and (.data[0] | has("object") | not)
4373+
and .has_more == false
4374+
and (.first_id != null)
4375+
and (.last_id != null)
4376+
' "$RESP_FILE" >/dev/null
4377+
```
4378+
4379+
### S180 `GET /v1/models` stays OpenAI-shaped without the header (regression)
4380+
4381+
Checks the default OpenAI-compatible listing shape is unchanged for callers
4382+
that do not send `anthropic-version`.
4383+
4384+
```bash
4385+
RESP_FILE="$QA_RUN_DIR/s180.models.json"
4386+
curl -fsS "$BASE_URL/v1/models" > "$RESP_FILE"
4387+
jq -e '.object == "list" and (.data[0].object == "model")' "$RESP_FILE" >/dev/null
4388+
```
4389+
4390+
### S181 Unknown route 404 renders in the caller's wire dialect
4391+
4392+
Checks the canonical 404 envelope added for unclassified routes: Anthropic
4393+
shape when `anthropic-version` is present, gateway/OpenAI shape otherwise.
4394+
4395+
```bash
4396+
ANTHROPIC_BODY="$QA_RUN_DIR/s181.anthropic.json"
4397+
DEFAULT_BODY="$QA_RUN_DIR/s181.default.json"
4398+
curl -sS -o "$ANTHROPIC_BODY" -w '%{http_code}\n' "$BASE_URL/v1/does-not-exist" -H 'anthropic-version: 2023-06-01'
4399+
curl -sS -o "$DEFAULT_BODY" -w '%{http_code}\n' "$BASE_URL/v1/does-not-exist"
4400+
jq '.' "$ANTHROPIC_BODY"
4401+
jq '.' "$DEFAULT_BODY"
4402+
jq -e '.type == "error" and .error.type == "not_found_error"' "$ANTHROPIC_BODY" >/dev/null
4403+
jq -e '(.type != "error") and .error.type == "not_found_error"' "$DEFAULT_BODY" >/dev/null
4404+
```
4405+
4406+
### S182 Known route with the wrong method still returns 405 (regression)
4407+
4408+
The dialect-aware 404 handler is registered as the router-level
4409+
`NotFoundHandler`, not a wildcard route, specifically so it does not shadow
4410+
echo's 405 method-not-allowed handling for routes that do exist.
4411+
4412+
```bash
4413+
HEADERS_FILE="$QA_RUN_DIR/s182.headers"
4414+
curl -sS -D "$HEADERS_FILE" -o /dev/null -X GET "$BASE_URL/v1/chat/completions"
4415+
grep -Eiq '^HTTP/.* 405 ' "$HEADERS_FILE"
4416+
```
4417+
4418+
## 25. Anthropic Message Batches API
4419+
4420+
These scenarios exercise `/v1/messages/batches*`, the Anthropic-dialect ingress
4421+
over the same native-batch pipeline that serves `/v1/batches`. A batch's
4422+
requests are translated per-item to canonical chat requests, so a Message
4423+
Batch can route to any provider with native batch support, not only
4424+
Anthropic. Batch IDs are pure prefix aliases of one underlying resource:
4425+
`msgbatch_<uuid>` on this dialect, `batch_<uuid>` on `/v1/batches`. Real
4426+
provider batches can take a long time to complete, so these scenarios check
4427+
create/get/list/cancel/delete-guard/validation behavior rather than waiting
4428+
for a batch to end; `S183`-`S191` are self-contained and rerunnable in any
4429+
order but leave `in_progress`/`canceling` batches behind, like `S47`-`S48`.
4430+
4431+
### S183 Create a native Anthropic Message Batch
4432+
4433+
```bash
4434+
RESP_FILE="$QA_RUN_DIR/s183.batch.json"
4435+
curl -fsS "$BASE_URL/v1/messages/batches" \
4436+
-H 'Content-Type: application/json' \
4437+
-d '{"requests":[{"custom_id":"qa-msgbatch-anthropic-1","params":{"model":"claude-sonnet-4-6","max_tokens":32,"messages":[{"role":"user","content":"Reply with exactly QA_MSGBATCH_ANTHROPIC_OK"}]}}]}' \
4438+
> "$RESP_FILE"
4439+
jq '{id,type,processing_status,request_counts}' "$RESP_FILE"
4440+
jq -e '.type == "message_batch" and (.id | startswith("msgbatch_")) and (.processing_status | type == "string")' "$RESP_FILE" >/dev/null
4441+
echo "$(jq -r .id "$RESP_FILE")" > "$QA_RUN_DIR/s183.batch-id"
4442+
```
4443+
4444+
### S184 Create a Message Batch routed to an OpenAI model (cross-provider)
4445+
4446+
Checks the Anthropic Message Batches dialect is provider-agnostic like
4447+
`/v1/messages`: an OpenAI model batch is created and materialized into an
4448+
uploaded JSONL input file under the hood.
4449+
4450+
```bash
4451+
RESP_FILE="$QA_RUN_DIR/s184.batch.json"
4452+
curl -fsS "$BASE_URL/v1/messages/batches" \
4453+
-H 'Content-Type: application/json' \
4454+
-d '{"requests":[{"custom_id":"qa-msgbatch-openai-1","params":{"model":"gpt-4.1-nano","max_tokens":32,"messages":[{"role":"user","content":"Reply with exactly QA_MSGBATCH_OPENAI_OK"}]}}]}' \
4455+
> "$RESP_FILE"
4456+
jq '{id,type,processing_status}' "$RESP_FILE"
4457+
jq -e '.type == "message_batch" and (.id | startswith("msgbatch_"))' "$RESP_FILE" >/dev/null
4458+
echo "$(jq -r .id "$RESP_FILE")" > "$QA_RUN_DIR/s184.batch-id"
4459+
```
4460+
4461+
### S185 Get and list Message Batches
4462+
4463+
```bash
4464+
BATCH_ID=$(cat "$QA_RUN_DIR/s183.batch-id")
4465+
GET_FILE="$QA_RUN_DIR/s185.get.json"
4466+
LIST_FILE="$QA_RUN_DIR/s185.list.json"
4467+
curl -fsS "$BASE_URL/v1/messages/batches/$BATCH_ID" > "$GET_FILE"
4468+
jq -e --arg id "$BATCH_ID" '.id == $id and .type == "message_batch" and (.expires_at | type == "string")' "$GET_FILE" >/dev/null
4469+
4470+
curl -fsS "$BASE_URL/v1/messages/batches?limit=20" > "$LIST_FILE"
4471+
jq '{has_more,first_id,last_id,count:(.data|length)}' "$LIST_FILE"
4472+
jq -e --arg id "$BATCH_ID" '[.data[].id] | index($id) != null' "$LIST_FILE" >/dev/null
4473+
```
4474+
4475+
### S186 Message Batch IDs are dialect aliases of `/v1/batches` resources
4476+
4477+
Checks the `msgbatch_`/`batch_` prefix aliasing holds in both directions: a
4478+
batch created on one dialect is retrievable on the other under the mapped ID.
4479+
4480+
```bash
4481+
BATCH_ID=$(cat "$QA_RUN_DIR/s183.batch-id")
4482+
ALIASED_FILE="$QA_RUN_DIR/s186.aliased.json"
4483+
curl -fsS "$BASE_URL/v1/batches/batch_${BATCH_ID#msgbatch_}" > "$ALIASED_FILE"
4484+
jq -e --arg id "batch_${BATCH_ID#msgbatch_}" '.id == $id and .object == "batch" and .provider == "anthropic"' "$ALIASED_FILE" >/dev/null
4485+
4486+
CREATE_FILE="$QA_RUN_DIR/s186.create.json"
4487+
curl -fsS "$BASE_URL/v1/batches" \
4488+
-H 'Content-Type: application/json' \
4489+
-d '{"endpoint":"/v1/chat/completions","requests":[{"custom_id":"qa-reverse-alias-1","method":"POST","url":"/v1/chat/completions","body":{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"Reply with exactly QA_REVERSE_BATCH_OK"}],"max_tokens":32}}]}' \
4490+
> "$CREATE_FILE"
4491+
NATIVE_ID=$(jq -er '.id' "$CREATE_FILE")
4492+
REVERSE_FILE="$QA_RUN_DIR/s186.reverse.json"
4493+
curl -fsS "$BASE_URL/v1/messages/batches/msgbatch_${NATIVE_ID#batch_}" > "$REVERSE_FILE"
4494+
jq -e --arg id "msgbatch_${NATIVE_ID#batch_}" '.id == $id and .type == "message_batch"' "$REVERSE_FILE" >/dev/null
4495+
```
4496+
4497+
### S187 Cancel a Message Batch
4498+
4499+
```bash
4500+
BATCH_ID=$(cat "$QA_RUN_DIR/s184.batch-id")
4501+
RESP_FILE="$QA_RUN_DIR/s187.cancel.json"
4502+
curl -fsS -X POST "$BASE_URL/v1/messages/batches/$BATCH_ID/cancel" > "$RESP_FILE"
4503+
jq '{id,processing_status,cancel_initiated_at}' "$RESP_FILE"
4504+
jq -e --arg id "$BATCH_ID" '.id == $id and (.processing_status == "canceling" or .processing_status == "ended")' "$RESP_FILE" >/dev/null
4505+
```
4506+
4507+
### S188 Delete guard rejects a still-processing Message Batch (negative)
4508+
4509+
Batches still processing must be canceled first, matching the Anthropic
4510+
Message Batches contract.
4511+
4512+
```bash
4513+
BATCH_ID=$(cat "$QA_RUN_DIR/s183.batch-id")
4514+
HEADERS_FILE="$QA_RUN_DIR/s188.headers"
4515+
BODY_FILE="$QA_RUN_DIR/s188.body"
4516+
curl -sS -D "$HEADERS_FILE" -o "$BODY_FILE" -X DELETE "$BASE_URL/v1/messages/batches/$BATCH_ID"
4517+
cat "$BODY_FILE"
4518+
grep -Eiq '^HTTP/.* 400 ' "$HEADERS_FILE"
4519+
jq -e '.type == "error" and .error.type == "invalid_request_error" and (.error.message | test("still processing"))' "$BODY_FILE" >/dev/null
4520+
```
4521+
4522+
### S189 Message Batch create validation negatives
4523+
4524+
```bash
4525+
HEADERS_FILE="$QA_RUN_DIR/s189.headers"
4526+
BODY_FILE="$QA_RUN_DIR/s189.body"
4527+
4528+
curl -sS -D "$HEADERS_FILE" -o "$BODY_FILE" "$BASE_URL/v1/messages/batches" \
4529+
-H 'Content-Type: application/json' -d '{"requests":[]}'
4530+
grep -Eiq '^HTTP/.* 400 ' "$HEADERS_FILE"
4531+
4532+
curl -sS -D "$HEADERS_FILE" -o "$BODY_FILE" "$BASE_URL/v1/messages/batches" \
4533+
-H 'Content-Type: application/json' \
4534+
-d '{"requests":[{"custom_id":"dup","params":{"model":"gpt-4.1-nano","max_tokens":10,"messages":[{"role":"user","content":"a"}]}},{"custom_id":"dup","params":{"model":"gpt-4.1-nano","max_tokens":10,"messages":[{"role":"user","content":"b"}]}}]}'
4535+
grep -Eiq '^HTTP/.* 400 ' "$HEADERS_FILE"
4536+
jq -e '.error.message | test("not unique")' "$BODY_FILE" >/dev/null
4537+
4538+
curl -sS -D "$HEADERS_FILE" -o "$BODY_FILE" "$BASE_URL/v1/messages/batches" \
4539+
-H 'Content-Type: application/json' \
4540+
-d '{"requests":[{"custom_id":" ","params":{"model":"gpt-4.1-nano","max_tokens":10,"messages":[{"role":"user","content":"a"}]}}]}'
4541+
grep -Eiq '^HTTP/.* 400 ' "$HEADERS_FILE"
4542+
jq -e '.error.message | test("required")' "$BODY_FILE" >/dev/null
4543+
```
4544+
4545+
### S190 Message Batch results before ready returns the not-ready envelope (negative)
4546+
4547+
```bash
4548+
BATCH_ID=$(cat "$QA_RUN_DIR/s183.batch-id")
4549+
HEADERS_FILE="$QA_RUN_DIR/s190.headers"
4550+
BODY_FILE="$QA_RUN_DIR/s190.body"
4551+
curl -sS -D "$HEADERS_FILE" -o "$BODY_FILE" "$BASE_URL/v1/messages/batches/$BATCH_ID/results"
4552+
cat "$BODY_FILE"
4553+
grep -Eiq '^HTTP/.* 409 ' "$HEADERS_FILE"
4554+
jq -e '.error.type == "invalid_request_error" and (.error.message | test("not ready"))' "$BODY_FILE" >/dev/null
4555+
```
4556+
4557+
### S191 Mixed-provider requests in one Message Batch are rejected (negative)
4558+
4559+
A single native batch is submitted to one upstream provider; checks a batch
4560+
mixing an Anthropic and an OpenAI model item is rejected before submission,
4561+
the same discipline as the file-based `/v1/batches` mixed-provider check
4562+
(`S48`).
4563+
4564+
```bash
4565+
HEADERS_FILE="$QA_RUN_DIR/s191.headers"
4566+
BODY_FILE="$QA_RUN_DIR/s191.body"
4567+
curl -sS -D "$HEADERS_FILE" -o "$BODY_FILE" "$BASE_URL/v1/messages/batches" \
4568+
-H 'Content-Type: application/json' \
4569+
-d '{"requests":[{"custom_id":"qa-mixed-a","params":{"model":"claude-sonnet-4-6","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}},{"custom_id":"qa-mixed-b","params":{"model":"gpt-4.1-nano","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}}]}'
4570+
cat "$BODY_FILE"
4571+
grep -Eiq '^HTTP/.* 400 ' "$HEADERS_FILE"
4572+
jq -e '.error.message | test("single provider per batch")' "$BODY_FILE" >/dev/null
4573+
```

0 commit comments

Comments
 (0)