Skip to content

feat(models): add Anthropic Messages dialect - #145

Merged
ethan-scitix merged 6 commits into
scitix:mainfrom
jack-scitix-ai:feat/anthropic-messages
Sep 18, 2026
Merged

ethan-scitix merged 6 commits into
scitix:mainfrom
jack-scitix-ai:feat/anthropic-messages

Conversation

@jack-scitix-ai

Copy link
Copy Markdown
Contributor

Type

  • feature — new benchmark, task, or capability

Summary

  • Add an active anthropic_messages dialect using Anthropic’s native Messages JSON and SSE protocols.
  • Translate provider-neutral chat, image, function-tool, structured-output, reasoning, prefill, and opaque-continuation inputs with independent request auditing.
  • Lift streaming and non-streaming responses through strict content, usage, tool-history, reasoning, and terminal-state contracts.
  • Preserve extended-thinking state through deterministic, replay-validated opaque continuations without persisting credentials.
  • Reject unsupported or malformed request shapes before I/O, including non-text system content and invalid text payloads.
  • Record request parameters from the final prepared wire body and fail loudly on unknown accounting fields that cannot be represented faithfully.
  • Recognize both max_tokens and model_context_window_exceeded as Anthropic truncation outcomes in shared anomaly and report accounting.
  • Declare per-request seed support unavailable, preventing deterministic mode from silently injecting an unsupported seed.
  • Document the anthropic_messages configuration contract, including required max_tokens, unsupported request seeds, and the absence of raw body/header passthrough such as anthropic-beta.

Related Issues

Refs #25

Test Plan

Automated

  • Lint/format clean (ruff check && ruff format --check)
  • Type check clean (ty check or mypy --strict)
  • Unit tests pass (pdm run pytest)

Manual

No live Anthropic endpoint was exercised. Native request, response, SSE, authentication, continuation, and failure paths were verified with real httpx request construction over mocked HTTP transports.

Checklist

Required (all PRs)

  • PR title follows conventional format (type(scope): description)
  • No internal paths, credentials, or personal info in committed files
  • AI-generated code has AI-Generated Code - <model> (<provider>) in module docstring
  • No new upper-layer dependencies added to core/
  • Deleted code verified — no remaining call sites depend on it

@ethan-scitix

Copy link
Copy Markdown
Collaborator

Commit messages and identities clean: single author matching 28 merged commits, no trailers, no private paths.

Verified empirically: 623 tests pass, ruff/ty clean, 95% coverage on the new module, and a two-turn tool-use continuation round-trips end to end.

Blocking

Bug: adaptive thinking is treated as guaranteed — sieval/core/models/dialects/anthropic_messages.py:2397

With summary="auto", a reply containing no thinking block raises OutputContractError: Anthropic reply omitted the requested visible thinking summary. Adaptive thinking is optional per request — the docs state a turn where Claude chose not to think contains no thinking block, and effort="low" (a legal pairing, only effort/budget_tokens are mutually exclusive) explicitly skips thinking on simple inputs. The dropped samples correlate with difficulty, so this biases the score rather than adding noise. The same raise fires on stop_reason="max_tokens", making the truncation accounting this PR adds in meta.py unreachable on that path.

Suggest requiring the summary only when a thinking block is present, and never when the reply was truncated.

Bug: refusal is unreachable — :2319 vs :81

refusal is in _STOP_REASONS, but any non-null stop_details is rejected — and stop_details is non-null exactly on refusal. Verified:

refusal + stop_details (the real shape) -> OutputContractError: stop_details has no shared-IR mapping
refusal, no stop_details (never occurs)  -> OK

A refusal arrives as HTTP 200 and is a scoreable outcome, not an error. Letting stop_details through when stop_reason == "refusal" is sufficient — no IR mapping needed. Note tests/unit/core/models/dialects/test_anthropic_messages.py:1131 and :1475 currently encode the rejection as intended.

Non-blocking

  • Nit: streaming errors lose the Anthropic error body — :2461. Calling raise_for_status() before aread() makes e.response.text throw ResponseNotRead (confirmed on an unbuffered stream), so overloaded_error / rate_limit_error detail is unrecoverable; the non-streaming path at :2451 keeps it. One line: await response.aread() before raising.
  • Question: no way to disable thinking — _thinking_config at :573 never emits {"type":"disabled"}. Since omitting thinking means adaptive on newer models but off on older ones, the same config means different things by model generation. IR-level decision — fine as a follow-up issue.
  • Nit: "PR 3" appears in shipped error strings (:203, :1526, :2127, :2163). Merged openai_chat.py carries the same pattern ("PR 1"), so this is family-wide cleanup, not this PR.

Verified clean

rules_hash is unchanged (56480a99ae65a3c0 on both sides) — the meta.py truncation addition does not invalidate resume compatibility fleet-wide. output_config.format / output_config.effort shapes match current docs, and both the legacy budget_tokens and the adaptive thinking forms are emitted correctly.

Bind the provider-neutral model IR to Anthropic's native Messages JSON and SSE protocols with explicit capability admission, independent request auditing, strict response lifting, and replay-safe extended-thinking continuations. Reject unsupported authority, tool, and response shapes before they can be silently reinterpreted.
Exercise reachable request rejection, opaque replay validation, wire verifier mismatches, malformed provider responses, and SSE state-machine failures. Raise the dialect module's combined line/branch coverage above the core gate so multi-dialect integration does not dilute coverage below 95%.
Assert that streaming HTTP failures are raised before the SSE parser runs, preserving the same status contract as non-streaming Messages requests.
Strengthen Anthropic request and response validation, classify context-window termination as truncation, and document the dialect's required max_tokens, seed policy, and passthrough limitations.
Allow adaptive-thinking turns without thinking blocks and lift refusal responses as scoreable terminal outcomes.

Normalize streaming and non-streaming refusals to empty text, validate refusal details, preserve streamed HTTP error bodies, and skip success-only output requirements for refused responses.
@jack-scitix-ai

Copy link
Copy Markdown
Contributor Author

Thanks — addressed the actionable findings in the latest commit.

Adaptive thinking

Adaptive thinking is no longer treated as guaranteed:

  • A response without a thinking block is accepted, including requests using
    effort="low".
  • Truncated responses do not require a complete visible summary.
  • If a normal, non-truncated response does contain a regular thinking block,
    a requested visible summary must still be non-empty.
  • Redacted thinking remains accepted.
  • Manual budget_tokens mode still requires replayable thinking state.

Refusal handling

Anthropic refusals are now reachable and treated as scoreable terminal outcomes:

  • stop_reason="refusal" requires a valid stop_details object.
  • stop_details.type must be "refusal".
  • category and explanation are validated when present.
  • Non-refusal responses must not carry stop_details.
  • Refusals are lifted with empty text and
    finish_reason="refusal", while retaining usage and provenance.
  • Success-only response requirements, including reasoning, structured output,
    and opaque-continuation round-trip state, are skipped only when every choice
    is a refusal.

For streaming refusals, any content received before the terminal refusal is
deliberately discarded. Tests cover both:

  1. refusal while a content block is still unfinished; and
  2. refusal after a text block has already been closed.

The second shape is defensive coverage rather than a claim that Anthropic
guarantees it. Normalizing both shapes to empty text is intentional: it keeps
streaming and non-streaming behavior consistent and prevents blocked partial
content from entering task scoring.

Streaming HTTP errors

Streaming error responses are now read before raise_for_status(), preserving
the Anthropic error body on an unbuffered HTTP stream.

Follow-ups

refusal remains distinct from truncation and is not included in
TRUNCATION_FINISH_REASONS. A cross-dialect follow-up should introduce a
dedicated refusal/blocked-output anomaly signal and reconsider OpenAI
content_filter, which is currently counted as truncation.

Explicitly disabling thinking requires a shared IR decision, so it remains a
follow-up rather than being added only to this dialect.

The shipped "PR 3" wording is also unchanged because the same internal-PR
terminology exists across the dialect family. It should be handled as a
cross-dialect error-message cleanup.

Validation

  • Full unit suite: 7,853 passed before the final defensive test was added
  • Affected contract/anomaly tests: 510 passed
  • All 3 streaming-refusal tests passed after the final addition
  • Ruff check and format passed
  • ty check sieval/ passed
  • Anthropic dialect mypy check passed
  • git diff --check passed

The refusal early return skipped `_validated_content_block` entirely, so a
refusal carrying an unparseable block lifted cleanly while the same block on
the success path raised. Validate then discard, keeping the documented
normalization to empty text.

Also document two things a reader cannot get from the code: why
`summary='none'` enables thinking here but is a no-op on Responses, and the
three beta-gated `stop_details` fields that would be rejected as unknown if
header passthrough ever lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ethan-scitix
ethan-scitix force-pushed the feat/anthropic-messages branch from 63a33fe to d601f57 Compare September 18, 2026 13:24
@ethan-scitix
ethan-scitix merged commit 88bf796 into scitix:main Sep 18, 2026
9 checks passed
@jack-scitix-ai
jack-scitix-ai deleted the feat/anthropic-messages branch September 24, 2026 02:42
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.

2 participants