ostruct-cli 0.7.2__py3-none-any.whl → 0.8.1__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.
Files changed (46) hide show
  1. ostruct/cli/__init__.py +21 -3
  2. ostruct/cli/base_errors.py +1 -1
  3. ostruct/cli/cli.py +66 -1983
  4. ostruct/cli/click_options.py +460 -28
  5. ostruct/cli/code_interpreter.py +238 -0
  6. ostruct/cli/commands/__init__.py +32 -0
  7. ostruct/cli/commands/list_models.py +128 -0
  8. ostruct/cli/commands/quick_ref.py +54 -0
  9. ostruct/cli/commands/run.py +137 -0
  10. ostruct/cli/commands/update_registry.py +71 -0
  11. ostruct/cli/config.py +277 -0
  12. ostruct/cli/cost_estimation.py +134 -0
  13. ostruct/cli/errors.py +310 -6
  14. ostruct/cli/exit_codes.py +1 -0
  15. ostruct/cli/explicit_file_processor.py +548 -0
  16. ostruct/cli/field_utils.py +69 -0
  17. ostruct/cli/file_info.py +42 -9
  18. ostruct/cli/file_list.py +301 -102
  19. ostruct/cli/file_search.py +455 -0
  20. ostruct/cli/file_utils.py +47 -13
  21. ostruct/cli/mcp_integration.py +541 -0
  22. ostruct/cli/model_creation.py +150 -1
  23. ostruct/cli/model_validation.py +204 -0
  24. ostruct/cli/progress_reporting.py +398 -0
  25. ostruct/cli/registry_updates.py +14 -9
  26. ostruct/cli/runner.py +1418 -0
  27. ostruct/cli/schema_utils.py +113 -0
  28. ostruct/cli/services.py +626 -0
  29. ostruct/cli/template_debug.py +748 -0
  30. ostruct/cli/template_debug_help.py +162 -0
  31. ostruct/cli/template_env.py +15 -6
  32. ostruct/cli/template_filters.py +55 -3
  33. ostruct/cli/template_optimizer.py +474 -0
  34. ostruct/cli/template_processor.py +1080 -0
  35. ostruct/cli/template_rendering.py +69 -34
  36. ostruct/cli/token_validation.py +286 -0
  37. ostruct/cli/types.py +78 -0
  38. ostruct/cli/unattended_operation.py +269 -0
  39. ostruct/cli/validators.py +386 -3
  40. {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.1.dist-info}/LICENSE +2 -0
  41. ostruct_cli-0.8.1.dist-info/METADATA +638 -0
  42. ostruct_cli-0.8.1.dist-info/RECORD +69 -0
  43. {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.1.dist-info}/WHEEL +1 -1
  44. ostruct_cli-0.7.2.dist-info/METADATA +0 -370
  45. ostruct_cli-0.7.2.dist-info/RECORD +0 -45
  46. {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.1.dist-info}/entry_points.txt +0 -0
@@ -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: Type[Any] = str
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