Resolve local JSON Schema refs in Code Mode types (#38664)

## Why

Code Mode rendered document-local `$ref` values as `unknown`, hiding referenced
input and structured-output shapes from generated TypeScript declarations.

## What changed

- Resolve fragment-only JSON Pointer references against the root schema,
  including escaped and percent-encoded pointer segments.
- Preserve `$ref` siblings as intersections and parenthesize unions used in
  `allOf` intersections.
- Bound recursive and repeated expansion, intermediate rendering work, and
  final output size, falling back to `unknown` when a limit is reached or a
  reference cannot be resolved safely.

## Testing

Add unit coverage for recursive, escaped, nested-resource, dangling, and
oversized references, plus integration coverage for generated Code Mode tool
descriptions and MCP structured output types.

GitOrigin-RevId: 95c6aa32e25325b5be9359dfac3b3328e5e7a499
This commit is contained in:
aphonpra-oai
2026-08-14 22:40:19 +00:00
committed by copyberry
parent 274727d37f
commit a186f5484d
5 changed files with 870 additions and 230 deletions
+47 -229
View File
@@ -5,6 +5,7 @@ use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
use crate::PUBLIC_TOOL_NAME;
use crate::json_schema_types::render_json_schema_to_typescript;
const MAX_JS_SAFE_INTEGER: u64 = (1_u64 << 53) - 1;
const DEFERRED_NESTED_TOOLS_GUIDANCE: &str = r#"Some deferred nested tools may be omitted from this description. They are still available on the global `tools` object and listed in `ALL_TOOLS`.
@@ -332,8 +333,7 @@ pub fn build_exec_tool_description(
}
}
let nested_tool_reference = nested_tool_sections.join("\n\n");
sections.push(nested_tool_reference);
sections.push(nested_tool_sections.join("\n\n"));
}
sections.join("\n\n")
@@ -400,7 +400,7 @@ pub fn render_code_mode_sample(
) -> String {
let declaration = format!(
"declare const tools: {{ {} }};",
render_code_mode_tool_declaration(tool_name, input_name, input_type, output_type)
render_code_mode_tool_declaration(tool_name, input_name, &input_type, &output_type)
);
format!("{description}\n\nexec tool declaration:\n```ts\n{declaration}\n```")
}
@@ -446,8 +446,8 @@ fn render_code_mode_sample_for_definition(definition: &ToolDefinition) -> String
fn render_code_mode_tool_declaration(
tool_name: &str,
input_name: &str,
input_type: String,
output_type: String,
input_type: &str,
output_type: &str,
) -> String {
let tool_name = normalize_code_mode_identifier(tool_name);
format!("{tool_name}({input_name}: {input_type}): Promise<{output_type}>;")
@@ -461,10 +461,6 @@ fn render_tool_heading(global_name: &str, raw_name: &str) -> String {
}
}
pub fn render_json_schema_to_typescript(schema: &JsonValue) -> String {
render_json_schema_to_typescript_inner(schema)
}
fn mcp_structured_content_schema(output_schema: Option<&JsonValue>) -> Option<&JsonValue> {
let output_schema = output_schema?;
let properties = output_schema
@@ -506,226 +502,6 @@ fn mcp_structured_content_schema(output_schema: Option<&JsonValue>) -> Option<&J
)
}
fn render_json_schema_to_typescript_inner(schema: &JsonValue) -> String {
match schema {
JsonValue::Bool(true) => "unknown".to_string(),
JsonValue::Bool(false) => "never".to_string(),
JsonValue::Object(map) => {
if let Some(value) = map.get("const") {
return render_json_schema_literal(value);
}
if let Some(values) = map.get("enum").and_then(JsonValue::as_array) {
let rendered = values
.iter()
.map(render_json_schema_literal)
.collect::<Vec<_>>();
if !rendered.is_empty() {
return rendered.join(" | ");
}
}
for key in ["anyOf", "oneOf"] {
if let Some(variants) = map.get(key).and_then(JsonValue::as_array) {
let rendered = variants
.iter()
.map(render_json_schema_to_typescript_inner)
.collect::<Vec<_>>();
if !rendered.is_empty() {
return rendered.join(" | ");
}
}
}
if let Some(variants) = map.get("allOf").and_then(JsonValue::as_array) {
let rendered = variants
.iter()
.map(render_json_schema_to_typescript_inner)
.collect::<Vec<_>>();
if !rendered.is_empty() {
return rendered.join(" & ");
}
}
if let Some(schema_type) = map.get("type") {
if let Some(types) = schema_type.as_array() {
let rendered = types
.iter()
.filter_map(JsonValue::as_str)
.map(|schema_type| render_json_schema_type_keyword(map, schema_type))
.collect::<Vec<_>>();
if !rendered.is_empty() {
return rendered.join(" | ");
}
}
if let Some(schema_type) = schema_type.as_str() {
return render_json_schema_type_keyword(map, schema_type);
}
}
if map.contains_key("properties")
|| map.contains_key("additionalProperties")
|| map.contains_key("required")
{
return render_json_schema_object(map);
}
if map.contains_key("items") || map.contains_key("prefixItems") {
return render_json_schema_array(map);
}
"unknown".to_string()
}
_ => "unknown".to_string(),
}
}
fn render_json_schema_type_keyword(
map: &serde_json::Map<String, JsonValue>,
schema_type: &str,
) -> String {
match schema_type {
"string" => "string".to_string(),
"number" | "integer" => "number".to_string(),
"boolean" => "boolean".to_string(),
"null" => "null".to_string(),
"array" => render_json_schema_array(map),
"object" => render_json_schema_object(map),
_ => "unknown".to_string(),
}
}
fn render_json_schema_array(map: &serde_json::Map<String, JsonValue>) -> String {
if let Some(items) = map.get("items") {
let item_type = render_json_schema_to_typescript_inner(items);
return format!("Array<{item_type}>");
}
if let Some(items) = map.get("prefixItems").and_then(JsonValue::as_array) {
let item_types = items
.iter()
.map(render_json_schema_to_typescript_inner)
.collect::<Vec<_>>();
if !item_types.is_empty() {
return format!("[{}]", item_types.join(", "));
}
}
"unknown[]".to_string()
}
fn append_additional_properties_line(
lines: &mut Vec<String>,
map: &serde_json::Map<String, JsonValue>,
properties: &serde_json::Map<String, JsonValue>,
line_prefix: &str,
) {
if let Some(additional_properties) = map.get("additionalProperties") {
let property_type = match additional_properties {
JsonValue::Bool(true) => Some("unknown".to_string()),
JsonValue::Bool(false) => None,
value => Some(render_json_schema_to_typescript_inner(value)),
};
if let Some(property_type) = property_type {
lines.push(format!("{line_prefix}[key: string]: {property_type};"));
}
} else if properties.is_empty() {
lines.push(format!("{line_prefix}[key: string]: unknown;"));
}
}
fn has_property_description(value: &JsonValue) -> bool {
value
.get("description")
.and_then(JsonValue::as_str)
.is_some_and(|description| !description.is_empty())
}
fn render_json_schema_object_property(name: &str, value: &JsonValue, required: &[&str]) -> String {
let optional = if required.iter().any(|required_name| required_name == &name) {
""
} else {
"?"
};
let property_name = render_json_schema_property_name(name);
let property_type = render_json_schema_to_typescript_inner(value);
format!("{property_name}{optional}: {property_type};")
}
fn render_json_schema_object(map: &serde_json::Map<String, JsonValue>) -> String {
let required = map
.get("required")
.and_then(JsonValue::as_array)
.map(|items| {
items
.iter()
.filter_map(JsonValue::as_str)
.collect::<Vec<_>>()
})
.unwrap_or_default();
let properties = map
.get("properties")
.and_then(JsonValue::as_object)
.cloned()
.unwrap_or_default();
let mut sorted_properties = properties.iter().collect::<Vec<_>>();
sorted_properties.sort_unstable_by_key(|(name_a, _)| *name_a);
if sorted_properties
.iter()
.any(|(_, value)| has_property_description(value))
{
let mut lines = vec!["{".to_string()];
for (name, value) in sorted_properties {
if let Some(description) = value.get("description").and_then(JsonValue::as_str) {
for description_line in description
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
{
lines.push(format!(" // {description_line}"));
}
}
lines.push(format!(
" {}",
render_json_schema_object_property(name, value, &required)
));
}
append_additional_properties_line(&mut lines, map, &properties, " ");
lines.push("}".to_string());
return lines.join("\n");
}
let mut lines = sorted_properties
.into_iter()
.map(|(name, value)| render_json_schema_object_property(name, value, &required))
.collect::<Vec<_>>();
append_additional_properties_line(&mut lines, map, &properties, "");
if lines.is_empty() {
return "{}".to_string();
}
format!("{{ {} }}", lines.join(" "))
}
fn render_json_schema_property_name(name: &str) -> String {
if normalize_code_mode_identifier(name) == name {
name.to_string()
} else {
serde_json::to_string(name).unwrap_or_else(|_| format!("\"{}\"", name.replace('"', "\\\"")))
}
}
fn render_json_schema_literal(value: &JsonValue) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "unknown".to_string())
}
#[cfg(test)]
mod tests {
use super::CodeModeToolKind;
@@ -875,6 +651,48 @@ mod tests {
));
}
#[test]
fn code_mode_types_structured_content_result_refs() {
let definition = ToolDefinition {
name: "mcp__sample__search".to_string(),
tool_name: ToolName::namespaced("mcp__sample__", "search"),
description: "Search".to_string(),
kind: CodeModeToolKind::Function,
input_schema: Some(json!({
"type": "object",
"properties": {},
"additionalProperties": false
})),
output_schema: Some(mcp_call_tool_result_schema(json!({
"type": "object",
"properties": {
"results": {
"type": "array",
"items": { "$ref": "#/definitions/Result~1item~0v1" }
}
},
"required": ["results"],
"additionalProperties": false,
"definitions": {
"Result/item~v1": {
"type": "object",
"properties": {
"id": { "type": "string" },
"score": { "type": "number" }
},
"required": ["id", "score"],
"additionalProperties": false
}
}
}))),
};
let description = augment_tool_definition(definition).description;
assert!(description.contains(
"mcp__sample__search(args: {}): Promise<CallToolResult<{ results: Array<{ id: string; score: number; }>; }>>;"
));
}
#[test]
fn code_mode_only_description_includes_nested_tools() {
let description = build_exec_tool_description(
@@ -0,0 +1,538 @@
use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
use crate::description::normalize_code_mode_identifier;
// Expose one nested recursive shape, then fall back to `unknown` on the next
// occurrence so generated tool declarations remain finite.
const MAX_LOCAL_REF_EXPANSIONS_PER_PATH: usize = 2;
// Bound repeated refs and DAG fan-out separately from cycle depth so one
// compact schema cannot expand into an arbitrarily large model-visible item.
const MAX_TOTAL_LOCAL_REF_EXPANSIONS: usize = 32;
// Bound individual schema rendering before assembling the final declaration.
const MAX_RENDERED_SCHEMA_BYTES: usize = 16_000;
// Charge intermediate render strings as they are built so repeated local refs
// cannot allocate unbounded expanded copies before the final schema cap runs.
const MAX_RENDER_WORK_BYTES: usize = MAX_RENDERED_SCHEMA_BYTES * 4;
pub fn render_json_schema_to_typescript(schema: &JsonValue) -> String {
let rendered = JsonSchemaTypeRenderer::new(schema).render(schema);
if rendered.len() > MAX_RENDERED_SCHEMA_BYTES {
"unknown".to_string()
} else {
rendered
}
}
struct JsonSchemaTypeRenderer<'a> {
root: &'a JsonValue,
nested_schema_resource_depth: usize,
active_local_ref_expansions: BTreeMap<String, usize>,
remaining_local_ref_expansions: usize,
remaining_render_work_bytes: usize,
render_work_budget_exhausted: bool,
}
impl<'a> JsonSchemaTypeRenderer<'a> {
fn new(root: &'a JsonValue) -> Self {
Self {
root,
nested_schema_resource_depth: 0,
active_local_ref_expansions: BTreeMap::new(),
remaining_local_ref_expansions: MAX_TOTAL_LOCAL_REF_EXPANSIONS,
remaining_render_work_bytes: MAX_RENDER_WORK_BYTES,
render_work_budget_exhausted: false,
}
}
fn render(&mut self, schema: &JsonValue) -> String {
if self.render_work_budget_exhausted {
return "unknown".to_string();
}
// A nested `$id` starts a new schema resource. Fragment-only refs below
// it are scoped to that resource, not to the outer document root.
let enters_nested_schema_resource = !std::ptr::eq(schema, self.root)
&& schema
.as_object()
.is_some_and(|map| map.contains_key("$id"));
if enters_nested_schema_resource {
self.nested_schema_resource_depth += 1;
}
let rendered = match schema {
JsonValue::Bool(true) => "unknown".to_string(),
JsonValue::Bool(false) => "never".to_string(),
JsonValue::Object(map) => self.render_map(map),
_ => "unknown".to_string(),
};
if enters_nested_schema_resource {
self.nested_schema_resource_depth -= 1;
}
self.finish_render(rendered)
}
fn render_map(&mut self, map: &serde_json::Map<String, JsonValue>) -> String {
if self.render_work_budget_exhausted {
return "unknown".to_string();
}
if map.contains_key("$ref") {
return self.render_ref(map);
}
if let Some(value) = map.get("const") {
return self.render_literal(value);
}
if let Some(values) = map.get("enum").and_then(JsonValue::as_array) {
let mut rendered = Vec::new();
for value in values {
let literal = self.render_literal(value);
if self.render_work_budget_exhausted {
return "unknown".to_string();
}
if !self.consume_render_work(literal.len()) {
return "unknown".to_string();
}
rendered.push(literal);
}
if !rendered.is_empty() {
return rendered.join(" | ");
}
}
for key in ["anyOf", "oneOf"] {
if let Some(variants) = map.get(key).and_then(JsonValue::as_array) {
let mut rendered = Vec::new();
for variant in variants {
if self.render_work_budget_exhausted {
return "unknown".to_string();
}
rendered.push(self.render(variant));
}
if !rendered.is_empty() {
return rendered.join(" | ");
}
}
}
if let Some(variants) = map.get("allOf").and_then(JsonValue::as_array) {
let mut rendered = Vec::new();
for variant in variants {
if self.render_work_budget_exhausted {
return "unknown".to_string();
}
rendered.push(parenthesize_union_for_intersection(self.render(variant)));
}
if !rendered.is_empty() {
return rendered.join(" & ");
}
}
if let Some(schema_type) = map.get("type") {
if let Some(types) = schema_type.as_array() {
let mut rendered = Vec::new();
for schema_type in types.iter().filter_map(JsonValue::as_str) {
if self.render_work_budget_exhausted {
return "unknown".to_string();
}
rendered.push(self.render_type_keyword(map, schema_type));
}
if !rendered.is_empty() {
return rendered.join(" | ");
}
}
if let Some(schema_type) = schema_type.as_str() {
return self.render_type_keyword(map, schema_type);
}
}
if map.contains_key("properties")
|| map.contains_key("additionalProperties")
|| map.contains_key("required")
{
return self.render_object(map);
}
if map.contains_key("items") || map.contains_key("prefixItems") {
return self.render_array(map);
}
"unknown".to_string()
}
fn render_ref(&mut self, map: &serde_json::Map<String, JsonValue>) -> String {
let referenced_type = if self.nested_schema_resource_depth > 0 {
None
} else {
map.get("$ref")
.and_then(JsonValue::as_str)
.and_then(local_json_pointer)
.and_then(|pointer| {
let active_expansions = self
.active_local_ref_expansions
.get(&pointer)
.copied()
.unwrap_or_default();
if active_expansions >= MAX_LOCAL_REF_EXPANSIONS_PER_PATH {
return Some("unknown".to_string());
}
if self.remaining_local_ref_expansions == 0 {
return Some("unknown".to_string());
}
let root = self.root;
let target = if pointer.is_empty() {
Some(root)
} else {
root.pointer(&pointer)
}?;
self.remaining_local_ref_expansions -= 1;
self.active_local_ref_expansions
.insert(pointer.clone(), active_expansions + 1);
let rendered = self.render(target);
if active_expansions == 0 {
self.active_local_ref_expansions.remove(&pointer);
} else {
self.active_local_ref_expansions
.insert(pointer, active_expansions);
}
Some(rendered)
})
}
.unwrap_or_else(|| "unknown".to_string());
if self.render_work_budget_exhausted {
return "unknown".to_string();
}
let siblings = map
.iter()
.filter(|(key, _)| !matches!(key.as_str(), "$ref" | "$defs" | "definitions"))
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
if !has_renderable_schema_keywords(&siblings) {
return referenced_type;
}
let sibling_type = self.render_map(&siblings);
match (referenced_type.as_str(), sibling_type.as_str()) {
("unknown", _) => sibling_type,
(_, "unknown") => referenced_type,
_ => format!("({referenced_type}) & ({sibling_type})"),
}
}
fn render_type_keyword(
&mut self,
map: &serde_json::Map<String, JsonValue>,
schema_type: &str,
) -> String {
match schema_type {
"string" => "string".to_string(),
"number" | "integer" => "number".to_string(),
"boolean" => "boolean".to_string(),
"null" => "null".to_string(),
"array" => self.render_array(map),
"object" => self.render_object(map),
_ => "unknown".to_string(),
}
}
fn render_array(&mut self, map: &serde_json::Map<String, JsonValue>) -> String {
if let Some(items) = map.get("items") {
let item_type = self.render(items);
if self.render_work_budget_exhausted {
return "unknown".to_string();
}
return format!("Array<{item_type}>");
}
if let Some(items) = map.get("prefixItems").and_then(JsonValue::as_array) {
let mut item_types = Vec::new();
for item in items {
if self.render_work_budget_exhausted {
return "unknown".to_string();
}
item_types.push(self.render(item));
}
if !item_types.is_empty() {
return format!("[{}]", item_types.join(", "));
}
}
"unknown[]".to_string()
}
fn append_additional_properties_line(
&mut self,
lines: &mut Vec<String>,
map: &serde_json::Map<String, JsonValue>,
properties: &serde_json::Map<String, JsonValue>,
line_prefix: &str,
) -> bool {
if let Some(additional_properties) = map.get("additionalProperties") {
let property_type = match additional_properties {
JsonValue::Bool(true) => Some("unknown".to_string()),
JsonValue::Bool(false) => None,
value => Some(self.render(value)),
};
if let Some(property_type) = property_type {
return self.push_render_line(
lines,
format!("{line_prefix}[key: string]: {property_type};"),
);
}
} else if properties.is_empty() {
return self.push_render_line(lines, format!("{line_prefix}[key: string]: unknown;"));
}
true
}
fn render_object_property(
&mut self,
name: &str,
value: &JsonValue,
required: &[&str],
) -> String {
if name.len() > self.remaining_render_work_bytes {
self.render_work_budget_exhausted = true;
return "unknown".to_string();
}
let optional = if required.iter().any(|required_name| required_name == &name) {
""
} else {
"?"
};
let property_name = render_json_schema_property_name(name);
let property_type = self.render(value);
if self.render_work_budget_exhausted {
return "unknown".to_string();
}
format!("{property_name}{optional}: {property_type};")
}
fn render_object(&mut self, map: &serde_json::Map<String, JsonValue>) -> String {
let required = map
.get("required")
.and_then(JsonValue::as_array)
.map(|items| {
items
.iter()
.filter_map(JsonValue::as_str)
.collect::<Vec<_>>()
})
.unwrap_or_default();
let empty_properties = serde_json::Map::new();
let properties = map
.get("properties")
.and_then(JsonValue::as_object)
.unwrap_or(&empty_properties);
let mut sorted_properties = properties.iter().collect::<Vec<_>>();
sorted_properties.sort_unstable_by_key(|(name_a, _)| *name_a);
if sorted_properties
.iter()
.any(|(_, value)| has_property_description(value))
{
let mut lines = Vec::new();
if !self.push_render_line(&mut lines, "{".to_string()) {
return "unknown".to_string();
}
for (name, value) in sorted_properties {
if let Some(description) = value.get("description").and_then(JsonValue::as_str) {
for description_line in description
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
{
if description_line.len().saturating_add(5)
> self.remaining_render_work_bytes
|| !self
.push_render_line(&mut lines, format!(" // {description_line}"))
{
return "unknown".to_string();
}
}
}
let property = self.render_object_property(name, value, &required);
if self.render_work_budget_exhausted
|| !self.push_render_line(&mut lines, format!(" {property}"))
{
return "unknown".to_string();
}
}
if !self.append_additional_properties_line(&mut lines, map, properties, " ")
|| !self.push_render_line(&mut lines, "}".to_string())
{
return "unknown".to_string();
}
return lines.join("\n");
}
let mut lines = Vec::new();
for (name, value) in sorted_properties {
let property = self.render_object_property(name, value, &required);
if self.render_work_budget_exhausted || !self.push_render_line(&mut lines, property) {
return "unknown".to_string();
}
}
if !self.append_additional_properties_line(&mut lines, map, properties, "") {
return "unknown".to_string();
}
if lines.is_empty() {
return "{}".to_string();
}
format!("{{ {} }}", lines.join(" "))
}
fn finish_render(&mut self, rendered: String) -> String {
if self.consume_render_work(rendered.len()) {
rendered
} else {
"unknown".to_string()
}
}
fn render_literal(&mut self, value: &JsonValue) -> String {
if json_literal_serialization_upper_bound(value) > self.remaining_render_work_bytes {
self.render_work_budget_exhausted = true;
"unknown".to_string()
} else {
render_json_schema_literal(value)
}
}
fn consume_render_work(&mut self, rendered_bytes: usize) -> bool {
if rendered_bytes > self.remaining_render_work_bytes {
self.render_work_budget_exhausted = true;
false
} else {
self.remaining_render_work_bytes -= rendered_bytes;
true
}
}
fn push_render_line(&mut self, lines: &mut Vec<String>, line: String) -> bool {
if !self.consume_render_work(line.len()) {
return false;
}
lines.push(line);
true
}
}
fn parenthesize_union_for_intersection(rendered: String) -> String {
if rendered.contains(" | ") {
format!("({rendered})")
} else {
rendered
}
}
fn local_json_pointer(reference: &str) -> Option<String> {
let fragment = reference.strip_prefix('#')?;
let pointer = percent_decode_uri_fragment(fragment)?;
if pointer.is_empty() || pointer.starts_with('/') {
Some(pointer)
} else {
None
}
}
fn percent_decode_uri_fragment(fragment: &str) -> Option<String> {
let bytes = fragment.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'%' {
let high = decode_hex_digit(*bytes.get(index + 1)?)?;
let low = decode_hex_digit(*bytes.get(index + 2)?)?;
decoded.push((high << 4) | low);
index += 3;
} else {
decoded.push(bytes[index]);
index += 1;
}
}
String::from_utf8(decoded).ok()
}
fn decode_hex_digit(digit: u8) -> Option<u8> {
match digit {
b'0'..=b'9' => Some(digit - b'0'),
b'a'..=b'f' => Some(digit - b'a' + 10),
b'A'..=b'F' => Some(digit - b'A' + 10),
_ => None,
}
}
fn has_renderable_schema_keywords(map: &serde_json::Map<String, JsonValue>) -> bool {
[
"const",
"enum",
"anyOf",
"oneOf",
"allOf",
"type",
"properties",
"additionalProperties",
"required",
"items",
"prefixItems",
]
.iter()
.any(|key| map.contains_key(*key))
}
fn has_property_description(value: &JsonValue) -> bool {
value
.get("description")
.and_then(JsonValue::as_str)
.is_some_and(|description| !description.is_empty())
}
fn render_json_schema_property_name(name: &str) -> String {
if normalize_code_mode_identifier(name) == name {
name.to_string()
} else {
serde_json::to_string(name).unwrap_or_else(|_| format!("\"{}\"", name.replace('"', "\\\"")))
}
}
fn render_json_schema_literal(value: &JsonValue) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "unknown".to_string())
}
fn json_literal_serialization_upper_bound(value: &JsonValue) -> usize {
match value {
JsonValue::Null => 4,
JsonValue::Bool(false) => 5,
JsonValue::Bool(true) => 4,
JsonValue::Number(number) => number.to_string().len(),
// JSON escaping can expand one UTF-8 byte to at most one six-byte
// Unicode escape, so this bounds allocation before serialization.
JsonValue::String(string) => string.len().saturating_mul(6).saturating_add(2),
JsonValue::Array(values) => values.iter().fold(2, |size, value| {
size.saturating_add(1)
.saturating_add(json_literal_serialization_upper_bound(value))
}),
JsonValue::Object(map) => map.iter().fold(2, |size, (key, value)| {
size.saturating_add(4)
.saturating_add(key.len().saturating_mul(6))
.saturating_add(json_literal_serialization_upper_bound(value))
}),
}
}
#[cfg(test)]
#[path = "json_schema_types_tests.rs"]
mod tests;
@@ -0,0 +1,200 @@
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn renders_recursive_local_refs_with_escaped_pointer_segments() {
let schema = json!({
"type": "object",
"properties": {
"clauses": {
"type": "array",
"items": { "$ref": "#/$defs/Boolean~1Clause~0v1" }
}
},
"$defs": {
"Boolean/Clause~v1": {
"type": "object",
"properties": {
"query": { "$ref": "#/$defs/Query" }
}
},
"Query": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"clauses": {
"type": "array",
"items": { "$ref": "#/$defs/Boolean~1Clause~0v1" }
}
}
}
]
}
}
});
let rendered = render_json_schema_to_typescript(&schema);
assert!(rendered.contains("clauses?: Array<{ query?: string | { clauses?: Array<{"));
assert!(rendered.contains("query?: string | { clauses?: Array<unknown>; };"));
}
#[test]
fn renders_ref_siblings_uri_fragments_and_all_of_precedence() {
assert_eq!(
render_json_schema_to_typescript(&json!({
"$ref": "#/$defs/Label",
"enum": ["A"],
"$defs": { "Label": { "type": "string" } }
})),
r#"(string) & ("A")"#
);
assert_eq!(
render_json_schema_to_typescript(&json!({
"$ref": "#/$defs/Foo%20Bar",
"$defs": { "Foo Bar": { "type": "string" } }
})),
"string"
);
assert_eq!(
render_json_schema_to_typescript(&json!({
"allOf": [
{ "$ref": "#/$defs/Choice" },
{ "type": "object", "properties": { "value": { "type": "string" } } }
],
"$defs": {
"Choice": { "oneOf": [{ "type": "string" }, { "type": "number" }] }
}
})),
"(string | number) & { value?: string; }"
);
}
#[test]
fn leaves_local_refs_under_nested_schema_resources_unresolved() {
let schema = json!({
"$defs": {
"Choice": { "type": "string" }
},
"type": "object",
"properties": {
"nested": {
"$id": "urn:nested",
"$defs": {
"Choice": { "type": "number" }
},
"type": "object",
"properties": {
"value": { "$ref": "#/$defs/Choice" }
}
}
}
});
assert_eq!(
render_json_schema_to_typescript(&schema),
"{ nested?: { value?: unknown; }; }"
);
}
#[test]
fn bounds_expansions_without_charging_dangling_refs() {
let mut properties = (0..MAX_TOTAL_LOCAL_REF_EXPANSIONS)
.map(|index| {
(
format!("a_missing_{index}"),
json!({ "$ref": format!("#/$defs/Missing{index}") }),
)
})
.collect::<serde_json::Map<_, _>>();
properties.insert("z_valid".to_string(), json!({ "$ref": "#/$defs/Valid" }));
let schema = json!({
"type": "object",
"properties": properties,
"$defs": { "Valid": { "type": "string" } }
});
let rendered = render_json_schema_to_typescript(&schema);
assert!(rendered.contains("z_valid?: string;"));
let properties = (0..MAX_TOTAL_LOCAL_REF_EXPANSIONS + 2)
.map(|index| {
(
format!("property_{index}"),
json!({ "$ref": "#/$defs/Item" }),
)
})
.collect::<serde_json::Map<_, _>>();
let rendered = render_json_schema_to_typescript(&json!({
"type": "object",
"properties": properties,
"$defs": { "Item": { "type": "string" } }
}));
assert_eq!(
rendered.matches("string").count(),
MAX_TOTAL_LOCAL_REF_EXPANSIONS
);
assert_eq!(rendered.matches("unknown").count(), 2);
}
#[test]
fn repeated_large_ref_expansions_exhaust_render_work_budget() {
let properties = (0..MAX_TOTAL_LOCAL_REF_EXPANSIONS)
.map(|index| {
(
format!("property_{index}"),
json!({ "$ref": "#/$defs/Item" }),
)
})
.collect::<serde_json::Map<_, _>>();
let schema = json!({
"type": "object",
"properties": properties,
"$defs": {
"Item": {
"type": "object",
"properties": {
"value": {
"type": "string",
"description": "x".repeat(MAX_RENDERED_SCHEMA_BYTES / 2)
}
}
}
}
});
let mut renderer = JsonSchemaTypeRenderer::new(&schema);
assert_eq!(renderer.render(&schema), "unknown");
assert!(renderer.render_work_budget_exhausted);
}
#[test]
fn oversized_ref_literal_exhausts_render_work_budget() {
let schema = json!({
"$ref": "#/$defs/Value",
"$defs": {
"Value": {
"const": "x".repeat(MAX_RENDER_WORK_BYTES)
}
}
});
let mut renderer = JsonSchemaTypeRenderer::new(&schema);
assert_eq!(renderer.render(&schema), "unknown");
assert!(renderer.render_work_budget_exhausted);
}
#[test]
fn rendered_schema_has_a_hard_size_cap() {
let description = "x".repeat(MAX_RENDERED_SCHEMA_BYTES);
let schema = json!({
"type": "object",
"properties": {
"value": { "type": "string", "description": description }
}
});
assert_eq!(render_json_schema_to_typescript(&schema), "unknown");
}
+2 -1
View File
@@ -1,6 +1,7 @@
mod description;
pub mod grpc;
pub mod host;
mod json_schema_types;
mod response;
mod runtime;
mod session;
@@ -19,7 +20,7 @@ pub use description::is_code_mode_nested_tool;
pub use description::normalize_code_mode_identifier;
pub use description::parse_exec_source;
pub use description::render_code_mode_sample;
pub use description::render_json_schema_to_typescript;
pub use json_schema_types::render_json_schema_to_typescript;
pub use response::DEFAULT_IMAGE_DETAIL;
pub use response::FunctionCallOutputContentItem;
pub use response::ImageDetail;
+83
View File
@@ -5417,6 +5417,89 @@ text(JSON.stringify({
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_renders_local_refs_in_outbound_exec_description() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let response = responses::mount_sse_once(
&server,
sse(vec![
ev_assistant_message("msg-1", "done"),
ev_completed("resp-1"),
]),
)
.await;
let mut builder = test_codex().with_config(|config| {
config
.features
.enable(Feature::CodeModeOnly)
.expect("code mode only should be enabled");
});
let base_test = builder.build_with_auto_env(&server).await?;
let new_thread = base_test
.thread_manager
.start_thread(StartThreadOptions {
dynamic_tools: vec![DynamicToolSpec::Function(DynamicToolFunctionSpec {
name: "boolean_search".to_string(),
description: "Search with nested Boolean clauses.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"clauses": {
"type": "array",
"items": { "$ref": "#/$defs/Clause" }
}
},
"$defs": {
"Clause": {
"type": "object",
"properties": {
"query": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"clauses": {
"type": "array",
"items": { "$ref": "#/$defs/Clause" }
}
}
}
]
}
}
}
}
}),
defer_loading: false,
})],
..StartThreadOptions::new(base_test.config.clone())
})
.await?;
let mut test = base_test;
test.codex = new_thread.thread;
test.session_configured = new_thread.session_configured;
test.submit_turn("inspect the tool schema").await?;
let body = response.single_request().body_json();
let exec_description = body["tools"]
.as_array()
.expect("request should contain tools")
.iter()
.find_map(|tool| {
(tool["name"].as_str() == Some("exec"))
.then(|| tool["description"].as_str())
.flatten()
})
.expect("Code Mode exec should remain available");
assert!(exec_description.contains("query?: string | { clauses?: Array<"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_can_call_hidden_dynamic_tools() -> Result<()> {
skip_if_no_network!(Ok(()));