2323logger .info ("Generating schema reference..." )
2424
2525
26- def get_type (annotation : Type ) -> str :
26+ def _is_linkable_type (annotation : Any ) -> bool :
27+ """Check if a type annotation contains a BaseModel subclass (excluding Range)."""
28+ if inspect .isclass (annotation ):
29+ return issubclass (annotation , BaseModel ) and not issubclass (annotation , Range )
30+ origin = get_origin (annotation )
31+ if origin is Annotated :
32+ return _is_linkable_type (get_args (annotation )[0 ])
33+ if origin is Union :
34+ return any (_is_linkable_type (arg ) for arg in get_args (annotation ))
35+ if origin is list :
36+ args = get_args (annotation )
37+ return bool (args ) and _is_linkable_type (args [0 ])
38+ return False
39+
40+
41+ def _type_sort_key (t : str ) -> tuple :
42+ """Sort key for type parts: primitives first, then literals, then compound types."""
43+ order = {"bool" : 0 , "int" : 1 , "float" : 2 , "str" : 3 }
44+ if t in order :
45+ return (0 , order [t ])
46+ if t .startswith ('"' ):
47+ return (1 , t )
48+ if t .startswith ("list" ):
49+ return (2 , t )
50+ if t == "dict" :
51+ return (3 , "" )
52+ if t == "object" :
53+ return (4 , "" )
54+ return (5 , t )
55+
56+
57+ def get_friendly_type (annotation : Type ) -> str :
58+ """Get a user-friendly type string for documentation.
59+
60+ Produces types like: ``int | str``, ``"rps"``, ``list[object]``, ``"spot" | "on-demand" | "auto"``.
61+ """
62+ # Unwrap Annotated
2763 if get_origin (annotation ) is Annotated :
28- return get_type (get_args (annotation )[0 ])
64+ return get_friendly_type (get_args (annotation )[0 ])
65+
66+ # Handle Union (including Optional)
2967 if get_origin (annotation ) is Union :
30- # Optional is Union with None.
31- # We don't want to show Optional[A, None] but just Optional[A]
32- if annotation .__name__ == "Optional" :
33- args = "," .join (get_type (arg ) for arg in get_args (annotation )[:- 1 ])
34- else :
35- args = "," .join (get_type (arg ) for arg in get_args (annotation ))
36- return f"{ annotation .__name__ } [{ args } ]"
68+ args = [a for a in get_args (annotation ) if a is not type (None )]
69+ if not args :
70+ return ""
71+ parts : list = []
72+ for arg in args :
73+ friendly = get_friendly_type (arg )
74+ # Split compound types (e.g., "int | str" from Range) to deduplicate,
75+ # but avoid splitting types that contain brackets (e.g., list[...])
76+ if "[" not in friendly :
77+ for part in friendly .split (" | " ):
78+ if part and part not in parts :
79+ parts .append (part )
80+ else :
81+ if friendly and friendly not in parts :
82+ parts .append (friendly )
83+ parts .sort (key = _type_sort_key )
84+ return " | " .join (parts )
85+
86+ # Handle Literal — show quoted values
3787 if get_origin (annotation ) is Literal :
38- return str (annotation ).split ("." , maxsplit = 1 )[- 1 ]
88+ values = get_args (annotation )
89+ return " | " .join (f'"{ v } "' for v in values )
90+
91+ # Handle list
3992 if get_origin (annotation ) is list :
40- return f"List[{ get_type (get_args (annotation )[0 ])} ]"
93+ args = get_args (annotation )
94+ if args :
95+ inner = get_friendly_type (args [0 ])
96+ # Simplify lists of enum/literal values to list[str]
97+ if inner .startswith ('"' ) and " | " in inner :
98+ inner = "str"
99+ return f"list[{ inner } ]"
100+ return "list"
101+
102+ # Handle dict
41103 if get_origin (annotation ) is dict :
42- return f"Dict[{ get_type (get_args (annotation )[0 ])} , { get_type (get_args (annotation )[1 ])} ]"
43- return annotation .__name__
104+ return "dict"
105+
106+ # Handle concrete classes
107+ if inspect .isclass (annotation ):
108+ # Enum — show quoted values
109+ if issubclass (annotation , Enum ):
110+ values = [e .value for e in annotation ]
111+ return " | " .join (f'"{ v } "' for v in values )
112+
113+ # Range — depends on inner type parameter
114+ if issubclass (annotation , Range ):
115+ min_field = annotation .__fields__ .get ("min" )
116+ if min_field and inspect .isclass (min_field .type_ ):
117+ # Range[Memory] → str, Range[int] → int | str
118+ if issubclass (min_field .type_ , float ):
119+ return "str"
120+ return "int | str"
121+
122+ # Memory (float subclass that parses "8GB" strings)
123+ from dstack ._internal .core .models .resources import Memory as _Memory
124+
125+ if issubclass (annotation , _Memory ):
126+ return "str"
127+
128+ # BaseModel subclass (not Range)
129+ if issubclass (annotation , BaseModel ) and not issubclass (annotation , Range ):
130+ # Root models (with __root__ field) — resolve from the root type
131+ if "__root__" in annotation .__fields__ :
132+ return get_friendly_type (annotation .__fields__ ["__root__" ].annotation )
133+ # Models with custom __get_validators__ accept primitive input (int, str)
134+ # in addition to the full object form (e.g., GPUSpec, CPUSpec, DiskSpec)
135+ if "__get_validators__" in annotation .__dict__ :
136+ return "int | str | object"
137+ return "object"
138+
139+ # ComputeCapability (tuple subclass that parses "7.5" strings)
140+ if annotation .__name__ == "ComputeCapability" :
141+ return "float | str"
142+
143+ # Constrained and primitive types — check MRO
144+ # bool must come before int (bool is a subclass of int)
145+ if issubclass (annotation , bool ):
146+ return "bool"
147+ if issubclass (annotation , int ):
148+ # Duration (int subclass that parses "5m" strings)
149+ if annotation .__name__ == "Duration" :
150+ return "int | str"
151+ return "int"
152+ if issubclass (annotation , float ):
153+ return "float"
154+ if issubclass (annotation , str ):
155+ return "str"
156+ if issubclass (annotation , (list , tuple )):
157+ return "list"
158+ if issubclass (annotation , dict ):
159+ return "dict"
160+
161+ return annotation .__name__
162+
163+ return str (annotation )
44164
45165
46166def generate_schema_reference (
@@ -70,7 +190,7 @@ def generate_schema_reference(
70190 values = dict (
71191 name = name ,
72192 description = field .field_info .description ,
73- type = get_type (field .annotation ),
193+ type = get_friendly_type (field .annotation ),
74194 default = default ,
75195 required = field .required ,
76196 )
@@ -84,11 +204,7 @@ def generate_schema_reference(
84204 if field .annotation .__name__ == "Annotated" :
85205 if field_type .__name__ in ["Optional" , "List" , "list" , "Union" ]:
86206 field_type = get_args (field_type )[0 ]
87- base_model = (
88- inspect .isclass (field_type )
89- and issubclass (field_type , BaseModel )
90- and not issubclass (field_type , Range )
91- )
207+ base_model = _is_linkable_type (field_type )
92208 else :
93209 base_model = False
94210 _defaults = (
@@ -114,29 +230,27 @@ def generate_schema_reference(
114230 if not base_model
115231 else f"[`{ values ['name' ]} `](#{ item_id_prefix } { link_name } )"
116232 )
117- item_optional_marker = "(Optional)" if not values ["required" ] else ""
233+ item_required_marker = "(Required)" if values ["required" ] else "(Optional)"
234+ item_type_display = f"`{ values ['type' ]} `" if values .get ("type" ) else ""
118235 item_description = (values ["description" ]).replace ("\n " , "<br>" ) + "."
119236 item_default = _defaults if not values ["required" ] else _must_be
120237 item_id = f"#{ values ['name' ]} " if not base_model else f"#_{ values ['name' ]} "
121238 item_toc_label = f"data-toc-label='{ values ['name' ]} '"
122239 item_css_cass = "class='reference-item'"
123- rows .append (
124- prefix
125- + " " .join (
126- [
127- f"###### { item_header } " ,
128- "-" ,
129- item_optional_marker ,
130- item_description ,
131- item_default ,
132- "{" ,
133- item_id ,
134- item_toc_label ,
135- item_css_cass ,
136- "}" ,
137- ]
138- )
139- )
240+ parts = [
241+ f"###### { item_header } " ,
242+ "-" ,
243+ item_required_marker ,
244+ item_type_display ,
245+ item_description ,
246+ item_default ,
247+ "{" ,
248+ item_id ,
249+ item_toc_label ,
250+ item_css_cass ,
251+ "}" ,
252+ ]
253+ rows .append (prefix + " " .join (p for p in parts if p ))
140254 return "\n " .join (rows )
141255
142256
0 commit comments