1use adk_core::SchemaAdapter;
48use adk_core::schema_utils;
49use serde_json::{Map, Value};
50use std::borrow::Cow;
51
52const GEMINI_ALLOWED_FORMATS: &[&str] =
54 &["date-time", "date", "time", "email", "uri", "uuid", "int32", "int64", "float", "double"];
55
56const UNSUPPORTED_KEYWORDS: &[&str] = &[
66 "$id",
67 "additionalProperties",
68 "contains",
69 "contentEncoding",
70 "contentMediaType",
71 "default",
72 "dependentRequired",
73 "dependentSchemas",
74 "deprecated",
75 "examples",
76 "exclusiveMaximum",
77 "exclusiveMinimum",
78 "maxItems",
79 "maxLength",
80 "maxProperties",
81 "maximum",
82 "minItems",
83 "minLength",
84 "minProperties",
85 "minimum",
86 "multipleOf",
87 "not",
88 "pattern",
89 "patternProperties",
90 "prefixItems",
91 "propertyNames",
92 "readOnly",
93 "title",
94 "unevaluatedProperties",
95 "uniqueItems",
96 "writeOnly",
97];
98
99const UNSUPPORTED_KEYWORDS_VERTEX: &[&str] = &[
106 "$id",
107 "contains",
108 "contentEncoding",
109 "contentMediaType",
110 "default",
111 "dependentRequired",
112 "dependentSchemas",
113 "deprecated",
114 "examples",
115 "exclusiveMaximum",
116 "exclusiveMinimum",
117 "maxItems",
118 "maxLength",
119 "maxProperties",
120 "maximum",
121 "minItems",
122 "minLength",
123 "minProperties",
124 "minimum",
125 "multipleOf",
126 "not",
127 "pattern",
128 "patternProperties",
129 "prefixItems",
130 "propertyNames",
131 "readOnly",
132 "title",
133 "unevaluatedProperties",
134 "uniqueItems",
135 "writeOnly",
136];
137
138#[derive(Debug)]
167pub struct GeminiSchemaAdapter {
168 vertex_ai: bool,
171}
172
173impl GeminiSchemaAdapter {
174 pub fn new() -> Self {
178 Self { vertex_ai: false }
179 }
180
181 pub fn vertex_ai() -> Self {
186 Self { vertex_ai: true }
187 }
188}
189
190impl Default for GeminiSchemaAdapter {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196impl SchemaAdapter for GeminiSchemaAdapter {
197 fn normalize_schema(&self, mut schema: Value) -> Value {
198 let definitions = extract_definitions(&schema);
202 schema_utils::resolve_refs(&mut schema, &definitions, 0);
203
204 schema_utils::strip_schema_keyword(&mut schema);
206
207 schema_utils::collapse_combiners(&mut schema);
209
210 schema_utils::merge_all_of(&mut schema);
212
213 schema_utils::collapse_type_arrays(&mut schema);
215
216 schema_utils::strip_conditional_keywords(&mut schema);
218
219 schema_utils::convert_const_to_enum(&mut schema);
221
222 schema_utils::strip_null_from_enum(&mut schema);
224
225 schema_utils::add_implicit_object_type(&mut schema);
227
228 if self.vertex_ai {
230 remove_unsupported_keywords_vertex(&mut schema);
231 } else {
232 remove_unsupported_keywords(&mut schema);
233 }
234
235 schema_utils::strip_unsupported_formats(&mut schema, GEMINI_ALLOWED_FORMATS);
237
238 schema_utils::enforce_nesting_depth(&mut schema, 5, 0);
240
241 if let Some(obj) = schema.as_object_mut() {
243 obj.remove("definitions");
244 obj.remove("$defs");
245 }
246
247 schema
248 }
249
250 fn normalize_tool_name<'a>(&self, name: &'a str) -> Cow<'a, str> {
254 if name.len() <= 64 {
255 Cow::Borrowed(name)
256 } else {
257 let mut end = 64;
258 while end > 0 && !name.is_char_boundary(end) {
259 end -= 1;
260 }
261 Cow::Owned(name[..end].to_string())
262 }
263 }
264
265 fn empty_schema(&self) -> Value {
270 serde_json::json!({"type": "object", "properties": {}})
271 }
272}
273
274fn extract_definitions(schema: &Value) -> Map<String, Value> {
277 let mut defs = Map::new();
278
279 if let Some(obj) = schema.as_object() {
280 if let Some(definitions) = obj.get("definitions").and_then(|v| v.as_object()) {
282 for (key, value) in definitions {
283 defs.insert(key.clone(), value.clone());
284 }
285 }
286
287 if let Some(dollar_defs) = obj.get("$defs").and_then(|v| v.as_object()) {
289 for (key, value) in dollar_defs {
290 defs.insert(key.clone(), value.clone());
291 }
292 }
293 }
294
295 defs
296}
297
298fn remove_unsupported_keywords(schema: &mut Value) {
304 let Some(obj) = schema.as_object_mut() else {
305 return;
306 };
307
308 for keyword in UNSUPPORTED_KEYWORDS {
310 obj.remove(*keyword);
311 }
312
313 let is_array_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "array");
321 if !is_array_type {
322 obj.remove("items");
323 } else if obj.get("items").is_some_and(|v| v.is_array()) {
324 let first_schema = obj
326 .get("items")
327 .and_then(|v| v.as_array())
328 .and_then(|arr| arr.first())
329 .cloned()
330 .unwrap_or_else(|| serde_json::json!({"type": "string"}));
331 obj.insert("items".to_string(), first_schema);
332 } else if !obj.contains_key("items") {
333 obj.insert("items".to_string(), serde_json::json!({"type": "string"}));
335 }
336
337 if let Some(props) = obj.get_mut("properties")
339 && let Some(props_obj) = props.as_object_mut()
340 {
341 for value in props_obj.values_mut() {
342 remove_unsupported_keywords(value);
343 }
344 }
345
346 if let Some(items) = obj.get_mut("items")
348 && items.is_object()
349 {
350 remove_unsupported_keywords(items);
351 }
352
353 for keyword in &["allOf", "anyOf", "oneOf"] {
355 if let Some(arr_val) = obj.get_mut(*keyword)
356 && let Some(arr) = arr_val.as_array_mut()
357 {
358 for sub in arr.iter_mut() {
359 remove_unsupported_keywords(sub);
360 }
361 }
362 }
363}
364
365fn remove_unsupported_keywords_vertex(schema: &mut Value) {
372 let Some(obj) = schema.as_object_mut() else {
373 return;
374 };
375
376 for keyword in UNSUPPORTED_KEYWORDS_VERTEX {
378 obj.remove(*keyword);
379 }
380
381 let is_object_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "object");
383 if is_object_type {
384 obj.insert("additionalProperties".to_string(), Value::Bool(false));
385 } else {
386 obj.remove("additionalProperties");
388 }
389
390 let is_array_type = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "array");
398 if !is_array_type {
399 obj.remove("items");
400 } else if obj.get("items").is_some_and(|v| v.is_array()) {
401 let first_schema = obj
402 .get("items")
403 .and_then(|v| v.as_array())
404 .and_then(|arr| arr.first())
405 .cloned()
406 .unwrap_or_else(|| serde_json::json!({"type": "string"}));
407 obj.insert("items".to_string(), first_schema);
408 } else if !obj.contains_key("items") {
409 obj.insert("items".to_string(), serde_json::json!({"type": "string"}));
410 }
411
412 if let Some(props) = obj.get_mut("properties")
414 && let Some(props_obj) = props.as_object_mut()
415 {
416 for value in props_obj.values_mut() {
417 remove_unsupported_keywords_vertex(value);
418 }
419 }
420
421 if let Some(items) = obj.get_mut("items")
423 && items.is_object()
424 {
425 remove_unsupported_keywords_vertex(items);
426 }
427
428 for keyword in &["allOf", "anyOf", "oneOf"] {
430 if let Some(arr_val) = obj.get_mut(*keyword)
431 && let Some(arr) = arr_val.as_array_mut()
432 {
433 for sub in arr.iter_mut() {
434 remove_unsupported_keywords_vertex(sub);
435 }
436 }
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443 use serde_json::json;
444
445 #[test]
446 fn test_strips_schema_keyword() {
447 let adapter = GeminiSchemaAdapter::new();
448 let schema = json!({
449 "$schema": "http://json-schema.org/draft-07/schema#",
450 "type": "object",
451 "properties": { "name": { "type": "string" } }
452 });
453 let result = adapter.normalize_schema(schema);
454 assert!(result.get("$schema").is_none());
455 }
456
457 #[test]
458 fn test_removes_additional_properties() {
459 let adapter = GeminiSchemaAdapter::new();
460 let schema = json!({
461 "type": "object",
462 "properties": { "name": { "type": "string" } },
463 "additionalProperties": true
464 });
465 let result = adapter.normalize_schema(schema);
466 assert!(result.get("additionalProperties").is_none());
467 }
468
469 #[test]
470 fn test_removes_exclusive_min_max() {
471 let adapter = GeminiSchemaAdapter::new();
472 let schema = json!({
473 "type": "number",
474 "exclusiveMinimum": 0,
475 "exclusiveMaximum": 100
476 });
477 let result = adapter.normalize_schema(schema);
478 assert!(result.get("exclusiveMinimum").is_none());
479 assert!(result.get("exclusiveMaximum").is_none());
480 }
481
482 #[test]
483 fn test_removes_items_when_not_array() {
484 let adapter = GeminiSchemaAdapter::new();
485 let schema = json!({
486 "type": "object",
487 "items": { "type": "string" }
488 });
489 let result = adapter.normalize_schema(schema);
490 assert!(result.get("items").is_none());
491 }
492
493 #[test]
494 fn test_preserves_items_when_array() {
495 let adapter = GeminiSchemaAdapter::new();
496 let schema = json!({
497 "type": "array",
498 "items": { "type": "string" }
499 });
500 let result = adapter.normalize_schema(schema);
501 assert!(result.get("items").is_some());
502 assert_eq!(result["items"]["type"], "string");
503 }
504
505 #[test]
506 fn test_converts_items_tuple_validation_to_single_schema() {
507 let adapter = GeminiSchemaAdapter::new();
510 let schema = json!({
511 "type": "array",
512 "items": [
513 { "type": "number" },
514 { "type": "number" }
515 ]
516 });
517 let result = adapter.normalize_schema(schema);
518 assert_eq!(result["items"], json!({"type": "number"}));
520 assert_eq!(result["type"], "array");
521 }
522
523 #[test]
524 fn test_vertex_ai_converts_items_tuple_validation() {
525 let adapter = GeminiSchemaAdapter::vertex_ai();
526 let schema = json!({
527 "type": "array",
528 "items": [
529 { "type": "integer" },
530 { "type": "boolean" }
531 ]
532 });
533 let result = adapter.normalize_schema(schema);
534 assert_eq!(result["items"], json!({"type": "integer"}));
536 }
537
538 #[test]
539 fn test_removes_not_keyword() {
540 let adapter = GeminiSchemaAdapter::new();
541 let schema = json!({
542 "type": "string",
543 "not": { "enum": ["bad"] }
544 });
545 let result = adapter.normalize_schema(schema);
546 assert!(result.get("not").is_none());
547 }
548
549 #[test]
550 fn test_removes_property_names() {
551 let adapter = GeminiSchemaAdapter::new();
552 let schema = json!({
553 "type": "object",
554 "propertyNames": { "pattern": "^[a-z]+$" }
555 });
556 let result = adapter.normalize_schema(schema);
557 assert!(result.get("propertyNames").is_none());
558 }
559
560 #[test]
561 fn test_removes_pattern_properties() {
562 let adapter = GeminiSchemaAdapter::new();
563 let schema = json!({
564 "type": "object",
565 "patternProperties": { "^S_": { "type": "string" } }
566 });
567 let result = adapter.normalize_schema(schema);
568 assert!(result.get("patternProperties").is_none());
569 }
570
571 #[test]
572 fn test_removes_unevaluated_properties() {
573 let adapter = GeminiSchemaAdapter::new();
574 let schema = json!({
575 "type": "object",
576 "unevaluatedProperties": false
577 });
578 let result = adapter.normalize_schema(schema);
579 assert!(result.get("unevaluatedProperties").is_none());
580 }
581
582 #[test]
583 fn test_collapses_any_of() {
584 let adapter = GeminiSchemaAdapter::new();
585 let schema = json!({
586 "anyOf": [
587 { "type": "null" },
588 { "type": "string", "description": "A non-empty string" }
589 ]
590 });
591 let result = adapter.normalize_schema(schema);
592 assert!(result.get("anyOf").is_none());
593 assert_eq!(result["type"], "string");
594 assert_eq!(result["description"], "A non-empty string");
595 }
596
597 #[test]
598 fn test_collapses_one_of() {
599 let adapter = GeminiSchemaAdapter::new();
600 let schema = json!({
601 "oneOf": [
602 { "type": "null" },
603 { "type": "integer", "minimum": 0 }
604 ]
605 });
606 let result = adapter.normalize_schema(schema);
607 assert!(result.get("oneOf").is_none());
608 assert_eq!(result["type"], "integer");
609 }
610
611 #[test]
612 fn test_merges_all_of() {
613 let adapter = GeminiSchemaAdapter::new();
614 let schema = json!({
615 "allOf": [
616 { "type": "object", "properties": { "a": { "type": "string" } } },
617 { "properties": { "b": { "type": "number" } }, "required": ["b"] }
618 ]
619 });
620 let result = adapter.normalize_schema(schema);
621 assert!(result.get("allOf").is_none());
622 assert_eq!(result["properties"]["a"]["type"], "string");
623 assert_eq!(result["properties"]["b"]["type"], "number");
624 assert_eq!(result["required"], json!(["b"]));
625 }
626
627 #[test]
628 fn test_collapses_type_arrays() {
629 let adapter = GeminiSchemaAdapter::new();
630 let schema = json!({
631 "type": ["string", "null"],
632 "minLength": 1
633 });
634 let result = adapter.normalize_schema(schema);
635 assert_eq!(result["type"], "string");
636 }
637
638 #[test]
639 fn test_strips_conditional_keywords() {
640 let adapter = GeminiSchemaAdapter::new();
641 let schema = json!({
642 "type": "object",
643 "if": { "properties": { "kind": { "const": "a" } } },
644 "then": { "required": ["extra"] },
645 "else": { "required": [] }
646 });
647 let result = adapter.normalize_schema(schema);
648 assert!(result.get("if").is_none());
649 assert!(result.get("then").is_none());
650 assert!(result.get("else").is_none());
651 }
652
653 #[test]
654 fn test_converts_const_to_enum() {
655 let adapter = GeminiSchemaAdapter::new();
656 let schema = json!({
657 "type": "string",
658 "const": "fixed"
659 });
660 let result = adapter.normalize_schema(schema);
661 assert!(result.get("const").is_none());
662 assert_eq!(result["enum"], json!(["fixed"]));
663 }
664
665 #[test]
666 fn test_strips_null_from_enum() {
667 let adapter = GeminiSchemaAdapter::new();
668 let schema = json!({
669 "type": "string",
670 "enum": ["a", null, "b"]
671 });
672 let result = adapter.normalize_schema(schema);
673 assert_eq!(result["enum"], json!(["a", "b"]));
674 }
675
676 #[test]
677 fn test_removes_empty_enum_after_null_strip() {
678 let adapter = GeminiSchemaAdapter::new();
679 let schema = json!({
680 "type": "string",
681 "enum": [null]
682 });
683 let result = adapter.normalize_schema(schema);
684 assert!(result.get("enum").is_none());
685 }
686
687 #[test]
688 fn test_adds_implicit_object_type() {
689 let adapter = GeminiSchemaAdapter::new();
690 let schema = json!({
691 "properties": { "name": { "type": "string" } }
692 });
693 let result = adapter.normalize_schema(schema);
694 assert_eq!(result["type"], "object");
695 }
696
697 #[test]
698 fn test_strips_unsupported_formats() {
699 let adapter = GeminiSchemaAdapter::new();
700 let schema = json!({
701 "type": "object",
702 "properties": {
703 "created": { "type": "string", "format": "date-time" },
704 "hostname": { "type": "string", "format": "hostname" },
705 "id": { "type": "string", "format": "uuid" }
706 }
707 });
708 let result = adapter.normalize_schema(schema);
709 assert_eq!(result["properties"]["created"]["format"], "date-time");
710 assert!(result["properties"]["hostname"].get("format").is_none());
711 assert_eq!(result["properties"]["id"]["format"], "uuid");
712 }
713
714 #[test]
715 fn test_preserves_all_allowed_formats() {
716 let adapter = GeminiSchemaAdapter::new();
717 for format in GEMINI_ALLOWED_FORMATS {
718 let schema = json!({ "type": "string", "format": format });
719 let result = adapter.normalize_schema(schema);
720 assert_eq!(result["format"], *format, "format '{format}' should be preserved");
721 }
722 }
723
724 #[test]
725 fn test_enforces_nesting_depth() {
726 let adapter = GeminiSchemaAdapter::new();
727 let schema = json!({
729 "type": "object",
730 "properties": {
731 "l1": {
732 "type": "object",
733 "properties": {
734 "l2": {
735 "type": "object",
736 "properties": {
737 "l3": {
738 "type": "object",
739 "properties": {
740 "l4": {
741 "type": "object",
742 "properties": {
743 "l5": {
744 "type": "object",
745 "properties": {
746 "l6": { "type": "string" }
747 }
748 }
749 }
750 }
751 }
752 }
753 }
754 }
755 }
756 }
757 }
758 });
759 let result = adapter.normalize_schema(schema);
760 let l5 = &result["properties"]["l1"]["properties"]["l2"]["properties"]["l3"]["properties"]
762 ["l4"]["properties"]["l5"];
763 assert_eq!(l5, &json!({"type": "object"}));
764 }
765
766 #[test]
767 fn test_resolves_refs() {
768 let adapter = GeminiSchemaAdapter::new();
769 let schema = json!({
770 "type": "object",
771 "properties": {
772 "address": { "$ref": "#/definitions/Address" }
773 },
774 "definitions": {
775 "Address": {
776 "type": "object",
777 "properties": {
778 "street": { "type": "string" }
779 }
780 }
781 }
782 });
783 let result = adapter.normalize_schema(schema);
784 assert!(result["properties"]["address"].get("$ref").is_none());
786 assert_eq!(result["properties"]["address"]["type"], "object");
787 assert_eq!(result["properties"]["address"]["properties"]["street"]["type"], "string");
788 assert!(result.get("definitions").is_none());
790 }
791
792 #[test]
793 fn test_resolves_dollar_defs() {
794 let adapter = GeminiSchemaAdapter::new();
795 let schema = json!({
796 "type": "object",
797 "properties": {
798 "item": { "$ref": "#/$defs/Item" }
799 },
800 "$defs": {
801 "Item": {
802 "type": "object",
803 "properties": {
804 "name": { "type": "string" }
805 }
806 }
807 }
808 });
809 let result = adapter.normalize_schema(schema);
810 assert!(result["properties"]["item"].get("$ref").is_none());
811 assert_eq!(result["properties"]["item"]["type"], "object");
812 assert!(result.get("$defs").is_none());
813 }
814
815 #[test]
816 fn test_unresolvable_ref_becomes_object() {
817 let adapter = GeminiSchemaAdapter::new();
818 let schema = json!({
819 "type": "object",
820 "properties": {
821 "unknown": { "$ref": "#/definitions/DoesNotExist" }
822 }
823 });
824 let result = adapter.normalize_schema(schema);
825 assert_eq!(result["properties"]["unknown"], json!({"type": "object"}));
826 }
827
828 #[test]
829 fn test_circular_ref_breaks() {
830 let adapter = GeminiSchemaAdapter::new();
831 let schema = json!({
832 "type": "object",
833 "properties": {
834 "self_ref": { "$ref": "#/definitions/Node" }
835 },
836 "definitions": {
837 "Node": {
838 "type": "object",
839 "properties": {
840 "child": { "$ref": "#/definitions/Node" }
841 }
842 }
843 }
844 });
845 let result = adapter.normalize_schema(schema);
846 assert_eq!(result["properties"]["self_ref"]["type"], "object");
848 assert!(result.get("definitions").is_none());
849 }
850
851 #[test]
852 fn test_removes_definitions_and_defs() {
853 let adapter = GeminiSchemaAdapter::new();
854 let schema = json!({
855 "type": "object",
856 "definitions": { "Foo": { "type": "string" } },
857 "$defs": { "Bar": { "type": "number" } }
858 });
859 let result = adapter.normalize_schema(schema);
860 assert!(result.get("definitions").is_none());
861 assert!(result.get("$defs").is_none());
862 }
863
864 #[test]
865 fn test_nested_unsupported_keywords_removed() {
866 let adapter = GeminiSchemaAdapter::new();
867 let schema = json!({
868 "type": "object",
869 "properties": {
870 "inner": {
871 "type": "object",
872 "additionalProperties": false,
873 "exclusiveMinimum": 5,
874 "properties": {
875 "deep": {
876 "type": "number",
877 "exclusiveMaximum": 100
878 }
879 }
880 }
881 }
882 });
883 let result = adapter.normalize_schema(schema);
884 let inner = &result["properties"]["inner"];
885 assert!(inner.get("additionalProperties").is_none());
886 assert!(inner.get("exclusiveMinimum").is_none());
887 assert!(inner["properties"]["deep"].get("exclusiveMaximum").is_none());
888 }
889
890 #[test]
891 fn test_full_transform_pipeline() {
892 let adapter = GeminiSchemaAdapter::new();
893 let schema = json!({
894 "$schema": "http://json-schema.org/draft-07/schema#",
895 "definitions": {
896 "Status": { "type": "string", "enum": ["active", null, "inactive"] }
897 },
898 "properties": {
899 "name": { "type": ["string", "null"], "format": "hostname" },
900 "status": { "$ref": "#/definitions/Status" },
901 "config": {
902 "type": "object",
903 "additionalProperties": true,
904 "properties": {
905 "value": { "const": "fixed" }
906 }
907 }
908 },
909 "if": { "properties": { "name": { "type": "string" } } },
910 "then": { "required": ["status"] },
911 "additionalProperties": false
912 });
913 let result = adapter.normalize_schema(schema);
914
915 assert!(result.get("$schema").is_none());
917 assert!(result.get("definitions").is_none());
919 assert!(result.get("if").is_none());
921 assert!(result.get("then").is_none());
922 assert!(result.get("additionalProperties").is_none());
924 assert_eq!(result["properties"]["name"]["type"], "string");
926 assert!(result["properties"]["name"].get("format").is_none());
928 assert_eq!(result["properties"]["status"]["enum"], json!(["active", "inactive"]));
930 assert_eq!(result["properties"]["config"]["properties"]["value"]["enum"], json!(["fixed"]));
932 assert!(result["properties"]["config"].get("additionalProperties").is_none());
934 assert_eq!(result["type"], "object");
936 }
937
938 #[test]
939 fn test_idempotent() {
940 let adapter = GeminiSchemaAdapter::new();
941 let schema = json!({
942 "$schema": "http://json-schema.org/draft-07/schema#",
943 "type": "object",
944 "properties": {
945 "name": { "type": ["string", "null"], "format": "hostname" },
946 "items": { "type": "array", "items": { "type": "string" } }
947 },
948 "additionalProperties": true,
949 "if": { "const": true },
950 "then": { "required": ["name"] }
951 });
952 let first = adapter.normalize_schema(schema);
953 let second = adapter.normalize_schema(first.clone());
954 assert_eq!(first, second);
955 }
956
957 #[test]
958 fn test_empty_schema() {
959 let adapter = GeminiSchemaAdapter::new();
960 let schema = json!({});
961 let result = adapter.normalize_schema(schema);
962 assert_eq!(result, json!({}));
963 }
964
965 #[test]
966 fn test_array_items_nested_cleanup() {
967 let adapter = GeminiSchemaAdapter::new();
968 let schema = json!({
969 "type": "array",
970 "items": {
971 "type": "object",
972 "additionalProperties": true,
973 "properties": {
974 "id": { "type": "integer", "exclusiveMinimum": 0 }
975 }
976 }
977 });
978 let result = adapter.normalize_schema(schema);
979 assert!(result["items"].get("additionalProperties").is_none());
980 assert!(result["items"]["properties"]["id"].get("exclusiveMinimum").is_none());
981 }
982
983 #[test]
986 fn test_vertex_ai_sets_additional_properties_false() {
987 let adapter = GeminiSchemaAdapter::vertex_ai();
988 let schema = json!({
989 "type": "object",
990 "properties": { "name": { "type": "string" } },
991 "additionalProperties": true
992 });
993 let result = adapter.normalize_schema(schema);
994 assert_eq!(result["additionalProperties"], json!(false));
995 }
996
997 #[test]
998 fn test_vertex_ai_sets_additional_properties_false_on_nested_objects() {
999 let adapter = GeminiSchemaAdapter::vertex_ai();
1000 let schema = json!({
1001 "type": "object",
1002 "properties": {
1003 "inner": {
1004 "type": "object",
1005 "properties": {
1006 "value": { "type": "string" }
1007 }
1008 }
1009 }
1010 });
1011 let result = adapter.normalize_schema(schema);
1012 assert_eq!(result["additionalProperties"], json!(false));
1013 assert_eq!(result["properties"]["inner"]["additionalProperties"], json!(false));
1014 }
1015
1016 #[test]
1017 fn test_vertex_ai_does_not_set_additional_properties_on_non_object() {
1018 let adapter = GeminiSchemaAdapter::vertex_ai();
1019 let schema = json!({
1020 "type": "string",
1021 "additionalProperties": true
1022 });
1023 let result = adapter.normalize_schema(schema);
1024 assert!(result.get("additionalProperties").is_none());
1026 }
1027
1028 #[test]
1029 fn test_standard_mode_removes_additional_properties() {
1030 let adapter = GeminiSchemaAdapter::new();
1031 let schema = json!({
1032 "type": "object",
1033 "properties": { "name": { "type": "string" } },
1034 "additionalProperties": true
1035 });
1036 let result = adapter.normalize_schema(schema);
1037 assert!(result.get("additionalProperties").is_none());
1038 }
1039
1040 #[test]
1041 fn test_vertex_ai_still_removes_other_unsupported_keywords() {
1042 let adapter = GeminiSchemaAdapter::vertex_ai();
1043 let schema = json!({
1044 "type": "object",
1045 "properties": { "x": { "type": "number" } },
1046 "exclusiveMinimum": 0,
1047 "exclusiveMaximum": 100,
1048 "not": { "type": "null" },
1049 "propertyNames": { "pattern": "^[a-z]" },
1050 "patternProperties": { "^S_": { "type": "string" } },
1051 "unevaluatedProperties": false
1052 });
1053 let result = adapter.normalize_schema(schema);
1054 assert!(result.get("exclusiveMinimum").is_none());
1055 assert!(result.get("exclusiveMaximum").is_none());
1056 assert!(result.get("not").is_none());
1057 assert!(result.get("propertyNames").is_none());
1058 assert!(result.get("patternProperties").is_none());
1059 assert!(result.get("unevaluatedProperties").is_none());
1060 assert_eq!(result["additionalProperties"], json!(false));
1062 }
1063
1064 #[test]
1067 fn test_normalize_tool_name_short_name_unchanged() {
1068 let adapter = GeminiSchemaAdapter::new();
1069 let name = "get_weather";
1070 let result = adapter.normalize_tool_name(name);
1071 assert_eq!(result, "get_weather");
1072 assert!(matches!(result, Cow::Borrowed(_)));
1073 }
1074
1075 #[test]
1076 fn test_normalize_tool_name_exactly_64_bytes() {
1077 let adapter = GeminiSchemaAdapter::new();
1078 let name = "a".repeat(64);
1079 let result = adapter.normalize_tool_name(&name);
1080 assert_eq!(result.len(), 64);
1081 assert!(matches!(result, Cow::Borrowed(_)));
1082 }
1083
1084 #[test]
1085 fn test_normalize_tool_name_truncates_at_64_bytes() {
1086 let adapter = GeminiSchemaAdapter::new();
1087 let name = "a".repeat(100);
1088 let result = adapter.normalize_tool_name(&name);
1089 assert_eq!(result.len(), 64);
1090 assert_eq!(result.as_ref(), "a".repeat(64));
1091 }
1092
1093 #[test]
1094 fn test_normalize_tool_name_multibyte_boundary() {
1095 let adapter = GeminiSchemaAdapter::new();
1096 let name = "日".repeat(22); let result = adapter.normalize_tool_name(&name);
1100 assert!(result.len() <= 64);
1101 assert_eq!(result.len(), 63);
1103 assert_eq!(result.as_ref(), "日".repeat(21));
1104 assert!(std::str::from_utf8(result.as_bytes()).is_ok());
1106 }
1107
1108 #[test]
1109 fn test_normalize_tool_name_emoji_boundary() {
1110 let adapter = GeminiSchemaAdapter::new();
1111 let name = "🎯".repeat(16);
1113 assert_eq!(name.len(), 64);
1114 let result = adapter.normalize_tool_name(&name);
1115 assert_eq!(result.len(), 64);
1116
1117 let name = "🎯".repeat(17);
1119 let result = adapter.normalize_tool_name(&name);
1120 assert_eq!(result.len(), 64);
1121 assert_eq!(result.as_ref(), "🎯".repeat(16));
1122 }
1123
1124 #[test]
1127 fn test_empty_schema_returns_object_with_properties() {
1128 let adapter = GeminiSchemaAdapter::new();
1129 let result = adapter.empty_schema();
1130 assert_eq!(result, json!({"type": "object", "properties": {}}));
1131 }
1132
1133 #[test]
1134 fn test_empty_schema_vertex_ai_same_as_standard() {
1135 let adapter = GeminiSchemaAdapter::vertex_ai();
1136 let result = adapter.empty_schema();
1137 assert_eq!(result, json!({"type": "object", "properties": {}}));
1138 }
1139
1140 #[test]
1144 fn test_removes_all_validation_keywords() {
1145 let adapter = GeminiSchemaAdapter::new();
1146 let schema = json!({
1147 "type": "object",
1148 "title": "MySchema",
1149 "$id": "https://example.com/schema",
1150 "default": {},
1151 "deprecated": true,
1152 "readOnly": true,
1153 "writeOnly": false,
1154 "examples": [{"name": "test"}],
1155 "minProperties": 1,
1156 "maxProperties": 10,
1157 "properties": {
1158 "name": {
1159 "type": "string",
1160 "title": "Name",
1161 "default": "",
1162 "minLength": 1,
1163 "maxLength": 100,
1164 "pattern": "^[a-z]+$"
1165 },
1166 "age": {
1167 "type": "integer",
1168 "minimum": 0,
1169 "maximum": 150,
1170 "multipleOf": 1
1171 },
1172 "tags": {
1173 "type": "array",
1174 "items": { "type": "string" },
1175 "minItems": 1,
1176 "maxItems": 10,
1177 "uniqueItems": true,
1178 "contains": { "type": "string" }
1179 }
1180 }
1181 });
1182 let result = adapter.normalize_schema(schema);
1183
1184 assert!(result.get("title").is_none());
1186 assert!(result.get("$id").is_none());
1187 assert!(result.get("default").is_none());
1188 assert!(result.get("deprecated").is_none());
1189 assert!(result.get("readOnly").is_none());
1190 assert!(result.get("writeOnly").is_none());
1191 assert!(result.get("examples").is_none());
1192 assert!(result.get("minProperties").is_none());
1193 assert!(result.get("maxProperties").is_none());
1194
1195 let name = &result["properties"]["name"];
1197 assert!(name.get("title").is_none());
1198 assert!(name.get("default").is_none());
1199 assert!(name.get("minLength").is_none());
1200 assert!(name.get("maxLength").is_none());
1201 assert!(name.get("pattern").is_none());
1202 assert_eq!(name["type"], "string");
1203
1204 let age = &result["properties"]["age"];
1206 assert!(age.get("minimum").is_none());
1207 assert!(age.get("maximum").is_none());
1208 assert!(age.get("multipleOf").is_none());
1209 assert_eq!(age["type"], "integer");
1210
1211 let tags = &result["properties"]["tags"];
1213 assert!(tags.get("minItems").is_none());
1214 assert!(tags.get("maxItems").is_none());
1215 assert!(tags.get("uniqueItems").is_none());
1216 assert!(tags.get("contains").is_none());
1217 assert_eq!(tags["type"], "array");
1218 assert_eq!(tags["items"]["type"], "string");
1219 }
1220
1221 #[test]
1222 fn test_removes_prefix_items() {
1223 let adapter = GeminiSchemaAdapter::new();
1224 let schema = json!({
1225 "type": "array",
1226 "prefixItems": [
1227 { "type": "string" },
1228 { "type": "integer" }
1229 ]
1230 });
1231 let result = adapter.normalize_schema(schema);
1232 assert!(result.get("prefixItems").is_none());
1233 }
1234
1235 #[test]
1236 fn test_removes_dependent_keywords() {
1237 let adapter = GeminiSchemaAdapter::new();
1238 let schema = json!({
1239 "type": "object",
1240 "properties": {
1241 "name": { "type": "string" },
1242 "credit_card": { "type": "string" }
1243 },
1244 "dependentRequired": {
1245 "credit_card": ["billing_address"]
1246 },
1247 "dependentSchemas": {
1248 "credit_card": {
1249 "properties": {
1250 "billing_address": { "type": "string" }
1251 }
1252 }
1253 }
1254 });
1255 let result = adapter.normalize_schema(schema);
1256 assert!(result.get("dependentRequired").is_none());
1257 assert!(result.get("dependentSchemas").is_none());
1258 }
1259
1260 #[test]
1261 fn test_removes_content_keywords() {
1262 let adapter = GeminiSchemaAdapter::new();
1263 let schema = json!({
1264 "type": "string",
1265 "contentMediaType": "application/json",
1266 "contentEncoding": "base64"
1267 });
1268 let result = adapter.normalize_schema(schema);
1269 assert!(result.get("contentMediaType").is_none());
1270 assert!(result.get("contentEncoding").is_none());
1271 }
1272}