ostruct-cli 0.7.2__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ostruct/cli/__init__.py +21 -3
- ostruct/cli/base_errors.py +1 -1
- ostruct/cli/cli.py +66 -1983
- ostruct/cli/click_options.py +460 -28
- ostruct/cli/code_interpreter.py +238 -0
- ostruct/cli/commands/__init__.py +32 -0
- ostruct/cli/commands/list_models.py +128 -0
- ostruct/cli/commands/quick_ref.py +50 -0
- ostruct/cli/commands/run.py +137 -0
- ostruct/cli/commands/update_registry.py +71 -0
- ostruct/cli/config.py +277 -0
- ostruct/cli/cost_estimation.py +134 -0
- ostruct/cli/errors.py +310 -6
- ostruct/cli/exit_codes.py +1 -0
- ostruct/cli/explicit_file_processor.py +548 -0
- ostruct/cli/field_utils.py +69 -0
- ostruct/cli/file_info.py +42 -9
- ostruct/cli/file_list.py +301 -102
- ostruct/cli/file_search.py +455 -0
- ostruct/cli/file_utils.py +47 -13
- ostruct/cli/mcp_integration.py +541 -0
- ostruct/cli/model_creation.py +150 -1
- ostruct/cli/model_validation.py +204 -0
- ostruct/cli/progress_reporting.py +398 -0
- ostruct/cli/registry_updates.py +14 -9
- ostruct/cli/runner.py +1418 -0
- ostruct/cli/schema_utils.py +113 -0
- ostruct/cli/services.py +626 -0
- ostruct/cli/template_debug.py +748 -0
- ostruct/cli/template_debug_help.py +162 -0
- ostruct/cli/template_env.py +15 -6
- ostruct/cli/template_filters.py +55 -3
- ostruct/cli/template_optimizer.py +474 -0
- ostruct/cli/template_processor.py +1080 -0
- ostruct/cli/template_rendering.py +69 -34
- ostruct/cli/token_validation.py +286 -0
- ostruct/cli/types.py +78 -0
- ostruct/cli/unattended_operation.py +269 -0
- ostruct/cli/validators.py +386 -3
- {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.0.dist-info}/LICENSE +2 -0
- ostruct_cli-0.8.0.dist-info/METADATA +633 -0
- ostruct_cli-0.8.0.dist-info/RECORD +69 -0
- {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.0.dist-info}/WHEEL +1 -1
- ostruct_cli-0.7.2.dist-info/METADATA +0 -370
- ostruct_cli-0.7.2.dist-info/RECORD +0 -45
- {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.0.dist-info}/entry_points.txt +0 -0
ostruct/cli/model_creation.py
CHANGED
@@ -26,6 +26,7 @@ from pydantic import (
|
|
26
26
|
ConfigDict,
|
27
27
|
EmailStr,
|
28
28
|
Field,
|
29
|
+
RootModel,
|
29
30
|
ValidationError,
|
30
31
|
create_model,
|
31
32
|
)
|
@@ -159,6 +160,96 @@ def _get_type_with_constraints(
|
|
159
160
|
|
160
161
|
field_type = field_schema.get("type")
|
161
162
|
|
163
|
+
# Handle union types (e.g., ["string", "null"])
|
164
|
+
if isinstance(field_type, list):
|
165
|
+
union_types: List[Type[Any]] = []
|
166
|
+
base_type: Type[Any] = str # Default base type for constraints
|
167
|
+
|
168
|
+
for type_name in field_type:
|
169
|
+
if type_name == "string":
|
170
|
+
base_type = str
|
171
|
+
union_types.append(str)
|
172
|
+
elif type_name == "integer":
|
173
|
+
base_type = int
|
174
|
+
union_types.append(int)
|
175
|
+
elif type_name == "number":
|
176
|
+
base_type = float
|
177
|
+
union_types.append(float)
|
178
|
+
elif type_name == "boolean":
|
179
|
+
base_type = bool
|
180
|
+
union_types.append(bool)
|
181
|
+
elif type_name == "null":
|
182
|
+
union_types.append(type(None))
|
183
|
+
elif type_name == "object":
|
184
|
+
# For object unions, we'd need more complex handling
|
185
|
+
base_type = dict
|
186
|
+
union_types.append(dict)
|
187
|
+
elif type_name == "array":
|
188
|
+
# For array unions, we'd need more complex handling
|
189
|
+
base_type = list
|
190
|
+
union_types.append(list)
|
191
|
+
|
192
|
+
if len(union_types) == 1:
|
193
|
+
# Single type, use it directly
|
194
|
+
field_type_cls: Any = union_types[0]
|
195
|
+
else:
|
196
|
+
# Create union type
|
197
|
+
field_type_cls = Union[tuple(union_types)]
|
198
|
+
|
199
|
+
# Apply constraints based on the base type (non-null type)
|
200
|
+
if base_type == str:
|
201
|
+
# Add string-specific constraints to field_kwargs
|
202
|
+
if "pattern" in field_schema:
|
203
|
+
field_kwargs["pattern"] = field_schema["pattern"]
|
204
|
+
if "minLength" in field_schema:
|
205
|
+
field_kwargs["min_length"] = field_schema["minLength"]
|
206
|
+
if "maxLength" in field_schema:
|
207
|
+
field_kwargs["max_length"] = field_schema["maxLength"]
|
208
|
+
|
209
|
+
# Handle special string formats
|
210
|
+
if "format" in field_schema:
|
211
|
+
if field_schema["format"] == "date-time":
|
212
|
+
# For union with null, we need Optional[datetime]
|
213
|
+
if type(None) in union_types:
|
214
|
+
field_type_cls = Union[datetime, type(None)]
|
215
|
+
else:
|
216
|
+
field_type_cls = datetime
|
217
|
+
elif field_schema["format"] == "date":
|
218
|
+
if type(None) in union_types:
|
219
|
+
field_type_cls = Union[date, type(None)]
|
220
|
+
else:
|
221
|
+
field_type_cls = date
|
222
|
+
elif field_schema["format"] == "time":
|
223
|
+
if type(None) in union_types:
|
224
|
+
field_type_cls = Union[time, type(None)]
|
225
|
+
else:
|
226
|
+
field_type_cls = time
|
227
|
+
elif field_schema["format"] == "email":
|
228
|
+
if type(None) in union_types:
|
229
|
+
field_type_cls = Union[EmailStr, type(None)]
|
230
|
+
else:
|
231
|
+
field_type_cls = EmailStr
|
232
|
+
elif field_schema["format"] == "uri":
|
233
|
+
if type(None) in union_types:
|
234
|
+
field_type_cls = Union[AnyUrl, type(None)]
|
235
|
+
else:
|
236
|
+
field_type_cls = AnyUrl
|
237
|
+
|
238
|
+
elif base_type in (int, float):
|
239
|
+
# Add number-specific constraints to field_kwargs
|
240
|
+
if "minimum" in field_schema:
|
241
|
+
field_kwargs["ge"] = field_schema["minimum"]
|
242
|
+
if "maximum" in field_schema:
|
243
|
+
field_kwargs["le"] = field_schema["maximum"]
|
244
|
+
if "exclusiveMinimum" in field_schema:
|
245
|
+
field_kwargs["gt"] = field_schema["exclusiveMinimum"]
|
246
|
+
if "exclusiveMaximum" in field_schema:
|
247
|
+
field_kwargs["lt"] = field_schema["exclusiveMaximum"]
|
248
|
+
if "multipleOf" in field_schema:
|
249
|
+
field_kwargs["multiple_of"] = field_schema["multipleOf"]
|
250
|
+
|
251
|
+
return (field_type_cls, Field(**field_kwargs))
|
252
|
+
|
162
253
|
# Handle array type
|
163
254
|
if field_type == "array":
|
164
255
|
items_schema = field_schema.get("items", {})
|
@@ -218,7 +309,7 @@ def _get_type_with_constraints(
|
|
218
309
|
|
219
310
|
# Handle other types
|
220
311
|
if field_type == "string":
|
221
|
-
field_type_cls
|
312
|
+
field_type_cls = str
|
222
313
|
|
223
314
|
# Add string-specific constraints to field_kwargs
|
224
315
|
if "pattern" in field_schema:
|
@@ -319,6 +410,64 @@ def create_dynamic_model(
|
|
319
410
|
|
320
411
|
validate_json_schema(schema)
|
321
412
|
|
413
|
+
# Handle top-level array schemas
|
414
|
+
if schema.get("type") == "array":
|
415
|
+
items_schema = schema.get("items", {})
|
416
|
+
if items_schema.get("type") == "object":
|
417
|
+
# Create the item model first
|
418
|
+
item_model = create_dynamic_model(
|
419
|
+
items_schema,
|
420
|
+
base_name=f"{base_name}Item",
|
421
|
+
show_schema=show_schema,
|
422
|
+
debug_validation=debug_validation,
|
423
|
+
)
|
424
|
+
|
425
|
+
# Return a RootModel that validates a list of the item model
|
426
|
+
class ArrayModel(RootModel[List[Any]]):
|
427
|
+
model_config = ConfigDict(
|
428
|
+
str_strip_whitespace=True,
|
429
|
+
validate_assignment=True,
|
430
|
+
use_enum_values=True,
|
431
|
+
)
|
432
|
+
|
433
|
+
def __init__(self, root: List[Any]) -> None:
|
434
|
+
# Validate each item against the item model
|
435
|
+
validated_items = []
|
436
|
+
for item in root:
|
437
|
+
if isinstance(item, dict):
|
438
|
+
validated_items.append(
|
439
|
+
item_model.model_validate(item)
|
440
|
+
)
|
441
|
+
else:
|
442
|
+
validated_items.append(
|
443
|
+
item_model.model_validate(item)
|
444
|
+
)
|
445
|
+
super().__init__(validated_items)
|
446
|
+
|
447
|
+
ArrayModel.__name__ = f"{base_name}List"
|
448
|
+
return ArrayModel
|
449
|
+
else:
|
450
|
+
# Handle array of primitives
|
451
|
+
item_type_map = {
|
452
|
+
"string": str,
|
453
|
+
"integer": int,
|
454
|
+
"number": float,
|
455
|
+
"boolean": bool,
|
456
|
+
}
|
457
|
+
# Get item type (not used in generic due to MyPy limitations)
|
458
|
+
item_type_map.get(items_schema.get("type", "string"), str)
|
459
|
+
|
460
|
+
# Use Any for the generic type to avoid MyPy issues with dynamic types
|
461
|
+
class PrimitiveArrayModel(RootModel[List[Any]]):
|
462
|
+
model_config = ConfigDict(
|
463
|
+
str_strip_whitespace=True,
|
464
|
+
validate_assignment=True,
|
465
|
+
use_enum_values=True,
|
466
|
+
)
|
467
|
+
|
468
|
+
PrimitiveArrayModel.__name__ = f"{base_name}List"
|
469
|
+
return PrimitiveArrayModel
|
470
|
+
|
322
471
|
# Process schema properties into fields
|
323
472
|
properties = schema.get("properties", {})
|
324
473
|
required = schema.get("required", [])
|
@@ -0,0 +1,204 @@
|
|
1
|
+
"""Model validation utilities for ostruct CLI."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple, Type
|
5
|
+
|
6
|
+
from openai_model_registry import (
|
7
|
+
ModelNotSupportedError,
|
8
|
+
ModelRegistry,
|
9
|
+
ParameterNotSupportedError,
|
10
|
+
ParameterValidationError,
|
11
|
+
)
|
12
|
+
from pydantic import BaseModel
|
13
|
+
|
14
|
+
from .errors import (
|
15
|
+
CLIError,
|
16
|
+
InvalidJSONError,
|
17
|
+
ModelCreationError,
|
18
|
+
SchemaFileError,
|
19
|
+
SchemaValidationError,
|
20
|
+
)
|
21
|
+
from .exit_codes import ExitCode
|
22
|
+
from .model_creation import create_dynamic_model
|
23
|
+
from .schema_utils import supports_structured_output
|
24
|
+
from .token_validation import validate_token_limits
|
25
|
+
from .types import CLIParams
|
26
|
+
|
27
|
+
logger = logging.getLogger(__name__)
|
28
|
+
|
29
|
+
|
30
|
+
def validate_model_parameters(model: str, params: Dict[str, Any]) -> None:
|
31
|
+
"""Validate model parameters against model capabilities.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
model: The model name to validate parameters for
|
35
|
+
params: Dictionary of parameter names and values to validate
|
36
|
+
|
37
|
+
Raises:
|
38
|
+
CLIError: If parameters are invalid for the model
|
39
|
+
"""
|
40
|
+
try:
|
41
|
+
registry = ModelRegistry.get_instance()
|
42
|
+
capabilities = registry.get_capabilities(model)
|
43
|
+
|
44
|
+
# Validate each parameter
|
45
|
+
for param_name, value in params.items():
|
46
|
+
try:
|
47
|
+
capabilities.validate_parameter(param_name, value)
|
48
|
+
except ParameterNotSupportedError as e:
|
49
|
+
raise CLIError(
|
50
|
+
f"Parameter '{param_name}' not supported for model '{model}': {e}",
|
51
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
52
|
+
) from e
|
53
|
+
except ParameterValidationError as e:
|
54
|
+
raise CLIError(
|
55
|
+
f"Invalid value for parameter '{param_name}' on model '{model}': {e}",
|
56
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
57
|
+
) from e
|
58
|
+
|
59
|
+
except ModelNotSupportedError as e:
|
60
|
+
raise CLIError(
|
61
|
+
f"Model '{model}' is not supported: {e}",
|
62
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
63
|
+
) from e
|
64
|
+
except Exception as e:
|
65
|
+
logger.warning(
|
66
|
+
f"Could not validate parameters for model '{model}': {e}"
|
67
|
+
)
|
68
|
+
# Don't fail for unexpected validation errors
|
69
|
+
|
70
|
+
|
71
|
+
async def validate_model_params(args: CLIParams) -> Dict[str, Any]:
|
72
|
+
"""Validate model parameters and return a dictionary of valid parameters.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
args: Command line arguments
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
Dictionary of validated model parameters
|
79
|
+
|
80
|
+
Raises:
|
81
|
+
CLIError: If model parameters are invalid
|
82
|
+
"""
|
83
|
+
params = {
|
84
|
+
"temperature": args.get("temperature"),
|
85
|
+
"max_output_tokens": args.get("max_output_tokens"),
|
86
|
+
"top_p": args.get("top_p"),
|
87
|
+
"frequency_penalty": args.get("frequency_penalty"),
|
88
|
+
"presence_penalty": args.get("presence_penalty"),
|
89
|
+
"reasoning_effort": args.get("reasoning_effort"),
|
90
|
+
}
|
91
|
+
# Remove None values
|
92
|
+
params = {k: v for k, v in params.items() if v is not None}
|
93
|
+
validate_model_parameters(args["model"], params)
|
94
|
+
return params
|
95
|
+
|
96
|
+
|
97
|
+
async def validate_model_and_schema(
|
98
|
+
args: CLIParams,
|
99
|
+
schema: Dict[str, Any],
|
100
|
+
system_prompt: str,
|
101
|
+
user_prompt: str,
|
102
|
+
template_context: Dict[str, Any],
|
103
|
+
) -> Tuple[
|
104
|
+
Type[BaseModel], List[Dict[str, str]], int, Optional[ModelRegistry]
|
105
|
+
]:
|
106
|
+
"""Validate model compatibility and schema, and check token limits.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
args: Command line arguments
|
110
|
+
schema: Schema dictionary
|
111
|
+
system_prompt: Processed system prompt
|
112
|
+
user_prompt: Processed user prompt
|
113
|
+
template_context: Template context with file information
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Tuple of (output_model, messages, total_tokens, registry)
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
CLIError: For validation errors
|
120
|
+
ModelCreationError: When model creation fails
|
121
|
+
SchemaValidationError: When schema is invalid
|
122
|
+
PromptTooLargeError: When prompt exceeds context window with actionable guidance
|
123
|
+
"""
|
124
|
+
logger.debug("=== Model & Schema Validation Phase ===")
|
125
|
+
try:
|
126
|
+
output_model = create_dynamic_model(
|
127
|
+
schema,
|
128
|
+
show_schema=args.get("show_model_schema", False),
|
129
|
+
debug_validation=args.get("debug_validation", False),
|
130
|
+
)
|
131
|
+
logger.debug("Successfully created output model")
|
132
|
+
except (
|
133
|
+
SchemaFileError,
|
134
|
+
InvalidJSONError,
|
135
|
+
SchemaValidationError,
|
136
|
+
ModelCreationError,
|
137
|
+
) as e:
|
138
|
+
logger.error("Schema error: %s", str(e))
|
139
|
+
# Pass through the error without additional wrapping
|
140
|
+
raise
|
141
|
+
|
142
|
+
if not supports_structured_output(args["model"]):
|
143
|
+
msg = f"Model {args['model']} does not support structured output"
|
144
|
+
logger.error(msg)
|
145
|
+
raise ModelNotSupportedError(msg)
|
146
|
+
|
147
|
+
messages = [
|
148
|
+
{"role": "system", "content": system_prompt},
|
149
|
+
{"role": "user", "content": user_prompt},
|
150
|
+
]
|
151
|
+
|
152
|
+
# Token validation - extract file paths from FileInfo objects
|
153
|
+
files = template_context.get("files", [])
|
154
|
+
file_paths = [str(f.path) if hasattr(f, "path") else str(f) for f in files]
|
155
|
+
combined_template_content = system_prompt + user_prompt
|
156
|
+
validate_token_limits(combined_template_content, file_paths, args["model"])
|
157
|
+
|
158
|
+
# For now, simplified token counting - the full implementation needs more imports
|
159
|
+
total_tokens = len(system_prompt) + len(user_prompt) # Rough estimate
|
160
|
+
registry = ModelRegistry.get_instance()
|
161
|
+
|
162
|
+
return output_model, messages, total_tokens, registry
|
163
|
+
|
164
|
+
|
165
|
+
def supports_web_search(model: str) -> bool:
|
166
|
+
"""Check if model supports web search capabilities.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
model: The model name to check
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
True if the model supports web search, False otherwise
|
173
|
+
"""
|
174
|
+
try:
|
175
|
+
registry = ModelRegistry.get_instance()
|
176
|
+
capabilities = registry.get_capabilities(model)
|
177
|
+
return getattr(capabilities, "supports_web_search", False)
|
178
|
+
except Exception:
|
179
|
+
# Default to False for safety if we can't determine support
|
180
|
+
return False
|
181
|
+
|
182
|
+
|
183
|
+
def validate_web_search_compatibility(
|
184
|
+
model: str, web_search_enabled: bool
|
185
|
+
) -> Optional[str]:
|
186
|
+
"""Validate web search compatibility and return warning message if needed.
|
187
|
+
|
188
|
+
Args:
|
189
|
+
model: The model name to validate
|
190
|
+
web_search_enabled: Whether web search is enabled
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
Warning message if there's a compatibility issue, None otherwise
|
194
|
+
"""
|
195
|
+
if not web_search_enabled:
|
196
|
+
return None
|
197
|
+
|
198
|
+
if not supports_web_search(model):
|
199
|
+
return (
|
200
|
+
f"Model '{model}' does not support web search capabilities. "
|
201
|
+
f"Consider using a compatible model like 'gpt-4o', 'gpt-4.1', or an O-series model."
|
202
|
+
)
|
203
|
+
|
204
|
+
return None
|