mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
* fix(databricks): keep #/$defs refs in json_schema response_format for non-Claude models Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(databricks): type the json_schema response_format override Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(databricks): integration cells for json_schema $defs refs across endpoints Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(databricks): split stacked loop in streaming json_schema cell Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(databricks): mark BaseConfig dict contract on json_schema override Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(databricks): cover json_schema refs on every endpoint, edge refs, sad paths and an outage burst --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
870 lines
35 KiB
Python
870 lines
35 KiB
Python
import itertools
|
|
import json
|
|
import uuid
|
|
from collections.abc import Mapping
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from typing import Final, Literal, TypeAlias
|
|
|
|
import pytest
|
|
from integration._support.client import Gateway, eventually
|
|
from integration._support.database import read_rows
|
|
from integration._support.wire import Reply, Request, wire_server
|
|
from pydantic import BaseModel, ConfigDict, JsonValue, TypeAdapter
|
|
|
|
_BACKEND: Final = "databricks-glm-5-2"
|
|
_API_KEY: Final = "synthetic-databricks-key"
|
|
_PROMPT: Final = "Summarise the cached briefing in one sentence."
|
|
_JSON_SCHEMA_BACKEND: Final = "databricks-qwen35-122b-a10b"
|
|
_CLAUDE_BACKEND: Final = "databricks-claude-haiku-4-5"
|
|
_JSON_SCHEMA_PROMPT: Final = "Return the requested JSON."
|
|
_JSON_CONTENT: Final = '{"p":{"name":"Ada"}}'
|
|
_PROVIDER_USAGE: Final[Mapping[str, JsonValue]] = {
|
|
"prompt_tokens": 12011,
|
|
"completion_tokens": 8,
|
|
"total_tokens": 12019,
|
|
"cache_read_input_tokens": 12002,
|
|
"cache_creation_input_tokens": 0,
|
|
}
|
|
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
|
|
_PERSON_SCHEMA: Final[Mapping[str, JsonValue]] = {
|
|
"$defs": {
|
|
"Person": {
|
|
"type": "object",
|
|
"properties": {"name": {"type": "string"}},
|
|
"required": ["name"],
|
|
}
|
|
},
|
|
"type": "object",
|
|
"properties": {"p": {"$ref": "#/$defs/Person"}},
|
|
"required": ["p"],
|
|
}
|
|
_JSON_SCHEMA_RESPONSE_FORMAT: Final[Mapping[str, JsonValue]] = {
|
|
"type": "json_schema",
|
|
"json_schema": {"name": "P", "strict": True, "schema": _PERSON_SCHEMA},
|
|
}
|
|
_MESSAGES_PERSON_SCHEMA: Final[Mapping[str, JsonValue]] = {
|
|
"$defs": {
|
|
"Person": {
|
|
"type": "object",
|
|
"properties": {"name": {"type": "string"}},
|
|
"required": ["name"],
|
|
"additionalProperties": False,
|
|
}
|
|
},
|
|
"type": "object",
|
|
"properties": {"p": {"$ref": "#/$defs/Person"}},
|
|
"required": ["p"],
|
|
"additionalProperties": False,
|
|
}
|
|
_MESSAGES_RESPONSE_FORMAT: Final[Mapping[str, JsonValue]] = {
|
|
"type": "json_schema",
|
|
"json_schema": {
|
|
"name": "structured_output",
|
|
"schema": _MESSAGES_PERSON_SCHEMA,
|
|
"strict": True,
|
|
},
|
|
}
|
|
_JSON_TOOL_CALL_MESSAGE: Final[dict[str, JsonValue]] = {
|
|
"role": "assistant",
|
|
"content": None,
|
|
"tool_calls": [
|
|
{
|
|
"id": "json-tool-call",
|
|
"type": "function",
|
|
"function": {"name": "json_tool_call", "arguments": _JSON_CONTENT},
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
def _chat_completion_response(model: str, message: dict[str, JsonValue], finish_reason: str) -> bytes:
|
|
response: Final = {
|
|
"id": "databricks-json-schema-response",
|
|
"object": "chat.completion",
|
|
"created": 1,
|
|
"model": model,
|
|
"choices": [{"index": 0, "message": message, "finish_reason": finish_reason}],
|
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
|
}
|
|
return json.dumps(response).encode()
|
|
|
|
|
|
class _PromptTokensDetails(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
cached_tokens: int | None = None
|
|
|
|
|
|
class _Usage(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
prompt_tokens: int
|
|
completion_tokens: int
|
|
total_tokens: int
|
|
prompt_tokens_details: _PromptTokensDetails | None = None
|
|
|
|
|
|
class _Delta(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
content: str | None = None
|
|
|
|
|
|
class _Choice(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
delta: _Delta
|
|
|
|
|
|
class _Chunk(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
id: str
|
|
choices: tuple[_Choice, ...]
|
|
usage: _Usage | None = None
|
|
|
|
|
|
def _frame(
|
|
identity: str,
|
|
choices: list[Mapping[str, object]],
|
|
usage: Mapping[str, JsonValue] | None = None,
|
|
model: str = _BACKEND,
|
|
) -> bytes:
|
|
value: Final = {
|
|
"id": identity,
|
|
"object": "chat.completion.chunk",
|
|
"created": 1,
|
|
"model": model,
|
|
"choices": choices,
|
|
**({} if usage is None else {"usage": usage}),
|
|
}
|
|
return b"data: " + json.dumps(value).encode() + b"\n\n"
|
|
|
|
|
|
@pytest.mark.covers("other.provider_wire.databricks.stream_usage_and_cache_reads_reach_client_and_spend_log")
|
|
def test_databricks_stream_final_usage_chunk_reaches_client_and_spend_log(gateway: Gateway) -> None:
|
|
identity: Final = f"databricks-stream-{uuid.uuid4().hex}"
|
|
frames: Final = (
|
|
_frame(
|
|
identity, [{"index": 0, "delta": {"role": "assistant", "content": "The briefing "}, "finish_reason": None}]
|
|
),
|
|
_frame(identity, [{"index": 0, "delta": {"content": "is short."}, "finish_reason": None}]),
|
|
_frame(identity, [{"index": 0, "delta": {}, "finish_reason": "stop"}]),
|
|
_frame(identity, [], usage=_PROVIDER_USAGE),
|
|
b"data: [DONE]\n\n",
|
|
)
|
|
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/chat/completions"
|
|
assert request.headers["authorization"] == f"Bearer {_API_KEY}"
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
assert body["model"] == _BACKEND
|
|
assert body["messages"] == [{"role": "user", "content": _PROMPT}]
|
|
assert body["stream"] is True
|
|
return Reply(content_type="text/event-stream", chunks=frames)
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(model=f"databricks/{_BACKEND}", api_base=wire.url, api_key=_API_KEY)
|
|
with gateway.client.stream(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
json={
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": _PROMPT}],
|
|
"stream": True,
|
|
"stream_options": {"include_usage": True},
|
|
},
|
|
headers={"Authorization": f"Bearer {gateway.key}"},
|
|
) as response:
|
|
assert response.status_code == 200, response.read()
|
|
lines: Final = tuple(line for line in response.iter_lines() if line.startswith("data: "))
|
|
assert lines[-1] == "data: [DONE]", lines
|
|
chunks: Final = tuple(_Chunk.model_validate_json(line.removeprefix("data: ")) for line in lines[:-1])
|
|
assert {chunk.id for chunk in chunks} == {identity}
|
|
assert (
|
|
"".join(choice.delta.content or "" for chunk in chunks for choice in chunk.choices)
|
|
== "The briefing is short."
|
|
)
|
|
usages: Final = tuple(chunk.usage for chunk in chunks if chunk.usage is not None)
|
|
assert len(usages) == 1, lines
|
|
assert (
|
|
usages[0].prompt_tokens,
|
|
usages[0].completion_tokens,
|
|
usages[0].total_tokens,
|
|
usages[0].prompt_tokens_details.cached_tokens if usages[0].prompt_tokens_details is not None else None,
|
|
) == (12011, 8, 12019, 12002), lines
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/chat/completions")]
|
|
rows: Final = eventually(
|
|
lambda: read_rows(
|
|
'SELECT prompt_tokens, completion_tokens, total_tokens FROM "LiteLLM_SpendLogs" WHERE request_id=%s',
|
|
(identity,),
|
|
),
|
|
lambda values: len(values) == 1,
|
|
seconds=70,
|
|
)
|
|
assert (rows[0]["prompt_tokens"], rows[0]["completion_tokens"], rows[0]["total_tokens"]) == (12011, 8, 12019)
|
|
|
|
|
|
def test_databricks_json_schema_refs_are_preserved_in_chat_completions(gateway: Gateway) -> None:
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/chat/completions"
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
assert body["model"] == _JSON_SCHEMA_BACKEND
|
|
assert body["messages"] == [{"role": "user", "content": _JSON_SCHEMA_PROMPT}]
|
|
assert body["stream"] is False
|
|
assert body["response_format"] == _JSON_SCHEMA_RESPONSE_FORMAT
|
|
return Reply(
|
|
body=_chat_completion_response(
|
|
_JSON_SCHEMA_BACKEND,
|
|
{"role": "assistant", "content": _JSON_CONTENT},
|
|
"stop",
|
|
)
|
|
)
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(model=f"databricks/{_JSON_SCHEMA_BACKEND}", api_base=wire.url, api_key=_API_KEY)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": _JSON_SCHEMA_PROMPT}],
|
|
"response_format": _JSON_SCHEMA_RESPONSE_FORMAT,
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
body: Final = _JSON_OBJECT.validate_json(response.content)
|
|
assert body["choices"] == [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": _JSON_CONTENT},
|
|
"finish_reason": "stop",
|
|
}
|
|
]
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/chat/completions")]
|
|
|
|
|
|
def test_databricks_streaming_json_schema_refs_are_preserved(gateway: Gateway) -> None:
|
|
identity: Final = f"databricks-json-schema-stream-{uuid.uuid4().hex}"
|
|
frames: Final = (
|
|
_frame(
|
|
identity,
|
|
[{"index": 0, "delta": {"role": "assistant", "content": '{"p":'}, "finish_reason": None}],
|
|
model=_JSON_SCHEMA_BACKEND,
|
|
),
|
|
_frame(
|
|
identity,
|
|
[{"index": 0, "delta": {"content": '{"name":"Ada"}}'}, "finish_reason": None}],
|
|
model=_JSON_SCHEMA_BACKEND,
|
|
),
|
|
_frame(identity, [{"index": 0, "delta": {}, "finish_reason": "stop"}], model=_JSON_SCHEMA_BACKEND),
|
|
b"data: [DONE]\n\n",
|
|
)
|
|
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/chat/completions"
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
assert body["model"] == _JSON_SCHEMA_BACKEND
|
|
assert body["messages"] == [{"role": "user", "content": _JSON_SCHEMA_PROMPT}]
|
|
assert body["stream"] is True
|
|
assert body["response_format"] == _JSON_SCHEMA_RESPONSE_FORMAT
|
|
return Reply(content_type="text/event-stream", chunks=frames)
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(model=f"databricks/{_JSON_SCHEMA_BACKEND}", api_base=wire.url, api_key=_API_KEY)
|
|
with gateway.client.stream(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
json={
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": _JSON_SCHEMA_PROMPT}],
|
|
"response_format": _JSON_SCHEMA_RESPONSE_FORMAT,
|
|
"stream": True,
|
|
},
|
|
headers={"Authorization": f"Bearer {gateway.key}"},
|
|
) as response:
|
|
assert response.status_code == 200, response.read()
|
|
lines: Final = tuple(line for line in response.iter_lines() if line.startswith("data: "))
|
|
assert lines[-1] == "data: [DONE]", lines
|
|
chunks: Final = tuple(_Chunk.model_validate_json(line.removeprefix("data: ")) for line in lines[:-1])
|
|
choices: Final = tuple(itertools.chain.from_iterable(chunk.choices for chunk in chunks))
|
|
assert "".join(choice.delta.content or "" for choice in choices) == _JSON_CONTENT
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/chat/completions")]
|
|
|
|
|
|
def test_databricks_claude_json_schema_refs_are_preserved_in_tool_parameters(gateway: Gateway) -> None:
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/chat/completions"
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
assert body["model"] == _CLAUDE_BACKEND
|
|
assert body["messages"] == [{"role": "user", "content": _JSON_SCHEMA_PROMPT}]
|
|
assert "response_format" not in body
|
|
assert body["tools"] == [
|
|
{
|
|
"type": "function",
|
|
"function": {"name": "json_tool_call", "parameters": _PERSON_SCHEMA},
|
|
}
|
|
]
|
|
assert body["tool_choice"] == {"type": "function", "function": {"name": "json_tool_call"}}
|
|
return Reply(body=_chat_completion_response(_CLAUDE_BACKEND, _JSON_TOOL_CALL_MESSAGE, "tool_calls"))
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(model=f"databricks/{_CLAUDE_BACKEND}", api_base=wire.url, api_key=_API_KEY)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": _JSON_SCHEMA_PROMPT}],
|
|
"response_format": _JSON_SCHEMA_RESPONSE_FORMAT,
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
body: Final = _JSON_OBJECT.validate_json(response.content)
|
|
assert body["choices"] == [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": _JSON_CONTENT},
|
|
"finish_reason": "stop",
|
|
}
|
|
]
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/chat/completions")]
|
|
|
|
|
|
def test_databricks_responses_json_schema_refs_are_preserved_in_chat_bridge(gateway: Gateway) -> None:
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/chat/completions"
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
assert body["model"] == _JSON_SCHEMA_BACKEND
|
|
assert body["messages"] == [{"role": "user", "content": _JSON_SCHEMA_PROMPT}]
|
|
assert body["response_format"] == {
|
|
"type": "json_schema",
|
|
"json_schema": {"name": "P", "schema": _PERSON_SCHEMA, "strict": True},
|
|
}
|
|
return Reply(
|
|
body=_chat_completion_response(
|
|
_JSON_SCHEMA_BACKEND,
|
|
{"role": "assistant", "content": _JSON_CONTENT},
|
|
"stop",
|
|
)
|
|
)
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(model=f"databricks/{_JSON_SCHEMA_BACKEND}", api_base=wire.url, api_key=_API_KEY)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/responses",
|
|
{
|
|
"model": model,
|
|
"input": _JSON_SCHEMA_PROMPT,
|
|
"text": {
|
|
"format": {
|
|
"type": "json_schema",
|
|
"name": "P",
|
|
"strict": True,
|
|
"schema": _PERSON_SCHEMA,
|
|
}
|
|
},
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/chat/completions")]
|
|
|
|
|
|
def test_databricks_messages_json_schema_refs_are_preserved_for_qwen(gateway: Gateway) -> None:
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/chat/completions"
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
assert body["model"] == _JSON_SCHEMA_BACKEND
|
|
assert body["response_format"] == _MESSAGES_RESPONSE_FORMAT
|
|
return Reply(
|
|
body=_chat_completion_response(
|
|
_JSON_SCHEMA_BACKEND,
|
|
{"role": "assistant", "content": _JSON_CONTENT},
|
|
"stop",
|
|
)
|
|
)
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(model=f"databricks/{_JSON_SCHEMA_BACKEND}", api_base=wire.url, api_key=_API_KEY)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/messages",
|
|
{
|
|
"model": model,
|
|
"max_tokens": 64,
|
|
"messages": [{"role": "user", "content": _JSON_SCHEMA_PROMPT}],
|
|
"output_format": {"type": "json_schema", "schema": _PERSON_SCHEMA},
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
body: Final = _JSON_OBJECT.validate_json(response.content)
|
|
assert body["content"] == [{"type": "text", "text": _JSON_CONTENT}]
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/chat/completions")]
|
|
|
|
|
|
def test_databricks_messages_claude_json_schema_refs_are_preserved_in_tool_parameters(gateway: Gateway) -> None:
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/chat/completions"
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
assert body["model"] == _CLAUDE_BACKEND
|
|
assert "response_format" not in body
|
|
assert body["tools"] == [
|
|
{
|
|
"type": "function",
|
|
"function": {"name": "json_tool_call", "parameters": _MESSAGES_PERSON_SCHEMA},
|
|
}
|
|
]
|
|
assert body["tool_choice"] == {"type": "function", "function": {"name": "json_tool_call"}}
|
|
return Reply(body=_chat_completion_response(_CLAUDE_BACKEND, _JSON_TOOL_CALL_MESSAGE, "tool_calls"))
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(model=f"databricks/{_CLAUDE_BACKEND}", api_base=wire.url, api_key=_API_KEY)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/messages",
|
|
{
|
|
"model": model,
|
|
"max_tokens": 64,
|
|
"messages": [{"role": "user", "content": _JSON_SCHEMA_PROMPT}],
|
|
"output_format": {"type": "json_schema", "schema": _PERSON_SCHEMA},
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
body: Final = _JSON_OBJECT.validate_json(response.content)
|
|
assert body["content"] == [{"type": "text", "text": _JSON_CONTENT}]
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/chat/completions")]
|
|
|
|
|
|
_Endpoint: TypeAlias = Literal["chat", "responses", "messages"]
|
|
_PATHS: Final[Mapping[_Endpoint, str]] = {
|
|
"chat": "/v1/chat/completions",
|
|
"responses": "/v1/responses",
|
|
"messages": "/v1/messages",
|
|
}
|
|
_OUTAGE: Final = "upstream-outage"
|
|
_OUTAGE_MESSAGE: Final = "The service is temporarily unavailable"
|
|
_EXPECTED_JSON: Final[JsonValue] = _JSON_OBJECT.validate_json(_JSON_CONTENT)
|
|
_CONTENT_DELTAS: Final[tuple[Mapping[str, JsonValue], ...]] = (
|
|
{"role": "assistant", "content": '{"p":'},
|
|
{"content": '{"name":"Ada"}}'},
|
|
)
|
|
_TOOL_CALL_DELTAS: Final[tuple[Mapping[str, JsonValue], ...]] = (
|
|
{
|
|
"role": "assistant",
|
|
"content": None,
|
|
"tool_calls": [
|
|
{
|
|
"index": 0,
|
|
"id": "json-tool-call",
|
|
"type": "function",
|
|
"function": {"name": "json_tool_call", "arguments": ""},
|
|
}
|
|
],
|
|
},
|
|
{"tool_calls": [{"index": 0, "function": {"arguments": _JSON_CONTENT}}]},
|
|
)
|
|
|
|
|
|
class _ChatMessage(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
content: str | None = None
|
|
|
|
|
|
class _ChatChoice(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
message: _ChatMessage
|
|
|
|
|
|
class _ChatCompletion(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
choices: tuple[_ChatChoice, ...]
|
|
|
|
|
|
class _OutputPart(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
type: str
|
|
text: str | None = None
|
|
|
|
|
|
class _OutputItem(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
type: str
|
|
content: tuple[_OutputPart, ...] = ()
|
|
name: str | None = None
|
|
arguments: str | None = None
|
|
|
|
|
|
class _ResponsesBody(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
output: tuple[_OutputItem, ...]
|
|
|
|
|
|
class _ResponsesEvent(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
type: str
|
|
response: _ResponsesBody | None = None
|
|
|
|
|
|
class _MessagesBlock(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
type: str
|
|
text: str | None = None
|
|
|
|
|
|
class _MessagesBody(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
content: tuple[_MessagesBlock, ...]
|
|
|
|
|
|
class _MessagesDelta(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
type: str | None = None
|
|
text: str | None = None
|
|
partial_json: str | None = None
|
|
|
|
|
|
class _MessagesEvent(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
type: str
|
|
delta: _MessagesDelta | None = None
|
|
|
|
|
|
class _FunctionDelta(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
arguments: str | None = None
|
|
|
|
|
|
class _ToolCallDelta(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
function: _FunctionDelta | None = None
|
|
|
|
|
|
class _StreamDelta(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
content: str | None = None
|
|
tool_calls: tuple[_ToolCallDelta, ...] | None = None
|
|
|
|
|
|
class _StreamChoice(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
delta: _StreamDelta
|
|
|
|
|
|
class _StreamChunk(BaseModel):
|
|
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
choices: tuple[_StreamChoice, ...]
|
|
|
|
|
|
def _json_schema_format(schema: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
|
|
return {"type": "json_schema", "json_schema": {"name": "P", "strict": True, "schema": dict(schema)}}
|
|
|
|
|
|
def _client_body(
|
|
endpoint: _Endpoint,
|
|
model: str,
|
|
stream: bool,
|
|
prompt: str = _JSON_SCHEMA_PROMPT,
|
|
) -> dict[str, JsonValue]:
|
|
match endpoint:
|
|
case "chat":
|
|
return {
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"response_format": _json_schema_format(_PERSON_SCHEMA),
|
|
"stream": stream,
|
|
}
|
|
case "responses":
|
|
return {
|
|
"model": model,
|
|
"input": prompt,
|
|
"text": {
|
|
"format": {"type": "json_schema", "name": "P", "strict": True, "schema": dict(_PERSON_SCHEMA)}
|
|
},
|
|
"stream": stream,
|
|
}
|
|
case "messages":
|
|
return {
|
|
"model": model,
|
|
"max_tokens": 64,
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"output_format": {"type": "json_schema", "schema": dict(_PERSON_SCHEMA)},
|
|
"stream": stream,
|
|
}
|
|
|
|
|
|
def _expected_outbound(endpoint: _Endpoint, backend: str) -> dict[str, JsonValue]:
|
|
schema: Final = dict(_MESSAGES_PERSON_SCHEMA if endpoint == "messages" else _PERSON_SCHEMA)
|
|
if "claude" in backend:
|
|
return _JSON_OBJECT.validate_python(
|
|
{
|
|
"tools": [{"type": "function", "function": {"name": "json_tool_call", "parameters": schema}}],
|
|
"tool_choice": {"type": "function", "function": {"name": "json_tool_call"}},
|
|
}
|
|
)
|
|
name: Final = "structured_output" if endpoint == "messages" else "P"
|
|
return _JSON_OBJECT.validate_python(
|
|
{"response_format": {"type": "json_schema", "json_schema": {"name": name, "schema": schema, "strict": True}}}
|
|
)
|
|
|
|
|
|
def _assert_outbound(request: Request, endpoint: _Endpoint, backend: str, stream: bool) -> None:
|
|
assert (request.method, request.target) == ("POST", "/chat/completions")
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
expected: Final = _expected_outbound(endpoint, backend)
|
|
assert {key: body.get(key) for key in expected} == expected
|
|
assert (body["model"], body.get("stream") is True) == (backend, stream)
|
|
assert "claude" not in backend or "response_format" not in body
|
|
|
|
|
|
def _upstream_reply(backend: str, stream: bool) -> Reply:
|
|
claude: Final = "claude" in backend
|
|
finish_reason: Final = "tool_calls" if claude else "stop"
|
|
if not stream:
|
|
message: Final[dict[str, JsonValue]] = (
|
|
_JSON_TOOL_CALL_MESSAGE if claude else {"role": "assistant", "content": _JSON_CONTENT}
|
|
)
|
|
return Reply(body=_chat_completion_response(backend, message, finish_reason))
|
|
identity: Final = f"databricks-json-schema-{uuid.uuid4().hex}"
|
|
deltas: Final = _TOOL_CALL_DELTAS if claude else _CONTENT_DELTAS
|
|
frames: Final = (
|
|
*(_frame(identity, [{"index": 0, "delta": delta, "finish_reason": None}], model=backend) for delta in deltas),
|
|
_frame(identity, [{"index": 0, "delta": {}, "finish_reason": finish_reason}], model=backend),
|
|
b"data: [DONE]\n\n",
|
|
)
|
|
return Reply(content_type="text/event-stream", chunks=frames)
|
|
|
|
|
|
def _outage_or_reply(request: Request) -> Reply:
|
|
if _OUTAGE.encode() in request.body:
|
|
error: Final = {"error_code": "TEMPORARILY_UNAVAILABLE", "message": _OUTAGE_MESSAGE}
|
|
return Reply(status=503, body=json.dumps(error).encode())
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
return _upstream_reply(str(body["model"]), body.get("stream") is True)
|
|
|
|
|
|
def _item_text(item: _OutputItem) -> str:
|
|
match item.type:
|
|
case "message":
|
|
return "".join(part.text or "" for part in item.content)
|
|
case "function_call" if item.name == "json_tool_call":
|
|
return item.arguments or ""
|
|
case _:
|
|
return ""
|
|
|
|
|
|
def _responses_text(body: _ResponsesBody) -> str:
|
|
return "".join(_item_text(item) for item in body.output)
|
|
|
|
|
|
def _chat_delta_text(delta: _StreamDelta) -> str:
|
|
calls: Final = delta.tool_calls or ()
|
|
return (delta.content or "") + "".join(call.function.arguments or "" for call in calls if call.function is not None)
|
|
|
|
|
|
def _messages_delta_text(delta: _MessagesDelta) -> str:
|
|
match delta.type:
|
|
case "text_delta":
|
|
return delta.text or ""
|
|
case "input_json_delta":
|
|
return delta.partial_json or ""
|
|
case _:
|
|
return ""
|
|
|
|
|
|
def _final_text(endpoint: _Endpoint, content: bytes) -> str:
|
|
match endpoint:
|
|
case "chat":
|
|
return _ChatCompletion.model_validate_json(content).choices[0].message.content or ""
|
|
case "responses":
|
|
return _responses_text(_ResponsesBody.model_validate_json(content))
|
|
case "messages":
|
|
return "".join(block.text or "" for block in _MessagesBody.model_validate_json(content).content)
|
|
|
|
|
|
def _streamed_text(endpoint: _Endpoint, data: tuple[str, ...]) -> str:
|
|
match endpoint:
|
|
case "chat":
|
|
assert data[-1] == "[DONE]", data
|
|
chunks: Final = tuple(_StreamChunk.model_validate_json(line) for line in data[:-1])
|
|
choices: Final = tuple(itertools.chain.from_iterable(chunk.choices for chunk in chunks))
|
|
return "".join(_chat_delta_text(choice.delta) for choice in choices)
|
|
case "responses":
|
|
events: Final = tuple(_ResponsesEvent.model_validate_json(line) for line in data if line != "[DONE]")
|
|
completed: Final = tuple(event.response for event in events if event.type == "response.completed")
|
|
assert len(completed) == 1 and completed[0] is not None, data
|
|
return _responses_text(completed[0])
|
|
case "messages":
|
|
message_events: Final = tuple(_MessagesEvent.model_validate_json(line) for line in data)
|
|
assert message_events[-1].type == "message_stop", data
|
|
deltas: Final = tuple(event.delta for event in message_events if event.delta is not None)
|
|
return "".join(_messages_delta_text(delta) for delta in deltas)
|
|
|
|
|
|
def _outcome(status: int, text: str) -> tuple[int, JsonValue]:
|
|
if status == 200:
|
|
return status, _JSON_OBJECT.validate_json(text)
|
|
return status, _OUTAGE_MESSAGE if _OUTAGE_MESSAGE in text else text
|
|
|
|
|
|
def _call(gateway: Gateway, endpoint: _Endpoint, body: Mapping[str, JsonValue]) -> tuple[int, str]:
|
|
if body["stream"] is not True:
|
|
response: Final = gateway.request("POST", _PATHS[endpoint], body)
|
|
return response.status_code, _final_text(endpoint, response.content) if response.is_success else response.text
|
|
with gateway.client.stream(
|
|
"POST", _PATHS[endpoint], json=dict(body), headers={"Authorization": f"Bearer {gateway.key}"}
|
|
) as streamed:
|
|
if not streamed.is_success:
|
|
return streamed.status_code, streamed.read().decode()
|
|
data: Final = tuple(line.removeprefix("data: ") for line in streamed.iter_lines() if line.startswith("data: "))
|
|
return 200, _streamed_text(endpoint, data)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("endpoint", "backend", "stream"),
|
|
[
|
|
pytest.param("chat", _CLAUDE_BACKEND, True, id="chat-stream-claude"),
|
|
pytest.param("responses", _CLAUDE_BACKEND, False, id="responses-claude"),
|
|
pytest.param("responses", _JSON_SCHEMA_BACKEND, True, id="responses-stream-qwen"),
|
|
pytest.param("responses", _CLAUDE_BACKEND, True, id="responses-stream-claude"),
|
|
pytest.param("messages", _JSON_SCHEMA_BACKEND, True, id="messages-stream-qwen"),
|
|
pytest.param("messages", _CLAUDE_BACKEND, True, id="messages-stream-claude"),
|
|
],
|
|
)
|
|
def test_databricks_json_schema_refs_are_preserved_on_every_endpoint(
|
|
gateway: Gateway, endpoint: _Endpoint, backend: str, stream: bool
|
|
) -> None:
|
|
with wire_server(lambda _request: _upstream_reply(backend, stream)) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(model=f"databricks/{backend}", api_base=wire.url, api_key=_API_KEY)
|
|
status, text = _call(gateway, endpoint, _client_body(endpoint, model, stream))
|
|
received: Final = wire.drain()
|
|
assert len(received) == 1, received
|
|
_assert_outbound(received[0], endpoint, backend, stream)
|
|
assert _outcome(status, text) == (200, _EXPECTED_JSON), text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"response_format",
|
|
[
|
|
pytest.param(
|
|
_json_schema_format(
|
|
{
|
|
"definitions": {
|
|
"Person": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}
|
|
},
|
|
"type": "object",
|
|
"properties": {"p": {"$ref": "#/definitions/Person"}},
|
|
"required": ["p"],
|
|
}
|
|
),
|
|
id="definitions-ref",
|
|
),
|
|
pytest.param(
|
|
_json_schema_format(
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"children": {"type": "array", "items": {"$ref": "#"}},
|
|
},
|
|
"required": ["name", "children"],
|
|
}
|
|
),
|
|
id="recursive-root-ref",
|
|
),
|
|
pytest.param(
|
|
_json_schema_format({"type": "object", "properties": {"$ref": {"type": "string"}}, "required": ["$ref"]}),
|
|
id="property-named-ref",
|
|
),
|
|
pytest.param(
|
|
_json_schema_format({"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}),
|
|
id="flat-schema",
|
|
),
|
|
pytest.param({"type": "json_object"}, id="json-object"),
|
|
],
|
|
)
|
|
def test_databricks_response_format_reaches_upstream_as_sent(
|
|
gateway: Gateway, response_format: dict[str, JsonValue]
|
|
) -> None:
|
|
with (
|
|
wire_server(lambda _request: _upstream_reply(_JSON_SCHEMA_BACKEND, False)) as wire,
|
|
gateway.scenario() as scenario,
|
|
):
|
|
model: Final = scenario.model(model=f"databricks/{_JSON_SCHEMA_BACKEND}", api_base=wire.url, api_key=_API_KEY)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": _JSON_SCHEMA_PROMPT}],
|
|
"response_format": response_format,
|
|
},
|
|
)
|
|
received: Final = wire.drain()
|
|
assert response.status_code == 200, response.text
|
|
assert _final_text("chat", response.content) == _JSON_CONTENT
|
|
assert [(request.method, request.target) for request in received] == [("POST", "/chat/completions")]
|
|
assert _JSON_OBJECT.validate_json(received[0].body)["response_format"] == response_format
|
|
|
|
|
|
def test_databricks_json_schema_without_schema_returns_the_upstream_error(gateway: Gateway) -> None:
|
|
response_format: Final[dict[str, JsonValue]] = {"type": "json_schema", "json_schema": {"name": "P", "strict": True}}
|
|
upstream_error: Final = {"error_code": "INVALID_PARAMETER_VALUE", "message": "json_schema.schema is required"}
|
|
|
|
def respond(_request: Request) -> Reply:
|
|
return Reply(status=400, body=json.dumps(upstream_error).encode())
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(model=f"databricks/{_JSON_SCHEMA_BACKEND}", api_base=wire.url, api_key=_API_KEY)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": _JSON_SCHEMA_PROMPT}],
|
|
"response_format": response_format,
|
|
},
|
|
)
|
|
received: Final = wire.drain()
|
|
assert (response.status_code, "json_schema.schema is required" in response.text) == (400, True), response.text
|
|
assert [(request.method, request.target) for request in received] == [("POST", "/chat/completions")]
|
|
assert _JSON_OBJECT.validate_json(received[0].body)["response_format"] == response_format
|
|
|
|
|
|
def _assert_burst_outbound(request: Request) -> None:
|
|
body: Final = _JSON_OBJECT.validate_json(request.body)
|
|
endpoint: Final[_Endpoint] = "messages" if body["response_format"] == _MESSAGES_RESPONSE_FORMAT else "chat"
|
|
_assert_outbound(request, endpoint, _JSON_SCHEMA_BACKEND, body.get("stream") is True)
|
|
|
|
|
|
def _burst_request(index: int) -> tuple[_Endpoint, bool, bool]:
|
|
endpoints: Final[tuple[_Endpoint, ...]] = ("chat", "responses", "messages")
|
|
return endpoints[index % 3], index % 2 == 1, index % 5 == 2
|
|
|
|
|
|
def test_databricks_json_schema_burst_through_an_upstream_outage_keeps_every_ref(gateway: Gateway) -> None:
|
|
plan: Final = tuple(_burst_request(index) for index in range(24))
|
|
recovery: Final[tuple[_Endpoint, ...]] = ("chat", "responses", "messages")
|
|
with wire_server(_outage_or_reply) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(model=f"databricks/{_JSON_SCHEMA_BACKEND}", api_base=wire.url, api_key=_API_KEY)
|
|
endpoints: Final = tuple(endpoint for endpoint, _stream, _down in plan)
|
|
bodies: Final = tuple(
|
|
_client_body(endpoint, model, stream, f"{_JSON_SCHEMA_PROMPT} {index} {_OUTAGE if down else ''}")
|
|
for index, (endpoint, stream, down) in enumerate(plan)
|
|
)
|
|
with ThreadPoolExecutor(max_workers=12) as pool:
|
|
results: Final = tuple(pool.map(_call, itertools.repeat(gateway), endpoints, bodies))
|
|
recovered: Final = tuple(
|
|
_call(gateway, endpoint, _client_body(endpoint, model, False)) for endpoint in recovery
|
|
)
|
|
received: Final = wire.drain()
|
|
assert len(received) == len(plan) + len(recovery), received
|
|
for request in received:
|
|
_assert_burst_outbound(request)
|
|
expected: Final = tuple(
|
|
(503, _OUTAGE_MESSAGE) if down else (200, _EXPECTED_JSON) for _endpoint, _stream, down in plan
|
|
)
|
|
assert tuple(_outcome(*result) for result in results) == expected
|
|
assert tuple(_outcome(*result) for result in recovered) == ((200, _EXPECTED_JSON),) * len(recovery)
|