test(integration): cover claude-fable-5-2 capability flags and prompt-cache pricing on Vertex AI and Bedrock Converse (#50742)

* test(integration): cover claude-fable-5-2 on Vertex AI and Bedrock Converse

Scripted-upstream wire tests for the Fable capability flags on both routes, the
vertex_ai and us.anthropic cost cases every other Fable row has, Fable 5.2 in
BEDROCK_CONVERSE_MODELS, and bedrock/claude-fable-latest repointed to 5.2 like
the Anthropic and Vertex aliases already are.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(cost-map): add GovCloud Bedrock rows for claude-fable-5-2

bedrock/us-gov-west-1 and bedrock/us-gov-east-1 rows copy the Fable 5.1 field
set with the GovCloud rates from the Bedrock price list.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Kerry Lu 2026-11-09 15:26:40 -08:00 • committed by GitHub
parent 5d6f32a6f6
commit df20f90d12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1250 additions and 141 deletions

View file

@ -1533,6 +1533,7 @@ BEDROCK_CONVERSE_MODELS: Final = [
"openai.gpt-oss-120b-1:0",
"anthropic.claude-haiku-4-5-20251001-v1:0",
"anthropic.claude-sonnet-4-5-20250929-v1:0",
"anthropic.claude-fable-5-2",
"anthropic.claude-fable-5-1",
"anthropic.claude-fable-5",
"anthropic.claude-sonnet-5",

View file

@ -1970,7 +1970,7 @@
"source": "https://aws.amazon.com/bedrock/pricing/"
},
"bedrock/claude-fable-latest": {
"alias_of": "global.anthropic.claude-fable-5-1"
"alias_of": "global.anthropic.claude-fable-5-2"
},
"us.anthropic.claude-fable-5": {
"cache_creation_input_token_cost": 1.375e-05,
@ -69958,6 +69958,44 @@
"region": "us-gov-west-1"
}
},
"bedrock/us-gov-west-1/anthropic.claude-fable-5-2": {
"cache_creation_input_token_cost": 1.2e-05,
"cache_creation_input_token_cost_above_1hr": 1.92e-05,
"cache_read_input_token_cost": 2.4e-07,
"input_cost_per_token": 9.6e-06,
"litellm_provider": "bedrock",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 4.8e-05,
"supports_adaptive_thinking": true,
"thinking_always_on": true,
"supports_mid_conversation_system": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_forced_tool_use": false,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_bedrock_native_structured_output": false,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh",
"supports_parallel_tool_use_config": true,
"prompt_cache_min_tokens": 512,
"token_count_model": {
"model": "anthropic.claude-fable-5-2",
"litellm_provider": "bedrock_mantle",
"region": "us-gov-west-1"
}
},
"bedrock/us-gov-east-1/nvidia.nemotron-nano-3-30b": {
"input_cost_per_token": 7.2e-08,
"input_cost_per_token_batches": 3.6e-08,
@ -70266,6 +70304,39 @@
"supports_parallel_tool_use_config": true,
"prompt_cache_min_tokens": 512
},
"bedrock/us-gov-east-1/anthropic.claude-fable-5-2": {
"cache_creation_input_token_cost": 1.2e-05,
"cache_creation_input_token_cost_above_1hr": 1.92e-05,
"cache_read_input_token_cost": 2.4e-07,
"input_cost_per_token": 9.6e-06,
"litellm_provider": "bedrock",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 4.8e-05,
"supports_adaptive_thinking": true,
"thinking_always_on": true,
"supports_mid_conversation_system": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_forced_tool_use": false,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_bedrock_native_structured_output": false,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh",
"supports_parallel_tool_use_config": true,
"prompt_cache_min_tokens": 512
},
"bedrock_mantle/us-gov-west-1/openai.gpt-5.6-terra": {
"litellm_provider": "bedrock_mantle",
"max_input_tokens": 922000,
@ -87903,74 +87974,6 @@
"supports_xhigh_reasoning_effort": true,
"thinking_always_on": true
},
"bedrock/us-gov-east-1/anthropic.claude-fable-5-2": {
"bedrock_output_config_effort_ceiling": "xhigh",
"cache_creation_input_token_cost": 1.2e-05,
"cache_creation_input_token_cost_above_1hr": 1.92e-05,
"cache_read_input_token_cost": 2.4e-07,
"input_cost_per_token": 9.6e-06,
"litellm_provider": "bedrock",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 4.8e-05,
"prompt_cache_min_tokens": 512.0,
"source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json",
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_bedrock_native_structured_output": false,
"supports_computer_use": true,
"supports_forced_tool_use": false,
"supports_function_calling": true,
"supports_max_reasoning_effort": true,
"supports_mid_conversation_system": true,
"supports_native_structured_output": false,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"thinking_always_on": true
},
"bedrock/us-gov-west-1/anthropic.claude-fable-5-2": {
"bedrock_output_config_effort_ceiling": "xhigh",
"cache_creation_input_token_cost": 1.2e-05,
"cache_creation_input_token_cost_above_1hr": 1.92e-05,
"cache_read_input_token_cost": 2.4e-07,
"input_cost_per_token": 9.6e-06,
"litellm_provider": "bedrock",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 4.8e-05,
"prompt_cache_min_tokens": 512.0,
"source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json",
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_bedrock_native_structured_output": false,
"supports_computer_use": true,
"supports_forced_tool_use": false,
"supports_function_calling": true,
"supports_max_reasoning_effort": true,
"supports_mid_conversation_system": true,
"supports_native_structured_output": false,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"thinking_always_on": true
},
"bedrock/us-gov-east-1/anthropic.claude-mythos-5-2": {
"bedrock_output_config_effort_ceiling": "xhigh",
"cache_creation_input_token_cost": 1.2e-05,

View file

@ -1970,7 +1970,7 @@
"source": "https://aws.amazon.com/bedrock/pricing/"
},
"bedrock/claude-fable-latest": {
"alias_of": "global.anthropic.claude-fable-5-1"
"alias_of": "global.anthropic.claude-fable-5-2"
},
"us.anthropic.claude-fable-5": {
"cache_creation_input_token_cost": 1.375e-05,
@ -69958,6 +69958,44 @@
"region": "us-gov-west-1"
}
},
"bedrock/us-gov-west-1/anthropic.claude-fable-5-2": {
"cache_creation_input_token_cost": 1.2e-05,
"cache_creation_input_token_cost_above_1hr": 1.92e-05,
"cache_read_input_token_cost": 2.4e-07,
"input_cost_per_token": 9.6e-06,
"litellm_provider": "bedrock",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 4.8e-05,
"supports_adaptive_thinking": true,
"thinking_always_on": true,
"supports_mid_conversation_system": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_forced_tool_use": false,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_bedrock_native_structured_output": false,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh",
"supports_parallel_tool_use_config": true,
"prompt_cache_min_tokens": 512,
"token_count_model": {
"model": "anthropic.claude-fable-5-2",
"litellm_provider": "bedrock_mantle",
"region": "us-gov-west-1"
}
},
"bedrock/us-gov-east-1/nvidia.nemotron-nano-3-30b": {
"input_cost_per_token": 7.2e-08,
"input_cost_per_token_batches": 3.6e-08,
@ -70266,6 +70304,39 @@
"supports_parallel_tool_use_config": true,
"prompt_cache_min_tokens": 512
},
"bedrock/us-gov-east-1/anthropic.claude-fable-5-2": {
"cache_creation_input_token_cost": 1.2e-05,
"cache_creation_input_token_cost_above_1hr": 1.92e-05,
"cache_read_input_token_cost": 2.4e-07,
"input_cost_per_token": 9.6e-06,
"litellm_provider": "bedrock",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 4.8e-05,
"supports_adaptive_thinking": true,
"thinking_always_on": true,
"supports_mid_conversation_system": true,
"supports_assistant_prefill": false,
"supports_computer_use": true,
"supports_forced_tool_use": false,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"supports_native_structured_output": false,
"supports_bedrock_native_structured_output": false,
"supports_max_reasoning_effort": true,
"supports_output_config": true,
"bedrock_output_config_effort_ceiling": "xhigh",
"supports_parallel_tool_use_config": true,
"prompt_cache_min_tokens": 512
},
"bedrock_mantle/us-gov-west-1/openai.gpt-5.6-terra": {
"litellm_provider": "bedrock_mantle",
"max_input_tokens": 922000,
@ -87903,74 +87974,6 @@
"supports_xhigh_reasoning_effort": true,
"thinking_always_on": true
},
"bedrock/us-gov-east-1/anthropic.claude-fable-5-2": {
"bedrock_output_config_effort_ceiling": "xhigh",
"cache_creation_input_token_cost": 1.2e-05,
"cache_creation_input_token_cost_above_1hr": 1.92e-05,
"cache_read_input_token_cost": 2.4e-07,
"input_cost_per_token": 9.6e-06,
"litellm_provider": "bedrock",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 4.8e-05,
"prompt_cache_min_tokens": 512.0,
"source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json",
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_bedrock_native_structured_output": false,
"supports_computer_use": true,
"supports_forced_tool_use": false,
"supports_function_calling": true,
"supports_max_reasoning_effort": true,
"supports_mid_conversation_system": true,
"supports_native_structured_output": false,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"thinking_always_on": true
},
"bedrock/us-gov-west-1/anthropic.claude-fable-5-2": {
"bedrock_output_config_effort_ceiling": "xhigh",
"cache_creation_input_token_cost": 1.2e-05,
"cache_creation_input_token_cost_above_1hr": 1.92e-05,
"cache_read_input_token_cost": 2.4e-07,
"input_cost_per_token": 9.6e-06,
"litellm_provider": "bedrock",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 4.8e-05,
"prompt_cache_min_tokens": 512.0,
"source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json",
"supports_adaptive_thinking": true,
"supports_assistant_prefill": false,
"supports_bedrock_native_structured_output": false,
"supports_computer_use": true,
"supports_forced_tool_use": false,
"supports_function_calling": true,
"supports_max_reasoning_effort": true,
"supports_mid_conversation_system": true,
"supports_native_structured_output": false,
"supports_output_config": true,
"supports_parallel_tool_use_config": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_sampling_params": false,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"thinking_always_on": true
},
"bedrock/us-gov-east-1/anthropic.claude-mythos-5-2": {
"bedrock_output_config_effort_ceiling": "xhigh",
"cache_creation_input_token_cost": 1.2e-05,

View file

@ -4672,6 +4672,57 @@ US_ANTHROPIC_CLAUDE_FABLE_5_1_INPUT_TEXT: Final = CostTrackingTestCase(
spend=0.0702, input_cost=0.03312, output_cost=0.03708, prompt_tokens=1840, completion_tokens=412
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_2_INPUT_TEXT: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-2-input_text",
covers="quota_management.spend_tracking.cost_matrix.logs_cost",
model="us.anthropic.claude-fable-5-2",
request={
"model": "$MODEL",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a deterministic pricing-harness assistant. Keep answers to a single short line.",
}
],
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "a5b0ca93d2e1 summarize the attached material in one line and name the city weather",
}
],
},
],
"stream": False,
"allowed_openai_params": [],
},
response=JsonResponse(
content_type="application/json",
body={
"metrics": {"latencyMs": 878},
"output": {"message": {"content": [{"text": "scripted answer a5b0ca93d2e1"}], "role": "assistant"}},
"stopReason": "end_turn",
"usage": {
"cacheReadInputTokenCount": 0,
"cacheReadInputTokens": 0,
"cacheWriteInputTokenCount": 0,
"cacheWriteInputTokens": 0,
"inputTokens": 1840,
"outputTokens": 412,
"serverToolUsage": {},
"totalTokens": 2252,
},
},
),
expected=ExactExpected(
spend=0.05616, input_cost=0.026496, output_cost=0.029664, prompt_tokens=1840, completion_tokens=412
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_1_STREAM: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-1-stream",
@ -4726,6 +4777,59 @@ US_ANTHROPIC_CLAUDE_FABLE_5_1_STREAM: Final = CostTrackingTestCase(
spend=0.0702, input_cost=0.03312, output_cost=0.03708, prompt_tokens=1840, completion_tokens=412
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_2_STREAM: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-2-stream",
covers="quota_management.spend_tracking.scripted_wire.logs_cost",
model="us.anthropic.claude-fable-5-2",
request={
"model": "$MODEL",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a deterministic pricing-harness assistant. Keep answers to a single short line.",
}
],
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "d62b383256ff summarize the attached material in one line and name the city weather",
}
],
},
],
"stream": True,
"stream_options": {"include_usage": True},
"allowed_openai_params": [],
},
response=EventStreamResponse(
content_type="application/vnd.amazon.eventstream",
events=(
EventStreamEvent(event_type="messageStart", payload={"role": "assistant"}),
EventStreamEvent(
event_type="contentBlockDelta",
payload={"contentBlockIndex": 0, "delta": {"text": "scripted answer d62b383256ff"}},
),
EventStreamEvent(event_type="contentBlockStop", payload={"contentBlockIndex": 0}),
EventStreamEvent(event_type="messageStop", payload={"stopReason": "end_turn"}),
EventStreamEvent(
event_type="metadata",
payload={
"metrics": {"latencyMs": 883},
"usage": {"inputTokens": 1840, "outputTokens": 412, "serverToolUsage": {}, "totalTokens": 2252},
},
),
),
),
expected=ExactExpected(
spend=0.05616, input_cost=0.026496, output_cost=0.029664, prompt_tokens=1840, completion_tokens=412
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_1_CACHE_READ: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-1-cache_read",
@ -4779,6 +4883,58 @@ US_ANTHROPIC_CLAUDE_FABLE_5_1_CACHE_READ: Final = CostTrackingTestCase(
spend=0.0678384, input_cost=0.0336384, output_cost=0.0342, prompt_tokens=12928, completion_tokens=380
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_2_CACHE_READ: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-2-cache_read",
covers="quota_management.spend_tracking.cost_matrix.logs_cost",
model="us.anthropic.claude-fable-5-2",
request={
"model": "$MODEL",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a deterministic pricing-harness assistant. Keep answers to a single short line.",
"cache_control": {"type": "ephemeral"},
}
],
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "1a5a397f1134 summarize the attached material in one line and name the city weather",
}
],
},
],
"stream": False,
"allowed_openai_params": [],
},
response=JsonResponse(
content_type="application/json",
body={
"metrics": {"latencyMs": 878},
"output": {"message": {"content": [{"text": "scripted answer 1a5a397f1134"}], "role": "assistant"}},
"stopReason": "end_turn",
"usage": {
"cacheReadInputTokenCount": 12288,
"cacheReadInputTokens": 12288,
"cacheWriteInputTokenCount": 0,
"cacheWriteInputTokens": 0,
"inputTokens": 640,
"outputTokens": 380,
"serverToolUsage": {},
"totalTokens": 13308,
},
},
),
expected=ExactExpected(
spend=0.05427072, input_cost=0.02691072, output_cost=0.02736, prompt_tokens=12928, completion_tokens=380
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_1_CACHE_WRITE_5M: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-1-cache_write_5m",
@ -4830,6 +4986,56 @@ US_ANTHROPIC_CLAUDE_FABLE_5_1_CACHE_WRITE_5M: Final = CostTrackingTestCase(
spend=0.248076, input_cost=0.216576, output_cost=0.0315, prompt_tokens=9728, completion_tokens=350
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_2_CACHE_WRITE_5M: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-2-cache_write_5m",
covers="quota_management.spend_tracking.cost_matrix.logs_cost",
model="us.anthropic.claude-fable-5-2",
request={
"model": "$MODEL",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a deterministic pricing-harness assistant. Keep answers to a single short line.",
"cache_control": {"type": "ephemeral"},
}
],
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "3481bca67e1a summarize the attached material in one line and name the city weather",
}
],
},
],
"stream": False,
"allowed_openai_params": [],
},
response=JsonResponse(
content_type="application/json",
body={
"metrics": {"latencyMs": 878},
"output": {"message": {"content": [{"text": "scripted answer 3481bca67e1a"}], "role": "assistant"}},
"stopReason": "end_turn",
"usage": {
"inputTokens": 512,
"outputTokens": 350,
"totalTokens": 10078,
"cacheReadInputTokens": 0,
"cacheWriteInputTokens": 9216,
"cacheDetails": [{"ttl": "5m", "inputTokens": 9216}],
},
},
),
expected=ExactExpected(
spend=0.1984608, input_cost=0.1732608, output_cost=0.0252, prompt_tokens=9728, completion_tokens=350
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_1_CACHE_WRITE_1H: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-1-cache_write_1h",
@ -4881,6 +5087,56 @@ US_ANTHROPIC_CLAUDE_FABLE_5_1_CACHE_WRITE_1H: Final = CostTrackingTestCase(
spend=0.372492, input_cost=0.340992, output_cost=0.0315, prompt_tokens=9728, completion_tokens=350
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_2_CACHE_WRITE_1H: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-2-cache_write_1h",
covers="quota_management.spend_tracking.cost_matrix.logs_cost",
model="us.anthropic.claude-fable-5-2",
request={
"model": "$MODEL",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a deterministic pricing-harness assistant. Keep answers to a single short line.",
"cache_control": {"type": "ephemeral", "ttl": "1h"},
}
],
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "3f037d5317b0 summarize the attached material in one line and name the city weather",
}
],
},
],
"stream": False,
"allowed_openai_params": [],
},
response=JsonResponse(
content_type="application/json",
body={
"metrics": {"latencyMs": 878},
"output": {"message": {"content": [{"text": "scripted answer 3f037d5317b0"}], "role": "assistant"}},
"stopReason": "end_turn",
"usage": {
"inputTokens": 512,
"outputTokens": 350,
"totalTokens": 10078,
"cacheReadInputTokens": 0,
"cacheWriteInputTokens": 9216,
"cacheDetails": [{"ttl": "1h", "inputTokens": 9216}],
},
},
),
expected=ExactExpected(
spend=0.2979936, input_cost=0.2727936, output_cost=0.0252, prompt_tokens=9728, completion_tokens=350
),
)
US_ANTHROPIC_CLAUDE_SONNET_5_TOOL_CALL: Final = CostTrackingTestCase(
name="us.anthropic.claude-sonnet-5-tool_call",
@ -5832,6 +6088,87 @@ US_ANTHROPIC_CLAUDE_FABLE_5_1_TOOL_CALL: Final = CostTrackingTestCase(
spend=0.0702, input_cost=0.03312, output_cost=0.03708, prompt_tokens=1840, completion_tokens=412
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_2_TOOL_CALL: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-2-tool_call",
covers="quota_management.spend_tracking.cost_matrix.logs_cost",
model="us.anthropic.claude-fable-5-2",
request={
"model": "$MODEL",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a deterministic pricing-harness assistant. Keep answers to a single short line.",
}
],
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "7f97ab74e808 summarize the attached material in one line and name the city weather",
}
],
},
],
"stream": False,
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather and a short forecast for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"days": {"type": "integer", "description": "Forecast horizon in days"},
"units": {"type": "string", "enum": ["metric", "imperial"]},
},
"required": ["city"],
},
},
}
],
"tool_choice": "auto",
"allowed_openai_params": ["tool_choice"],
},
response=JsonResponse(
content_type="application/json",
body={
"output": {
"message": {
"role": "assistant",
"content": [
{
"toolUse": {
"toolUseId": "tooluse_$REQUEST_ID",
"name": "get_weather",
"input": {"city": "Berlin", "days": 7, "units": "metric"},
"type": "tool_use",
}
}
],
}
},
"stopReason": "tool_use",
"usage": {
"inputTokens": 1840,
"outputTokens": 412,
"totalTokens": 2252,
"cacheReadInputTokens": 0,
"cacheWriteInputTokens": 0,
},
"metrics": {"latencyMs": 2071},
},
),
expected=ExactExpected(
spend=0.05616, input_cost=0.026496, output_cost=0.029664, prompt_tokens=1840, completion_tokens=412
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_1_THINKING: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-1-thinking",
@ -5885,6 +6222,58 @@ US_ANTHROPIC_CLAUDE_FABLE_5_1_THINKING: Final = CostTrackingTestCase(
spend=0.0594, input_cost=0.02232, output_cost=0.03708, prompt_tokens=1240, completion_tokens=412
),
)
US_ANTHROPIC_CLAUDE_FABLE_5_2_THINKING: Final = CostTrackingTestCase(
name="us.anthropic.claude-fable-5-2-thinking",
covers="quota_management.spend_tracking.cost_matrix.logs_cost",
model="us.anthropic.claude-fable-5-2",
request={
"model": "$MODEL",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a deterministic pricing-harness assistant. Keep answers to a single short line.",
}
],
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "bca700fd52a2 summarize the attached material in one line and name the city weather",
}
],
},
],
"stream": False,
"reasoning_effort": "medium",
"allowed_openai_params": ["reasoning_effort"],
},
response=JsonResponse(
content_type="application/json",
body={
"output": {"message": {"role": "assistant", "content": [{"text": "scripted answer bca700fd52a2"}]}},
"stopReason": "end_turn",
"usage": {
"cacheReadInputTokenCount": 0,
"cacheReadInputTokens": 0,
"cacheWriteInputTokenCount": 0,
"cacheWriteInputTokens": 0,
"inputTokens": 1240,
"outputTokens": 412,
"serverToolUsage": {},
"totalTokens": 1652,
},
"metrics": {"latencyMs": 2071},
},
),
expected=ExactExpected(
spend=0.04752, input_cost=0.017856, output_cost=0.029664, prompt_tokens=1240, completion_tokens=412
),
)
US_ANTHROPIC_CLAUDE_OPUS_5_5_UPSTREAM_400_ZERO_SPEND: Final = CostTrackingTestCase(
name="us.anthropic.claude-opus-5-5-upstream_400_zero_spend",
@ -6014,6 +6403,13 @@ US_ANTHROPIC_CLAUDE_FABLE_5_1_RESPONSES_BRIDGED_JSON: Final = US_ANTHROPIC_CLAUD
"request": {"model": "$MODEL", "input": "summarize the attached material in one line"},
}
)
US_ANTHROPIC_CLAUDE_FABLE_5_2_RESPONSES_BRIDGED_JSON: Final = US_ANTHROPIC_CLAUDE_FABLE_5_2_INPUT_TEXT.model_copy(
update={
"name": "us.anthropic.claude-fable-5-2-responses_bridged_json",
"endpoint": "/v1/responses",
"request": {"model": "$MODEL", "input": "summarize the attached material in one line"},
}
)
US_ANTHROPIC_CLAUDE_HAIKU_5_5_TIERED_INPUT_ABOVE_100K: Final = with_usage(
US_ANTHROPIC_CLAUDE_HAIKU_5_5_INPUT_TEXT,
@ -6088,6 +6484,7 @@ US_ANTHROPIC_CLAUDE_HAIKU_5_5_TIERED_CACHE_WRITE_1H_ABOVE_100K: Final = with_usa
),
)
def _us_anthropic_messages_bridge_pairs(
input_case: CostTrackingTestCase, stream_case: CostTrackingTestCase
) -> tuple[BridgeCostParityTestCase, ...]:
@ -6125,6 +6522,11 @@ US_ANTHROPIC_CLAUDE_FABLE_5_1_RESPONSES_BRIDGED_STREAM: Final = _us_anthropic_re
US_ANTHROPIC_CLAUDE_FABLE_5_1_STREAM,
"us.anthropic.claude-fable-5-1-responses_bridged_stream",
)
US_ANTHROPIC_CLAUDE_FABLE_5_2_RESPONSES_BRIDGED_STREAM: Final = _us_anthropic_responses_stream_case(
US_ANTHROPIC_CLAUDE_FABLE_5_2_RESPONSES_BRIDGED_JSON,
US_ANTHROPIC_CLAUDE_FABLE_5_2_STREAM,
"us.anthropic.claude-fable-5-2-responses_bridged_stream",
)
US_ANTHROPIC_CLAUDE_HAIKU_5_5_RESPONSES_BRIDGED_STREAM: Final = _us_anthropic_responses_stream_case(
US_ANTHROPIC_CLAUDE_HAIKU_5_5_RESPONSES_BRIDGED_JSON,
US_ANTHROPIC_CLAUDE_HAIKU_5_5_STREAM,
@ -6143,6 +6545,9 @@ US_ANTHROPIC_CLAUDE_SONNET_5_5_RESPONSES_BRIDGED_STREAM: Final = _us_anthropic_r
_US_ANTHROPIC_FABLE_5_1_MESSAGES_PAIRS: Final = _us_anthropic_messages_bridge_pairs(
US_ANTHROPIC_CLAUDE_FABLE_5_1_INPUT_TEXT, US_ANTHROPIC_CLAUDE_FABLE_5_1_STREAM
)
_US_ANTHROPIC_FABLE_5_2_MESSAGES_PAIRS: Final = _us_anthropic_messages_bridge_pairs(
US_ANTHROPIC_CLAUDE_FABLE_5_2_INPUT_TEXT, US_ANTHROPIC_CLAUDE_FABLE_5_2_STREAM
)
_US_ANTHROPIC_HAIKU_5_5_MESSAGES_PAIRS: Final = _us_anthropic_messages_bridge_pairs(
US_ANTHROPIC_CLAUDE_HAIKU_5_5_INPUT_TEXT, US_ANTHROPIC_CLAUDE_HAIKU_5_5_STREAM
)
@ -6161,6 +6566,7 @@ CASES: Final[tuple[CostTrackingTestCase, ...]] = (
US_ANTHROPIC_CLAUDE_HAIKU_5_5_RESPONSES_BRIDGED_JSON,
US_ANTHROPIC_CLAUDE_FABLE_5_RESPONSES_BRIDGED_JSON,
US_ANTHROPIC_CLAUDE_FABLE_5_1_RESPONSES_BRIDGED_JSON,
US_ANTHROPIC_CLAUDE_FABLE_5_2_RESPONSES_BRIDGED_JSON,
US_ANTHROPIC_CLAUDE_HAIKU_5_5_TIERED_INPUT_ABOVE_100K,
US_ANTHROPIC_CLAUDE_HAIKU_5_5_TIERED_AT_100K_BOUNDARY,
US_ANTHROPIC_CLAUDE_HAIKU_5_5_TIERED_CACHE_READ_ABOVE_100K,
@ -6255,10 +6661,15 @@ CASES: Final[tuple[CostTrackingTestCase, ...]] = (
US_ANTHROPIC_CLAUDE_FABLE_5_CACHE_WRITE_5M,
US_ANTHROPIC_CLAUDE_FABLE_5_CACHE_WRITE_1H,
US_ANTHROPIC_CLAUDE_FABLE_5_1_INPUT_TEXT,
US_ANTHROPIC_CLAUDE_FABLE_5_2_INPUT_TEXT,
US_ANTHROPIC_CLAUDE_FABLE_5_1_STREAM,
US_ANTHROPIC_CLAUDE_FABLE_5_2_STREAM,
US_ANTHROPIC_CLAUDE_FABLE_5_1_CACHE_READ,
US_ANTHROPIC_CLAUDE_FABLE_5_2_CACHE_READ,
US_ANTHROPIC_CLAUDE_FABLE_5_1_CACHE_WRITE_5M,
US_ANTHROPIC_CLAUDE_FABLE_5_2_CACHE_WRITE_5M,
US_ANTHROPIC_CLAUDE_FABLE_5_1_CACHE_WRITE_1H,
US_ANTHROPIC_CLAUDE_FABLE_5_2_CACHE_WRITE_1H,
US_ANTHROPIC_CLAUDE_SONNET_5_TOOL_CALL,
US_ANTHROPIC_CLAUDE_SONNET_5_THINKING,
US_ANTHROPIC_CLAUDE_SONNET_5_5_TOOL_CALL,
@ -6272,14 +6683,18 @@ CASES: Final[tuple[CostTrackingTestCase, ...]] = (
US_ANTHROPIC_CLAUDE_FABLE_5_TOOL_CALL,
US_ANTHROPIC_CLAUDE_FABLE_5_THINKING,
US_ANTHROPIC_CLAUDE_FABLE_5_1_TOOL_CALL,
US_ANTHROPIC_CLAUDE_FABLE_5_2_TOOL_CALL,
US_ANTHROPIC_CLAUDE_FABLE_5_1_THINKING,
US_ANTHROPIC_CLAUDE_FABLE_5_2_THINKING,
US_ANTHROPIC_CLAUDE_OPUS_5_5_UPSTREAM_400_ZERO_SPEND,
US_ANTHROPIC_CLAUDE_OPUS_5_5_INPUT_TEXT_ROLLUPS,
US_ANTHROPIC_CLAUDE_FABLE_5_1_RESPONSES_BRIDGED_STREAM,
US_ANTHROPIC_CLAUDE_FABLE_5_2_RESPONSES_BRIDGED_STREAM,
US_ANTHROPIC_CLAUDE_HAIKU_5_5_RESPONSES_BRIDGED_STREAM,
US_ANTHROPIC_CLAUDE_OPUS_5_5_RESPONSES_BRIDGED_STREAM,
US_ANTHROPIC_CLAUDE_SONNET_5_5_RESPONSES_BRIDGED_STREAM,
*tuple(pair.bridged for pair in _US_ANTHROPIC_FABLE_5_1_MESSAGES_PAIRS),
*tuple(pair.bridged for pair in _US_ANTHROPIC_FABLE_5_2_MESSAGES_PAIRS),
*tuple(pair.bridged for pair in _US_ANTHROPIC_HAIKU_5_5_MESSAGES_PAIRS),
*tuple(pair.bridged for pair in _US_ANTHROPIC_OPUS_5_5_MESSAGES_PAIRS),
*tuple(pair.bridged for pair in _US_ANTHROPIC_SONNET_5_5_MESSAGES_PAIRS),
@ -6308,10 +6723,15 @@ PARITY: Final[tuple[StreamParityTestCase, ...]] = (
StreamParityTestCase(plain=US_ANTHROPIC_CLAUDE_HAIKU_5_5_INPUT_TEXT, streamed=US_ANTHROPIC_CLAUDE_HAIKU_5_5_STREAM),
StreamParityTestCase(plain=US_ANTHROPIC_CLAUDE_FABLE_5_INPUT_TEXT, streamed=US_ANTHROPIC_CLAUDE_FABLE_5_STREAM),
StreamParityTestCase(plain=US_ANTHROPIC_CLAUDE_FABLE_5_1_INPUT_TEXT, streamed=US_ANTHROPIC_CLAUDE_FABLE_5_1_STREAM),
StreamParityTestCase(plain=US_ANTHROPIC_CLAUDE_FABLE_5_2_INPUT_TEXT, streamed=US_ANTHROPIC_CLAUDE_FABLE_5_2_STREAM),
StreamParityTestCase(
plain=US_ANTHROPIC_CLAUDE_FABLE_5_1_RESPONSES_BRIDGED_JSON,
streamed=US_ANTHROPIC_CLAUDE_FABLE_5_1_RESPONSES_BRIDGED_STREAM,
),
StreamParityTestCase(
plain=US_ANTHROPIC_CLAUDE_FABLE_5_2_RESPONSES_BRIDGED_JSON,
streamed=US_ANTHROPIC_CLAUDE_FABLE_5_2_RESPONSES_BRIDGED_STREAM,
),
StreamParityTestCase(
plain=US_ANTHROPIC_CLAUDE_HAIKU_5_5_RESPONSES_BRIDGED_JSON,
streamed=US_ANTHROPIC_CLAUDE_HAIKU_5_5_RESPONSES_BRIDGED_STREAM,
@ -6328,6 +6748,10 @@ PARITY: Final[tuple[StreamParityTestCase, ...]] = (
plain=_US_ANTHROPIC_FABLE_5_1_MESSAGES_PAIRS[0].bridged,
streamed=_US_ANTHROPIC_FABLE_5_1_MESSAGES_PAIRS[1].bridged,
),
StreamParityTestCase(
plain=_US_ANTHROPIC_FABLE_5_2_MESSAGES_PAIRS[0].bridged,
streamed=_US_ANTHROPIC_FABLE_5_2_MESSAGES_PAIRS[1].bridged,
),
StreamParityTestCase(
plain=_US_ANTHROPIC_HAIKU_5_5_MESSAGES_PAIRS[0].bridged,
streamed=_US_ANTHROPIC_HAIKU_5_5_MESSAGES_PAIRS[1].bridged,
@ -6372,10 +6796,17 @@ BRIDGE_COST_PARITY: Final[tuple[BridgeCostParityTestCase, ...]] = (
BridgeCostParityTestCase(
native=US_ANTHROPIC_CLAUDE_FABLE_5_1_INPUT_TEXT, bridged=US_ANTHROPIC_CLAUDE_FABLE_5_1_RESPONSES_BRIDGED_JSON
),
BridgeCostParityTestCase(
native=US_ANTHROPIC_CLAUDE_FABLE_5_2_INPUT_TEXT, bridged=US_ANTHROPIC_CLAUDE_FABLE_5_2_RESPONSES_BRIDGED_JSON
),
BridgeCostParityTestCase(
native=US_ANTHROPIC_CLAUDE_FABLE_5_1_STREAM,
bridged=US_ANTHROPIC_CLAUDE_FABLE_5_1_RESPONSES_BRIDGED_STREAM,
),
BridgeCostParityTestCase(
native=US_ANTHROPIC_CLAUDE_FABLE_5_2_STREAM,
bridged=US_ANTHROPIC_CLAUDE_FABLE_5_2_RESPONSES_BRIDGED_STREAM,
),
BridgeCostParityTestCase(
native=US_ANTHROPIC_CLAUDE_HAIKU_5_5_STREAM,
bridged=US_ANTHROPIC_CLAUDE_HAIKU_5_5_RESPONSES_BRIDGED_STREAM,
@ -6389,6 +6820,7 @@ BRIDGE_COST_PARITY: Final[tuple[BridgeCostParityTestCase, ...]] = (
bridged=US_ANTHROPIC_CLAUDE_SONNET_5_5_RESPONSES_BRIDGED_STREAM,
),
*_US_ANTHROPIC_FABLE_5_1_MESSAGES_PAIRS,
*_US_ANTHROPIC_FABLE_5_2_MESSAGES_PAIRS,
*_US_ANTHROPIC_HAIKU_5_5_MESSAGES_PAIRS,
*_US_ANTHROPIC_OPUS_5_5_MESSAGES_PAIRS,
*_US_ANTHROPIC_SONNET_5_5_MESSAGES_PAIRS,

View file

@ -5,8 +5,7 @@ PARITY pairs the streamed case with its plain twin where both bill the same row.
import json
from typing import Final, Literal, cast
from pydantic import JsonValue
from integration.cost_calculation.bridge_cost_parity.case import BridgeCostParityTestCase, bridge_pairs
from integration.cost_calculation.cases.anthropic import (
CLAUDE_FABLE_5_1_CACHE_READ,
CLAUDE_FABLE_5_1_CACHE_WRITE_1H,
@ -17,6 +16,15 @@ from integration.cost_calculation.cases.anthropic import (
CLAUDE_FABLE_5_1_STREAM,
CLAUDE_FABLE_5_1_THINKING,
CLAUDE_FABLE_5_1_TOOL_CALL,
CLAUDE_FABLE_5_2_CACHE_READ,
CLAUDE_FABLE_5_2_CACHE_WRITE_1H,
CLAUDE_FABLE_5_2_CACHE_WRITE_5M,
CLAUDE_FABLE_5_2_INPUT_TEXT,
CLAUDE_FABLE_5_2_MESSAGES_INPUT_TEXT,
CLAUDE_FABLE_5_2_MESSAGES_STREAM,
CLAUDE_FABLE_5_2_STREAM,
CLAUDE_FABLE_5_2_THINKING,
CLAUDE_FABLE_5_2_TOOL_CALL,
CLAUDE_HAIKU_5_5_MESSAGES_INPUT_TEXT,
CLAUDE_HAIKU_5_5_MESSAGES_STREAM,
CLAUDE_OPUS_5_5_CACHE_READ,
@ -38,7 +46,6 @@ from integration.cost_calculation.cases.anthropic import (
CLAUDE_SONNET_5_5_THINKING,
CLAUDE_SONNET_5_5_TOOL_CALL,
)
from integration.cost_calculation.bridge_cost_parity.case import BridgeCostParityTestCase, bridge_pairs
from integration.cost_calculation.cost_tracking_case import (
CostTrackingTestCase,
Deployment,
@ -49,6 +56,7 @@ from integration.cost_calculation.cost_tracking_case import (
SseResponse,
)
from integration.cost_calculation.stream_parity.case import StreamParityTestCase, sse_frames
from pydantic import JsonValue
def _gemini_case_response(
@ -3009,6 +3017,37 @@ VERTEX_AI_CLAUDE_FABLE_5_1_INPUT_TEXT: Final = CLAUDE_FABLE_5_1_INPUT_TEXT.model
),
}
)
VERTEX_AI_CLAUDE_FABLE_5_2_INPUT_TEXT: Final = CLAUDE_FABLE_5_2_INPUT_TEXT.model_copy(
update={
"name": "vertex_ai/claude-fable-5-2-input_text",
"model": "vertex_ai/claude-fable-5-2",
"response": JsonResponse(
content_type="application/json",
body={
"model": "claude-fable-5-2",
"id": "msg_$REQUEST_ID",
"type": "message",
"role": "assistant",
"content": [{"type": "thinking", "thinking": "scripted thought", "signature": "scripted signature"}],
"container": None,
"stop_reason": "max_tokens",
"stop_sequence": None,
"stop_details": None,
"usage": {
"input_tokens": 1840,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"cache_creation": {"ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0},
"output_tokens": 412,
"output_tokens_details": {"thinking_tokens": 0},
},
},
),
"expected": ExactExpected(
spend=0.05928, input_cost=0.027968, output_cost=0.031312, prompt_tokens=1840, completion_tokens=412
),
}
)
VERTEX_AI_CLAUDE_FABLE_5_1_STREAM: Final = CLAUDE_FABLE_5_1_STREAM.model_copy(
update={
"name": "vertex_ai/claude-fable-5-1-stream",
@ -3017,6 +3056,14 @@ VERTEX_AI_CLAUDE_FABLE_5_1_STREAM: Final = CLAUDE_FABLE_5_1_STREAM.model_copy(
"expected": VERTEX_AI_CLAUDE_FABLE_5_1_INPUT_TEXT.expected,
}
)
VERTEX_AI_CLAUDE_FABLE_5_2_STREAM: Final = CLAUDE_FABLE_5_2_STREAM.model_copy(
update={
"name": "vertex_ai/claude-fable-5-2-stream",
"model": "vertex_ai/claude-fable-5-2",
"response": _vertex_claude_stream_response("claude-fable-5-2", "max_tokens", ()),
"expected": VERTEX_AI_CLAUDE_FABLE_5_2_INPUT_TEXT.expected,
}
)
VERTEX_AI_CLAUDE_SONNET_5_5_CACHE_READ: Final = _vertex_claude_shape_case(
CLAUDE_SONNET_5_5_CACHE_READ,
@ -3135,6 +3182,15 @@ VERTEX_AI_CLAUDE_FABLE_5_1_CACHE_READ: Final = _vertex_claude_shape_case(
spend=0.013567425, input_cost=0.013092425, output_cost=0.000475, prompt_tokens=27017, completion_tokens=5
),
)
VERTEX_AI_CLAUDE_FABLE_5_2_CACHE_READ: Final = _vertex_claude_shape_case(
CLAUDE_FABLE_5_2_CACHE_READ,
"vertex_ai/claude-fable-5-2-cache_read",
"vertex_ai/claude-fable-5-2",
_vertex_claude_usage(input_tokens=14, output_tokens=5, cache_read_input_tokens=27003),
ExactExpected(
spend=0.01085394, input_cost=0.01047394, output_cost=0.00038, prompt_tokens=27017, completion_tokens=5
),
)
VERTEX_AI_CLAUDE_FABLE_5_1_CACHE_WRITE_5M: Final = _vertex_claude_shape_case(
CLAUDE_FABLE_5_1_CACHE_WRITE_5M,
"vertex_ai/claude-fable-5-1-cache_write_5m",
@ -3146,6 +3202,17 @@ VERTEX_AI_CLAUDE_FABLE_5_1_CACHE_WRITE_5M: Final = _vertex_claude_shape_case(
spend=0.64225225, input_cost=0.64177725, output_cost=0.000475, prompt_tokens=27025, completion_tokens=5
),
)
VERTEX_AI_CLAUDE_FABLE_5_2_CACHE_WRITE_5M: Final = _vertex_claude_shape_case(
CLAUDE_FABLE_5_2_CACHE_WRITE_5M,
"vertex_ai/claude-fable-5-2-cache_write_5m",
"vertex_ai/claude-fable-5-2",
_vertex_claude_usage(
input_tokens=14, output_tokens=5, cache_creation_input_tokens=27011, ephemeral_5m_input_tokens=27011
),
ExactExpected(
spend=0.5138018, input_cost=0.5134218, output_cost=0.00038, prompt_tokens=27025, completion_tokens=5
),
)
VERTEX_AI_CLAUDE_FABLE_5_1_CACHE_WRITE_1H: Final = _vertex_claude_shape_case(
CLAUDE_FABLE_5_1_CACHE_WRITE_1H,
"vertex_ai/claude-fable-5-1-cache_write_1h",
@ -3157,6 +3224,17 @@ VERTEX_AI_CLAUDE_FABLE_5_1_CACHE_WRITE_1H: Final = _vertex_claude_shape_case(
spend=1.027159, input_cost=1.026684, output_cost=0.000475, prompt_tokens=27025, completion_tokens=5
),
)
VERTEX_AI_CLAUDE_FABLE_5_2_CACHE_WRITE_1H: Final = _vertex_claude_shape_case(
CLAUDE_FABLE_5_2_CACHE_WRITE_1H,
"vertex_ai/claude-fable-5-2-cache_write_1h",
"vertex_ai/claude-fable-5-2",
_vertex_claude_usage(
input_tokens=14, output_tokens=5, cache_creation_input_tokens=27011, ephemeral_1h_input_tokens=27011
),
ExactExpected(
spend=0.8217272, input_cost=0.8213472, output_cost=0.00038, prompt_tokens=27025, completion_tokens=5
),
)
VERTEX_AI_CLAUDE_FABLE_5_1_THINKING: Final = _vertex_claude_shape_case(
CLAUDE_FABLE_5_1_THINKING,
"vertex_ai/claude-fable-5-1-thinking",
@ -3171,6 +3249,20 @@ VERTEX_AI_CLAUDE_FABLE_5_1_THINKING: Final = _vertex_claude_shape_case(
],
adaptive_thinking=True,
)
VERTEX_AI_CLAUDE_FABLE_5_2_THINKING: Final = _vertex_claude_shape_case(
CLAUDE_FABLE_5_2_THINKING,
"vertex_ai/claude-fable-5-2-thinking",
"vertex_ai/claude-fable-5-2",
_vertex_claude_usage(input_tokens=18, output_tokens=98, thinking_tokens=33),
ExactExpected(
spend=0.0077216, input_cost=0.0002736, output_cost=0.007448, prompt_tokens=18, completion_tokens=98
),
[
{"type": "thinking", "thinking": "scripted thought", "signature": "scripted signature"},
{"type": "text", "text": "scripted answer for cost fixtures"},
],
adaptive_thinking=True,
)
VERTEX_AI_CLAUDE_FABLE_5_1_TOOL_CALL: Final = _vertex_claude_shape_case(
CLAUDE_FABLE_5_1_TOOL_CALL,
"vertex_ai/claude-fable-5-1-tool_call",
@ -3180,6 +3272,15 @@ VERTEX_AI_CLAUDE_FABLE_5_1_TOOL_CALL: Final = _vertex_claude_shape_case(
spend=0.012065, input_cost=0.007315, output_cost=0.00475, prompt_tokens=385, completion_tokens=50
),
)
VERTEX_AI_CLAUDE_FABLE_5_2_TOOL_CALL: Final = _vertex_claude_shape_case(
CLAUDE_FABLE_5_2_TOOL_CALL,
"vertex_ai/claude-fable-5-2-tool_call",
"vertex_ai/claude-fable-5-2",
_vertex_claude_usage(input_tokens=385, output_tokens=50),
ExactExpected(
spend=0.009652, input_cost=0.005852, output_cost=0.0038, prompt_tokens=385, completion_tokens=50
),
)
VERTEX_AI_CLAUDE_HAIKU_5_5_INPUT_TEXT: Final = _vertex_claude_shape_case(
VERTEX_AI_CLAUDE_SONNET_5_5_INPUT_TEXT,
@ -3325,6 +3426,15 @@ VERTEX_AI_CLAUDE_FABLE_5_1_MESSAGES_INPUT_TEXT: Final = CLAUDE_FABLE_5_1_MESSAGE
"expected": VERTEX_AI_CLAUDE_FABLE_5_1_INPUT_TEXT.expected,
}
)
VERTEX_AI_CLAUDE_FABLE_5_2_MESSAGES_INPUT_TEXT: Final = CLAUDE_FABLE_5_2_MESSAGES_INPUT_TEXT.model_copy(
update={
"name": "vertex_ai/claude-fable-5-2-messages_input_text",
"model": "vertex_ai/claude-fable-5-2",
"endpoint": "/v1/messages",
"response": VERTEX_AI_CLAUDE_FABLE_5_2_INPUT_TEXT.response,
"expected": VERTEX_AI_CLAUDE_FABLE_5_2_INPUT_TEXT.expected,
}
)
VERTEX_AI_CLAUDE_FABLE_5_1_MESSAGES_STREAM: Final = CLAUDE_FABLE_5_1_MESSAGES_STREAM.model_copy(
update={
"name": "vertex_ai/claude-fable-5-1-messages_stream",
@ -3334,10 +3444,23 @@ VERTEX_AI_CLAUDE_FABLE_5_1_MESSAGES_STREAM: Final = CLAUDE_FABLE_5_1_MESSAGES_ST
"expected": VERTEX_AI_CLAUDE_FABLE_5_1_INPUT_TEXT.expected,
}
)
VERTEX_AI_CLAUDE_FABLE_5_2_MESSAGES_STREAM: Final = CLAUDE_FABLE_5_2_MESSAGES_STREAM.model_copy(
update={
"name": "vertex_ai/claude-fable-5-2-messages_stream",
"model": "vertex_ai/claude-fable-5-2",
"endpoint": "/v1/messages",
"response": VERTEX_AI_CLAUDE_FABLE_5_2_STREAM.response,
"expected": VERTEX_AI_CLAUDE_FABLE_5_2_INPUT_TEXT.expected,
}
)
VERTEX_AI_CLAUDE_FABLE_5_1_BRIDGE_PAIRS: Final = bridge_pairs(
(VERTEX_AI_CLAUDE_FABLE_5_1_MESSAGES_INPUT_TEXT, VERTEX_AI_CLAUDE_FABLE_5_1_MESSAGES_STREAM),
("/v1/responses",),
)
VERTEX_AI_CLAUDE_FABLE_5_2_BRIDGE_PAIRS: Final = bridge_pairs(
(VERTEX_AI_CLAUDE_FABLE_5_2_MESSAGES_INPUT_TEXT, VERTEX_AI_CLAUDE_FABLE_5_2_MESSAGES_STREAM),
("/v1/responses",),
)
VERTEX_AI_CLAUDE_OPUS_5_5_MESSAGES_INPUT_TEXT: Final = CLAUDE_OPUS_5_5_MESSAGES_INPUT_TEXT.model_copy(
update={
"name": "vertex_ai/claude-opus-5-5-messages_input_text",
@ -3915,7 +4038,9 @@ CASES: Final[tuple[CostTrackingTestCase, ...]] = (
VERTEX_AI_CLAUDE_OPUS_5_5_INPUT_TEXT,
VERTEX_AI_CLAUDE_OPUS_5_5_STREAM,
VERTEX_AI_CLAUDE_FABLE_5_1_INPUT_TEXT,
VERTEX_AI_CLAUDE_FABLE_5_2_INPUT_TEXT,
VERTEX_AI_CLAUDE_FABLE_5_1_STREAM,
VERTEX_AI_CLAUDE_FABLE_5_2_STREAM,
VERTEX_AI_CLAUDE_SONNET_5_5_CACHE_READ,
VERTEX_AI_CLAUDE_SONNET_5_5_CACHE_WRITE_5M,
VERTEX_AI_CLAUDE_SONNET_5_5_CACHE_WRITE_1H,
@ -3927,10 +4052,15 @@ CASES: Final[tuple[CostTrackingTestCase, ...]] = (
VERTEX_AI_CLAUDE_OPUS_5_5_THINKING,
VERTEX_AI_CLAUDE_OPUS_5_5_TOOL_CALL,
VERTEX_AI_CLAUDE_FABLE_5_1_CACHE_READ,
VERTEX_AI_CLAUDE_FABLE_5_2_CACHE_READ,
VERTEX_AI_CLAUDE_FABLE_5_1_CACHE_WRITE_5M,
VERTEX_AI_CLAUDE_FABLE_5_2_CACHE_WRITE_5M,
VERTEX_AI_CLAUDE_FABLE_5_1_CACHE_WRITE_1H,
VERTEX_AI_CLAUDE_FABLE_5_2_CACHE_WRITE_1H,
VERTEX_AI_CLAUDE_FABLE_5_1_THINKING,
VERTEX_AI_CLAUDE_FABLE_5_2_THINKING,
VERTEX_AI_CLAUDE_FABLE_5_1_TOOL_CALL,
VERTEX_AI_CLAUDE_FABLE_5_2_TOOL_CALL,
VERTEX_AI_CLAUDE_HAIKU_5_5_INPUT_TEXT,
VERTEX_AI_CLAUDE_HAIKU_5_5_STREAM,
VERTEX_AI_CLAUDE_HAIKU_5_5_CACHE_READ,
@ -3944,8 +4074,11 @@ CASES: Final[tuple[CostTrackingTestCase, ...]] = (
VERTEX_AI_CLAUDE_HAIKU_5_5_MESSAGES_STREAM,
*(pair.bridged for pair in VERTEX_AI_CLAUDE_HAIKU_5_5_BRIDGE_PAIRS),
VERTEX_AI_CLAUDE_FABLE_5_1_MESSAGES_INPUT_TEXT,
VERTEX_AI_CLAUDE_FABLE_5_2_MESSAGES_INPUT_TEXT,
VERTEX_AI_CLAUDE_FABLE_5_1_MESSAGES_STREAM,
VERTEX_AI_CLAUDE_FABLE_5_2_MESSAGES_STREAM,
*(pair.bridged for pair in VERTEX_AI_CLAUDE_FABLE_5_1_BRIDGE_PAIRS),
*(pair.bridged for pair in VERTEX_AI_CLAUDE_FABLE_5_2_BRIDGE_PAIRS),
VERTEX_AI_CLAUDE_OPUS_5_5_MESSAGES_INPUT_TEXT,
VERTEX_AI_CLAUDE_OPUS_5_5_MESSAGES_STREAM,
*(pair.bridged for pair in VERTEX_AI_CLAUDE_OPUS_5_5_BRIDGE_PAIRS),
@ -3995,6 +4128,7 @@ PARITY: Final[tuple[StreamParityTestCase, ...]] = (
),
StreamParityTestCase(plain=VERTEX_AI_CLAUDE_OPUS_5_5_INPUT_TEXT, streamed=VERTEX_AI_CLAUDE_OPUS_5_5_STREAM),
StreamParityTestCase(plain=VERTEX_AI_CLAUDE_FABLE_5_1_INPUT_TEXT, streamed=VERTEX_AI_CLAUDE_FABLE_5_1_STREAM),
StreamParityTestCase(plain=VERTEX_AI_CLAUDE_FABLE_5_2_INPUT_TEXT, streamed=VERTEX_AI_CLAUDE_FABLE_5_2_STREAM),
StreamParityTestCase(
plain=VERTEX_AI_CLAUDE_HAIKU_5_5_INPUT_TEXT, streamed=VERTEX_AI_CLAUDE_HAIKU_5_5_STREAM
),
@ -4010,10 +4144,18 @@ PARITY: Final[tuple[StreamParityTestCase, ...]] = (
plain=VERTEX_AI_CLAUDE_FABLE_5_1_MESSAGES_INPUT_TEXT,
streamed=VERTEX_AI_CLAUDE_FABLE_5_1_MESSAGES_STREAM,
),
StreamParityTestCase(
plain=VERTEX_AI_CLAUDE_FABLE_5_2_MESSAGES_INPUT_TEXT,
streamed=VERTEX_AI_CLAUDE_FABLE_5_2_MESSAGES_STREAM,
),
StreamParityTestCase(
plain=VERTEX_AI_CLAUDE_FABLE_5_1_BRIDGE_PAIRS[0].bridged,
streamed=VERTEX_AI_CLAUDE_FABLE_5_1_BRIDGE_PAIRS[1].bridged,
),
StreamParityTestCase(
plain=VERTEX_AI_CLAUDE_FABLE_5_2_BRIDGE_PAIRS[0].bridged,
streamed=VERTEX_AI_CLAUDE_FABLE_5_2_BRIDGE_PAIRS[1].bridged,
),
StreamParityTestCase(
plain=VERTEX_AI_CLAUDE_OPUS_5_5_MESSAGES_INPUT_TEXT,
streamed=VERTEX_AI_CLAUDE_OPUS_5_5_MESSAGES_STREAM,
@ -4068,6 +4210,7 @@ BRIDGE_COST_PARITY: Final[tuple[BridgeCostParityTestCase, ...]] = (
*GEMINI_4_ARGON_ROUTE_BRIDGES,
*VERTEX_AI_CLAUDE_HAIKU_5_5_BRIDGE_PAIRS,
*VERTEX_AI_CLAUDE_FABLE_5_1_BRIDGE_PAIRS,
*VERTEX_AI_CLAUDE_FABLE_5_2_BRIDGE_PAIRS,
*VERTEX_AI_CLAUDE_OPUS_5_5_BRIDGE_PAIRS,
*VERTEX_AI_CLAUDE_SONNET_5_5_BRIDGE_PAIRS,
)

View file

@ -906,6 +906,28 @@ COST_MAP_ENTRIES: Final[dict[str, dict[str, JsonValue]]] = {
"max_output_tokens": 128000,
"max_tokens": 128000,
},
"vertex_ai/claude-fable-5-2": {
"litellm_provider": "vertex_ai-anthropic_models",
"mode": "chat",
"input_cost_per_token": 1.52e-05,
"input_cost_per_token_batches": 7.6e-06,
"output_cost_per_token": 7.6e-05,
"output_cost_per_token_batches": 3.8e-05,
"cache_creation_input_token_cost": 1.9e-05,
"cache_creation_input_token_cost_above_1hr": 3.04e-05,
"cache_read_input_token_cost": 3.8e-07,
"search_context_cost_per_query": {
"search_context_size_high": 0.019,
"search_context_size_low": 0.019,
"search_context_size_medium": 0.019,
},
"supports_function_calling": True,
"supports_prompt_caching": True,
"supports_reasoning": True,
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
},
"vertex_ai/claude-haiku-5-5": {
"litellm_provider": "vertex_ai-anthropic_models",
"mode": "chat",
@ -1494,6 +1516,19 @@ COST_MAP_ENTRIES: Final[dict[str, dict[str, JsonValue]]] = {
"output_cost_per_token": 9e-05,
"supports_function_calling": True,
},
"us.anthropic.claude-fable-5-2": {
"cache_creation_input_token_cost": 1.8e-05,
"cache_creation_input_token_cost_above_1hr": 2.88e-05,
"cache_read_input_token_cost": 1.44e-06,
"input_cost_per_token": 1.44e-05,
"litellm_provider": "bedrock_converse",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 7.2e-05,
"supports_function_calling": True,
},
"gpt-6-sol": {
"cache_read_input_token_cost": 2.2e-07,
"input_cost_per_token": 2.2e-06,

View file

@ -0,0 +1,234 @@
"""us.anthropic.claude-fable-5-2 on Bedrock Converse: the row's capability flags shape the Converse body.
Fable rejects sampling params and forced tool choice and always thinks, so the proxy must strip
or refuse those before the request reaches Bedrock, the same way it does on the Anthropic API.
"""
import json
from collections.abc import Callable
from typing import Final
from integration._support.client import Gateway, Scenario
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue, TypeAdapter
_MODEL_ID: Final = "us.anthropic.claude-fable-5-2"
_TARGET: Final = f"/model/{_MODEL_ID}/converse"
_TOKEN: Final = "synthetic-bedrock-bearer"
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_FORCED_TOOL_CHOICES: Final[tuple[JsonValue, ...]] = ("required", {"type": "function", "function": {"name": "lookup"}})
_TOOLS: Final[list[JsonValue]] = [
{
"type": "function",
"function": {
"name": "lookup",
"description": "Look up a synthetic record.",
"parameters": {
"type": "object",
"properties": {"record_id": {"type": "string"}},
"required": ["record_id"],
},
},
}
]
def _reply(usage: dict[str, JsonValue] | None = None) -> bytes:
return json.dumps(
{
"output": {
"message": {
"role": "assistant",
"content": [
{
"reasoningContent": {
"reasoningText": {"text": "scripted thought", "signature": "scripted-signature"}
}
},
{"text": "done"},
],
}
},
"stopReason": "end_turn",
"usage": usage or {"inputTokens": 12, "outputTokens": 3, "totalTokens": 15},
"metrics": {"latencyMs": 1},
}
).encode()
def _deployment(scenario: Scenario, api_base: str, *, drop_params: bool = False) -> str:
if drop_params:
return scenario.model(
model=f"bedrock/converse/{_MODEL_ID}",
api_key=_TOKEN,
aws_region_name="us-east-1",
aws_bedrock_runtime_endpoint=api_base,
num_retries=0,
drop_params=True,
)
return scenario.model(
model=f"bedrock/converse/{_MODEL_ID}",
api_key=_TOKEN,
aws_region_name="us-east-1",
aws_bedrock_runtime_endpoint=api_base,
num_retries=0,
)
def _peer(on_body: Callable[[dict[str, JsonValue]], bytes]) -> Callable[[Request], Reply]:
def respond(request: Request) -> Reply:
assert request.method == "POST" and request.target == _TARGET, (request.method, request.target)
assert request.headers["authorization"] == f"Bearer {_TOKEN}", request.headers
return Reply(body=on_body(_JSON_OBJECT.validate_json(request.body)))
return respond
def _chat_body(model: str, **extra: JsonValue) -> dict[str, JsonValue]:
return {
"model": model,
"messages": [{"role": "user", "content": "synthetic fable request"}],
"max_tokens": 64,
**extra,
}
def test_drop_params_removes_temperature_and_top_p_before_bedrock(gateway: Gateway) -> None:
def on_body(body: dict[str, JsonValue]) -> bytes:
assert body["inferenceConfig"] == {"maxTokens": 64}, body
return _reply()
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url, drop_params=True)
response: Final = gateway.request("POST", "/v1/chat/completions", _chat_body(model, temperature=0.2, top_p=0.9))
assert response.status_code == 200, response.text
assert response.json()["choices"][0]["message"]["content"] == "done", response.text
assert len(wire.drain()) == 1
def test_sampling_params_are_refused_before_reaching_bedrock_without_drop_params(gateway: Gateway) -> None:
def on_body(body: dict[str, JsonValue]) -> bytes:
raise AssertionError(f"temperature must not reach Bedrock: {body}")
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url)
response: Final = gateway.request("POST", "/v1/chat/completions", _chat_body(model, temperature=0.2))
assert response.status_code == 400, response.text
assert "temperature" in response.text and "drop_params" in response.text, response.text
assert len(wire.drain()) == 0
def test_drop_params_downgrades_forced_and_named_tool_choice_to_auto(gateway: Gateway) -> None:
def on_body(body: dict[str, JsonValue]) -> bytes:
tool_config: Final = body["toolConfig"]
assert isinstance(tool_config, dict), body
assert tool_config.get("toolChoice") in (None, {"auto": {}}), body
tools: Final = tool_config["tools"]
assert isinstance(tools, list) and len(tools) == 1, body
return _reply()
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url, drop_params=True)
responses: Final = tuple(
gateway.request("POST", "/v1/chat/completions", _chat_body(model, tools=_TOOLS, tool_choice=tool_choice))
for tool_choice in _FORCED_TOOL_CHOICES
)
assert [response.status_code for response in responses] == [200, 200], [response.text for response in responses]
assert len(wire.drain()) == 2
def test_forced_tool_choice_is_refused_before_reaching_bedrock_without_drop_params(gateway: Gateway) -> None:
def on_body(body: dict[str, JsonValue]) -> bytes:
raise AssertionError(f"forced tool_choice must not reach Bedrock: {body}")
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url)
response: Final = gateway.request(
"POST", "/v1/chat/completions", _chat_body(model, tools=_TOOLS, tool_choice="required")
)
assert response.status_code == 400, response.text
assert "tool_choice" in response.text, response.text
assert len(wire.drain()) == 0
def test_thinking_disabled_is_dropped_for_an_always_on_model(gateway: Gateway) -> None:
def on_body(body: dict[str, JsonValue]) -> bytes:
assert "thinking" not in json.dumps(body.get("additionalModelRequestFields", {})), body
return _reply()
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url)
response: Final = gateway.request(
"POST", "/v1/chat/completions", _chat_body(model, thinking={"type": "disabled"})
)
assert response.status_code == 200, response.text
assert response.json()["choices"][0]["message"]["reasoning_content"] == "scripted thought", response.text
assert len(wire.drain()) == 1
def test_reasoning_effort_becomes_adaptive_thinking_with_output_config_effort(gateway: Gateway) -> None:
def on_body(body: dict[str, JsonValue]) -> bytes:
extra: Final = body["additionalModelRequestFields"]
assert isinstance(extra, dict), body
assert extra["thinking"] == {"type": "adaptive", "display": "summarized"}, body
assert extra["output_config"] == {"effort": "max"}, body
assert "budget_tokens" not in json.dumps(body), body
return _reply()
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url)
response: Final = gateway.request("POST", "/v1/chat/completions", _chat_body(model, reasoning_effort="max"))
assert response.status_code == 200, response.text
assert len(wire.drain()) == 1
def test_one_hour_cache_point_reaches_bedrock_and_its_usage_comes_back(gateway: Gateway) -> None:
usage: Final[dict[str, JsonValue]] = {
"inputTokens": 9,
"outputTokens": 3,
"totalTokens": 7237,
"cacheReadInputTokens": 0,
"cacheWriteInputTokens": 7225,
"cacheDetails": [{"ttl": "1h", "inputTokens": 7225}],
}
def on_body(body: dict[str, JsonValue]) -> bytes:
assert body["system"] == [
{"text": "synthetic cached system prompt"},
{"cachePoint": {"type": "default", "ttl": "1h"}},
], body
return _reply(usage)
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url)
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
_chat_body(
model,
messages=[
{
"role": "system",
"content": [
{
"type": "text",
"text": "synthetic cached system prompt",
"cache_control": {"type": "ephemeral", "ttl": "1h"},
}
],
},
{"role": "user", "content": "synthetic fable request"},
],
),
)
assert response.status_code == 200, response.text
usage_out: Final = _JSON_OBJECT.validate_json(response.text)["usage"]
assert isinstance(usage_out, dict), response.text
prompt_details: Final = usage_out["prompt_tokens_details"]
assert isinstance(prompt_details, dict), response.text
assert prompt_details["cache_creation_token_details"] == {
"ephemeral_5m_input_tokens": 0,
"ephemeral_1h_input_tokens": 7225,
}, response.text
assert usage_out["cache_creation_input_tokens"] == 7225, response.text
assert len(wire.drain()) == 1

View file

@ -0,0 +1,242 @@
"""claude-fable-5-2 on Vertex AI: the row's capability flags shape the rawPredict body the proxy sends.
Fable rejects sampling params and forced tool choice and always thinks, so the proxy must strip
or refuse those before the request reaches Vertex, the same way it does on the Anthropic API.
"""
import json
import uuid
from collections.abc import Callable
from typing import Final
from urllib.parse import parse_qs
from integration._support.client import Gateway, Scenario
from integration._support.vertex import service_account_json
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue, TypeAdapter
_MODEL: Final = "claude-fable-5-2"
_PROJECT: Final = "scripted-project"
_LOCATION: Final = "global"
_TARGET: Final = f"/v1/projects/{_PROJECT}/locations/{_LOCATION}/publishers/anthropic/models/{_MODEL}:rawPredict"
_TOKEN: Final = "scripted-vertex-access-token"
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_FORCED_TOOL_CHOICES: Final[tuple[JsonValue, ...]] = ("required", {"type": "function", "function": {"name": "lookup"}})
_TOOLS: Final[list[JsonValue]] = [
{
"type": "function",
"function": {
"name": "lookup",
"description": "Look up a synthetic record.",
"parameters": {
"type": "object",
"properties": {"record_id": {"type": "string"}},
"required": ["record_id"],
},
},
}
]
def _reply(identity: str, usage: dict[str, JsonValue] | None = None) -> bytes:
return json.dumps(
{
"id": identity,
"type": "message",
"role": "assistant",
"model": _MODEL,
"content": [
{"type": "thinking", "thinking": "scripted thought", "signature": "scripted-signature"},
{"type": "text", "text": "done"},
],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": usage or {"input_tokens": 12, "output_tokens": 3},
}
).encode()
def _deployment(scenario: Scenario, api_base: str, *, drop_params: bool = False) -> str:
credentials: Final = service_account_json(_PROJECT, api_base)
if drop_params:
return scenario.model(
model=f"vertex_ai/{_MODEL}",
api_base=api_base,
api_key=None,
vertex_project=_PROJECT,
vertex_location=_LOCATION,
vertex_credentials=credentials,
num_retries=0,
drop_params=True,
)
return scenario.model(
model=f"vertex_ai/{_MODEL}",
api_base=api_base,
api_key=None,
vertex_project=_PROJECT,
vertex_location=_LOCATION,
vertex_credentials=credentials,
num_retries=0,
)
def _peer(on_body: Callable[[dict[str, JsonValue]], bytes]) -> Callable[[Request], Reply]:
def respond(request: Request) -> Reply:
if request.target == "/_oauth/token":
form: Final = parse_qs(request.body.decode())
assert form["grant_type"] == ["urn:ietf:params:oauth:grant-type:jwt-bearer"], form
return Reply(body=json.dumps({"access_token": _TOKEN, "token_type": "Bearer", "expires_in": 3600}).encode())
assert request.method == "POST" and request.target == _TARGET, (request.method, request.target)
assert request.headers["authorization"] == f"Bearer {_TOKEN}", request.headers
body: Final = _JSON_OBJECT.validate_json(request.body)
assert body["anthropic_version"] == "vertex-2023-10-16", body
return Reply(body=on_body(body))
return respond
def _chat_body(model: str, **extra: JsonValue) -> dict[str, JsonValue]:
return {
"model": model,
"messages": [{"role": "user", "content": "synthetic fable request"}],
"max_tokens": 64,
**extra,
}
def _model_calls(requests: tuple[Request, ...]) -> int:
return sum(request.target == _TARGET for request in requests)
def test_drop_params_removes_temperature_and_top_p_before_vertex(gateway: Gateway) -> None:
identity: Final = f"msg_{uuid.uuid4().hex}"
def on_body(body: dict[str, JsonValue]) -> bytes:
assert "temperature" not in body and "top_p" not in body, body
assert body["max_tokens"] == 64, body
return _reply(identity)
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url, drop_params=True)
response: Final = gateway.request("POST", "/v1/chat/completions", _chat_body(model, temperature=0.2, top_p=0.9))
assert response.status_code == 200, response.text
assert response.json()["choices"][0]["message"]["content"] == "done", response.text
assert _model_calls(wire.drain()) == 1
def test_sampling_params_are_refused_before_reaching_vertex_without_drop_params(gateway: Gateway) -> None:
def on_body(body: dict[str, JsonValue]) -> bytes:
raise AssertionError(f"temperature must not reach Vertex: {body}")
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url)
response: Final = gateway.request("POST", "/v1/chat/completions", _chat_body(model, temperature=0.2))
assert response.status_code == 400, response.text
assert "temperature" in response.text and "drop_params" in response.text, response.text
assert _model_calls(wire.drain()) == 0
def test_drop_params_downgrades_forced_and_named_tool_choice_to_auto(gateway: Gateway) -> None:
identity: Final = f"msg_{uuid.uuid4().hex}"
def on_body(body: dict[str, JsonValue]) -> bytes:
assert body["tool_choice"] == {"type": "auto"}, body
tools: Final = body["tools"]
assert isinstance(tools, list) and [tool["name"] for tool in tools if isinstance(tool, dict)] == ["lookup"], (
body
)
return _reply(identity)
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url, drop_params=True)
responses: Final = tuple(
gateway.request("POST", "/v1/chat/completions", _chat_body(model, tools=_TOOLS, tool_choice=tool_choice))
for tool_choice in _FORCED_TOOL_CHOICES
)
assert [response.status_code for response in responses] == [200, 200], [response.text for response in responses]
assert _model_calls(wire.drain()) == 2
def test_forced_tool_choice_is_refused_before_reaching_vertex_without_drop_params(gateway: Gateway) -> None:
def on_body(body: dict[str, JsonValue]) -> bytes:
raise AssertionError(f"forced tool_choice must not reach Vertex: {body}")
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url)
response: Final = gateway.request(
"POST", "/v1/chat/completions", _chat_body(model, tools=_TOOLS, tool_choice="required")
)
assert response.status_code == 400, response.text
assert "tool_choice" in response.text, response.text
assert _model_calls(wire.drain()) == 0
def test_thinking_disabled_is_dropped_for_an_always_on_model(gateway: Gateway) -> None:
identity: Final = f"msg_{uuid.uuid4().hex}"
def on_body(body: dict[str, JsonValue]) -> bytes:
assert "thinking" not in body, body
return _reply(identity)
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url)
response: Final = gateway.request(
"POST", "/v1/chat/completions", _chat_body(model, thinking={"type": "disabled"})
)
assert response.status_code == 200, response.text
assert response.json()["choices"][0]["message"]["reasoning_content"] == "scripted thought", response.text
assert _model_calls(wire.drain()) == 1
def test_reasoning_effort_becomes_adaptive_thinking_with_output_config_effort(gateway: Gateway) -> None:
identity: Final = f"msg_{uuid.uuid4().hex}"
def on_body(body: dict[str, JsonValue]) -> bytes:
assert body["thinking"] == {"type": "adaptive", "display": "summarized"}, body
assert body["output_config"] == {"effort": "max"}, body
assert "budget_tokens" not in json.dumps(body), body
return _reply(identity)
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url)
response: Final = gateway.request("POST", "/v1/chat/completions", _chat_body(model, reasoning_effort="max"))
assert response.status_code == 200, response.text
assert _model_calls(wire.drain()) == 1
def test_messages_one_hour_cache_control_reaches_vertex_and_its_usage_comes_back(gateway: Gateway) -> None:
identity: Final = f"msg_{uuid.uuid4().hex}"
system: Final[list[JsonValue]] = [
{"type": "text", "text": "synthetic cached system prompt", "cache_control": {"type": "ephemeral", "ttl": "1h"}}
]
usage: Final[dict[str, JsonValue]] = {
"input_tokens": 9,
"output_tokens": 3,
"cache_creation_input_tokens": 7225,
"cache_read_input_tokens": 0,
"cache_creation": {"ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 7225},
}
def on_body(body: dict[str, JsonValue]) -> bytes:
assert body["system"] == system, body
return _reply(identity, usage)
with wire_server(_peer(on_body)) as wire, gateway.scenario() as scenario:
model: Final = _deployment(scenario, wire.url)
response: Final = gateway.request(
"POST",
"/v1/messages",
{
"model": model,
"max_tokens": 64,
"system": system,
"messages": [{"role": "user", "content": "synthetic fable request"}],
},
)
assert response.status_code == 200, response.text
assert response.json()["usage"]["cache_creation"] == {
"ephemeral_5m_input_tokens": 0,
"ephemeral_1h_input_tokens": 7225,
}, response.text
assert response.json()["usage"]["cache_creation_input_tokens"] == 7225, response.text
assert _model_calls(wire.drain()) == 1

View file

@ -98,6 +98,13 @@ def test_adaptive_thinking_detected_for_fable_5_1(local_model_cost_map, model):
FABLE_5_2_VARIANTS = (
"claude-fable-5-2",
"anthropic.claude-fable-5-2",
"global.anthropic.claude-fable-5-2",
"us.anthropic.claude-fable-5-2",
"eu.anthropic.claude-fable-5-2",
"us-gov.anthropic.claude-fable-5-2",
"bedrock/us-gov-west-1/anthropic.claude-fable-5-2",
"bedrock/us-gov-east-1/anthropic.claude-fable-5-2",
"vertex_ai/claude-fable-5-2",
"vertex_ai/claude-fable-5-2@default",
"bedrock/claude_platform/claude-fable-5-2",
@ -118,6 +125,11 @@ def test_fable_5_2_present_in_bundled_backup() -> None:
"claude-fable-5-2",
"anthropic/claude-fable-5-2",
"claude-fable-latest",
"anthropic.claude-fable-5-2",
"bedrock/us.anthropic.claude-fable-5-2",
"bedrock/invoke/eu.anthropic.claude-fable-5-2",
"bedrock/global.anthropic.claude-fable-5-2",
"bedrock/claude-fable-latest",
"vertex_ai/claude-fable-5-2",
"vertex_ai/claude-fable-5-2@default",
"bedrock/claude_platform/claude-fable-5-2",
@ -129,6 +141,10 @@ def test_adaptive_thinking_detected_for_fable_5_2(local_model_cost_map, model: s
assert AnthropicModelInfo.is_adaptive_thinking_model(model, "anthropic") is True
def test_fable_5_2_registered_for_bedrock_converse() -> None:
assert "anthropic.claude-fable-5-2" in BEDROCK_CONVERSE_MODELS
def test_fable_5_2_fast_mode_doubles_every_token_price_on_the_anthropic_api(local_model_cost_map) -> None:
"""Anthropic bills fast mode at 2x the standard rate on every token type
(platform.claude.com/docs/en/about-claude/pricing, read 2026-11-09), and the