mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
* fix(proxy): restore spend logging for /cursor/chat/completions (#30126) The /cursor/chat/completions route processes requests through the Responses API pipeline, but _get_metadata_variable_name did not treat it as a litellm_metadata route. Proxy auth metadata therefore landed in the user-facing metadata param, so the spend tracking callback saw user_api_key=None and silently skipped the SpendLogs write. Add the route to LITELLM_METADATA_ROUTES and cover it with regression tests. * test(proxy): pin provider metadata and key billing on /cursor/chat/completions A Responses-format body on the Cursor route now sends OpenAI only the metadata the client sent and writes a spend row that bills the key. The chat-body branch, which this fix also moves onto litellm_metadata, still bills the key with its team and request tags. The scripted chat reply gets a unique id per test, since a shared id let only the first spend row per database land * fix(proxy): pick the Cursor route's metadata field from the body shape A Responses-style body on /cursor/chat/completions keeps proxy metadata in litellm_metadata, the same as /v1/responses, while a Chat Completions body keeps metadata, the same as /v1/chat/completions. Adding the whole route to the internal-metadata list let a client's chat-body tags replace the key's tags in spend logs and skip tag budgets. * fix(proxy): keep get_metadata_variable_name callable with only the request * fix(proxy): check client tags against tag budgets and read agent limits from litellm_metadata on Responses bodies A Responses-style body on /cursor/chat/completions now keeps its proxy fields in litellm_metadata, so the pre-call tag budget check and the v3 agent rate limiter stopped seeing the client's metadata.tags and the x-litellm-agent-id value. The pre-auth tag check now merges the caller's metadata.tags into the selected metadata field, sharing one helper with request setup, and the limiter reads agent_id from either field * fix(proxy): check client tags sent in a JSON-string metadata against tag budgets on Responses bodies * fix(proxy): answer 422, not 500, when a budget rejects a Responses body whose metadata is a JSON string * fix(proxy): keep key guardrails, metadata type errors and failure-row client details on Responses-style bodies --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
577 lines
24 KiB
Python
577 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import uuid
|
|
from collections.abc import Mapping
|
|
from hashlib import sha256
|
|
from importlib.resources import files
|
|
from typing import Final
|
|
|
|
import httpx
|
|
import pytest
|
|
from integration._support.client import Gateway, Scenario, eventually, list_value, object_value
|
|
from integration._support.responses_vendor import same_response
|
|
from integration._support.wire import Reply, Request, Wire, wire_server
|
|
from integration._support.worker_readiness import wait_for_worker_readiness
|
|
from pydantic import JsonValue, TypeAdapter
|
|
|
|
_MODEL: Final = "gpt-5"
|
|
_API_KEY: Final = "synthetic-cursor-key"
|
|
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
|
|
_PROXY_WORKERS: Final = max(1, int(os.environ.get("INTEGRATION_PROXY_WORKERS", "1")))
|
|
_COST_MAP: Final = TypeAdapter(dict[str, dict[str, JsonValue]])
|
|
_MODEL_ROW: Final = _COST_MAP.validate_json(
|
|
(files("litellm") / "model_prices_and_context_window_backup.json").read_bytes()
|
|
)[_MODEL]
|
|
_THINKING_LEVELS: Final = tuple(
|
|
level
|
|
for level in ("none", "minimal", "low", "medium", "high", "xhigh", "max")
|
|
if _MODEL_ROW.get(f"supports_{level}_reasoning_effort") is not False
|
|
)
|
|
_CHAT_THINKING_LEVELS: Final = tuple(level for level in _THINKING_LEVELS if level != "xhigh")
|
|
_RESPONSES_SUFFIXES: Final[tuple[tuple[str, dict[str, JsonValue]], ...]] = tuple(
|
|
(f"-thinking-{level}", {"reasoning": {"effort": level}}) for level in _THINKING_LEVELS
|
|
) + (
|
|
("-fast", {}),
|
|
("-thinking-low-fast", {"reasoning": {"effort": "low"}}),
|
|
("-thinking-high-fast", {"reasoning": {"effort": "high"}}),
|
|
)
|
|
_CHAT_SUFFIXES: Final[tuple[tuple[str, str], ...]] = (("", ""),) + tuple(
|
|
(f"-thinking-{level}", level) for level in _CHAT_THINKING_LEVELS
|
|
)
|
|
_PROMPT: Final = "Inspect the requested change."
|
|
_REQUEST_TAG: Final = "cursor-integration"
|
|
_INPUT: Final = [{"role": "user", "content": [{"type": "input_text", "text": _PROMPT}]}]
|
|
_CUSTOM_TOOL: Final = {
|
|
"type": "custom",
|
|
"name": "ApplyPatch",
|
|
"description": "Apply a patch",
|
|
"format": {"type": "grammar", "syntax": "lark", "definition": 'start: "ok"'},
|
|
}
|
|
_FUNCTION_TOOL: Final = {
|
|
"type": "function",
|
|
"name": "lookup",
|
|
"description": "Look up a record",
|
|
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
|
|
"strict": True,
|
|
}
|
|
|
|
|
|
def model_discovery_reply(model: str) -> Reply:
|
|
return Reply(
|
|
body=json.dumps(
|
|
{"object": "list", "data": [{"id": model, "object": "model", "created": 1, "owned_by": "openai"}]}
|
|
).encode()
|
|
)
|
|
|
|
|
|
def drain_contract_requests(wire: Wire) -> tuple[Request, ...]:
|
|
requests: Final = wire.drain()
|
|
discovery: Final = tuple(request for request in requests if request.target == "/v1/models")
|
|
assert all(request.method == "GET" and request.body == b"" for request in discovery), requests
|
|
return tuple(request for request in requests if request.target != "/v1/models")
|
|
|
|
|
|
def _response(identity: str) -> dict[str, JsonValue]:
|
|
return {
|
|
"id": identity,
|
|
"object": "response",
|
|
"created_at": 1,
|
|
"status": "completed",
|
|
"model": _MODEL,
|
|
"output": [
|
|
{
|
|
"id": "msg_cursor",
|
|
"type": "message",
|
|
"status": "completed",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "Patch inspected.", "annotations": []}],
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 4, "output_tokens": 3, "total_tokens": 7},
|
|
}
|
|
|
|
|
|
def _stream(identity: str) -> tuple[bytes, ...]:
|
|
response: Final = _response(identity)
|
|
return tuple(
|
|
f"data: {json.dumps(event)}\n\n".encode()
|
|
for event in (
|
|
{"type": "response.created", "response": {**response, "status": "in_progress", "output": []}},
|
|
{
|
|
"type": "response.output_text.delta",
|
|
"item_id": "msg_cursor",
|
|
"output_index": 0,
|
|
"content_index": 0,
|
|
"delta": "Patch inspected.",
|
|
},
|
|
{"type": "response.completed", "response": response},
|
|
)
|
|
) + (b"data: [DONE]\n\n",)
|
|
|
|
|
|
def _chat_completion(stream: bool, identity: str) -> bytes | tuple[bytes, ...]:
|
|
if stream:
|
|
return tuple(
|
|
f"data: {json.dumps(chunk)}\n\n".encode()
|
|
for chunk in (
|
|
{
|
|
"id": identity,
|
|
"object": "chat.completion.chunk",
|
|
"created": 1,
|
|
"model": _MODEL,
|
|
"choices": [{"index": 0, "delta": {"content": "Patch inspected."}, "finish_reason": None}],
|
|
},
|
|
{
|
|
"id": identity,
|
|
"object": "chat.completion.chunk",
|
|
"created": 1,
|
|
"model": _MODEL,
|
|
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
|
|
},
|
|
)
|
|
) + (b"data: [DONE]\n\n",)
|
|
return json.dumps(
|
|
{
|
|
"id": identity,
|
|
"object": "chat.completion",
|
|
"created": 1,
|
|
"model": _MODEL,
|
|
"system_fingerprint": "fp_cursor",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": "Patch inspected."},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 4, "completion_tokens": 3, "total_tokens": 7},
|
|
}
|
|
).encode()
|
|
|
|
|
|
def _deployment(scenario: Scenario, wire: Wire, **pricing: JsonValue) -> str:
|
|
model: Final = scenario.model(
|
|
model=f"openai/{_MODEL}",
|
|
api_key=_API_KEY,
|
|
api_base=f"{wire.url}/v1",
|
|
**pricing,
|
|
)
|
|
wait_for_worker_readiness(scenario.gateway, model, 1, _PROXY_WORKERS)
|
|
return model
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("suffix", "reasoning"),
|
|
_RESPONSES_SUFFIXES,
|
|
)
|
|
@pytest.mark.parametrize("stream", (False, True))
|
|
def test_cursor_responses_input_uses_responses_wire_and_chat_output(
|
|
gateway: Gateway,
|
|
stream: bool,
|
|
suffix: str,
|
|
reasoning: dict[str, JsonValue],
|
|
) -> None:
|
|
raw_id: Final = f"resp_cursor_{uuid.uuid4().hex}"
|
|
with gateway.scenario() as scenario:
|
|
team_alias: Final = f"integration-{uuid.uuid4().hex}"
|
|
team: Final = scenario.team(team_alias=team_alias)
|
|
user: Final = scenario.member(team)
|
|
key: Final = scenario.key(team_id=team, user_id=user)
|
|
|
|
def respond(request: Request) -> Reply:
|
|
if request.method == "GET" and request.target == "/v1/models":
|
|
return model_discovery_reply(_MODEL)
|
|
assert request.method == "POST"
|
|
assert request.headers["authorization"] == f"Bearer {_API_KEY}", dict(request.headers)
|
|
if request.target == "/v1/chat/completions":
|
|
completion: Final = _chat_completion(stream, raw_id)
|
|
return Reply(
|
|
content_type="text/event-stream" if stream else "application/json",
|
|
chunks=completion if isinstance(completion, tuple) else None,
|
|
body=completion if isinstance(completion, bytes) else b"",
|
|
)
|
|
assert request.target == "/v1/responses", request.target
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
assert body == {
|
|
"model": _MODEL,
|
|
"input": _INPUT,
|
|
**reasoning,
|
|
"tools": [_CUSTOM_TOOL],
|
|
"stream": stream,
|
|
}, request.body
|
|
if stream:
|
|
return Reply(content_type="text/event-stream", chunks=_stream(raw_id))
|
|
return Reply(body=json.dumps(_response(raw_id)).encode())
|
|
|
|
with wire_server(respond) as wire:
|
|
model: Final = _deployment(scenario, wire)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/cursor/chat/completions",
|
|
{
|
|
"model": f"{model}{suffix}",
|
|
"input": _INPUT,
|
|
"messages": [],
|
|
"tools": [_CUSTOM_TOOL],
|
|
"stream_options": {"include_usage": True, "include_obfuscation": False},
|
|
"stream": stream,
|
|
},
|
|
key=key,
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
if stream:
|
|
events: Final = tuple(
|
|
_JSON_OBJECT.validate_json(line.removeprefix("data: "))
|
|
for line in response.text.splitlines()
|
|
if line.startswith("data: ") and line != "data: [DONE]"
|
|
)
|
|
assert events, response.text
|
|
assert all(event.get("object") == "chat.completion.chunk" for event in events), response.text
|
|
assert all(not str(event.get("type", "")).startswith("response.") for event in events), response.text
|
|
assert [{k: v for k, v in event.items() if k not in ("id", "created")} for event in events] == [
|
|
{
|
|
"object": "chat.completion.chunk",
|
|
"model": model,
|
|
"choices": [{"index": 0, "delta": {"role": "assistant", "content": "Patch inspected."}}],
|
|
},
|
|
{
|
|
"object": "chat.completion.chunk",
|
|
"model": model,
|
|
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
|
|
},
|
|
], response.text
|
|
else:
|
|
payload: Final = _JSON_OBJECT.validate_json(response.content)
|
|
identity: Final = payload.get("id")
|
|
created: Final = payload.get("created")
|
|
assert isinstance(identity, str) and same_response(identity, raw_id), response.text
|
|
assert isinstance(created, int) and created > 0, response.text
|
|
assert payload == {
|
|
"id": identity,
|
|
"created": created,
|
|
"model": model,
|
|
"object": "chat.completion",
|
|
"system_fingerprint": None,
|
|
"choices": [
|
|
{
|
|
"finish_reason": "stop",
|
|
"index": 0,
|
|
"message": {
|
|
"content": "Patch inspected.",
|
|
"role": "assistant",
|
|
"tool_calls": None,
|
|
"function_call": None,
|
|
"provider_specific_fields": None,
|
|
},
|
|
}
|
|
],
|
|
"usage": {
|
|
"completion_tokens": 3,
|
|
"prompt_tokens": 4,
|
|
"total_tokens": 7,
|
|
"completion_tokens_details": None,
|
|
"prompt_tokens_details": None,
|
|
},
|
|
}, response.text
|
|
assert [request.target for request in drain_contract_requests(wire)] == ["/v1/responses"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"client_metadata", ({}, {"cursor_conversation": "integration"}), ids=("no_client_metadata", "client_metadata")
|
|
)
|
|
@pytest.mark.parametrize("stream", (False, True))
|
|
def test_cursor_responses_body_sends_only_client_metadata_and_bills_the_key(
|
|
gateway: Gateway,
|
|
stream: bool,
|
|
client_metadata: Mapping[str, JsonValue],
|
|
) -> None:
|
|
raw_id: Final = f"resp_cursor_metadata_{uuid.uuid4().hex}"
|
|
alias: Final = f"integration-cursor-{uuid.uuid4().hex}"
|
|
sent_metadata: Final = {"metadata": client_metadata} if client_metadata else {}
|
|
|
|
def respond(request: Request) -> Reply:
|
|
if request.method == "GET" and request.target == "/v1/models":
|
|
return model_discovery_reply(_MODEL)
|
|
assert request.method == "POST" and request.target == "/v1/responses", request.target
|
|
if stream:
|
|
return Reply(content_type="text/event-stream", chunks=_stream(raw_id))
|
|
return Reply(body=json.dumps(_response(raw_id)).encode())
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = _deployment(scenario, wire)
|
|
key: Final = scenario.key(key_alias=alias)
|
|
hashed_key: Final = sha256(key.encode()).hexdigest()
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/cursor/chat/completions",
|
|
{
|
|
"model": model,
|
|
"input": _INPUT,
|
|
"messages": [],
|
|
"tools": [_CUSTOM_TOOL],
|
|
"stream": stream,
|
|
**sent_metadata,
|
|
},
|
|
key=key,
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
requests: Final = drain_contract_requests(wire)
|
|
assert len(requests) == 1, requests
|
|
assert _JSON_OBJECT.validate_json(requests[0].body) == {
|
|
"model": _MODEL,
|
|
"input": _INPUT,
|
|
"tools": [_CUSTOM_TOOL],
|
|
"stream": stream,
|
|
**sent_metadata,
|
|
}, requests[0].body
|
|
(row,) = eventually(
|
|
lambda: gateway.rows(
|
|
'SELECT call_type, spend, metadata FROM "LiteLLM_SpendLogs" WHERE api_key = %s', (hashed_key,)
|
|
),
|
|
lambda rows: len(rows) == 1,
|
|
seconds=70,
|
|
)
|
|
assert row["call_type"] == "aresponses", row
|
|
assert object_value(row["metadata"])["user_api_key_alias"] == alias, row
|
|
assert isinstance(row["spend"], float) and row["spend"] > 0, row
|
|
eventually(
|
|
lambda: gateway.rows('SELECT spend FROM "LiteLLM_VerificationToken" WHERE token = %s', (hashed_key,)),
|
|
lambda tokens: len(tokens) == 1 and tokens[0]["spend"] == pytest.approx(row["spend"]),
|
|
seconds=70,
|
|
)
|
|
|
|
|
|
def test_cursor_stream_reports_usage_when_the_client_asks_for_it(gateway: Gateway) -> None:
|
|
pytest.skip(
|
|
"BUG: /cursor/chat/completions with stream_options.include_usage streams no usage chunk; "
|
|
"the Responses usage from response.completed never reaches the chat client"
|
|
)
|
|
raw_id: Final = f"resp_cursor_usage_{uuid.uuid4().hex}"
|
|
|
|
def respond(request: Request) -> Reply:
|
|
if request.method == "GET" and request.target == "/v1/models":
|
|
return model_discovery_reply(_MODEL)
|
|
assert (request.method, request.target) == ("POST", "/v1/responses"), request.target
|
|
return Reply(content_type="text/event-stream", chunks=_stream(raw_id))
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = _deployment(scenario, wire)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/cursor/chat/completions",
|
|
{
|
|
"model": model,
|
|
"input": _INPUT,
|
|
"stream_options": {"include_usage": True},
|
|
"stream": True,
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
events: Final = tuple(
|
|
_JSON_OBJECT.validate_json(line.removeprefix("data: "))
|
|
for line in response.text.splitlines()
|
|
if line.startswith("data: ") and line != "data: [DONE]"
|
|
)
|
|
assert [event["usage"] for event in events if event.get("usage")] == [
|
|
{"completion_tokens": 3, "prompt_tokens": 4, "total_tokens": 7}
|
|
], response.text
|
|
assert [request.target for request in drain_contract_requests(wire)] == ["/v1/responses"]
|
|
|
|
|
|
@pytest.mark.parametrize(("suffix", "reasoning_effort"), _CHAT_SUFFIXES)
|
|
@pytest.mark.parametrize("stream", (False, True))
|
|
def test_cursor_messages_body_uses_chat_completions_wire(
|
|
gateway: Gateway,
|
|
stream: bool,
|
|
suffix: str,
|
|
reasoning_effort: str,
|
|
) -> None:
|
|
chat_id: Final = f"chat_cursor_{uuid.uuid4().hex}"
|
|
|
|
def respond(request: Request) -> Reply:
|
|
if request.method == "GET" and request.target == "/v1/models":
|
|
return model_discovery_reply(_MODEL)
|
|
completion: Final = _chat_completion(stream, chat_id)
|
|
return Reply(
|
|
content_type="text/event-stream" if stream else "application/json",
|
|
chunks=completion if isinstance(completion, tuple) else None,
|
|
body=completion if isinstance(completion, bytes) else b"",
|
|
)
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = _deployment(scenario, wire)
|
|
team: Final = scenario.team(models=[model])
|
|
user: Final = scenario.member(team)
|
|
key: Final = scenario.key(team_id=team, user_id=user, models=[model])
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/cursor/chat/completions",
|
|
{
|
|
"model": f"{model}{suffix}",
|
|
"messages": [{"role": "user", "content": "genuine chat request"}],
|
|
"tools": [_FUNCTION_TOOL, _CUSTOM_TOOL],
|
|
"tool_choice": {"type": "function", "name": "lookup"},
|
|
"stream": stream,
|
|
},
|
|
key=key,
|
|
headers={"x-litellm-tags": _REQUEST_TAG},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
if stream:
|
|
events: Final = tuple(
|
|
_JSON_OBJECT.validate_json(line.removeprefix("data: "))
|
|
for line in response.text.splitlines()
|
|
if line.startswith("data: ") and line != "data: [DONE]"
|
|
)
|
|
assert [
|
|
{key: value for key, value in event.items() if key not in ("id", "created")} for event in events
|
|
] == [
|
|
{
|
|
"object": "chat.completion.chunk",
|
|
"model": model,
|
|
"choices": [{"index": 0, "delta": {"role": "assistant", "content": "Patch inspected."}}],
|
|
},
|
|
{
|
|
"object": "chat.completion.chunk",
|
|
"model": model,
|
|
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
|
|
},
|
|
], response.text
|
|
else:
|
|
payload: Final = _JSON_OBJECT.validate_json(response.content)
|
|
identity: Final = payload.get("id")
|
|
created: Final = payload.get("created")
|
|
assert isinstance(identity, str) and same_response(identity, chat_id), response.text
|
|
assert isinstance(created, int) and created > 0, response.text
|
|
assert payload == {
|
|
"id": identity,
|
|
"created": created,
|
|
"model": model,
|
|
"object": "chat.completion",
|
|
"system_fingerprint": "fp_cursor",
|
|
"choices": [
|
|
{
|
|
"finish_reason": "stop",
|
|
"index": 0,
|
|
"message": {
|
|
"content": "Patch inspected.",
|
|
"role": "assistant",
|
|
},
|
|
"provider_specific_fields": {},
|
|
}
|
|
],
|
|
"usage": {
|
|
"completion_tokens": 3,
|
|
"prompt_tokens": 4,
|
|
"total_tokens": 7,
|
|
},
|
|
}, response.text
|
|
requests: Final = drain_contract_requests(wire)
|
|
assert [(request.method, request.target) for request in requests] == [("POST", "/v1/chat/completions")], (
|
|
requests
|
|
)
|
|
assert _JSON_OBJECT.validate_json(requests[0].body) == {
|
|
"model": _MODEL,
|
|
"messages": [{"role": "user", "content": "genuine chat request"}],
|
|
"tools": [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "lookup",
|
|
"description": "Look up a record",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"query": {"type": "string"}},
|
|
"required": ["query"],
|
|
},
|
|
"strict": True,
|
|
},
|
|
},
|
|
{
|
|
"type": "custom",
|
|
"custom": {
|
|
"name": "ApplyPatch",
|
|
"description": "Apply a patch",
|
|
"format": {
|
|
"type": "grammar",
|
|
"grammar": {"syntax": "lark", "definition": 'start: "ok"'},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
"tool_choice": {"type": "function", "function": {"name": "lookup"}},
|
|
**({"stream": True} if stream else {}),
|
|
**({"stream_options": {"include_usage": True}} if stream else {}),
|
|
**({"reasoning_effort": reasoning_effort} if reasoning_effort else {}),
|
|
}, requests[0].body
|
|
(row,) = eventually(
|
|
lambda: gateway.rows(
|
|
'SELECT call_type, team_id, request_tags FROM "LiteLLM_SpendLogs" WHERE api_key = %s',
|
|
(sha256(key.encode()).hexdigest(),),
|
|
),
|
|
lambda rows: len(rows) == 1,
|
|
seconds=70,
|
|
)
|
|
assert (row["call_type"], row["team_id"]) == ("acompletion", team), row
|
|
assert _REQUEST_TAG in list_value(row["request_tags"]), row
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("body_shape", "metadata_as_json_string"), (("messages", False), ("input", False), ("input", True))
|
|
)
|
|
def test_cursor_body_keeps_client_and_key_tags_and_enforces_the_client_tag_budget(
|
|
gateway: Gateway, body_shape: str, metadata_as_json_string: bool
|
|
) -> None:
|
|
chat_id: Final = f"chat_cursor_{uuid.uuid4().hex}"
|
|
response_id: Final = f"resp_cursor_tags_{uuid.uuid4().hex}"
|
|
budget_tag: Final = f"cursor-tag-budget-{uuid.uuid4().hex}"
|
|
key_tag: Final = f"cursor-key-tag-{uuid.uuid4().hex}"
|
|
client_metadata: Final = {"tags": [budget_tag]}
|
|
|
|
def respond(request: Request) -> Reply:
|
|
if request.method == "GET" and request.target == "/v1/models":
|
|
return model_discovery_reply(_MODEL)
|
|
if request.target == "/v1/responses":
|
|
return Reply(body=json.dumps(_response(response_id)).encode())
|
|
completion: Final = _chat_completion(False, chat_id)
|
|
return Reply(body=completion if isinstance(completion, bytes) else b"")
|
|
|
|
def delete_tag() -> None:
|
|
gateway.post("/tag/delete", {"name": budget_tag})
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = _deployment(scenario, wire, input_cost_per_token=0.01, output_cost_per_token=0.01)
|
|
key: Final = scenario.key(models=[model], metadata={"tags": [key_tag]})
|
|
gateway.post("/tag/new", {"name": budget_tag, "max_budget": 0.0001})
|
|
scenario.cleanups.callback(delete_tag)
|
|
|
|
def tagged_chat(text: str) -> httpx.Response:
|
|
return gateway.request(
|
|
"POST",
|
|
"/cursor/chat/completions",
|
|
{
|
|
"model": model,
|
|
body_shape: [{"role": "user", "content": text}],
|
|
"metadata": json.dumps(client_metadata) if metadata_as_json_string else client_metadata,
|
|
},
|
|
key=key,
|
|
)
|
|
|
|
first: Final = tagged_chat("first tagged chat")
|
|
assert first.status_code == 200, first.text
|
|
(row,) = eventually(
|
|
lambda: gateway.rows(
|
|
'SELECT request_tags FROM "LiteLLM_SpendLogs" WHERE api_key = %s',
|
|
(sha256(key.encode()).hexdigest(),),
|
|
),
|
|
lambda rows: len(rows) == 1,
|
|
seconds=70,
|
|
)
|
|
assert {budget_tag, key_tag} <= set(list_value(row["request_tags"])), row
|
|
blocked: Final = eventually(
|
|
lambda: tagged_chat("tag budget probe"), lambda response: response.status_code != 200, seconds=70
|
|
)
|
|
assert blocked.status_code == 422, blocked.text
|
|
assert "budget" in blocked.text.lower(), blocked.text
|