848 lines
26 KiB
Python
848 lines
26 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
Tests for OpenAI API Pydantic models.
|
|
|
|
Tests the request and response models for OpenAI-compatible chat completions,
|
|
text completions, tool calling, and structured output.
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from omlx.api.openai_models import (
|
|
AssistantMessage,
|
|
ChatCompletionChoice,
|
|
ChatCompletionChunk,
|
|
ChatCompletionChunkChoice,
|
|
ChatCompletionChunkDelta,
|
|
ChatCompletionRequest,
|
|
ChatCompletionResponse,
|
|
CompletionChoice,
|
|
CompletionRequest,
|
|
CompletionResponse,
|
|
ContentPart,
|
|
FunctionCall,
|
|
Message,
|
|
ModelInfo,
|
|
ModelsResponse,
|
|
ResponseFormat,
|
|
ResponseFormatJsonSchema,
|
|
ToolCall,
|
|
ToolDefinition,
|
|
Usage,
|
|
)
|
|
|
|
|
|
class TestContentPart:
|
|
"""Tests for ContentPart model."""
|
|
|
|
def test_text_content_part(self):
|
|
"""Test creating text content part."""
|
|
part = ContentPart(type="text", text="Hello world")
|
|
|
|
assert part.type == "text"
|
|
assert part.text == "Hello world"
|
|
|
|
def test_text_content_part_empty_text(self):
|
|
"""Test creating text content part with empty text."""
|
|
part = ContentPart(type="text", text="")
|
|
|
|
assert part.type == "text"
|
|
assert part.text == ""
|
|
|
|
def test_content_part_none_text(self):
|
|
"""Test creating content part without text."""
|
|
part = ContentPart(type="text")
|
|
|
|
assert part.type == "text"
|
|
assert part.text is None
|
|
|
|
def test_file_content_part(self):
|
|
"""Test creating file content part for document preprocessing."""
|
|
part = ContentPart(
|
|
type="file",
|
|
file={
|
|
"filename": "sample.pdf",
|
|
"file_data": "data:application/pdf;base64,ZA==",
|
|
},
|
|
)
|
|
|
|
assert part.type == "file"
|
|
assert part.file.filename == "sample.pdf"
|
|
assert part.file.file_data.endswith("ZA==")
|
|
|
|
|
|
class TestMessage:
|
|
"""Tests for Message model."""
|
|
|
|
def test_simple_user_message(self):
|
|
"""Test creating simple user message."""
|
|
msg = Message(role="user", content="Hello")
|
|
|
|
assert msg.role == "user"
|
|
assert msg.content == "Hello"
|
|
|
|
def test_simple_assistant_message(self):
|
|
"""Test creating simple assistant message."""
|
|
msg = Message(role="assistant", content="Hi there!")
|
|
|
|
assert msg.role == "assistant"
|
|
assert msg.content == "Hi there!"
|
|
|
|
def test_system_message(self):
|
|
"""Test creating system message."""
|
|
msg = Message(role="system", content="You are a helpful assistant.")
|
|
|
|
assert msg.role == "system"
|
|
|
|
def test_message_with_content_array(self):
|
|
"""Test creating message with content array."""
|
|
msg = Message(
|
|
role="user",
|
|
content=[
|
|
{"type": "text", "text": "Hello"},
|
|
{"type": "text", "text": "World"},
|
|
],
|
|
)
|
|
|
|
assert msg.role == "user"
|
|
assert isinstance(msg.content, list)
|
|
assert len(msg.content) == 2
|
|
|
|
def test_message_with_content_parts(self):
|
|
"""Test creating message with ContentPart objects."""
|
|
msg = Message(
|
|
role="user",
|
|
content=[
|
|
ContentPart(type="text", text="Hello"),
|
|
],
|
|
)
|
|
|
|
assert isinstance(msg.content, list)
|
|
assert len(msg.content) == 1
|
|
|
|
def test_assistant_message_with_tool_calls(self):
|
|
"""Test creating assistant message with tool calls."""
|
|
msg = Message(
|
|
role="assistant",
|
|
content=None,
|
|
tool_calls=[
|
|
{
|
|
"id": "call_abc123",
|
|
"type": "function",
|
|
"function": {"name": "get_weather", "arguments": "{}"},
|
|
}
|
|
],
|
|
)
|
|
|
|
assert msg.role == "assistant"
|
|
assert msg.content is None
|
|
assert len(msg.tool_calls) == 1
|
|
|
|
def test_tool_response_message(self):
|
|
"""Test creating tool response message."""
|
|
msg = Message(
|
|
role="tool",
|
|
content='{"weather": "sunny"}',
|
|
tool_call_id="call_abc123",
|
|
)
|
|
|
|
assert msg.role == "tool"
|
|
assert msg.tool_call_id == "call_abc123"
|
|
|
|
|
|
class TestFunctionCallAndToolCall:
|
|
"""Tests for FunctionCall and ToolCall models."""
|
|
|
|
def test_function_call(self):
|
|
"""Test creating function call."""
|
|
fc = FunctionCall(name="get_weather", arguments='{"location": "Tokyo"}')
|
|
|
|
assert fc.name == "get_weather"
|
|
assert fc.arguments == '{"location": "Tokyo"}'
|
|
|
|
def test_function_call_empty_arguments(self):
|
|
"""Test creating function call with empty arguments."""
|
|
fc = FunctionCall(name="no_args", arguments="{}")
|
|
|
|
assert fc.name == "no_args"
|
|
assert fc.arguments == "{}"
|
|
|
|
def test_function_call_strips_name_whitespace(self):
|
|
"""Model-emitted function names must not include incidental whitespace."""
|
|
fc = FunctionCall(name="Bash\n\n", arguments={})
|
|
|
|
assert fc.name == "Bash"
|
|
assert fc.arguments == "{}"
|
|
|
|
def test_function_call_rejects_non_string_name(self):
|
|
"""Name normalization must not widen FunctionCall.name beyond str."""
|
|
for name in (None, 123, {"name": "Bash"}, ["Bash"]):
|
|
with pytest.raises(ValidationError):
|
|
FunctionCall(name=name, arguments={})
|
|
|
|
def test_tool_call(self):
|
|
"""Test creating tool call."""
|
|
tc = ToolCall(
|
|
id="call_abc123",
|
|
type="function",
|
|
function=FunctionCall(
|
|
name="get_weather", arguments='{"location": "Tokyo"}'
|
|
),
|
|
)
|
|
|
|
assert tc.id == "call_abc123"
|
|
assert tc.type == "function"
|
|
assert tc.function.name == "get_weather"
|
|
|
|
def test_tool_call_default_type(self):
|
|
"""Test that tool call defaults to function type."""
|
|
tc = ToolCall(
|
|
id="call_123",
|
|
function=FunctionCall(name="test", arguments="{}"),
|
|
)
|
|
|
|
assert tc.type == "function"
|
|
|
|
|
|
class TestFunctionCallArgumentsValidation:
|
|
"""Tests for FunctionCall.arguments JSON-object validator.
|
|
|
|
Native tool-calling chat templates iterate `arguments.items()`, which
|
|
requires the echoed value to parse back into a dict. Malformed inputs
|
|
must be rejected at the request boundary with a 422-friendly ValueError.
|
|
"""
|
|
|
|
def test_accepts_empty_string_as_empty_object(self):
|
|
"""Some clients send ``""`` — normalize to ``"{}"`` instead of rejecting."""
|
|
fc = FunctionCall(name="f", arguments="")
|
|
assert fc.arguments == "{}"
|
|
|
|
def test_accepts_whitespace_only_string(self):
|
|
"""Whitespace-only input normalizes to ``"{}"`` too."""
|
|
fc = FunctionCall(name="f", arguments=" ")
|
|
assert fc.arguments == "{}"
|
|
|
|
def test_accepts_dict_and_serializes(self):
|
|
"""Some clients wrongly send a dict; coerce to JSON string instead of 422."""
|
|
fc = FunctionCall(name="f", arguments={"a": 1, "b": "x"})
|
|
assert json.loads(fc.arguments) == {"a": 1, "b": "x"}
|
|
|
|
def test_rejects_invalid_json(self):
|
|
"""Truncated or otherwise malformed JSON must raise."""
|
|
with pytest.raises(ValidationError) as exc:
|
|
FunctionCall(name="f", arguments='{"a": "b')
|
|
msg = str(exc.value)
|
|
assert "valid JSON" in msg
|
|
|
|
def test_rejects_python_repr_with_single_quotes(self):
|
|
"""``{'a': 1}`` is not valid JSON and must be rejected."""
|
|
with pytest.raises(ValidationError):
|
|
FunctionCall(name="f", arguments="{'a': 1}")
|
|
|
|
def test_rejects_bare_string(self):
|
|
"""``"Tokyo"`` parses as a JSON string, not an object — reject."""
|
|
with pytest.raises(ValidationError) as exc:
|
|
FunctionCall(name="f", arguments='"Tokyo"')
|
|
assert "JSON object" in str(exc.value)
|
|
|
|
def test_rejects_json_array(self):
|
|
"""Arrays are valid JSON but not objects; reject."""
|
|
with pytest.raises(ValidationError) as exc:
|
|
FunctionCall(name="f", arguments="[1, 2, 3]")
|
|
assert "JSON object" in str(exc.value)
|
|
|
|
def test_rejects_plain_text(self):
|
|
"""Plain text that isn't JSON at all — reject."""
|
|
with pytest.raises(ValidationError):
|
|
FunctionCall(name="f", arguments="Tokyo")
|
|
|
|
def test_rejects_numeric_type(self):
|
|
"""Non-string, non-dict types are rejected."""
|
|
with pytest.raises(ValidationError):
|
|
FunctionCall(name="f", arguments=123)
|
|
|
|
|
|
class TestMessageToolCallsArgumentsValidation:
|
|
"""Tests that Message.tool_calls validates nested arguments.
|
|
|
|
Since tool_calls is typed as List[dict], Pydantic does not transitively
|
|
run FunctionCall's own validator on the nested entries. A Message-level
|
|
validator rejects malformed echoes so downstream template rendering is
|
|
safe.
|
|
"""
|
|
|
|
def test_valid_tool_calls_accepted(self):
|
|
msg = Message(
|
|
role="assistant",
|
|
content=None,
|
|
tool_calls=[
|
|
{
|
|
"id": "c1",
|
|
"type": "function",
|
|
"function": {"name": "f", "arguments": '{"a": 1}'},
|
|
}
|
|
],
|
|
)
|
|
assert msg.tool_calls[0]["function"]["arguments"] == '{"a": 1}'
|
|
|
|
def test_empty_arguments_normalized(self):
|
|
msg = Message(
|
|
role="assistant",
|
|
tool_calls=[{"function": {"name": "f", "arguments": ""}}],
|
|
)
|
|
assert msg.tool_calls[0]["function"]["arguments"] == "{}"
|
|
|
|
def test_dict_arguments_coerced(self):
|
|
msg = Message(
|
|
role="assistant",
|
|
tool_calls=[{"function": {"name": "f", "arguments": {"a": 1}}}],
|
|
)
|
|
assert json.loads(msg.tool_calls[0]["function"]["arguments"]) == {"a": 1}
|
|
|
|
def test_malformed_json_rejected(self):
|
|
with pytest.raises(ValidationError):
|
|
Message(
|
|
role="assistant",
|
|
tool_calls=[{"function": {"name": "f", "arguments": '{"a":'}}],
|
|
)
|
|
|
|
def test_python_repr_rejected(self):
|
|
with pytest.raises(ValidationError):
|
|
Message(
|
|
role="assistant",
|
|
tool_calls=[{"function": {"name": "f", "arguments": "{'a': 1}"}}],
|
|
)
|
|
|
|
def test_plain_text_rejected(self):
|
|
with pytest.raises(ValidationError):
|
|
Message(
|
|
role="assistant",
|
|
tool_calls=[{"function": {"name": "f", "arguments": "Tokyo"}}],
|
|
)
|
|
|
|
def test_missing_arguments_key_allowed(self):
|
|
"""Some payloads omit arguments entirely — don't invent a failure."""
|
|
msg = Message(
|
|
role="assistant",
|
|
tool_calls=[{"function": {"name": "f"}}],
|
|
)
|
|
assert msg.tool_calls[0]["function"] == {"name": "f"}
|
|
|
|
|
|
class TestToolDefinition:
|
|
"""Tests for ToolDefinition model."""
|
|
|
|
def test_tool_definition(self):
|
|
"""Test creating tool definition."""
|
|
tool = ToolDefinition(
|
|
type="function",
|
|
function={
|
|
"name": "get_weather",
|
|
"description": "Get weather info",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"location": {"type": "string"},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
assert tool.type == "function"
|
|
assert tool.function["name"] == "get_weather"
|
|
|
|
def test_tool_definition_default_type(self):
|
|
"""Test that tool definition defaults to function type."""
|
|
tool = ToolDefinition(
|
|
function={"name": "test", "parameters": {}},
|
|
)
|
|
|
|
assert tool.type == "function"
|
|
|
|
|
|
class TestResponseFormat:
|
|
"""Tests for ResponseFormat model."""
|
|
|
|
def test_text_format(self):
|
|
"""Test text response format."""
|
|
rf = ResponseFormat(type="text")
|
|
|
|
assert rf.type == "text"
|
|
assert rf.json_schema is None
|
|
|
|
def test_json_object_format(self):
|
|
"""Test JSON object response format."""
|
|
rf = ResponseFormat(type="json_object")
|
|
|
|
assert rf.type == "json_object"
|
|
|
|
def test_json_schema_format(self):
|
|
"""Test JSON schema response format."""
|
|
rf = ResponseFormat(
|
|
type="json_schema",
|
|
json_schema=ResponseFormatJsonSchema(
|
|
name="person",
|
|
description="A person object",
|
|
schema={"type": "object", "properties": {"name": {"type": "string"}}},
|
|
),
|
|
)
|
|
|
|
assert rf.type == "json_schema"
|
|
assert rf.json_schema.name == "person"
|
|
assert rf.json_schema.description == "A person object"
|
|
|
|
def test_response_format_default_type(self):
|
|
"""Test response format defaults to text."""
|
|
rf = ResponseFormat()
|
|
|
|
assert rf.type == "text"
|
|
|
|
|
|
class TestChatCompletionRequest:
|
|
"""Tests for ChatCompletionRequest model."""
|
|
|
|
def test_minimal_request(self):
|
|
"""Test creating minimal chat completion request."""
|
|
req = ChatCompletionRequest(
|
|
model="gpt-4",
|
|
messages=[Message(role="user", content="Hello")],
|
|
)
|
|
|
|
assert req.model == "gpt-4"
|
|
assert len(req.messages) == 1
|
|
assert req.temperature is None
|
|
assert req.top_p is None
|
|
assert req.max_tokens is None
|
|
assert req.stream is False
|
|
assert req.tools is None
|
|
|
|
def test_request_with_all_fields(self):
|
|
"""Test creating request with all fields."""
|
|
req = ChatCompletionRequest(
|
|
model="gpt-4",
|
|
messages=[
|
|
Message(role="system", content="Be helpful"),
|
|
Message(role="user", content="Hello"),
|
|
],
|
|
temperature=0.7,
|
|
top_p=0.9,
|
|
max_tokens=1024,
|
|
stream=True,
|
|
stop=["STOP"],
|
|
tools=[
|
|
ToolDefinition(
|
|
function={"name": "test", "parameters": {}},
|
|
)
|
|
],
|
|
tool_choice="auto",
|
|
response_format=ResponseFormat(type="json_object"),
|
|
)
|
|
|
|
assert req.model == "gpt-4"
|
|
assert len(req.messages) == 2
|
|
assert req.temperature == 0.7
|
|
assert req.top_p == 0.9
|
|
assert req.max_tokens == 1024
|
|
assert req.stream is True
|
|
assert req.stop == ["STOP"]
|
|
assert len(req.tools) == 1
|
|
assert req.tool_choice == "auto"
|
|
|
|
def test_max_completion_tokens_alias(self):
|
|
"""Test OpenAI max_completion_tokens maps to max_tokens."""
|
|
req = ChatCompletionRequest.model_validate(
|
|
{
|
|
"model": "gpt-4",
|
|
"messages": [{"role": "user", "content": "Hello"}],
|
|
"max_completion_tokens": 65536,
|
|
}
|
|
)
|
|
|
|
assert req.max_tokens == 65536
|
|
|
|
def test_max_tokens_preferred_over_alias(self):
|
|
"""Test canonical max_tokens wins when both aliases are present."""
|
|
req = ChatCompletionRequest.model_validate(
|
|
{
|
|
"model": "gpt-4",
|
|
"messages": [{"role": "user", "content": "Hello"}],
|
|
"max_tokens": 4096,
|
|
"max_completion_tokens": 65536,
|
|
}
|
|
)
|
|
|
|
assert req.max_tokens == 4096
|
|
|
|
def test_request_validation_requires_model(self):
|
|
"""Test that model is required."""
|
|
with pytest.raises(ValidationError):
|
|
ChatCompletionRequest(
|
|
messages=[Message(role="user", content="Hello")],
|
|
)
|
|
|
|
def test_request_validation_requires_messages(self):
|
|
"""Test that messages is required."""
|
|
with pytest.raises(ValidationError):
|
|
ChatCompletionRequest(model="gpt-4")
|
|
|
|
def test_request_serialization(self):
|
|
"""Test request can be serialized to JSON."""
|
|
req = ChatCompletionRequest(
|
|
model="gpt-4",
|
|
messages=[Message(role="user", content="Hello")],
|
|
)
|
|
|
|
json_str = req.model_dump_json()
|
|
data = json.loads(json_str)
|
|
|
|
assert data["model"] == "gpt-4"
|
|
assert data["messages"][0]["role"] == "user"
|
|
|
|
def test_xtc_defaults_to_none(self):
|
|
"""Test XTC params default to None (not sent by client)."""
|
|
req = ChatCompletionRequest(
|
|
model="gpt-4",
|
|
messages=[Message(role="user", content="Hello")],
|
|
)
|
|
assert req.xtc_probability is None
|
|
assert req.xtc_threshold is None
|
|
|
|
def test_xtc_accepted(self):
|
|
"""Test XTC params are accepted in request."""
|
|
req = ChatCompletionRequest(
|
|
model="gpt-4",
|
|
messages=[Message(role="user", content="Hello")],
|
|
xtc_probability=0.5,
|
|
xtc_threshold=0.1,
|
|
)
|
|
assert req.xtc_probability == 0.5
|
|
assert req.xtc_threshold == 0.1
|
|
|
|
def test_guided_grammar_accepted(self):
|
|
"""Test guided_grammar is accepted as a grammar alias."""
|
|
req = ChatCompletionRequest(
|
|
model="gpt-4",
|
|
messages=[Message(role="user", content="Hello")],
|
|
guided_grammar='root ::= "YES"',
|
|
)
|
|
assert req.guided_grammar == 'root ::= "YES"'
|
|
|
|
|
|
class TestChatCompletionResponse:
|
|
"""Tests for ChatCompletionResponse model."""
|
|
|
|
def test_basic_response(self):
|
|
"""Test creating basic chat completion response."""
|
|
resp = ChatCompletionResponse(
|
|
model="gpt-4",
|
|
choices=[
|
|
ChatCompletionChoice(
|
|
message=AssistantMessage(content="Hello!"),
|
|
finish_reason="stop",
|
|
)
|
|
],
|
|
)
|
|
|
|
assert resp.model == "gpt-4"
|
|
assert resp.object == "chat.completion"
|
|
assert len(resp.choices) == 1
|
|
assert resp.choices[0].message.content == "Hello!"
|
|
assert resp.choices[0].finish_reason == "stop"
|
|
|
|
def test_response_with_usage(self):
|
|
"""Test creating response with usage stats."""
|
|
resp = ChatCompletionResponse(
|
|
model="gpt-4",
|
|
choices=[
|
|
ChatCompletionChoice(
|
|
message=AssistantMessage(content="Hello!"),
|
|
)
|
|
],
|
|
usage=Usage(
|
|
prompt_tokens=10,
|
|
completion_tokens=5,
|
|
total_tokens=15,
|
|
),
|
|
)
|
|
|
|
assert resp.usage.prompt_tokens == 10
|
|
assert resp.usage.completion_tokens == 5
|
|
assert resp.usage.total_tokens == 15
|
|
|
|
def test_response_generates_id(self):
|
|
"""Test that response generates an ID."""
|
|
resp = ChatCompletionResponse(
|
|
model="gpt-4",
|
|
choices=[
|
|
ChatCompletionChoice(
|
|
message=AssistantMessage(content="Hello!"),
|
|
)
|
|
],
|
|
)
|
|
|
|
assert resp.id is not None
|
|
assert resp.id.startswith("chatcmpl-")
|
|
|
|
def test_response_with_tool_calls(self):
|
|
"""Test response with tool calls."""
|
|
resp = ChatCompletionResponse(
|
|
model="gpt-4",
|
|
choices=[
|
|
ChatCompletionChoice(
|
|
message=AssistantMessage(
|
|
content=None,
|
|
tool_calls=[
|
|
ToolCall(
|
|
id="call_123",
|
|
function=FunctionCall(name="test", arguments="{}"),
|
|
)
|
|
],
|
|
),
|
|
finish_reason="tool_calls",
|
|
)
|
|
],
|
|
)
|
|
|
|
assert resp.choices[0].finish_reason == "tool_calls"
|
|
assert resp.choices[0].message.tool_calls[0].id == "call_123"
|
|
|
|
def test_response_serialization(self):
|
|
"""Test response can be serialized to JSON."""
|
|
resp = ChatCompletionResponse(
|
|
model="gpt-4",
|
|
choices=[
|
|
ChatCompletionChoice(
|
|
message=AssistantMessage(content="Hello!"),
|
|
)
|
|
],
|
|
)
|
|
|
|
json_str = resp.model_dump_json()
|
|
data = json.loads(json_str)
|
|
|
|
assert data["model"] == "gpt-4"
|
|
assert data["object"] == "chat.completion"
|
|
|
|
|
|
class TestChatCompletionChunk:
|
|
"""Tests for streaming chunk models."""
|
|
|
|
def test_basic_chunk(self):
|
|
"""Test creating basic streaming chunk."""
|
|
chunk = ChatCompletionChunk(
|
|
model="gpt-4",
|
|
choices=[
|
|
ChatCompletionChunkChoice(
|
|
delta=ChatCompletionChunkDelta(content="Hello"),
|
|
)
|
|
],
|
|
)
|
|
|
|
assert chunk.model == "gpt-4"
|
|
assert chunk.object == "chat.completion.chunk"
|
|
assert chunk.choices[0].delta.content == "Hello"
|
|
|
|
def test_chunk_with_role(self):
|
|
"""Test chunk with role in delta."""
|
|
chunk = ChatCompletionChunk(
|
|
model="gpt-4",
|
|
choices=[
|
|
ChatCompletionChunkChoice(
|
|
delta=ChatCompletionChunkDelta(role="assistant"),
|
|
)
|
|
],
|
|
)
|
|
|
|
assert chunk.choices[0].delta.role == "assistant"
|
|
|
|
def test_chunk_with_finish_reason(self):
|
|
"""Test chunk with finish reason."""
|
|
chunk = ChatCompletionChunk(
|
|
model="gpt-4",
|
|
choices=[
|
|
ChatCompletionChunkChoice(
|
|
delta=ChatCompletionChunkDelta(),
|
|
finish_reason="stop",
|
|
)
|
|
],
|
|
)
|
|
|
|
assert chunk.choices[0].finish_reason == "stop"
|
|
|
|
def test_chunk_generates_id(self):
|
|
"""Test that chunk generates an ID."""
|
|
chunk = ChatCompletionChunk(
|
|
model="gpt-4",
|
|
choices=[
|
|
ChatCompletionChunkChoice(
|
|
delta=ChatCompletionChunkDelta(content="Hi"),
|
|
)
|
|
],
|
|
)
|
|
|
|
assert chunk.id is not None
|
|
assert chunk.id.startswith("chatcmpl-")
|
|
|
|
|
|
class TestCompletionModels:
|
|
"""Tests for text completion models."""
|
|
|
|
def test_completion_request(self):
|
|
"""Test creating completion request."""
|
|
req = CompletionRequest(
|
|
model="gpt-3.5-turbo-instruct",
|
|
prompt="Once upon a time",
|
|
)
|
|
|
|
assert req.model == "gpt-3.5-turbo-instruct"
|
|
assert req.prompt == "Once upon a time"
|
|
assert req.stream is False
|
|
|
|
def test_completion_request_with_prompt_list(self):
|
|
"""Test completion request with prompt list."""
|
|
req = CompletionRequest(
|
|
model="gpt-3.5-turbo-instruct",
|
|
prompt=["Hello", "World"],
|
|
)
|
|
|
|
assert isinstance(req.prompt, list)
|
|
assert len(req.prompt) == 2
|
|
|
|
def test_completion_request_xtc_defaults_to_none(self):
|
|
"""Test XTC params default to None on CompletionRequest."""
|
|
req = CompletionRequest(
|
|
model="gpt-3.5-turbo-instruct",
|
|
prompt="Hello",
|
|
)
|
|
assert req.xtc_probability is None
|
|
assert req.xtc_threshold is None
|
|
|
|
def test_completion_request_xtc_accepted(self):
|
|
"""Test XTC params are accepted in CompletionRequest."""
|
|
req = CompletionRequest(
|
|
model="gpt-3.5-turbo-instruct",
|
|
prompt="Hello",
|
|
xtc_probability=0.3,
|
|
xtc_threshold=0.2,
|
|
)
|
|
assert req.xtc_probability == 0.3
|
|
assert req.xtc_threshold == 0.2
|
|
|
|
def test_completion_response(self):
|
|
"""Test creating completion response."""
|
|
resp = CompletionResponse(
|
|
model="gpt-3.5-turbo-instruct",
|
|
choices=[
|
|
CompletionChoice(text=" there was a dragon"),
|
|
],
|
|
)
|
|
|
|
assert resp.model == "gpt-3.5-turbo-instruct"
|
|
assert resp.object == "text_completion"
|
|
assert resp.choices[0].text == " there was a dragon"
|
|
|
|
|
|
class TestUsage:
|
|
"""Tests for Usage model."""
|
|
|
|
def test_usage_creation(self):
|
|
"""Test creating usage stats."""
|
|
usage = Usage(
|
|
prompt_tokens=100,
|
|
completion_tokens=50,
|
|
total_tokens=150,
|
|
)
|
|
|
|
assert usage.prompt_tokens == 100
|
|
assert usage.completion_tokens == 50
|
|
assert usage.total_tokens == 150
|
|
|
|
def test_usage_defaults(self):
|
|
"""Test usage with defaults."""
|
|
usage = Usage()
|
|
|
|
assert usage.prompt_tokens == 0
|
|
assert usage.completion_tokens == 0
|
|
assert usage.total_tokens == 0
|
|
|
|
|
|
class TestModelInfo:
|
|
"""Tests for ModelInfo model."""
|
|
|
|
def test_model_info(self):
|
|
"""Test creating model info."""
|
|
info = ModelInfo(id="gpt-4")
|
|
|
|
assert info.id == "gpt-4"
|
|
assert info.object == "model"
|
|
assert info.owned_by == "omlx"
|
|
|
|
def test_models_response(self):
|
|
"""Test creating models list response."""
|
|
resp = ModelsResponse(
|
|
data=[
|
|
ModelInfo(id="gpt-4"),
|
|
ModelInfo(id="gpt-3.5-turbo"),
|
|
],
|
|
)
|
|
|
|
assert resp.object == "list"
|
|
assert len(resp.data) == 2
|
|
assert resp.data[0].id == "gpt-4"
|
|
|
|
|
|
# =============================================================================
|
|
# Stop Field Coercion
|
|
# =============================================================================
|
|
|
|
|
|
class TestStopCoercion:
|
|
"""Tests for stop field string-to-list coercion (OpenAI compat)."""
|
|
|
|
def test_chat_stop_string_coerced_to_list(self):
|
|
"""A bare string for stop should be wrapped in a list."""
|
|
req = ChatCompletionRequest(
|
|
model="m",
|
|
messages=[Message(role="user", content="hi")],
|
|
stop="<|endoftext|>",
|
|
)
|
|
assert req.stop == ["<|endoftext|>"]
|
|
|
|
def test_chat_stop_list_unchanged(self):
|
|
"""A list value for stop should remain unchanged."""
|
|
req = ChatCompletionRequest(
|
|
model="m",
|
|
messages=[Message(role="user", content="hi")],
|
|
stop=["a", "b"],
|
|
)
|
|
assert req.stop == ["a", "b"]
|
|
|
|
def test_chat_stop_none_unchanged(self):
|
|
"""None value for stop should remain None."""
|
|
req = ChatCompletionRequest(
|
|
model="m",
|
|
messages=[Message(role="user", content="hi")],
|
|
)
|
|
assert req.stop is None
|
|
|
|
def test_completion_stop_string_coerced_to_list(self):
|
|
"""CompletionRequest stop string should also be coerced."""
|
|
req = CompletionRequest(
|
|
model="m",
|
|
prompt="hello",
|
|
stop="eos",
|
|
)
|
|
assert req.stop == ["eos"]
|
|
|
|
def test_completion_stop_list_unchanged(self):
|
|
"""CompletionRequest stop list should remain unchanged."""
|
|
req = CompletionRequest(
|
|
model="m",
|
|
prompt="hello",
|
|
stop=["a"],
|
|
)
|
|
assert req.stop == ["a"]
|