mirror of
https://github.com/deepseek-ai/deepseek-recipe.git
synced 2026-10-28 17:17:08 +08:00
Merge 9894e0386d0b0ae4139f3255a00d26629d42190e into 935817c66f5d6e64838f419692545f06787e485f
This commit is contained in:
commit
9699e79292
@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! Strict schemas are deserialized into this type and normalized: every object
|
||||
//! lists all of its properties as required and forbids additional properties.
|
||||
//! Array length and uniqueness constraints are preserved during normalization.
|
||||
//! Non-strict schemas are only validated; see [`validate_schema`].
|
||||
|
||||
use deepseek_recipe_macros::UntaggedDeserialize;
|
||||
@ -127,6 +128,12 @@ pub(crate) enum JsonSchemaType {
|
||||
items: Box<JsonSchema>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
min_items: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_items: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unique_items: Option<bool>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Null {
|
||||
@ -316,6 +323,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_preserves_nested_array_constraints_and_normalizes_item_objects() {
|
||||
let normalized = normalize(json!({
|
||||
"type": "object",
|
||||
"properties": {"groups": {
|
||||
"type": "array", "minItems": 1, "maxItems": 2, "uniqueItems": true,
|
||||
"items": {
|
||||
"type": "array", "minItems": 0, "maxItems": 0, "uniqueItems": false,
|
||||
"items": {"type": "object", "properties": {"name": {"type": "string"}}},
|
||||
},
|
||||
}},
|
||||
}))
|
||||
.expect("the schema normalizes");
|
||||
let groups = &normalized["properties"]["groups"];
|
||||
assert_eq!(groups["minItems"], 1);
|
||||
assert_eq!(groups["maxItems"], 2);
|
||||
assert_eq!(groups["uniqueItems"], true);
|
||||
let items = &groups["items"];
|
||||
assert_eq!(items["minItems"], 0);
|
||||
assert_eq!(items["maxItems"], 0);
|
||||
assert_eq!(items["uniqueItems"], false);
|
||||
assert_eq!(items["items"]["required"], json!(["name"]));
|
||||
assert_eq!(items["items"]["additionalProperties"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_rejects_inconsistent_required_lists() {
|
||||
let base = json!({"type": "object", "properties": {"a": {"type": "string"}}});
|
||||
|
||||
80
deepseek-recipe/tests/strict_array_constraints.rs
Normal file
80
deepseek-recipe/tests/strict_array_constraints.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use deepseek_recipe::openai::{ChatCompletionRequest, ResponsesRequest};
|
||||
use deepseek_recipe::request::{
|
||||
ConversationRequest, ConversionOptions, OutputConstraint, ProtocolRequest,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn convert<T: ProtocolRequest + DeserializeOwned>(request: Value) -> ConversationRequest {
|
||||
serde_json::from_value::<T>(request)
|
||||
.expect("the request deserializes")
|
||||
.convert(ConversionOptions::default())
|
||||
.expect("the request converts")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_array_constraints_survive_protocol_conversion() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"labels": {
|
||||
"type": "array", "items": {"type": "string"},
|
||||
"minItems": 2, "maxItems": 3, "uniqueItems": true,
|
||||
},
|
||||
},
|
||||
});
|
||||
let model = "DeepSeek-V4.1-Flash";
|
||||
let messages = json!([{"role": "user", "content": "Return JSON labels."}]);
|
||||
let function = json!({"name": "label", "parameters": schema, "strict": true});
|
||||
let requests = [
|
||||
convert::<ChatCompletionRequest>(json!({
|
||||
"model": model, "messages": messages,
|
||||
"tools": [{"type": "function", "function": function}],
|
||||
})),
|
||||
convert::<ResponsesRequest>(json!({
|
||||
"model": model, "input": "Return labels.",
|
||||
"tools": [{"type": "function", "name": "label", "parameters": schema, "strict": true}],
|
||||
})),
|
||||
convert::<ChatCompletionRequest>(json!({
|
||||
"model": model, "messages": messages,
|
||||
"response_format": {"type": "json_schema", "json_schema": {
|
||||
"name": "labels", "schema": schema, "strict": true,
|
||||
}},
|
||||
})),
|
||||
convert::<ResponsesRequest>(json!({
|
||||
"model": model, "input": "Return JSON labels.",
|
||||
"text": {"format": {"type": "json_schema", "name": "labels", "schema": schema, "strict": true}},
|
||||
})),
|
||||
];
|
||||
|
||||
for request in requests {
|
||||
let normalized = match request.inference_options.output_constraint.unwrap() {
|
||||
OutputConstraint::ToolCalls { tools, .. } => {
|
||||
assert_eq!(
|
||||
request.conversation.tools[0].parameters,
|
||||
tools[0].parameters
|
||||
);
|
||||
tools[0].parameters.clone()
|
||||
}
|
||||
OutputConstraint::Json {
|
||||
schema: Some(schema),
|
||||
} => schema,
|
||||
other => panic!("unexpected constraint: {other:?}"),
|
||||
};
|
||||
let validator = jsonschema::validator_for(&normalized).expect("valid normalized schema");
|
||||
for labels in [json!(["a", "b"]), json!(["a", "b", "c"])] {
|
||||
assert!(validator.is_valid(&json!({"labels": labels})));
|
||||
}
|
||||
for labels in [
|
||||
json!([]),
|
||||
json!(["a"]),
|
||||
json!(["a", "b", "c", "d"]),
|
||||
json!(["a", "a"]),
|
||||
] {
|
||||
assert!(
|
||||
!validator.is_valid(&json!({"labels": labels})),
|
||||
"normalized schema accepted invalid labels {labels}: {normalized}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user