Skip to content
Merged
1 change: 1 addition & 0 deletions tests/integration/_support/upstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ def _response(response: StoredResponse, scenario_id: str) -> Response:
"$REQUEST_ID", scenario_id
).encode(),
media_type=response.content_type,
status_code=response.status,
)
case SseResponse():
stream_body: Final = ("\n\n".join(response.frames) + "\n\n").replace(
Expand Down
9 changes: 9 additions & 0 deletions tests/integration/contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,15 @@
"tests/integration/cost_calculation/test_cost_tracking.py::test_case_bills_expected_cost[azure-gpt-5.6-stream_full_usage]": [
"quota_management.spend_tracking.scripted_wire.logs_cost"
],
"tests/integration/cost_calculation/test_cost_tracking.py::test_case_bills_expected_cost[gpt-5.6-responses_native_json]": [
"quota_management.spend_tracking.cost_matrix.logs_cost"
],
"tests/integration/cost_calculation/test_cost_tracking.py::test_case_bills_expected_cost[gpt-5.6-upstream_500_zero_spend]": [
"quota_management.spend_tracking.cost_matrix.logs_cost"
],
"tests/integration/cost_calculation/test_cost_tracking.py::test_case_bills_expected_cost[gpt-5.6-upstream_429_zero_spend]": [
"quota_management.spend_tracking.cost_matrix.logs_cost"
],
"tests/integration/cost_calculation/test_cost_tracking.py::test_case_bills_expected_cost[claude-haiku-4-5-input_text]": [
"quota_management.spend_tracking.cost_matrix.logs_cost"
],
Expand Down
31 changes: 31 additions & 0 deletions tests/integration/cost_calculation/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ def breakdown(self) -> CostBreakdown:
return self.metadata.cost_breakdown


class FailureRow(BaseModel):
model_config = ConfigDict(extra="ignore")

spend: float
status: str
prompt_tokens: int | None = None
completion_tokens: int | None = None


def approx_equal(actual: float, expected: float) -> bool:
return abs(actual - expected) <= max(1e-9, abs(expected) * 1e-2)

Expand Down Expand Up @@ -92,6 +101,28 @@ def read() -> CostRow | None:
return result


def poll_failure_row(key: str) -> FailureRow:
digest: Final = sha256(key.encode()).hexdigest()

def read() -> FailureRow | None:
rows: Final = read_rows(
'SELECT spend, status, prompt_tokens, completion_tokens FROM "LiteLLM_SpendLogs" WHERE api_key=%s',
(digest,),
)
return next(
(
parsed
for row in rows
if (parsed := FailureRow.model_validate(row)).status == "failure"
),
None,
)

result: Final = eventually(read, lambda row: row is not None, seconds=60)
assert result is not None
return result


@functools.cache
def _vertex_private_key_pem() -> str:
return rsa.generate_private_key(public_exponent=65537, key_size=2048).private_bytes(
Expand Down
78 changes: 77 additions & 1 deletion tests/integration/cost_calculation/cost_tracking_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class JsonResponse(BaseModel):

content_type: Literal["application/json"]
body: dict[str, JsonValue]
status: int = 200


class SseResponse(BaseModel):
Expand Down Expand Up @@ -108,6 +109,10 @@ class ExactExpected(BaseModel):
output_cost: float
prompt_tokens: int
completion_tokens: int
cache_read_cost: float | None = None
cache_creation_cost: float | None = None
reasoning_cost: float | None = None
tool_usage_cost: float | None = None


class RecountRates(BaseModel):
Expand All @@ -123,7 +128,19 @@ class RecountExpected(BaseModel):
recount: RecountRates


Expected: TypeAlias = ExactExpected | RecountExpected
class FailureDetails(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")

status: int


class FailureExpected(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")

failure: FailureDetails


Expected: TypeAlias = ExactExpected | RecountExpected | FailureExpected


class CostTrackingTestCase(BaseModel):
Expand All @@ -132,6 +149,15 @@ class CostTrackingTestCase(BaseModel):
name: str
covers: str
model: str
endpoint: Literal[
"/v1/chat/completions",
"/v1/responses",
"/v1/messages",
"/v1/embeddings",
"/v1/rerank",
"/v1/completions",
"/v1/moderations",
] = "/v1/chat/completions"
deployment: Deployment | None = None
request: dict[str, JsonValue]
response: StoredResponse
Expand Down Expand Up @@ -240,6 +266,48 @@ def data_errors() -> tuple[str, ...]:
or case.expected.recount.output_cost_per_token != (COST_MAP[case.model].output_cost_per_token or 0.0)
)
)
component_mismatches: Final = sorted(
case.name
for case in CASES
if isinstance(case.expected, ExactExpected)
and any(
component is not None
for component in (
case.expected.cache_read_cost,
case.expected.cache_creation_cost,
case.expected.reasoning_cost,
case.expected.tool_usage_cost,
)
)
and (
(case.expected.cache_read_cost or 0.0) + (case.expected.cache_creation_cost or 0.0)
> case.expected.input_cost
or (case.expected.reasoning_cost or 0.0) > case.expected.output_cost
or not _approx_equal(
case.expected.input_cost
+ case.expected.output_cost
+ (case.expected.tool_usage_cost or 0.0),
case.expected.spend,
)
)
)
failure_response_mismatches: Final = sorted(
case.name
for case in CASES
if (
isinstance(case.expected, FailureExpected)
and (
not isinstance(case.response, JsonResponse)
or not 400 <= case.response.status <= 599
or not 400 <= case.expected.failure.status <= 599
)
)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
or (
not isinstance(case.expected, FailureExpected)
and isinstance(case.response, JsonResponse)
and case.response.status != 200
)
)
return tuple(
message
for message in (
Expand All @@ -248,6 +316,14 @@ def data_errors() -> tuple[str, ...]:
f"duplicate case names: {duplicate_names}" if duplicate_names else None,
f"cost-map entries share input_cost_per_token: {shared_input_rates}" if shared_input_rates else None,
f"recount rates differ from cost-map rates: {recount_mismatches}" if recount_mismatches else None,
f"breakdown components are inconsistent: {component_mismatches}" if component_mismatches else None,
f"failure response statuses are inconsistent: {failure_response_mismatches}"
if failure_response_mismatches
else None,
)
if message is not None
)


def _approx_equal(actual: float, expected: float) -> bool:
return abs(actual - expected) <= max(1e-9, abs(expected) * 1e-2)
137 changes: 130 additions & 7 deletions tests/integration/cost_calculation/cost_tracking_cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,8 @@
"input_cost": 0.00616704,
"output_cost": 0.00627,
"prompt_tokens": 12928,
"completion_tokens": 380
"completion_tokens": 380,
"cache_read_cost": 0.00405504
}
},
{
Expand Down Expand Up @@ -605,7 +606,8 @@
"input_cost": 0.0397056,
"output_cost": 0.005775,
"prompt_tokens": 9728,
"completion_tokens": 350
"completion_tokens": 350,
"cache_creation_cost": 0.038016
}
},
{
Expand Down Expand Up @@ -681,7 +683,8 @@
"input_cost": 0.0574464,
"output_cost": 0.005775,
"prompt_tokens": 9728,
"completion_tokens": 350
"completion_tokens": 350,
"cache_creation_cost": 0.0557568
}
},
{
Expand Down Expand Up @@ -3417,7 +3420,8 @@
"input_cost": 0.002232,
"output_cost": 0.065484,
"prompt_tokens": 1240,
"completion_tokens": 4040
"completion_tokens": 4040,
"reasoning_cost": 0.05742
}
},
{
Expand Down Expand Up @@ -3638,7 +3642,8 @@
"input_cost": 0.003312,
"output_cost": 0.0059328,
"prompt_tokens": 1840,
"completion_tokens": 412
"completion_tokens": 412,
"tool_usage_cost": 0.0125
}
},
{
Expand Down Expand Up @@ -4494,7 +4499,8 @@
"input_cost": 0.0018688,
"output_cost": 0.0019,
"prompt_tokens": 12928,
"completion_tokens": 380
"completion_tokens": 380,
"cache_read_cost": 0.0012288
}
},
{
Expand Down Expand Up @@ -16955,7 +16961,8 @@
"input_cost": 0.00276,
"output_cost": 0.004944,
"prompt_tokens": 1840,
"completion_tokens": 412
"completion_tokens": 412,
"tool_usage_cost": 0.0025
}
},
{
Expand Down Expand Up @@ -21797,6 +21804,122 @@
"completion_tokens": 1592
}
},
{
"name": "gpt-5.6-responses_native_json",
"covers": "quota_management.spend_tracking.cost_matrix.logs_cost",
"model": "gpt-5.6",
"endpoint": "/v1/responses",
"request": {
"model": "$MODEL",
"input": "responses native fixture",
"stream": false
},
"response": {
"content_type": "application/json",
"body": {
"id": "resp_$REQUEST_ID",
"object": "response",
"status": "completed",
"created_at": 1700000000,
"model": "gpt-5.6",
"output": [
{
"type": "message",
"id": "msg_$REQUEST_ID",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "scripted response",
"annotations": []
}
]
}
],
"usage": {
"input_tokens": 11,
"output_tokens": 7,
"total_tokens": 18,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens_details": {
"reasoning_tokens": 0
}
}
}
},
"expected": {
"spend": 0.00011725,
"input_cost": 1.925e-05,
"output_cost": 9.8e-05,
"prompt_tokens": 11,
"completion_tokens": 7
}
},
{
"name": "gpt-5.6-upstream_500_zero_spend",
"covers": "quota_management.spend_tracking.cost_matrix.logs_cost",
"model": "gpt-5.6",
"request": {
"model": "$MODEL",
"messages": [
{
"role": "user",
"content": "scripted upstream failure 500"
}
],
"stream": false
},
"response": {
"content_type": "application/json",
"status": 500,
"body": {
"error": {
"message": "scripted upstream failure",
"type": "server_error",
"code": "500"
}
}
},
"expected": {
"failure": {
"status": 500
}
}
},
{
"name": "gpt-5.6-upstream_429_zero_spend",
"covers": "quota_management.spend_tracking.cost_matrix.logs_cost",
"model": "gpt-5.6",
"request": {
"model": "$MODEL",
"messages": [
{
"role": "user",
"content": "scripted upstream failure 429"
}
],
"stream": false
},
"response": {
"content_type": "application/json",
"status": 429,
"body": {
"error": {
"message": "scripted upstream failure",
"type": "rate_limit_error",
"code": "429"
}
}
},
"expected": {
"failure": {
"status": 429
}
}
},
{
"name": "meta.llama4-maverick-17b-instruct-v1:0-input_text",
"covers": "quota_management.spend_tracking.cost_matrix.logs_cost",
Expand Down
Loading
Loading