This commit is contained in:
devin-ai-integration[bot] 2026-10-01 00:53:44 +00:00 • committed by GitHub
commit 2e495a5a8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1731 additions and 0 deletions

View file

@ -0,0 +1,796 @@
"""Shared Claude Code-shaped request builders and upstream stream fixtures for integration contracts."""
import json
from collections.abc import Mapping
from itertools import chain
from typing import Final
from pydantic import JsonValue, TypeAdapter
JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
ANTHROPIC_API_KEY: Final = "synthetic-anthropic-key"
FABLE: Final = "claude-fable-5-1"
OPUS: Final = "claude-opus-5-5"
CLI_BETA: Final = (
"claude-code-20250219,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,"
"context-management-2025-06-27,prompt-caching-scope-2026-01-05"
)
FRONTIER_CLI_BETA: Final = (
f"{CLI_BETA},mid-conversation-system-2026-04-07,per-turn-control-2026-07-01,"
"mid-conversation-tool-changes-2026-07-01,effort-2025-11-24"
)
CACHE: Final = {"type": "ephemeral"}
CONTEXT_MANAGEMENT: Final = {"edits": [{"type": "clear_thinking_20251015", "keep": "all"}]}
THINKING_BUDGET: Final = {"budget_tokens": 31999, "type": "enabled", "display": "omitted"}
THINKING_ADAPTIVE: Final = {"type": "adaptive", "display": "omitted"}
METADATA_USER_ID: Final = json.dumps(
{
"device_id": "0" * 64,
"account_uuid": "",
"session_id": "00000000-0000-4000-8000-000000000000",
}
)
def schema(properties: JsonValue, required: tuple[str, ...]) -> dict[str, JsonValue]:
return {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": properties,
"required": list(required),
"additionalProperties": False,
}
def field(description: str, **extra: JsonValue) -> dict[str, JsonValue]:
return {"description": description, **extra}
def tools() -> tuple[dict[str, JsonValue], ...]:
_MAX: Final = 9007199254740991
return (
{
"name": "Agent",
"description": "Launch a new agent to handle complex, multi-step tasks.",
"input_schema": schema(
{
"description": field("A short (3-5 word) description of the task", type="string"),
"prompt": field("The task for the agent to perform", type="string"),
"subagent_type": field("The type of specialized agent to use for this task", type="string"),
"model": field(
"Optional model override for this agent.",
type="string",
enum=["sonnet", "opus", "haiku", "fable"],
),
"run_in_background": field(
"Agents run in the background by default; you will be notified when one completes.",
type="boolean",
),
"isolation": field("Isolation mode.", type="string", enum=["worktree", "remote"]),
},
("description", "prompt"),
),
},
{
"name": "Bash",
"description": "Executes a given bash command and returns its output.",
"input_schema": schema(
{
"command": field("The command to execute", type="string"),
"timeout": field("Optional timeout in milliseconds (max 600000)", type="number"),
"description": field(
"Clear, concise description of what this command does in active voice.",
type="string",
),
"run_in_background": field("Set to true to run this command in the background.", type="boolean"),
"dangerouslyDisableSandbox": field(
"Set this to true to dangerously override sandbox mode and run commands without sandboxing.",
type="boolean",
),
},
("command",),
),
},
{
"name": "CronCreate",
"description": "Schedule a prompt to be enqueued at a future time.",
"input_schema": schema(
{
"cron": field(
'Standard 5-field cron expression in local time: "M H DoM Mon DoW" (e.g.',
type="string",
),
"prompt": field("The prompt to enqueue at each fire time.", type="string"),
"recurring": field(
"true (default) = fire on every cron match until deleted or auto-expired after 7 days.",
type="boolean",
),
"durable": field(
"true = persist to .claude/scheduled_tasks.json and survive restarts.",
type="boolean",
),
},
("cron", "prompt"),
),
},
{
"name": "CronDelete",
"description": "Cancel a cron job previously scheduled with CronCreate.",
"input_schema": schema(
{
"id": field("Job ID returned by CronCreate.", type="string"),
},
("id",),
),
},
{
"name": "CronList",
"description": "List all cron jobs scheduled via CronCreate, both durable (.claude/scheduled_tasks.json) and session-only.",
"input_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"additionalProperties": False,
},
},
{
"name": "Edit",
"description": "Performs exact string replacements in files.",
"input_schema": schema(
{
"file_path": field("The absolute path to the file to modify", type="string"),
"old_string": field("The text to replace", type="string"),
"new_string": field(
"The text to replace it with (must be different from old_string)", type="string"
),
"replace_all": field(
"Replace all occurrences of old_string (default false)",
default=False,
type="boolean",
),
},
("file_path", "old_string", "new_string"),
),
},
{
"name": "EnterWorktree",
"description": "Use this tool ONLY when explicitly instructed to work in a worktree — either by the user directly, or by project instruc",
"input_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": field("Optional name for a new worktree.", type="string"),
"path": field(
"Path to an existing worktree to switch into instead of creating a new one.",
type="string",
),
},
"additionalProperties": False,
},
},
{
"name": "ExitWorktree",
"description": "Exit a worktree session created by EnterWorktree and return the session to the original working directory.",
"input_schema": schema(
{
"action": field(
'"keep" leaves the worktree and branch on disk; "remove" deletes both.',
type="string",
enum=["keep", "remove"],
),
"discard_changes": field(
'Required true when action is "remove" and the worktree has uncommitted files or unmerged commits.',
type="boolean",
),
},
("action",),
),
},
{
"name": "ListAgents",
"description": "Lists agents you can SendMessage to — in-process subagents you spawned, the teammates on your team, other local Claude s",
"input_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"channel": field("Not available in this build; leave unset.", type="string", maxLength=256),
"q": field("Not available in this build; leave unset.", type="string", maxLength=256),
},
"additionalProperties": False,
},
},
{
"name": "NotebookEdit",
"description": "Replaces, inserts, or deletes a single cell in a Jupyter notebook (.ipynb file).",
"input_schema": schema(
{
"notebook_path": field(
"The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)",
type="string",
),
"cell_id": field("The ID of the cell to edit.", type="string"),
"new_source": field("The new source for the cell", type="string"),
"cell_type": field(
"The type of the cell (code or markdown).",
type="string",
enum=["code", "markdown"],
),
"edit_mode": field(
"The type of edit to make (replace, insert, delete).",
type="string",
enum=["replace", "insert", "delete"],
),
},
("notebook_path", "new_source"),
),
},
{
"name": "Read",
"description": "Reads a file from the local filesystem.",
"input_schema": schema(
{
"file_path": field("The absolute path to the file to read", type="string"),
"offset": field("The line number to start reading from.", type="integer", minimum=0, maximum=_MAX),
"limit": field(
"The number of lines to read.",
type="integer",
exclusiveMinimum=0,
maximum=_MAX,
),
"pages": field('Page range for PDF files (e.g., "1-5", "3", "10-20").', type="string"),
},
("file_path",),
),
},
{
"name": "ReportFindings",
"description": "Report code-review findings as a typed list so the host UI can render them.",
"input_schema": schema(
{
"level": field(
"Effort level the review ran at",
type="string",
enum=["low", "medium", "high", "xhigh", "max"],
),
"findings": field(
"Verified findings, most-severe first; empty if none survived",
maxItems=32,
type="array",
items={
"type": "object",
"properties": {
"file": field("Repo-relative path of the file the finding is in", type="string"),
"line": field(
"1-indexed line the finding anchors to",
type="integer",
minimum=-_MAX,
maximum=_MAX,
),
"summary": field("One-sentence statement of the defect", type="string"),
"short_summary": field(
"Compressed label for compact UI (≤60 chars): the claim alone, no rationale or consequence clause",
type="string",
maxLength=60,
),
"failure_scenario": field("Concrete inputs/state → wrong output/crash", type="string"),
"category": field(
"Short kebab-case slug of the finding type, e.g.",
type="string",
maxLength=40,
),
"verdict": field(
"Set when a verify pass ran; absent on inline-only reviews",
type="string",
enum=["CONFIRMED", "PLAUSIBLE"],
),
"outcome": field(
"Set ONLY when re-reporting after applying fixes: what happened to this finding",
type="string",
enum=["fixed", "skipped", "no_change_needed"],
),
},
"required": ["file", "summary", "failure_scenario"],
"additionalProperties": False,
},
),
},
("findings",),
),
},
{
"name": "ScheduleWakeup",
"description": "Schedule when to resume work in /loop dynamic mode — the user invoked /loop without an interval, asking you to self-pace",
"input_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"delaySeconds": field("Seconds from now to wake up.", type="number"),
"reason": field("One short sentence explaining the chosen delay.", type="string"),
"prompt": field("The /loop input to fire on wake-up.", type="string"),
"stop": field(
"Set to true to end the dynamic loop immediately instead of scheduling another wakeup.",
type="boolean",
),
"noop": field(
"true = nothing changed (you checked and there is nothing to report).",
type="boolean",
),
},
"additionalProperties": False,
},
},
{
"name": "SendMessage",
"description": "# SendMessage\n\nSend a message to another agent.",
"input_schema": schema(
{
"to": field(
'Recipient: a name from ListAgents (append its " [ref]" only when a listing or an error shows one), a teammate name, "mai',
type="string",
allOf=[{"pattern": "^[^\\n\\r]*$"}, {"pattern": "^[\\s\\S]{0,300}$"}],
),
"summary": field(
"A 5-10 word label for your own transcript row (not transmitted — the recipient previews the first line of `message`).",
type="string",
maxLength=200,
),
"message": field("Plain text message content.", default="", type="string"),
"notify_when_idle": field(
"Ask a session ON THIS MACHINE to send you ONE notice when it next goes idle (finishes its turn with nothing queued) or e",
type="boolean",
),
},
("to", "message"),
),
},
{
"name": "Skill",
"description": "Invoke a skill.",
"input_schema": schema(
{
"skill": field("The name of a skill from the available-skills list.", type="string"),
"args": field("Optional arguments for the skill", type="string"),
},
("skill",),
),
},
{
"name": "TaskCreate",
"description": "Use this tool to create a structured task list for your current coding session.",
"input_schema": schema(
{
"subject": field("A brief title for the task", type="string"),
"description": field("What needs to be done", type="string"),
"activeForm": field(
'Present continuous form shown in spinner when in_progress (e.g., "Running tests")',
type="string",
),
"metadata": field(
"Arbitrary metadata to attach to the task",
type="object",
propertyNames={"type": "string"},
additionalProperties={},
),
},
("subject", "description"),
),
},
{
"name": "TaskGet",
"description": "Use this tool to retrieve a task by its ID from the task list.",
"input_schema": schema(
{
"taskId": field("The ID of the task to retrieve", type="string"),
},
("taskId",),
),
},
{
"name": "TaskList",
"description": "Use this tool to list all tasks in the task list.",
"input_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"additionalProperties": False,
},
},
{
"name": "TaskStop",
"description": "- Stops a running background task by its ID\n- Takes a task_id parameter identifying the task to stop\n- To stop an agent-",
"input_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"task_id": field("The ID of the background task to stop.", type="string"),
"shell_id": field("Deprecated: use task_id instead", type="string"),
},
"additionalProperties": False,
},
},
{
"name": "TaskUpdate",
"description": "Use this tool to update a task in the task list.",
"input_schema": schema(
{
"taskId": field("The ID of the task to update", type="string"),
"subject": field("New subject for the task", type="string"),
"description": field("New description for the task", type="string"),
"activeForm": field(
'Present continuous form shown in spinner when in_progress (e.g., "Running tests")',
type="string",
),
"status": field(
"New status for the task",
anyOf=[
{"type": "string", "enum": ["pending", "in_progress", "completed"]},
{"type": "string", "const": "deleted"},
],
),
"addBlocks": field("Task IDs that this task blocks", type="array", items={"type": "string"}),
"addBlockedBy": field("Task IDs that block this task", type="array", items={"type": "string"}),
"owner": field("New owner for the task", type="string"),
"metadata": field(
"Metadata keys to merge into the task.",
type="object",
propertyNames={"type": "string"},
additionalProperties={},
),
},
("taskId",),
),
},
{
"name": "WebFetch",
"description": "IMPORTANT: WebFetch WILL FAIL for authenticated or private URLs.",
"input_schema": schema(
{
"url": field("The URL to fetch content from", type="string", format="uri"),
"prompt": field("The prompt to run on the fetched content", type="string"),
},
("url", "prompt"),
),
},
{
"name": "WebSearch",
"description": "- Allows Claude to search the web and use the results to inform responses\n- Provides up-to-date information for current ",
"input_schema": schema(
{
"query": field("The search query to use", type="string", minLength=2),
"allowed_domains": field(
"Only include search results from these domains", type="array", items={"type": "string"}
),
"blocked_domains": field(
"Never include search results from these domains", type="array", items={"type": "string"}
),
},
("query",),
),
},
{
"name": "Workflow",
"description": "Execute a workflow script that orchestrates multiple subagents deterministically.",
"input_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"script": field("Self-contained workflow script.", type="string", maxLength=524288),
"name": field(
"Name of a predefined workflow (built-in or from .claude/workflows/).", type="string"
),
"description": field(
"Ignored — set the workflow description in the script's `meta` block.", type="string"
),
"title": field("Ignored — set the workflow title in the script's `meta` block.", type="string"),
"args": field("Optional input value exposed to the script as the global `args`, verbatim."),
"scriptPath": field("Path to a workflow script file on disk.", type="string"),
"resumeFromRunId": field(
"Run ID of a prior Workflow invocation to resume from.",
type="string",
pattern="^wf_[a-z0-9-]{6,}$",
),
},
"additionalProperties": False,
},
},
{
"name": "Write",
"description": "Writes a file to the local filesystem.",
"input_schema": schema(
{
"file_path": field(
"The absolute path to the file to write (must be absolute, not relative)", type="string"
),
"content": field("The content to write to the file", type="string"),
},
("file_path", "content"),
),
},
)
def system_blocks() -> tuple[dict[str, JsonValue], ...]:
return (
{"type": "text", "text": "x-anthropic-billing-header: cc_version=2.1.283.00; cc_entrypoint=sdk-cli;"},
{"type": "text", "text": "Synthetic agent identity system prompt.", "cache_control": CACHE},
{"type": "text", "text": "Synthetic interactive agent instructions.", "cache_control": CACHE},
)
def claude_code_request(cache_bust: str) -> dict[str, JsonValue]:
reminders: Final = (
f"<system-reminder>\n{cache_bust}\n</system-reminder>",
"<system-reminder>\nSynthetic model identity reminder.\n</system-reminder>",
"<system-reminder>\nSynthetic agent types reminder.\n</system-reminder>",
"<system-reminder>\nSynthetic skills reminder.\n</system-reminder>",
"<system-reminder>\n<total_tokens>15000000 tokens left</total_tokens>\n</system-reminder>",
"<system-reminder>\nSynthetic date reminder.\n</system-reminder>",
"<system-reminder>\nSynthetic attribution reminder.\n</system-reminder>",
)
return {
"model": "",
"system": list(system_blocks()),
"messages": [
{
"role": "user",
"content": [
*[{"type": "text", "text": reminder} for reminder in reminders],
{"type": "text", "text": "Reply with exactly the word PONG", "cache_control": CACHE},
],
}
],
"tools": list(tools()),
"metadata": {"user_id": METADATA_USER_ID},
"max_tokens": 32000,
"thinking": dict(THINKING_BUDGET),
"context_management": dict(CONTEXT_MANAGEMENT),
"stream": True,
}
def frontier_request(
cache_bust: str,
effort: str,
max_tokens: int,
prompt_text: str = "Reply with exactly the word PONG",
stream: bool = True,
) -> dict[str, JsonValue]:
return {
"model": "",
"system": list(system_blocks()),
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": f"<system-reminder>\n{cache_bust}\n</system-reminder>"},
{"type": "text", "text": prompt_text},
],
},
{
"role": "system",
"content": [
{
"type": "text",
"text": "# Environment\nSynthetic environment block.",
"cache_control": CACHE,
}
],
},
],
"tools": list(tools()),
"metadata": {"user_id": METADATA_USER_ID},
"max_tokens": max_tokens,
"thinking": dict(THINKING_ADAPTIVE),
"context_management": dict(CONTEXT_MANAGEMENT),
"output_config": {"effort": effort},
"stream": stream,
}
def tool_loop_turn2(
base: dict[str, JsonValue],
assistant_content: tuple[dict[str, JsonValue], ...],
tool_results: tuple[tuple[str, JsonValue], ...],
) -> dict[str, JsonValue]:
return {
**base,
"messages": [
*base["messages"],
{"role": "assistant", "content": list(assistant_content)},
{
"role": "user",
"content": [
{"tool_use_id": tool_use_id, "type": "tool_result", "content": content}
for tool_use_id, content in tool_results
],
},
{
"role": "system",
"content": [
{
"type": "text",
"text": "<total_tokens>14999970 tokens left</total_tokens>",
"cache_control": CACHE,
},
{
"type": "text",
"text": "First privately list what you need next; then request every item that doesn't depend on another's result in this one response.",
},
],
},
],
}
def cli_headers(key: str, beta: str = CLI_BETA) -> dict[str, str]:
return {
"accept": "application/json",
"content-type": "application/json",
"user-agent": "claude-cli/2.1.283 (external, sdk-cli)",
"x-claude-code-session-id": "00000000-0000-4000-8000-000000000000",
"x-stainless-arch": "x64",
"x-stainless-lang": "js",
"x-stainless-os": "Linux",
"x-stainless-package-version": "0.112.1",
"x-stainless-retry-count": "0",
"x-stainless-runtime": "node",
"x-stainless-runtime-version": "v26.3.0",
"x-stainless-timeout": "600",
"anthropic-beta": beta,
"anthropic-dangerous-direct-browser-access": "true",
"anthropic-version": "2023-06-01",
"x-app": "cli",
"x-api-key": key,
}
def sse_frame(event: str, data: JsonValue) -> bytes:
return f"event: {event}\ndata: {json.dumps(data)}\n\n".encode()
def sse_events(text: str) -> tuple[tuple[str, dict[str, object]], ...]:
frames: Final = tuple(frame for frame in text.split("\n\n") if frame.strip())
return tuple(
(
event,
json.loads(next(line.removeprefix("data: ") for line in frame.splitlines() if line.startswith("data: "))),
)
for frame in frames
if (event := next(line.removeprefix("event: ") for line in frame.splitlines() if line.startswith("event: ")))
!= "ping"
)
def _start_usage(usage: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
return {key: value for key, value in usage.items() if key != "output_tokens"}
def text_stream(identity: str, model: str, text: str, usage: dict[str, int]) -> tuple[bytes, ...]:
return (
sse_frame(
"message_start",
{
"type": "message_start",
"message": {
"id": identity,
"type": "message",
"role": "assistant",
"model": model,
"content": [],
"stop_reason": None,
"stop_sequence": None,
"usage": _start_usage(usage),
},
},
),
sse_frame(
"content_block_start",
{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}},
),
sse_frame(
"content_block_delta",
{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": text}},
),
sse_frame("content_block_stop", {"type": "content_block_stop", "index": 0}),
sse_frame(
"message_delta",
{
"type": "message_delta",
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
"usage": {"output_tokens": usage["output_tokens"]},
},
),
sse_frame("message_stop", {"type": "message_stop"}),
)
def _tool_use_frames(index: int, tool_id: str, name: str, tool_input: JsonValue) -> tuple[bytes, ...]:
arguments: Final = json.dumps(tool_input)
return (
sse_frame(
"content_block_start",
{
"type": "content_block_start",
"index": index,
"content_block": {"type": "tool_use", "id": tool_id, "name": name, "input": {}},
},
),
sse_frame(
"content_block_delta",
{
"type": "content_block_delta",
"index": index,
"delta": {"type": "input_json_delta", "partial_json": arguments[: len(arguments) // 2]},
},
),
sse_frame(
"content_block_delta",
{
"type": "content_block_delta",
"index": index,
"delta": {"type": "input_json_delta", "partial_json": arguments[len(arguments) // 2 :]},
},
),
sse_frame("content_block_stop", {"type": "content_block_stop", "index": index}),
)
def tool_use_stream(
identity: str,
model: str,
thinking: str,
signature: str,
tool_calls: tuple[tuple[str, str, JsonValue], ...],
usage: dict[str, int],
) -> tuple[bytes, ...]:
head: Final = (
sse_frame(
"message_start",
{
"type": "message_start",
"message": {
"id": identity,
"type": "message",
"role": "assistant",
"model": model,
"content": [],
"stop_reason": None,
"stop_sequence": None,
"usage": _start_usage(usage),
},
},
),
sse_frame(
"content_block_start",
{"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}},
),
sse_frame(
"content_block_delta",
{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": thinking}},
),
sse_frame(
"content_block_delta",
{"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": signature}},
),
sse_frame("content_block_stop", {"type": "content_block_stop", "index": 0}),
)
tail: Final = (
sse_frame(
"message_delta",
{
"type": "message_delta",
"delta": {"stop_reason": "tool_use", "stop_sequence": None},
"usage": {"output_tokens": usage["output_tokens"]},
},
),
sse_frame("message_stop", {"type": "message_stop"}),
)
frames: Final = (
*head,
*chain.from_iterable(
_tool_use_frames(index, tool_id, name, tool_input)
for index, (tool_id, name, tool_input) in enumerate(tool_calls, start=1)
),
*tail,
)
return frames

View file

@ -0,0 +1,111 @@
import uuid
from typing import Final
import pytest
from integration._support import claude_code as cc
from integration._support.client import Gateway, eventually
from integration._support.database import read_rows
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue
def _diff(expected: dict[str, JsonValue], body: dict[str, JsonValue]) -> dict[str, JsonValue]:
return {
key: {"expected": expected.get(key), "upstream": body.get(key)}
for key in expected.keys() | body.keys()
if expected.get(key) != body.get(key)
}
def test_adaptive_thinking_and_effort_reach_anthropic_intact(gateway: Gateway) -> None:
identity: Final = f"msg_fable_{uuid.uuid4().hex}"
request_body: Final = cc.frontier_request(f"cache-bust-{uuid.uuid4().hex}", "high", 64000)
cli_beta: Final = frozenset(cc.FRONTIER_CLI_BETA.split(","))
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/messages", request.target
assert request.headers["x-api-key"] == cc.ANTHROPIC_API_KEY
assert request.headers["anthropic-version"] == "2023-06-01"
upstream_beta: Final = request.headers.get("anthropic-beta", "")
assert cli_beta <= frozenset(upstream_beta.split(",")), upstream_beta
assert upstream_beta.split(",").count("effort-2025-11-24") == 1, upstream_beta
body: Final = cc.JSON_OBJECT.validate_json(request.body)
expected: Final = {**request_body, "model": cc.FABLE}
assert body == expected, _diff(expected, body)
return Reply(
content_type="text/event-stream",
chunks=cc.text_stream(identity, cc.FABLE, "PONG", {"input_tokens": 12, "output_tokens": 4}),
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(model=f"anthropic/{cc.FABLE}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY)
response: Final = gateway.request(
"POST",
"/v1/messages",
{**request_body, "model": model},
params={"beta": "true"},
headers=cc.cli_headers(gateway.key, cc.FRONTIER_CLI_BETA),
)
assert response.status_code == 200, response.text
events: Final = cc.sse_events(response.text)
assert [event for event, _ in events][-1] == "message_stop"
assert events[4][1]["delta"]["stop_reason"] == "end_turn"
assert len(wire.drain()) == 1
rows: Final = eventually(
lambda: read_rows(
'SELECT prompt_tokens, completion_tokens FROM "LiteLLM_SpendLogs" WHERE request_id=%s',
(identity,),
),
lambda values: len(values) == 1,
seconds=70,
)
assert rows[0]["prompt_tokens"] == 12 and rows[0]["completion_tokens"] == 4
def test_xhigh_effort_reaches_anthropic_and_charges_by_usage(gateway: Gateway) -> None:
identity: Final = f"msg_opus_{uuid.uuid4().hex}"
request_body: Final = cc.frontier_request(f"cache-bust-{uuid.uuid4().hex}", "xhigh", 128000)
cli_beta: Final = frozenset(cc.FRONTIER_CLI_BETA.split(","))
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/messages", request.target
assert request.headers["x-api-key"] == cc.ANTHROPIC_API_KEY
upstream_beta: Final = request.headers.get("anthropic-beta", "")
assert cli_beta <= frozenset(upstream_beta.split(",")), upstream_beta
assert upstream_beta.split(",").count("effort-2025-11-24") == 1, upstream_beta
body: Final = cc.JSON_OBJECT.validate_json(request.body)
expected: Final = {**request_body, "model": cc.OPUS}
assert body == expected, _diff(expected, body)
return Reply(
content_type="text/event-stream",
chunks=cc.text_stream(identity, cc.OPUS, "PONG", {"input_tokens": 10, "output_tokens": 5}),
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model=f"anthropic/{cc.OPUS}",
api_base=wire.url,
api_key=cc.ANTHROPIC_API_KEY,
input_cost_per_token=1e-6,
output_cost_per_token=5e-6,
)
response: Final = gateway.request(
"POST",
"/v1/messages",
{**request_body, "model": model},
params={"beta": "true"},
headers=cc.cli_headers(gateway.key, cc.FRONTIER_CLI_BETA),
)
assert response.status_code == 200, response.text
assert len(wire.drain()) == 1
rows: Final = eventually(
lambda: read_rows(
'SELECT spend, prompt_tokens, completion_tokens FROM "LiteLLM_SpendLogs" WHERE request_id=%s',
(identity,),
),
lambda values: len(values) == 1,
seconds=70,
)
assert float(rows[0]["spend"]) == pytest.approx(10 * 1e-6 + 5 * 5e-6)

View file

@ -0,0 +1,167 @@
import uuid
from typing import Final
from integration._support import claude_code as cc
from integration._support.client import Gateway
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue
_TURN1_TOOL: Final = ("toolu_a", "Read", {"file_path": "/tmp/cc_probe/a.txt"})
_TURN2_TOOL: Final = ("toolu_b", "Read", {"file_path": "/tmp/cc_probe/b.txt"})
def _diff(expected: dict[str, JsonValue], body: dict[str, JsonValue]) -> dict[str, JsonValue]:
return {
key: {"expected": expected.get(key), "upstream": body.get(key)}
for key in expected.keys() | body.keys()
if expected.get(key) != body.get(key)
}
def _turn2(base: dict[str, JsonValue]) -> dict[str, JsonValue]:
return cc.tool_loop_turn2(
base,
(
{"type": "thinking", "thinking": "plan", "signature": "sig1"},
{"type": "tool_use", "id": _TURN1_TOOL[0], "name": _TURN1_TOOL[1], "input": _TURN1_TOOL[2]},
),
((_TURN1_TOOL[0], "ALPHA"),),
)
def _turn3(turn2: dict[str, JsonValue]) -> dict[str, JsonValue]:
return cc.tool_loop_turn2(
turn2,
(
{"type": "thinking", "thinking": "got A", "signature": "sig2"},
{"type": "text", "text": "got A"},
{"type": "tool_use", "id": _TURN2_TOOL[0], "name": _TURN2_TOOL[1], "input": _TURN2_TOOL[2]},
),
((_TURN2_TOOL[0], "BRAVO"),),
)
def _interleaved_stream(identity: str) -> tuple[bytes, ...]:
usage: Final = {"input_tokens": 20, "output_tokens": 12}
return (
cc.sse_frame(
"message_start",
{
"type": "message_start",
"message": {
"id": identity,
"type": "message",
"role": "assistant",
"model": cc.FABLE,
"content": [],
"stop_reason": None,
"stop_sequence": None,
"usage": {"input_tokens": usage["input_tokens"], "output_tokens": 1},
},
},
),
cc.sse_frame(
"content_block_start",
{"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}},
),
cc.sse_frame(
"content_block_delta",
{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "got A"}},
),
cc.sse_frame(
"content_block_delta",
{"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "sig2"}},
),
cc.sse_frame("content_block_stop", {"type": "content_block_stop", "index": 0}),
cc.sse_frame(
"content_block_start",
{"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}},
),
cc.sse_frame(
"content_block_delta",
{"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "got A"}},
),
cc.sse_frame("content_block_stop", {"type": "content_block_stop", "index": 1}),
cc.sse_frame(
"content_block_start",
{
"type": "content_block_start",
"index": 2,
"content_block": {"type": "tool_use", "id": _TURN2_TOOL[0], "name": _TURN2_TOOL[1], "input": {}},
},
),
cc.sse_frame(
"content_block_delta",
{
"type": "content_block_delta",
"index": 2,
"delta": {"type": "input_json_delta", "partial_json": '{"file_path": "/tmp/cc_pr'},
},
),
cc.sse_frame(
"content_block_delta",
{
"type": "content_block_delta",
"index": 2,
"delta": {"type": "input_json_delta", "partial_json": 'obe/b.txt"}'},
},
),
cc.sse_frame("content_block_stop", {"type": "content_block_stop", "index": 2}),
cc.sse_frame(
"message_delta",
{
"type": "message_delta",
"delta": {"stop_reason": "tool_use", "stop_sequence": None},
"usage": {"output_tokens": usage["output_tokens"]},
},
),
cc.sse_frame("message_stop", {"type": "message_stop"}),
)
def test_interleaved_thinking_text_and_tool_use_history_reaches_anthropic_identical(gateway: Gateway) -> None:
identity: Final = f"msg_il_{uuid.uuid4().hex}"
turn1: Final = cc.frontier_request(
f"cache-bust-{uuid.uuid4().hex}",
"high",
64000,
prompt_text="Read /tmp/cc_probe/a.txt then /tmp/cc_probe/b.txt one at a time and reply with both words",
)
turn2: Final = _turn2(turn1)
turn3: Final = _turn3(turn2)
turn2_expected: Final = {**turn2, "model": cc.FABLE}
turn3_expected: Final = {**turn3, "model": cc.FABLE}
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/messages", request.target
upstream_beta: Final = request.headers.get("anthropic-beta", "")
assert upstream_beta.split(",").count("interleaved-thinking-2025-05-14") == 1, upstream_beta
body: Final = cc.JSON_OBJECT.validate_json(request.body)
if body == turn2_expected:
return Reply(
content_type="text/event-stream",
chunks=cc.text_stream("msg_il_turn2", cc.FABLE, "got A", {"input_tokens": 20, "output_tokens": 4}),
)
assert body == turn3_expected, _diff(turn3_expected, body)
return Reply(content_type="text/event-stream", chunks=_interleaved_stream(identity))
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(model=f"anthropic/{cc.FABLE}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY)
headers: Final = cc.cli_headers(gateway.key, cc.FRONTIER_CLI_BETA)
response2: Final = gateway.request(
"POST", "/v1/messages", {**turn2, "model": model}, params={"beta": "true"}, headers=headers
)
assert response2.status_code == 200, response2.text
response3: Final = gateway.request(
"POST", "/v1/messages", {**turn3, "model": model}, params={"beta": "true"}, headers=headers
)
assert response3.status_code == 200, response3.text
events: Final = cc.sse_events(response3.text)
started: Final = [
(data["index"], data["content_block"]["type"]) for event, data in events if event == "content_block_start"
]
assert started == [(0, "thinking"), (1, "text"), (2, "tool_use")], started
assert events[-1][0] == "message_stop"
bodies: Final = tuple(cc.JSON_OBJECT.validate_json(request.body) for request in wire.drain())
assert bodies == (turn2_expected, turn3_expected), bodies

View file

@ -0,0 +1,83 @@
import uuid
from typing import Final
from integration._support import claude_code as cc
from integration._support.client import Gateway, eventually
from integration._support.database import read_rows
from integration._support.wire import Reply, Request, wire_server
def test_mid_loop_model_switch_replays_history_byte_identical(gateway: Gateway) -> None:
identity1: Final = f"msg_sw1_{uuid.uuid4().hex}"
identity2: Final = f"msg_sw2_{uuid.uuid4().hex}"
turn1: Final = cc.frontier_request(
f"cache-bust-{uuid.uuid4().hex}",
"high",
64000,
prompt_text="Read /tmp/cc_probe/hello.txt and reply with its single word",
)
turn2: Final = cc.tool_loop_turn2(
turn1,
(
{"type": "thinking", "thinking": "need to read the file", "signature": "sig_anthropic_1"},
{
"type": "tool_use",
"id": "toolu_read_1",
"name": "Read",
"input": {"file_path": "/tmp/cc_probe/hello.txt"},
},
),
(("toolu_read_1", "1\tPROBE\n2\t"),),
)
first_expected: Final = {**turn1, "model": cc.FABLE}
second_expected: Final = {**turn2, "model": cc.OPUS}
def respond(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/messages", request.target
body: Final = cc.JSON_OBJECT.validate_json(request.body)
if body == first_expected:
return Reply(
content_type="text/event-stream",
chunks=cc.tool_use_stream(
identity1,
cc.FABLE,
"need to read the file",
"sig_anthropic_1",
(("toolu_read_1", "Read", {"file_path": "/tmp/cc_probe/hello.txt"}),),
{"input_tokens": 20, "output_tokens": 10},
),
)
assert body == second_expected, {
key: (second_expected.get(key), body.get(key))
for key in second_expected.keys() | body.keys()
if second_expected.get(key) != body.get(key)
}
return Reply(
content_type="text/event-stream",
chunks=cc.text_stream(identity2, cc.OPUS, "PROBE", {"input_tokens": 30, "output_tokens": 3}),
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
fable: Final = scenario.model(model=f"anthropic/{cc.FABLE}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY)
opus: Final = scenario.model(model=f"anthropic/{cc.OPUS}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY)
headers: Final = cc.cli_headers(gateway.key, cc.FRONTIER_CLI_BETA)
response1: Final = gateway.request(
"POST", "/v1/messages", {**turn1, "model": fable}, params={"beta": "true"}, headers=headers
)
assert response1.status_code == 200, response1.text
response2: Final = gateway.request(
"POST", "/v1/messages", {**turn2, "model": opus}, params={"beta": "true"}, headers=headers
)
assert response2.status_code == 200, response2.text
bodies: Final = tuple(cc.JSON_OBJECT.validate_json(request.body) for request in wire.drain())
assert bodies == (first_expected, second_expected), bodies
rows: Final = eventually(
lambda: read_rows(
'SELECT model FROM "LiteLLM_SpendLogs" WHERE request_id=%s',
(identity2,),
),
lambda values: len(values) == 1,
seconds=70,
)
assert rows[0]["model"] == f"anthropic/{cc.OPUS}"

View file

@ -0,0 +1,366 @@
import json
import uuid
from collections.abc import Mapping
from typing import Final
import pytest
from integration._support import claude_code as cc
from integration._support.client import Gateway
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue
_HAIKU_4_5: Final = "claude-haiku-4-5"
_OPUS_4_5: Final = "claude-opus-4-5"
_OPUS_4_6: Final = "claude-opus-4-6"
_OPUS_4_7: Final = "claude-opus-4-7"
_FABLE_5_1: Final = "claude-fable-5-1"
_ADAPTIVE: Final = {"type": "adaptive", "display": "omitted"}
_ADAPTIVE_SUMMARIZED: Final = {"type": "adaptive", "display": "summarized"}
def _budget(tokens: int) -> dict[str, JsonValue]:
return {"type": "enabled", "budget_tokens": tokens}
def _client_body(**reasoning: JsonValue) -> dict[str, JsonValue]:
base: Final = {
key: value
for key, value in cc.claude_code_request(f"cache-bust-{uuid.uuid4().hex}").items()
if key != "thinking"
}
return {**base, "stream": False, **reasoning}
def _without(body: Mapping[str, JsonValue], *keys: str) -> dict[str, JsonValue]:
return {key: value for key, value in body.items() if key not in keys}
def _diff(expected: Mapping[str, JsonValue], body: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
return {
key: {"expected": expected.get(key), "upstream": body.get(key)}
for key in expected.keys() | body.keys()
if expected.get(key) != body.get(key)
}
def _forwarded_body(
gateway: Gateway, upstream_model: str, client_body: Mapping[str, JsonValue]
) -> dict[str, JsonValue]:
def respond(request: Request) -> Reply:
return Reply(
body=json.dumps(
{
"id": f"msg_{uuid.uuid4().hex}",
"type": "message",
"role": "assistant",
"model": upstream_model,
"content": [{"type": "text", "text": "PONG"}],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 12, "output_tokens": 4},
}
).encode()
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model=f"anthropic/{upstream_model}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY
)
response: Final = gateway.request("POST", "/v1/messages", {**client_body, "model": model})
assert response.status_code == 200, response.text
received: Final = wire.drain()
assert len(received) == 1, received
return cc.JSON_OBJECT.validate_json(received[0].body)
def _assert_forwarded(
gateway: Gateway,
upstream_model: str,
client_body: dict[str, JsonValue],
expected_changes: Mapping[str, JsonValue],
removed: tuple[str, ...],
) -> None:
expected: Final = {**_without(client_body, *removed), **expected_changes, "model": upstream_model}
body: Final = _forwarded_body(gateway, upstream_model, client_body)
assert body == expected, _diff(expected, body)
@pytest.mark.parametrize(
("upstream_model", "effort", "expected_changes", "removed"),
(
pytest.param(
_OPUS_4_5,
"high",
{},
("thinking",),
id="opus-4.5-keeps-supported-effort-drops-adaptive",
),
pytest.param(
_OPUS_4_5,
"xhigh",
{"thinking": _budget(8192)},
("output_config",),
id="opus-4.5-xhigh-falls-back-to-budget",
),
pytest.param(_HAIKU_4_5, "low", {"thinking": _budget(1024)}, ("output_config",), id="haiku-4.5-low"),
pytest.param(_HAIKU_4_5, "medium", {"thinking": _budget(2048)}, ("output_config",), id="haiku-4.5-medium"),
pytest.param(_HAIKU_4_5, "high", {"thinking": _budget(4096)}, ("output_config",), id="haiku-4.5-high"),
pytest.param(_HAIKU_4_5, "xhigh", {"thinking": _budget(8192)}, ("output_config",), id="haiku-4.5-xhigh"),
pytest.param(_HAIKU_4_5, "max", {"thinking": _budget(16384)}, ("output_config",), id="haiku-4.5-max"),
pytest.param(_OPUS_4_6, "high", {}, (), id="opus-4.6-adaptive-unchanged"),
pytest.param(_OPUS_4_7, "xhigh", {}, (), id="opus-4.7-adaptive-unchanged"),
),
)
def test_adaptive_thinking_and_effort_are_reshaped_only_for_models_without_adaptive_thinking(
gateway: Gateway,
upstream_model: str,
effort: str,
expected_changes: dict[str, JsonValue],
removed: tuple[str, ...],
) -> None:
client_body: Final = _client_body(thinking=dict(_ADAPTIVE), output_config={"effort": effort})
_assert_forwarded(gateway, upstream_model, client_body, expected_changes, removed)
def test_adaptive_effort_fallback_budget_is_capped_below_max_tokens(gateway: Gateway) -> None:
client_body: Final = _client_body(thinking=dict(_ADAPTIVE), output_config={"effort": "max"}, max_tokens=4000)
_assert_forwarded(gateway, _HAIKU_4_5, client_body, {"thinking": _budget(3999)}, ("output_config",))
def test_adaptive_effort_fallback_drops_thinking_when_max_tokens_cannot_fit_the_minimum_budget(
gateway: Gateway,
) -> None:
client_body: Final = _client_body(thinking=dict(_ADAPTIVE), output_config={"effort": "high"}, max_tokens=1024)
_assert_forwarded(gateway, _HAIKU_4_5, client_body, {}, ("thinking", "output_config"))
@pytest.mark.parametrize(
("budget_tokens", "effort"),
(
pytest.param(1024, "low", id="below-medium-threshold"),
pytest.param(2048, "medium", id="medium-threshold"),
pytest.param(4096, "high", id="high-threshold"),
pytest.param(8192, "xhigh", id="xhigh-threshold"),
),
)
def test_legacy_thinking_budget_becomes_adaptive_effort_on_models_that_reject_budgets(
gateway: Gateway, budget_tokens: int, effort: str
) -> None:
client_body: Final = _client_body(thinking=_budget(budget_tokens))
_assert_forwarded(
gateway,
_OPUS_4_7,
client_body,
{"thinking": {"type": "adaptive"}, "output_config": {"effort": effort}},
(),
)
def test_legacy_thinking_translation_keeps_the_callers_effort(gateway: Gateway) -> None:
client_body: Final = _client_body(thinking=_budget(8192), output_config={"effort": "medium"})
_assert_forwarded(gateway, _OPUS_4_7, client_body, {"thinking": {"type": "adaptive"}}, ())
@pytest.mark.parametrize(
("upstream_model", "removed"),
(
pytest.param(_FABLE_5_1, ("thinking",), id="always-on-model-drops-disabled"),
pytest.param(_OPUS_4_7, (), id="other-model-keeps-disabled"),
),
)
def test_disabled_thinking_is_dropped_only_for_always_on_thinking_models(
gateway: Gateway, upstream_model: str, removed: tuple[str, ...]
) -> None:
client_body: Final = _client_body(thinking={"type": "disabled"})
_assert_forwarded(gateway, upstream_model, client_body, {}, removed)
@pytest.mark.parametrize(
("reasoning_effort", "effort"),
(
pytest.param("minimal", "low", id="minimal"),
pytest.param("low", "low", id="low"),
pytest.param("medium", "medium", id="medium"),
pytest.param("high", "high", id="high"),
pytest.param("xhigh", "xhigh", id="xhigh"),
pytest.param("max", "max", id="max"),
),
)
def test_reasoning_effort_becomes_adaptive_thinking_and_effort_on_adaptive_models(
gateway: Gateway, reasoning_effort: str, effort: str
) -> None:
client_body: Final = _client_body(reasoning_effort=reasoning_effort)
_assert_forwarded(
gateway,
_OPUS_4_7,
client_body,
{"thinking": dict(_ADAPTIVE_SUMMARIZED), "output_config": {"effort": effort}},
("reasoning_effort",),
)
@pytest.mark.parametrize(
("reasoning_effort", "budget_tokens"),
(
pytest.param("minimal", 1024, id="minimal"),
pytest.param("low", 1024, id="low"),
pytest.param("medium", 2048, id="medium"),
pytest.param("high", 4096, id="high"),
pytest.param("xhigh", 8192, id="xhigh"),
pytest.param("max", 16384, id="max"),
),
)
def test_reasoning_effort_becomes_a_thinking_budget_on_models_without_adaptive_thinking(
gateway: Gateway, reasoning_effort: str, budget_tokens: int
) -> None:
client_body: Final = _client_body(reasoning_effort=reasoning_effort)
_assert_forwarded(gateway, _HAIKU_4_5, client_body, {"thinking": _budget(budget_tokens)}, ("reasoning_effort",))
def test_reasoning_effort_none_clears_thinking_and_effort(gateway: Gateway) -> None:
client_body: Final = _client_body(
reasoning_effort="none", thinking=dict(_ADAPTIVE), output_config={"effort": "high"}
)
_assert_forwarded(gateway, _OPUS_4_7, client_body, {}, ("reasoning_effort", "thinking", "output_config"))
def test_caller_thinking_wins_over_reasoning_effort(gateway: Gateway) -> None:
client_body: Final = _client_body(reasoning_effort="high", thinking=_budget(2000))
_assert_forwarded(gateway, _HAIKU_4_5, client_body, {}, ("reasoning_effort",))
def test_caller_effort_wins_over_reasoning_effort(gateway: Gateway) -> None:
client_body: Final = _client_body(reasoning_effort="high", output_config={"effort": "low"})
_assert_forwarded(gateway, _OPUS_4_7, client_body, {"thinking": dict(_ADAPTIVE_SUMMARIZED)}, ("reasoning_effort",))
def test_reasoning_effort_budget_is_capped_below_max_tokens(gateway: Gateway) -> None:
client_body: Final = _client_body(reasoning_effort="max", max_tokens=4000)
_assert_forwarded(gateway, _HAIKU_4_5, client_body, {"thinking": _budget(3999)}, ("reasoning_effort",))
def test_reasoning_effort_is_dropped_when_max_tokens_cannot_fit_the_minimum_budget(gateway: Gateway) -> None:
client_body: Final = _client_body(reasoning_effort="high", max_tokens=1024)
_assert_forwarded(gateway, _HAIKU_4_5, client_body, {}, ("reasoning_effort",))
@pytest.mark.parametrize(
("upstream_model", "reasoning", "expected_changes", "removed"),
(
pytest.param(
_HAIKU_4_5,
{"thinking": dict(_ADAPTIVE), "output_config": {"effort": "high"}},
{"thinking": _budget(4096)},
("output_config", "temperature"),
id="haiku-4.5-effort-translated-to-budget",
),
pytest.param(
_OPUS_4_5,
{"thinking": dict(_ADAPTIVE), "output_config": {"effort": "high"}},
{},
("thinking", "temperature"),
id="opus-4.5-effort-kept",
),
pytest.param(_HAIKU_4_5, {"thinking": _budget(2048)}, {}, ("temperature",), id="haiku-4.5-legacy-budget"),
),
)
def test_non_default_temperature_is_dropped_when_a_non_adaptive_model_thinks(
gateway: Gateway,
upstream_model: str,
reasoning: dict[str, JsonValue],
expected_changes: dict[str, JsonValue],
removed: tuple[str, ...],
) -> None:
client_body: Final = _client_body(temperature=0, **reasoning)
_assert_forwarded(gateway, upstream_model, client_body, expected_changes, removed)
@pytest.mark.parametrize(
("upstream_model", "temperature", "reasoning"),
(
pytest.param(_HAIKU_4_5, 1, {"thinking": _budget(2048)}, id="temperature-1-with-thinking"),
pytest.param(_HAIKU_4_5, 0, {}, id="temperature-0-without-thinking"),
pytest.param(
_OPUS_4_6,
0,
{"thinking": dict(_ADAPTIVE), "output_config": {"effort": "high"}},
id="adaptive-model",
),
),
)
def test_temperature_is_kept_when_it_does_not_conflict_with_thinking(
gateway: Gateway, upstream_model: str, temperature: int, reasoning: dict[str, JsonValue]
) -> None:
client_body: Final = _client_body(temperature=temperature, **reasoning)
_assert_forwarded(gateway, upstream_model, client_body, {}, ())
_SIGNED_THINKING: Final = {"type": "thinking", "thinking": "check the config first", "signature": "EqQBCkgIBRABGAIiQL"}
_TOOL_CALL: Final = {"type": "tool_use", "id": "toolu_01", "name": "Read", "input": {"file_path": "/repo/config.yaml"}}
_TOOL_RESULT: Final = {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_01", "content": "ok"}]}
def _history_body(assistant_content: tuple[dict[str, JsonValue], ...]) -> dict[str, JsonValue]:
base: Final = _client_body(thinking=_budget(2048))
first_turn: Final = base["messages"]
assert isinstance(first_turn, list)
return {**base, "messages": [*first_turn, {"role": "assistant", "content": list(assistant_content)}, _TOOL_RESULT]}
def _with_assistant_content(
body: Mapping[str, JsonValue], assistant_content: tuple[dict[str, JsonValue], ...]
) -> dict[str, JsonValue]:
messages: Final = body["messages"]
assert isinstance(messages, list)
return {
**body,
"messages": [*messages[:-2], {"role": "assistant", "content": list(assistant_content)}, messages[-1]],
}
def test_encrypted_reasoning_from_another_provider_is_stripped_and_anthropic_signed_thinking_is_kept(
gateway: Gateway,
) -> None:
client_body: Final = _history_body(
(
{"type": "thinking", "thinking": "bridge reasoning", "signature": "litellm_encrypted_reasoning:gAAAAB"},
{"type": "redacted_thinking", "data": "litellm_encrypted_reasoning:gAAAAC"},
_SIGNED_THINKING,
_TOOL_CALL,
)
)
expected: Final = {**_with_assistant_content(client_body, (_SIGNED_THINKING, _TOOL_CALL)), "model": _HAIKU_4_5}
body: Final = _forwarded_body(gateway, _HAIKU_4_5, client_body)
assert body == expected, _diff(expected, body)
def test_empty_thinking_block_is_stripped_and_redacted_thinking_is_kept(gateway: Gateway) -> None:
redacted: Final = {"type": "redacted_thinking", "data": "EmwKAhgBEgy3va3pzix"}
client_body: Final = _history_body(
({"type": "thinking", "thinking": "", "signature": "EqQBCkgIBRABGAIiQM"}, redacted, _TOOL_CALL)
)
expected: Final = {**_with_assistant_content(client_body, (redacted, _TOOL_CALL)), "model": _HAIKU_4_5}
body: Final = _forwarded_body(gateway, _HAIKU_4_5, client_body)
assert body == expected, _diff(expected, body)
@pytest.mark.parametrize(
("upstream_model", "reasoning_effort"),
(
pytest.param(_HAIKU_4_5, "turbo", id="unknown-value"),
pytest.param(_OPUS_4_6, "xhigh", id="level-the-model-lacks"),
),
)
def test_unsupported_reasoning_effort_is_rejected_before_reaching_anthropic(
gateway: Gateway, upstream_model: str, reasoning_effort: str
) -> None:
with wire_server(lambda request: Reply()) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model=f"anthropic/{upstream_model}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY
)
response: Final = gateway.request(
"POST", "/v1/messages", {**_client_body(reasoning_effort=reasoning_effort), "model": model}
)
assert response.status_code == 400, response.text
assert response.json()["error"]["type"] == "invalid_request_error", response.text
assert wire.drain() == ()

View file

@ -0,0 +1,137 @@
import json
import uuid
from typing import Final
from integration._support import claude_code as cc
from integration._support.client import Gateway
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue
_MODEL: Final = "claude-haiku-4-5"
_THINKING: Final = "the user wants a single word"
_SIGNATURE: Final = "EqQBCkgIBRABGAIiQLz"
_REDACTED: Final = "EmwKAhgBEgy3va3pzixlit"
def _reasoning_stream(identity: str) -> tuple[bytes, ...]:
return (
cc.sse_frame(
"message_start",
{
"type": "message_start",
"message": {
"id": identity,
"type": "message",
"role": "assistant",
"model": _MODEL,
"content": [],
"stop_reason": None,
"stop_sequence": None,
"usage": {"input_tokens": 12, "output_tokens": 1},
},
},
),
cc.sse_frame(
"content_block_start",
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "thinking", "thinking": "", "signature": ""},
},
),
cc.sse_frame(
"content_block_delta",
{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": _THINKING}},
),
cc.sse_frame(
"content_block_delta",
{"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": _SIGNATURE}},
),
cc.sse_frame("content_block_stop", {"type": "content_block_stop", "index": 0}),
cc.sse_frame(
"content_block_start",
{
"type": "content_block_start",
"index": 1,
"content_block": {"type": "redacted_thinking", "data": _REDACTED},
},
),
cc.sse_frame("content_block_stop", {"type": "content_block_stop", "index": 1}),
cc.sse_frame(
"content_block_start",
{"type": "content_block_start", "index": 2, "content_block": {"type": "text", "text": ""}},
),
cc.sse_frame(
"content_block_delta",
{"type": "content_block_delta", "index": 2, "delta": {"type": "text_delta", "text": "PONG"}},
),
cc.sse_frame("content_block_stop", {"type": "content_block_stop", "index": 2}),
cc.sse_frame(
"message_delta",
{
"type": "message_delta",
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
"usage": {"output_tokens": 30},
},
),
cc.sse_frame("message_stop", {"type": "message_stop"}),
)
def _reasoning_message(identity: str) -> dict[str, JsonValue]:
return {
"id": identity,
"type": "message",
"role": "assistant",
"model": _MODEL,
"content": [
{"type": "thinking", "thinking": _THINKING, "signature": _SIGNATURE},
{"type": "redacted_thinking", "data": _REDACTED},
{"type": "text", "text": "PONG"},
],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 12, "output_tokens": 30},
}
def _content_events(stream: str) -> tuple[tuple[str, dict[str, object]], ...]:
return tuple(event for event in cc.sse_events(stream) if event[0].startswith("content_block_"))
def _client_body(stream: bool) -> dict[str, JsonValue]:
return {
**cc.claude_code_request(f"cache-bust-{uuid.uuid4().hex}"),
"thinking": {"type": "enabled", "budget_tokens": 2048},
"stream": stream,
}
def test_streamed_thinking_signature_and_redacted_thinking_reach_the_client_unchanged(gateway: Gateway) -> None:
identity: Final = f"msg_{uuid.uuid4().hex}"
upstream_frames: Final = _reasoning_stream(identity)
def respond(request: Request) -> Reply:
return Reply(chunks=upstream_frames, content_type="text/event-stream")
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(model=f"anthropic/{_MODEL}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY)
response: Final = gateway.request("POST", "/v1/messages", {**_client_body(stream=True), "model": model})
assert response.status_code == 200, response.text
assert len(wire.drain()) == 1
assert _content_events(response.text) == _content_events(b"".join(upstream_frames).decode()), response.text
def test_non_streamed_thinking_signature_and_redacted_thinking_reach_the_client_unchanged(gateway: Gateway) -> None:
identity: Final = f"msg_{uuid.uuid4().hex}"
upstream_message: Final = _reasoning_message(identity)
def respond(request: Request) -> Reply:
return Reply(body=json.dumps(upstream_message).encode())
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(model=f"anthropic/{_MODEL}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY)
response: Final = gateway.request("POST", "/v1/messages", {**_client_body(stream=False), "model": model})
assert response.status_code == 200, response.text
assert len(wire.drain()) == 1
assert response.json()["content"] == upstream_message["content"], response.text

View file

@ -0,0 +1,71 @@
import json
import uuid
from typing import Final
import pytest
from integration._support import claude_code as cc
from integration._support.client import Gateway, eventually
from integration._support.database import read_rows
from integration._support.wire import Reply, Request, wire_server
_MODEL: Final = "claude-haiku-4-5"
_INPUT_RATE: Final = 1e-6
_OUTPUT_RATE: Final = 2e-6
_REASONING_RATE: Final = 7e-6
def test_reported_thinking_tokens_are_billed_at_the_reasoning_rate_and_the_rest_at_the_output_rate(
gateway: Gateway,
) -> None:
identity: Final = f"msg_{uuid.uuid4().hex}"
def respond(request: Request) -> Reply:
return Reply(
body=json.dumps(
{
"id": identity,
"type": "message",
"role": "assistant",
"model": _MODEL,
"content": [
{"type": "thinking", "thinking": "count the words", "signature": "EqQBCkgIBRABGAIiQLz"},
{"type": "text", "text": "PONG"},
],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {
"input_tokens": 100,
"output_tokens": 50,
"output_tokens_details": {"thinking_tokens": 30},
},
}
).encode()
)
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model=f"anthropic/{_MODEL}",
api_base=wire.url,
api_key=cc.ANTHROPIC_API_KEY,
input_cost_per_token=_INPUT_RATE,
output_cost_per_token=_OUTPUT_RATE,
output_cost_per_reasoning_token=_REASONING_RATE,
)
response: Final = gateway.request(
"POST",
"/v1/messages",
{
**cc.claude_code_request(f"cache-bust-{uuid.uuid4().hex}"),
"thinking": {"type": "enabled", "budget_tokens": 2048},
"stream": False,
"model": model,
},
)
assert response.status_code == 200, response.text
assert len(wire.drain()) == 1
rows: Final = eventually(
lambda: read_rows('SELECT spend FROM "LiteLLM_SpendLogs" WHERE request_id=%s', (identity,)),
lambda values: len(values) == 1,
seconds=70,
)
assert float(rows[0]["spend"]) == pytest.approx(100 * _INPUT_RATE + 20 * _OUTPUT_RATE + 30 * _REASONING_RATE)