icrim 57b9c84242 Initial commit
Co-authored-by: xinchengxx <xinchengxx@deepseek.com>
Co-authored-by: XieJiSS <xiejiss@deepseek.com>
Co-authored-by: yqaty <jiayu@deepseek.com>
Co-authored-by: zhu-he <hezhu@deepseek.com>
Co-authored-by: season-guo <seasonguo@deepseek.com>
2026-09-10 18:07:01 +08:00

336 lines
12 KiB
Python

"""Smoke tests for the Rust conversion, rendering, response, and image bindings."""
import json
from pathlib import Path
import pytest
from deepseek_recipe import (
IMAGE_SPECIAL_TOKEN,
THINKING_END_TOKEN,
ChatCompletionRequest,
ChatCompletionResponse,
Conversation,
ConversionError,
ConversionOptions,
DeepseekV41Encoding,
ImageQuota,
ImageResolver,
InferenceChunk,
InferenceFinishReason,
MessagesRequest,
OpenCvImagePreprocessor,
PromptUsage,
ReqwestImageFetcher,
ResponsesRequest,
StreamProcessor,
Tokenizer,
ToolDefinition,
UserMessage,
WebSearchBehavior,
)
REQUEST = {
"model": "smoke-model",
"messages": [{"role": "user", "content": "hi"}],
}
TOKENIZER_PATH = (
Path(__file__).resolve().parents[2] / "static" / "tokenizers" / "v41" / "tokenizer.json"
)
def test_rendering():
converted = ChatCompletionRequest(REQUEST).convert(ConversionOptions())
encoding = DeepseekV41Encoding()
rendered = encoding.render_conversation(Conversation(messages=[UserMessage("hi")]))
assert converted.conversation.messages[0].content == "hi"
assert "hi" in rendered.prompt
assert rendered.image_sources == []
assert encoding.render_conversation(converted.conversation).prompt == rendered.prompt
def test_encoding_with_tokenizer():
converted = ChatCompletionRequest(REQUEST).convert(ConversionOptions())
tokenizer = Tokenizer.from_file(str(TOKENIZER_PATH))
with pytest.raises(RuntimeError, match="no tokenizer"):
DeepseekV41Encoding().encode(converted.conversation)
encoding = DeepseekV41Encoding().with_tokenizer(tokenizer)
prompt_ids = encoding.encode(converted.conversation)
assert prompt_ids == tokenizer.encode(encoding.render_conversation(converted.conversation).prompt)
def test_web_search_conversion_options():
defaults = ConversionOptions()
constructed = ConversionOptions(
default_thinking_mode=False,
responses_web_search=WebSearchBehavior.Reject,
messages_web_search=WebSearchBehavior.Ignore,
)
configured = (
defaults.with_default_thinking_mode(False)
.with_responses_web_search(WebSearchBehavior.Reject)
.with_messages_web_search(WebSearchBehavior.Ignore)
)
assert defaults.default_thinking_mode
assert defaults.responses_web_search == WebSearchBehavior.Ignore
assert defaults.messages_web_search == WebSearchBehavior.Reject
for options in (constructed, configured):
assert not options.default_thinking_mode
assert options.responses_web_search == WebSearchBehavior.Reject
assert options.messages_web_search == WebSearchBehavior.Ignore
responses = {
"model": "smoke-model",
"input": "hi",
"tools": [{"type": "web_search"}],
}
messages = {
**REQUEST,
"tools": [{"type": "web_search_20250305", "name": "web_search"}],
}
assert ResponsesRequest(responses).convert(defaults).conversation.tools == []
with pytest.raises(ConversionError, match="server tools are not supported"):
MessagesRequest(messages).convert(defaults)
converted = MessagesRequest(messages).convert(configured)
assert converted.conversation.tools == []
assert not converted.conversation.thinking_mode
with pytest.raises(ConversionError, match="Server tools are not supported"):
ResponsesRequest(responses).convert(configured)
def test_custom_inference():
converted = ChatCompletionRequest({**REQUEST, "temperature": 0.25}).convert(
ConversionOptions()
)
generator = ChatCompletionRequest.chunk_generator(converted, "smoke-response", "smoke-model")
processor = StreamProcessor(generator, converted.parsing_options)
response = ChatCompletionResponse("smoke-response", "smoke-model", 0, 0, 0)
try:
assert converted.inference_options.temperature == 0.25
for chunk in (
InferenceChunk.ready(prompt_usage=PromptUsage(prompt_tokens=4)),
InferenceChunk.text(
THINKING_END_TOKEN + "Custom answer", content_tokens=2
),
InferenceChunk.finish(InferenceFinishReason.Stop),
):
for output in processor.push(chunk):
response.append(output)
for output in processor.finish():
response.append(output)
body = json.loads(response.to_json())
assert processor.finished
assert body["id"] == "smoke-response"
assert body["choices"][0]["message"]["content"] == "Custom answer"
assert body["usage"]["prompt_tokens"] == 4
assert body["usage"]["completion_tokens"] == 2
finally:
processor.close()
def test_token_chunks():
converted = ChatCompletionRequest(REQUEST).convert(ConversionOptions())
generator = ChatCompletionRequest.chunk_generator(converted, "token-response", "smoke-model")
tokenizer = Tokenizer.from_file(str(TOKENIZER_PATH))
token_ids = tokenizer.encode(THINKING_END_TOKEN + "Custom answer")
processor = StreamProcessor(generator, converted.parsing_options, tokenizer)
response = ChatCompletionResponse("token-response", "smoke-model", 0, 0, 0)
try:
for chunk in (
InferenceChunk.ready(),
*(InferenceChunk.token(token_id) for token_id in token_ids),
InferenceChunk.finish(InferenceFinishReason.Stop),
):
for output in processor.push(chunk):
response.append(output)
for output in processor.finish():
response.append(output)
body = json.loads(response.to_json())
assert body["choices"][0]["message"]["content"] == "Custom answer"
assert body["usage"]["completion_tokens"] == len(token_ids)
finally:
processor.close()
def test_split_character_tokens():
# A character outside the vocabulary decodes from several byte tokens; the
# ids buffered while it was incomplete still count as completion tokens.
converted = ChatCompletionRequest(REQUEST).convert(ConversionOptions())
generator = ChatCompletionRequest.chunk_generator(converted, "token-response", "smoke-model")
tokenizer = Tokenizer.from_file(str(TOKENIZER_PATH))
token_ids = tokenizer.encode(THINKING_END_TOKEN + "🦀")
assert len(token_ids) > 2
processor = StreamProcessor(generator, converted.parsing_options, tokenizer)
response = ChatCompletionResponse("token-response", "smoke-model", 0, 0, 0)
try:
for chunk in (
InferenceChunk.ready(),
*(InferenceChunk.token(token_id) for token_id in token_ids),
InferenceChunk.finish(InferenceFinishReason.Stop),
):
for output in processor.push(chunk):
response.append(output)
for output in processor.finish():
response.append(output)
body = json.loads(response.to_json())
assert body["choices"][0]["message"]["content"] == "🦀"
assert body["usage"]["completion_tokens"] == len(token_ids)
finally:
processor.close()
def test_undecodable_token_ignored():
converted = ChatCompletionRequest(REQUEST).convert(ConversionOptions())
generator = ChatCompletionRequest.chunk_generator(converted, "token-response", "smoke-model")
tokenizer = Tokenizer.from_file(str(TOKENIZER_PATH))
processor = StreamProcessor(generator, converted.parsing_options, tokenizer)
response = ChatCompletionResponse("token-response", "smoke-model", 0, 0, 0)
try:
for chunk in (
InferenceChunk.ready(),
InferenceChunk.token(999_999_999),
InferenceChunk.text(THINKING_END_TOKEN + "Hi", content_tokens=1),
InferenceChunk.finish(InferenceFinishReason.Stop),
):
for output in processor.push(chunk):
response.append(output)
for output in processor.finish():
response.append(output)
body = json.loads(response.to_json())
assert body["choices"][0]["message"]["content"] == "Hi"
assert body["usage"]["completion_tokens"] == 1
finally:
processor.close()
def test_token_chunk_without_tokenizer_errors():
converted = ChatCompletionRequest(REQUEST).convert(ConversionOptions())
generator = ChatCompletionRequest.chunk_generator(converted, "token-response", "smoke-model")
processor = StreamProcessor(generator, converted.parsing_options)
try:
with pytest.raises(RuntimeError, match="without a tokenizer"):
processor.push(InferenceChunk.token(1))
finally:
processor.close()
def test_tokenizer_from_str():
# Loading from the file's contents gives the same ids as loading the file.
text = TOKENIZER_PATH.read_text()
assert Tokenizer.from_str(text).encode("Custom answer") == Tokenizer.from_file(
str(TOKENIZER_PATH)
).encode("Custom answer")
def test_tokenizer_from_str_errors():
with pytest.raises(RuntimeError):
Tokenizer.from_str("not json")
def test_foreign_tokenizer_errors():
converted = ChatCompletionRequest(REQUEST).convert(ConversionOptions())
generator = ChatCompletionRequest.chunk_generator(converted, "py-response", "smoke-model")
with pytest.raises(TypeError):
StreamProcessor(generator, converted.parsing_options, object())
def test_image_resolution():
# One 50-by-40 RGB PNG exercises the native image decoder and WebP encoder.
png = (
"iVBORw0KGgoAAAANSUhEUgAAADIAAAAoCAIAAAAzED4bAAAAPElEQVR4nO3OUQkAIBBA"
"sQvht/2jGMsYPmSwAJu1T9A8H2hpaWlpaTVoaWkVaGlpFWhpaRVoaWkVaH3QuqTYo"
"YpppISkAAAAAElFTkSuQmCC"
)
converted = ChatCompletionRequest({
**REQUEST,
"messages": [{"role": "user", "content": [{
"type": "image_url",
"image_url": {"url": "data:image/png;base64," + png},
}]}],
}).convert(ConversionOptions())
rendered = DeepseekV41Encoding().render_conversation(converted.conversation)
resolver = ImageResolver(ReqwestImageFetcher(), OpenCvImagePreprocessor())
multimodal = resolver.resolve(rendered.image_sources, ImageQuota())
assert IMAGE_SPECIAL_TOKEN in rendered.prompt
(image,) = multimodal.images
assert image.width > 0 and image.height > 0
assert image.data[:4] == b"RIFF" and image.data[8:12] == b"WEBP"
assert multimodal.image_token_adjustment() > 0
def test_circular_dict_rejected():
# A self-referencing container must raise instead of exhausting the stack.
parameters = {}
parameters["self"] = parameters
with pytest.raises(ValueError, match="circular reference"):
ToolDefinition(name="f", parameters=parameters)
def test_circular_list_rejected():
parameters = []
parameters.append(parameters)
with pytest.raises(ValueError, match="circular reference"):
ToolDefinition(name="f", parameters=parameters)
def test_indirect_cycle_rejected():
outer = {}
inner = {"outer": outer}
outer["inner"] = inner
with pytest.raises(ValueError, match="circular reference"):
ToolDefinition(name="f", parameters=outer)
def test_cycle_through_tuple_rejected():
parameters = {"items": []}
parameters["items"].append((parameters,))
with pytest.raises(ValueError, match="circular reference"):
ToolDefinition(name="f", parameters=parameters)
def test_circular_request_body_rejected():
body = dict(REQUEST)
body["self"] = body
with pytest.raises(ValueError, match="circular reference"):
ChatCompletionRequest(body)
def test_deep_nesting_rejected():
parameters = {}
node = parameters
for _ in range(200):
child = {}
node["next"] = child
node = child
with pytest.raises(RecursionError):
ToolDefinition(name="f", parameters=parameters)
def test_nesting_within_limit_accepted():
parameters = {}
node = parameters
for _ in range(127):
child = {}
node["next"] = child
node = child
assert ToolDefinition(name="f", parameters=parameters).parameters == parameters
def test_shared_subtree_accepted():
# A reference used twice is a shared subtree, not a circular reference.
shared = {"type": "string"}
parameters = {"type": "object", "properties": {"a": shared, "b": shared}}
assert ToolDefinition(name="f", parameters=parameters).parameters == parameters