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
|
//! Strict schemas are deserialized into this type and normalized: every object
|
||||||
//! lists all of its properties as required and forbids additional properties.
|
//! 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`].
|
//! Non-strict schemas are only validated; see [`validate_schema`].
|
||||||
|
|
||||||
use deepseek_recipe_macros::UntaggedDeserialize;
|
use deepseek_recipe_macros::UntaggedDeserialize;
|
||||||
@ -127,6 +128,12 @@ pub(crate) enum JsonSchemaType {
|
|||||||
items: Box<JsonSchema>,
|
items: Box<JsonSchema>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
description: Option<String>,
|
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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
Null {
|
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]
|
#[test]
|
||||||
fn normalize_rejects_inconsistent_required_lists() {
|
fn normalize_rejects_inconsistent_required_lists() {
|
||||||
let base = json!({"type": "object", "properties": {"a": {"type": "string"}}});
|
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