test(integration): streamed request bills the same SpendLogs row as the non-streamed request (#45730)

* test(integration): streamed request bills the same SpendLogs row as the non-streamed request

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

* test(integration): write the stream parity cells as StreamParityTestCase literals

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

* test(integration): express stream parity cells as CostTrackingTestCase pairs

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

* test(integration): stream parity asserts exactly one SpendLogs row per leg after a settle window

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

* test(integration): stream parity forces a spend-log flush with a /v1/responses row instead of a settle sleep

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

* test(integration): stream parity covers messages -> chat and messages -> converse bridges

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

* test(integration): stream parity bridges use xai/grok-4.7 and us.anthropic.claude-opus-5-5

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

* test(integration): use latest frontier models in stream parity cells (gpt-5.6, claude-opus-5-5)

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

* test(integration): switch OpenAI parity cells to gpt-6.1-sol and the chat -> responses bridge to gpt-5.5-pro

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

---------

Co-authored-by: kerry <kerry@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-10-10 15:22:20 -07:00 • committed by GitHub
parent 6fe46522d3
commit ff60d27b60
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 857 additions and 11 deletions

View file

@ -642,20 +642,20 @@
"gpt-realtime-mini-2025-12-15": {
"litellm_provider": "openai",
"mode": "realtime",
"input_cost_per_token": 6.0e-07,
"input_cost_per_token": 6e-07,
"output_cost_per_token": 2.4e-06,
"input_cost_per_audio_token": 1.0e-05,
"cache_read_input_token_cost": 6.0e-08,
"cache_read_input_audio_token_cost": 3.0e-07,
"output_cost_per_audio_token": 2.0e-05
"input_cost_per_audio_token": 1e-05,
"cache_read_input_token_cost": 6e-08,
"cache_read_input_audio_token_cost": 3e-07,
"output_cost_per_audio_token": 2e-05
},
"gpt-realtime-2.1": {
"litellm_provider": "openai",
"mode": "realtime",
"input_cost_per_token": 4.0e-06,
"input_cost_per_token": 4e-06,
"input_cost_per_audio_token": 3.2e-05,
"cache_read_input_token_cost": 4.0e-07,
"cache_read_input_audio_token_cost": 4.0e-07,
"cache_read_input_token_cost": 4e-07,
"cache_read_input_audio_token_cost": 4e-07,
"output_cost_per_token": 2.4e-05,
"output_cost_per_audio_token": 6.4e-05
},
@ -769,6 +769,52 @@
"search_context_size_medium": 0.014
},
"web_search_billing_unit": "per_query"
},
"xai/grok-4.7": {
"litellm_provider": "xai",
"mode": "chat",
"input_cost_per_token": 2e-06,
"output_cost_per_token": 6e-06,
"cache_read_input_token_cost": 5e-07,
"max_tokens": 500000,
"max_input_tokens": 500000,
"max_output_tokens": 500000,
"supports_function_calling": true
},
"us.anthropic.claude-opus-5-5": {
"litellm_provider": "bedrock_converse",
"mode": "chat",
"input_cost_per_token": 4.4e-06,
"output_cost_per_token": 2.2e-05,
"cache_read_input_token_cost": 2.2e-07,
"cache_creation_input_token_cost": 5.5e-06,
"max_tokens": 128000,
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"supports_function_calling": true
},
"claude-opus-5-5": {
"litellm_provider": "anthropic",
"mode": "chat",
"input_cost_per_token": 4e-06,
"output_cost_per_token": 2e-05,
"cache_read_input_token_cost": 2e-07,
"cache_creation_input_token_cost": 5e-06,
"max_tokens": 128000,
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"supports_function_calling": true
},
"gpt-6.1-sol": {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 2e-06,
"output_cost_per_token": 1e-05,
"cache_read_input_token_cost": 1e-07,
"max_tokens": 128000,
"max_input_tokens": 922000,
"max_output_tokens": 128000,
"supports_function_calling": true
}
},
"cases": [
@ -29687,7 +29733,13 @@
}
}
},
"expected": {"spend": 0.0025118, "input_cost": 0.0020792, "output_cost": 0.0004326, "prompt_tokens": 1840, "completion_tokens": 412}
"expected": {
"spend": 0.0025118,
"input_cost": 0.0020792,
"output_cost": 0.0004326,
"prompt_tokens": 1840,
"completion_tokens": 412
}
},
{
"name": "deepseek-deepseek-v4-chat-prompt_cache_hit",
@ -32102,6 +32154,190 @@
"prompt_tokens": 100,
"completion_tokens": 100
}
},
{
"name": "xai-grok-4.7-input_text",
"covers": "quota_management.spend_tracking.cost_matrix.logs_cost",
"model": "xai/grok-4.7",
"request": {
"model": "$MODEL",
"messages": [
{
"role": "user",
"content": "Say hello."
}
],
"stream": false,
"allowed_openai_params": []
},
"response": {
"content_type": "application/json",
"body": {
"id": "chatcmpl-$REQUEST_ID",
"object": "chat.completion",
"model": "grok-4.7",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 1840,
"completion_tokens": 412,
"total_tokens": 2252
}
}
},
"expected": {
"spend": 0.006152,
"input_cost": 0.00368,
"output_cost": 0.002472,
"prompt_tokens": 1840,
"completion_tokens": 412
}
},
{
"name": "us.anthropic.claude-opus-5-5-input_text",
"covers": "quota_management.spend_tracking.cost_matrix.logs_cost",
"model": "us.anthropic.claude-opus-5-5",
"request": {
"model": "$MODEL",
"messages": [
{
"role": "user",
"content": "Say hello."
}
],
"stream": false,
"allowed_openai_params": []
},
"response": {
"content_type": "application/json",
"body": {
"output": {
"message": {
"role": "assistant",
"content": [
{
"text": "Hello."
}
]
}
},
"stopReason": "end_turn",
"usage": {
"inputTokens": 1840,
"outputTokens": 412,
"totalTokens": 2252
},
"metrics": {
"latencyMs": 42
}
}
},
"expected": {
"spend": 0.01716,
"input_cost": 0.008096,
"output_cost": 0.009064,
"prompt_tokens": 1840,
"completion_tokens": 412
}
},
{
"name": "claude-opus-5-5-input_text",
"covers": "quota_management.spend_tracking.cost_matrix.logs_cost",
"model": "claude-opus-5-5",
"request": {
"model": "$MODEL",
"messages": [
{
"role": "user",
"content": "Say hello."
}
],
"stream": false,
"allowed_openai_params": []
},
"response": {
"content_type": "application/json",
"body": {
"id": "msg_$REQUEST_ID",
"type": "message",
"role": "assistant",
"model": "claude-opus-5-5",
"content": [
{
"type": "text",
"text": "Hello."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 1840,
"output_tokens": 412
}
}
},
"expected": {
"spend": 0.0156,
"input_cost": 0.00736,
"output_cost": 0.00824,
"prompt_tokens": 1840,
"completion_tokens": 412
}
},
{
"name": "gpt-6.1-sol-input_text",
"covers": "quota_management.spend_tracking.cost_matrix.logs_cost",
"model": "gpt-6.1-sol",
"request": {
"model": "$MODEL",
"messages": [
{
"role": "user",
"content": "Say hello."
}
],
"stream": false,
"allowed_openai_params": []
},
"response": {
"content_type": "application/json",
"body": {
"id": "chatcmpl-$REQUEST_ID",
"object": "chat.completion",
"created": 1,
"model": "gpt-6.1-sol",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 1840,
"completion_tokens": 412,
"total_tokens": 2252
}
}
},
"expected": {
"spend": 0.0078,
"input_cost": 0.00368,
"output_cost": 0.00412,
"prompt_tokens": 1840,
"completion_tokens": 412
}
}
],
"batch_cases": [
@ -32149,8 +32385,8 @@
],
"expected": {
"spend": 0.000126,
"input_cost": 0.000056,
"output_cost": 0.00007,
"input_cost": 5.6e-05,
"output_cost": 7e-05,
"prompt_tokens": 100,
"completion_tokens": 10,
"cost_header": false

View file

@ -0,0 +1,123 @@
from typing import Final
from integration.cost_calculation.cost_tracking_case import (
CostTrackingTestCase,
Deployment,
ExactExpected,
JsonResponse,
SseResponse,
)
from integration.cost_calculation.stream_parity.case import COVERS, MODEL, REQUEST_ID, StreamParityTestCase, sse_frames
CLAUDE_OPUS_5_5_MESSAGES: Final = CostTrackingTestCase(
name="claude-opus-5-5-messages-parity",
covers=COVERS,
model="claude-opus-5-5",
endpoint="/v1/messages",
deployment=Deployment(input_cost_per_token=0.001, output_cost_per_token=0.002),
request={
"model": MODEL,
"max_tokens": 64,
"messages": [{"role": "user", "content": "Say hello."}],
"cache": {"no-cache": True},
},
response=JsonResponse(
content_type="application/json",
body={
"id": f"msg_{REQUEST_ID}",
"type": "message",
"role": "assistant",
"model": "claude-opus-5-5",
"content": [{"type": "text", "text": "Hello."}],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 30, "output_tokens": 40},
},
),
expected=ExactExpected(spend=0.11, input_cost=0.03, output_cost=0.08, prompt_tokens=30, completion_tokens=40),
)
CLAUDE_OPUS_5_5_STREAM_RESPONSE: Final = SseResponse(
content_type="text/event-stream",
frames=sse_frames(
{
"type": "message_start",
"message": {
"id": f"msg_{REQUEST_ID}",
"type": "message",
"role": "assistant",
"model": "claude-opus-5-5",
"content": [],
"stop_reason": None,
"stop_sequence": None,
"usage": {"input_tokens": 30, "output_tokens": 1},
},
},
{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}},
{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hello."}},
{"type": "content_block_stop", "index": 0},
{
"type": "message_delta",
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
"usage": {"output_tokens": 40},
},
{"type": "message_stop"},
),
)
CLAUDE_OPUS_5_5_MESSAGES_PARITY: Final = StreamParityTestCase(
plain=CLAUDE_OPUS_5_5_MESSAGES,
streamed=CLAUDE_OPUS_5_5_MESSAGES.model_copy(
update={
"name": "claude-opus-5-5-messages-parity-stream",
"request": {**CLAUDE_OPUS_5_5_MESSAGES.request, "stream": True},
"response": CLAUDE_OPUS_5_5_STREAM_RESPONSE,
}
),
)
CLAUDE_OPUS_5_5_CHAT_COMPLETIONS: Final = CLAUDE_OPUS_5_5_MESSAGES.model_copy(
update={
"name": "claude-opus-5-5-chat-parity",
"endpoint": "/v1/chat/completions",
"request": {
"model": MODEL,
"messages": [{"role": "user", "content": "Say hello."}],
"cache": {"no-cache": True},
},
}
)
CLAUDE_OPUS_5_5_CHAT_COMPLETIONS_PARITY: Final = StreamParityTestCase(
plain=CLAUDE_OPUS_5_5_CHAT_COMPLETIONS,
streamed=CLAUDE_OPUS_5_5_CHAT_COMPLETIONS.model_copy(
update={
"name": "claude-opus-5-5-chat-parity-stream",
"request": {
**CLAUDE_OPUS_5_5_CHAT_COMPLETIONS.request,
"stream": True,
"stream_options": {"include_usage": True},
},
"response": CLAUDE_OPUS_5_5_STREAM_RESPONSE,
}
),
)
CLAUDE_OPUS_5_5_RESPONSES: Final = CLAUDE_OPUS_5_5_MESSAGES.model_copy(
update={
"name": "claude-opus-5-5-responses-parity",
"endpoint": "/v1/responses",
"request": {"model": MODEL, "input": "Say hello.", "cache": {"no-cache": True}},
}
)
CLAUDE_OPUS_5_5_RESPONSES_PARITY: Final = StreamParityTestCase(
plain=CLAUDE_OPUS_5_5_RESPONSES,
streamed=CLAUDE_OPUS_5_5_RESPONSES.model_copy(
update={
"name": "claude-opus-5-5-responses-parity-stream",
"request": {**CLAUDE_OPUS_5_5_RESPONSES.request, "stream": True},
"response": CLAUDE_OPUS_5_5_STREAM_RESPONSE,
}
),
)

View file

@ -0,0 +1,62 @@
"""/v1/messages on a Bedrock Converse deployment: the messages -> converse bridge."""
from typing import Final
from integration.cost_calculation.cost_tracking_case import (
CostTrackingTestCase,
Deployment,
EventStreamEvent,
EventStreamResponse,
ExactExpected,
JsonResponse,
)
from integration.cost_calculation.stream_parity.case import COVERS, MODEL, StreamParityTestCase
_USAGE: Final = {"inputTokens": 30, "outputTokens": 40, "totalTokens": 70}
CLAUDE_OPUS_5_5_CONVERSE_MESSAGES: Final = CostTrackingTestCase(
name="bedrock-converse-claude-opus-5-5-messages-parity",
covers=COVERS,
model="us.anthropic.claude-opus-5-5",
endpoint="/v1/messages",
deployment=Deployment(input_cost_per_token=0.001, output_cost_per_token=0.002),
request={
"model": MODEL,
"max_tokens": 64,
"messages": [{"role": "user", "content": "Say hello."}],
"cache": {"no-cache": True},
},
response=JsonResponse(
content_type="application/json",
body={
"output": {"message": {"role": "assistant", "content": [{"text": "Hello."}]}},
"stopReason": "end_turn",
"usage": _USAGE,
"metrics": {"latencyMs": 1},
},
),
expected=ExactExpected(spend=0.11, input_cost=0.03, output_cost=0.08, prompt_tokens=30, completion_tokens=40),
)
CLAUDE_OPUS_5_5_CONVERSE_MESSAGES_PARITY: Final = StreamParityTestCase(
plain=CLAUDE_OPUS_5_5_CONVERSE_MESSAGES,
streamed=CLAUDE_OPUS_5_5_CONVERSE_MESSAGES.model_copy(
update={
"name": "bedrock-converse-claude-opus-5-5-messages-parity-stream",
"request": {**CLAUDE_OPUS_5_5_CONVERSE_MESSAGES.request, "stream": True},
"response": EventStreamResponse(
content_type="application/vnd.amazon.eventstream",
framing="converse",
events=(
EventStreamEvent(event_type="messageStart", payload={"role": "assistant"}),
EventStreamEvent(
event_type="contentBlockDelta", payload={"delta": {"text": "Hello."}, "contentBlockIndex": 0}
),
EventStreamEvent(event_type="contentBlockStop", payload={"contentBlockIndex": 0}),
EventStreamEvent(event_type="messageStop", payload={"stopReason": "end_turn"}),
EventStreamEvent(event_type="metadata", payload={"usage": _USAGE, "metrics": {"latencyMs": 1}}),
),
),
}
),
)

View file

@ -0,0 +1,91 @@
from typing import Final
from integration.cost_calculation.cost_tracking_case import (
CostTrackingTestCase,
Deployment,
ExactExpected,
JsonResponse,
SseResponse,
)
from integration.cost_calculation.stream_parity.bases.openai_responses import (
GPT_6_1_SOL_RESPONSES,
GPT_6_1_SOL_RESPONSES_PARITY_STREAM_RESPONSE,
)
from integration.cost_calculation.stream_parity.case import COVERS, MODEL, REQUEST_ID, StreamParityTestCase, sse_frames
_USAGE: Final = {"prompt_tokens": 30, "completion_tokens": 40, "total_tokens": 70}
_CHUNK: Final = {
"id": f"chatcmpl-{REQUEST_ID}",
"object": "chat.completion.chunk",
"created": 1,
"model": "gpt-6.1-sol",
}
GPT_6_1_SOL_CHAT_COMPLETIONS: Final = CostTrackingTestCase(
name="gpt-6.1-sol-chat-parity",
covers=COVERS,
model="gpt-6.1-sol",
endpoint="/v1/chat/completions",
deployment=Deployment(input_cost_per_token=0.001, output_cost_per_token=0.002),
request={"model": MODEL, "messages": [{"role": "user", "content": "Say hello."}], "cache": {"no-cache": True}},
response=JsonResponse(
content_type="application/json",
body={
"id": f"chatcmpl-{REQUEST_ID}",
"object": "chat.completion",
"created": 1,
"model": "gpt-6.1-sol",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "Hello."}, "finish_reason": "stop"}],
"usage": _USAGE,
},
),
expected=ExactExpected(spend=0.11, input_cost=0.03, output_cost=0.08, prompt_tokens=30, completion_tokens=40),
)
GPT_6_1_SOL_CHAT_COMPLETIONS_PARITY: Final = StreamParityTestCase(
plain=GPT_6_1_SOL_CHAT_COMPLETIONS,
streamed=GPT_6_1_SOL_CHAT_COMPLETIONS.model_copy(
update={
"name": "gpt-6.1-sol-chat-parity-stream",
"request": {
**GPT_6_1_SOL_CHAT_COMPLETIONS.request,
"stream": True,
"stream_options": {"include_usage": True},
},
"response": SseResponse(
content_type="text/event-stream",
frames=sse_frames(
{**_CHUNK, "choices": [{"index": 0, "delta": {"role": "assistant", "content": "Hello."}}]},
{**_CHUNK, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]},
{**_CHUNK, "choices": [], "usage": _USAGE},
done=True,
),
),
}
),
)
GPT_6_1_SOL_MESSAGES: Final = GPT_6_1_SOL_RESPONSES.model_copy(
update={
"name": "gpt-6.1-sol-messages-parity",
"model": "gpt-6.1-sol",
"endpoint": "/v1/messages",
"request": {
"model": MODEL,
"max_tokens": 64,
"messages": [{"role": "user", "content": "Say hello."}],
"cache": {"no-cache": True},
},
}
)
GPT_6_1_SOL_MESSAGES_PARITY: Final = StreamParityTestCase(
plain=GPT_6_1_SOL_MESSAGES,
streamed=GPT_6_1_SOL_MESSAGES.model_copy(
update={
"name": "gpt-6.1-sol-messages-parity-stream",
"request": {**GPT_6_1_SOL_MESSAGES.request, "stream": True},
"response": GPT_6_1_SOL_RESPONSES_PARITY_STREAM_RESPONSE,
}
),
)

View file

@ -0,0 +1,115 @@
from typing import Final
from integration.cost_calculation.cost_tracking_case import (
CostTrackingTestCase,
Deployment,
ExactExpected,
JsonResponse,
SseResponse,
)
from integration.cost_calculation.stream_parity.case import COVERS, MODEL, REQUEST_ID, StreamParityTestCase, sse_frames
_ITEM: Final = {
"id": "msg_1",
"type": "message",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": "Hello.", "annotations": []}],
}
_RESPONSE: Final = {
"id": f"resp_{REQUEST_ID}",
"object": "response",
"created_at": 1,
"status": "completed",
"model": "gpt-6.1-sol",
"output": [_ITEM],
"usage": {"input_tokens": 30, "output_tokens": 40, "total_tokens": 70},
}
GPT_6_1_SOL_RESPONSES: Final = CostTrackingTestCase(
name="gpt-6.1-sol-responses-parity",
covers=COVERS,
model="gpt-6.1-sol",
endpoint="/v1/responses",
deployment=Deployment(input_cost_per_token=0.001, output_cost_per_token=0.002),
request={"model": MODEL, "input": "Say hello.", "cache": {"no-cache": True}},
response=JsonResponse(content_type="application/json", body=_RESPONSE),
expected=ExactExpected(spend=0.11, input_cost=0.03, output_cost=0.08, prompt_tokens=30, completion_tokens=40),
)
GPT_6_1_SOL_RESPONSES_PARITY_STREAM_RESPONSE: Final = SseResponse(
content_type="text/event-stream",
frames=sse_frames(
{
"type": "response.created",
"sequence_number": 0,
"response": {**_RESPONSE, "status": "in_progress", "output": []},
},
{
"type": "response.output_item.added",
"sequence_number": 1,
"output_index": 0,
"item": {**_ITEM, "status": "in_progress", "content": []},
},
{
"type": "response.output_text.delta",
"sequence_number": 2,
"item_id": "msg_1",
"output_index": 0,
"content_index": 0,
"delta": "Hello.",
},
{"type": "response.output_item.done", "sequence_number": 3, "output_index": 0, "item": _ITEM},
{"type": "response.completed", "sequence_number": 4, "response": _RESPONSE},
),
)
GPT_6_1_SOL_RESPONSES_PARITY: Final = StreamParityTestCase(
plain=GPT_6_1_SOL_RESPONSES,
streamed=GPT_6_1_SOL_RESPONSES.model_copy(
update={
"name": "gpt-6.1-sol-responses-parity-stream",
"request": {**GPT_6_1_SOL_RESPONSES.request, "stream": True},
"response": GPT_6_1_SOL_RESPONSES_PARITY_STREAM_RESPONSE,
}
),
)
_PRO_RESPONSE: Final = {**_RESPONSE, "model": "gpt-5.5-pro"}
GPT_5_5_PRO_STREAM_RESPONSE: Final = GPT_6_1_SOL_RESPONSES_PARITY_STREAM_RESPONSE.model_copy(
update={
"frames": tuple(
frame.replace('"model": "gpt-6.1-sol"', '"model": "gpt-5.5-pro"')
for frame in GPT_6_1_SOL_RESPONSES_PARITY_STREAM_RESPONSE.frames
)
}
)
GPT_5_5_PRO_CHAT_COMPLETIONS: Final = GPT_6_1_SOL_RESPONSES.model_copy(
update={
"name": "gpt-5.5-pro-chat-parity",
"model": "gpt-5.5-pro",
"response": JsonResponse(content_type="application/json", body=_PRO_RESPONSE),
"endpoint": "/v1/chat/completions",
"request": {
"model": MODEL,
"messages": [{"role": "user", "content": "Say hello."}],
"cache": {"no-cache": True},
},
}
)
GPT_5_5_PRO_CHAT_COMPLETIONS_PARITY: Final = StreamParityTestCase(
plain=GPT_5_5_PRO_CHAT_COMPLETIONS,
streamed=GPT_5_5_PRO_CHAT_COMPLETIONS.model_copy(
update={
"name": "gpt-5.5-pro-chat-parity-stream",
"request": {
**GPT_5_5_PRO_CHAT_COMPLETIONS.request,
"stream": True,
"stream_options": {"include_usage": True},
},
"response": GPT_5_5_PRO_STREAM_RESPONSE,
}
),
)

View file

@ -0,0 +1,65 @@
"""/v1/messages on a chat-completions provider: the messages -> chat/completions bridge."""
from typing import Final
from integration.cost_calculation.cost_tracking_case import (
CostTrackingTestCase,
Deployment,
ExactExpected,
JsonResponse,
SseResponse,
)
from integration.cost_calculation.stream_parity.case import COVERS, MODEL, REQUEST_ID, StreamParityTestCase, sse_frames
_USAGE: Final = {"prompt_tokens": 30, "completion_tokens": 40, "total_tokens": 70}
_CHUNK: Final = {
"id": f"chatcmpl-{REQUEST_ID}",
"object": "chat.completion.chunk",
"created": 1,
"model": "grok-4.7",
}
GROK_4_7_MESSAGES: Final = CostTrackingTestCase(
name="grok-4.7-messages-parity",
covers=COVERS,
model="xai/grok-4.7",
endpoint="/v1/messages",
deployment=Deployment(input_cost_per_token=0.001, output_cost_per_token=0.002),
request={
"model": MODEL,
"max_tokens": 64,
"messages": [{"role": "user", "content": "Say hello."}],
"cache": {"no-cache": True},
},
response=JsonResponse(
content_type="application/json",
body={
"id": f"chatcmpl-{REQUEST_ID}",
"object": "chat.completion",
"created": 1,
"model": "grok-4.7",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "Hello."}, "finish_reason": "stop"}],
"usage": _USAGE,
},
),
expected=ExactExpected(spend=0.11, input_cost=0.03, output_cost=0.08, prompt_tokens=30, completion_tokens=40),
)
GROK_4_7_MESSAGES_PARITY: Final = StreamParityTestCase(
plain=GROK_4_7_MESSAGES,
streamed=GROK_4_7_MESSAGES.model_copy(
update={
"name": "grok-4.7-messages-parity-stream",
"request": {**GROK_4_7_MESSAGES.request, "stream": True},
"response": SseResponse(
content_type="text/event-stream",
frames=sse_frames(
{**_CHUNK, "choices": [{"index": 0, "delta": {"role": "assistant", "content": "Hello."}}]},
{**_CHUNK, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]},
{**_CHUNK, "choices": [], "usage": _USAGE},
done=True,
),
),
}
),
)

View file

@ -0,0 +1,44 @@
import json
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Final
from integration.cost_calculation.cost_tracking_case import CostTrackingTestCase
from pydantic import JsonValue
COVERS: Final = "quota_management.spend_tracking.scripted_wire.logs_cost"
REQUEST_ID: Final = "$REQUEST_ID"
MODEL: Final = "$MODEL"
def sse_frames(*events: Mapping[str, JsonValue], done: bool = False) -> tuple[str, ...]:
frames: Final = tuple(
f"event: {event['type']}\ndata: {json.dumps(event)}" if "type" in event else f"data: {json.dumps(event)}"
for event in events
)
return (*frames, "data: [DONE]") if done else frames
@dataclass(frozen=True, slots=True, kw_only=True)
class StreamParityTestCase:
"""The same request as two CostTrackingTestCases, `plain` answered with JSON and `streamed` answered with SSE,
that must both bill the one `expected` row."""
plain: CostTrackingTestCase
streamed: CostTrackingTestCase
def __post_init__(self) -> None:
if self.plain.expected != self.streamed.expected:
raise ValueError(f"{self.id}: plain and streamed cases expect different rows")
if self.streamed.request.get("stream") is not True or self.plain.request.get("stream") is True:
raise ValueError(f"{self.id}: streamed case must set stream: true and the plain case must not")
if (self.plain.model, self.plain.endpoint, self.plain.deployment) != (
self.streamed.model,
self.streamed.endpoint,
self.streamed.deployment,
):
raise ValueError(f"{self.id}: plain and streamed cases must share model, endpoint and deployment")
@property
def id(self) -> str:
return self.plain.name

View file

@ -0,0 +1,70 @@
from collections import Counter
from hashlib import sha256
from typing import Final
from integration._support.client import Gateway, Scenario
from integration.cost_calculation.assertions import assert_exact
from integration.cost_calculation.conftest import (
poll_rows,
poll_rows_where,
read_rows_now,
register_scenario_deployment,
)
from integration.cost_calculation.cost_tracking_case import ExactExpected
from integration.cost_calculation.stream_parity.bases import openai_responses
from integration.cost_calculation.stream_parity.case import StreamParityTestCase
FLUSH_CASE: Final = openai_responses.GPT_6_1_SOL_RESPONSES
def flush_spend_logs(scenario: Scenario, gateway: Gateway, key: str) -> str:
"""A /v1/responses row wakes the spend-log writer at once, and the queue is FIFO, so every row queued before it lands."""
flush: Final = register_scenario_deployment(scenario, FLUSH_CASE, "flush", key)
response: Final = gateway.request(
"POST", FLUSH_CASE.endpoint, {**FLUSH_CASE.request, "model": flush.model_name}, key=key
)
assert response.status_code == 200, ("flush", response.text[:400])
poll_rows_where(key, 1, lambda row: row.model_id == flush.identity)
return flush.identity
def assert_stream_parity(case: StreamParityTestCase, gateway: Gateway) -> None:
expected: Final = case.plain.expected
assert isinstance(expected, ExactExpected), case.id
with gateway.scenario() as scenario:
key: Final = scenario.key()
deployments: Final = {
leg.name: register_scenario_deployment(scenario, leg, sha256(leg.name.encode()).hexdigest()[:12], key)
for leg in (case.plain, case.streamed)
}
responses: Final = {
leg.name: gateway.request(
"POST", leg.endpoint, {**leg.request, "model": deployments[leg.name].model_name}, key=key
)
for leg in (case.plain, case.streamed)
}
for leg in (case.plain, case.streamed):
assert responses[leg.name].status_code == 200, (leg.name, responses[leg.name].text[:400])
poll_rows(key, 2)
flush_identity: Final = flush_spend_logs(scenario, gateway, key)
rows: Final = read_rows_now(key)
rows_per_deployment: Final = Counter(row.model_id for row in rows)
expected_rows: Final = {deployments[leg.name].identity: 1 for leg in (case.plain, case.streamed)}
assert rows_per_deployment == {**expected_rows, flush_identity: 1}, (
f"{case.id}: expected exactly one SpendLogs row per leg plus the flush row, got {dict(rows_per_deployment)}",
rows,
)
billed: Final = {
leg.name: next(row for row in rows if row.model_id == deployments[leg.name].identity)
for leg in (case.plain, case.streamed)
}
for leg in (case.plain, case.streamed):
assert_exact(leg.name, leg.response.content_type, expected, billed[leg.name], responses[leg.name])
plain: Final = billed[case.plain.name]
streamed: Final = billed[case.streamed.name]
assert (streamed.spend, streamed.prompt_tokens, streamed.completion_tokens) == (
plain.spend,
plain.prompt_tokens,
plain.completion_tokens,
), (plain, streamed)

View file

@ -0,0 +1,40 @@
"""A streamed request bills exactly what the same request bills without streaming.
Streaming reconstructs the response, its usage and its model name from chunks after the fact, in a separate code
path from the non-streamed one (LIT-6241, LIT-6872, LIT-7632, LIT-7729, LIT-9044, LIT-9065). Each case is a plain and
a streamed CostTrackingTestCase for one request, sent through the same key, and both LiteLLM_SpendLogs rows must
bill the case's expected row.
Three cells are the native routes (chat, /v1/messages, /v1/responses); the rest are the bridges that translate one
API into another before the provider call, each with its own stream reassembly
"""
from typing import Final
import pytest
from integration._support.client import Gateway
from integration.cost_calculation.stream_parity.bases import anthropic, bedrock, openai, openai_responses, xai
from integration.cost_calculation.stream_parity.case import StreamParityTestCase
from integration.cost_calculation.stream_parity.runner import assert_stream_parity
NATIVE: Final = (
openai.GPT_6_1_SOL_CHAT_COMPLETIONS_PARITY,
anthropic.CLAUDE_OPUS_5_5_MESSAGES_PARITY,
openai_responses.GPT_6_1_SOL_RESPONSES_PARITY,
)
BRIDGES: Final = (
xai.GROK_4_7_MESSAGES_PARITY,
openai.GPT_6_1_SOL_MESSAGES_PARITY,
bedrock.CLAUDE_OPUS_5_5_CONVERSE_MESSAGES_PARITY,
openai_responses.GPT_5_5_PRO_CHAT_COMPLETIONS_PARITY,
anthropic.CLAUDE_OPUS_5_5_RESPONSES_PARITY,
)
CASES: Final = (*NATIVE, *BRIDGES)
@pytest.mark.parametrize("case", CASES, ids=[case.id for case in CASES])
@pytest.mark.timeout(180)
def test_streamed_request_bills_the_same_row_as_the_non_streamed_request(
case: StreamParityTestCase, gateway: Gateway
) -> None:
assert_stream_parity(case, gateway)