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/errors.py
CHANGED
@@ -4,6 +4,8 @@ import json
|
|
4
4
|
import logging
|
5
5
|
from typing import Any, Dict, List, Optional
|
6
6
|
|
7
|
+
from openai import OpenAIError
|
8
|
+
|
7
9
|
from .base_errors import CLIError, OstructFileNotFoundError
|
8
10
|
from .exit_codes import ExitCode
|
9
11
|
from .security.base import SecurityErrorBase
|
@@ -30,6 +32,27 @@ class VariableValueError(VariableError):
|
|
30
32
|
pass
|
31
33
|
|
32
34
|
|
35
|
+
class DuplicateFileMappingError(VariableError):
|
36
|
+
"""Raised when duplicate file mappings are detected."""
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
message: str,
|
41
|
+
context: Optional[Dict[str, Any]] = None,
|
42
|
+
) -> None:
|
43
|
+
"""Initialize error.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
message: Error message
|
47
|
+
context: Additional error context
|
48
|
+
"""
|
49
|
+
super().__init__(
|
50
|
+
message,
|
51
|
+
context=context,
|
52
|
+
exit_code=ExitCode.USAGE_ERROR,
|
53
|
+
)
|
54
|
+
|
55
|
+
|
33
56
|
class InvalidJSONError(CLIError):
|
34
57
|
"""Error raised when JSON is invalid."""
|
35
58
|
|
@@ -426,12 +449,89 @@ class InvalidResponseFormatError(CLIError):
|
|
426
449
|
)
|
427
450
|
|
428
451
|
|
429
|
-
|
430
|
-
"""Exception raised when there's an error with the OpenAI client.
|
452
|
+
# Tool-specific error classes (T3.1)
|
431
453
|
|
432
|
-
|
433
|
-
|
434
|
-
"""
|
454
|
+
|
455
|
+
class FileSearchError(CLIError):
|
456
|
+
"""File Search tool failures with retry guidance."""
|
457
|
+
|
458
|
+
def __init__(
|
459
|
+
self,
|
460
|
+
message: str,
|
461
|
+
exit_code: ExitCode = ExitCode.API_ERROR,
|
462
|
+
context: Optional[Dict[str, Any]] = None,
|
463
|
+
):
|
464
|
+
super().__init__(message, exit_code=exit_code, context=context)
|
465
|
+
|
466
|
+
|
467
|
+
class FileSearchUploadError(FileSearchError):
|
468
|
+
"""File upload to vector store failed."""
|
469
|
+
|
470
|
+
pass
|
471
|
+
|
472
|
+
|
473
|
+
class MCPConnectionError(CLIError):
|
474
|
+
"""MCP server connection failures."""
|
475
|
+
|
476
|
+
def __init__(
|
477
|
+
self,
|
478
|
+
message: str,
|
479
|
+
exit_code: ExitCode = ExitCode.API_ERROR,
|
480
|
+
context: Optional[Dict[str, Any]] = None,
|
481
|
+
):
|
482
|
+
super().__init__(message, exit_code=exit_code, context=context)
|
483
|
+
|
484
|
+
|
485
|
+
class ContainerExpiredError(CLIError):
|
486
|
+
"""Code Interpreter container expired (20-minute limit)."""
|
487
|
+
|
488
|
+
def __init__(
|
489
|
+
self,
|
490
|
+
message: str,
|
491
|
+
exit_code: ExitCode = ExitCode.API_ERROR,
|
492
|
+
context: Optional[Dict[str, Any]] = None,
|
493
|
+
):
|
494
|
+
super().__init__(message, exit_code=exit_code, context=context)
|
495
|
+
|
496
|
+
|
497
|
+
class UnattendedOperationTimeoutError(CLIError):
|
498
|
+
"""Operation timed out during unattended execution."""
|
499
|
+
|
500
|
+
def __init__(
|
501
|
+
self,
|
502
|
+
message: str,
|
503
|
+
exit_code: ExitCode = ExitCode.OPERATION_TIMEOUT,
|
504
|
+
context: Optional[Dict[str, Any]] = None,
|
505
|
+
):
|
506
|
+
super().__init__(message, exit_code=exit_code, context=context)
|
507
|
+
|
508
|
+
|
509
|
+
class PromptTooLargeError(CLIError):
|
510
|
+
"""Prompt exceeds context window limits."""
|
511
|
+
|
512
|
+
def __init__(
|
513
|
+
self,
|
514
|
+
message: str,
|
515
|
+
exit_code: ExitCode = ExitCode.VALIDATION_ERROR,
|
516
|
+
context: Optional[Dict[str, Any]] = None,
|
517
|
+
):
|
518
|
+
super().__init__(message, exit_code=exit_code, context=context)
|
519
|
+
|
520
|
+
|
521
|
+
class AuthenticationError(CLIError):
|
522
|
+
"""API authentication failures."""
|
523
|
+
|
524
|
+
def __init__(
|
525
|
+
self,
|
526
|
+
message: str,
|
527
|
+
exit_code: ExitCode = ExitCode.API_ERROR,
|
528
|
+
context: Optional[Dict[str, Any]] = None,
|
529
|
+
):
|
530
|
+
super().__init__(message, exit_code=exit_code, context=context)
|
531
|
+
|
532
|
+
|
533
|
+
class RateLimitError(CLIError):
|
534
|
+
"""API rate limiting errors."""
|
435
535
|
|
436
536
|
def __init__(
|
437
537
|
self,
|
@@ -442,6 +542,133 @@ class OpenAIClientError(CLIError):
|
|
442
542
|
super().__init__(message, exit_code=exit_code, context=context)
|
443
543
|
|
444
544
|
|
545
|
+
class APIError(CLIError):
|
546
|
+
"""Generic API errors."""
|
547
|
+
|
548
|
+
def __init__(
|
549
|
+
self,
|
550
|
+
message: str,
|
551
|
+
exit_code: ExitCode = ExitCode.API_ERROR,
|
552
|
+
context: Optional[Dict[str, Any]] = None,
|
553
|
+
):
|
554
|
+
super().__init__(message, exit_code=exit_code, context=context)
|
555
|
+
|
556
|
+
|
557
|
+
# API Error Mapping (T3.1)
|
558
|
+
|
559
|
+
|
560
|
+
class APIErrorMapper:
|
561
|
+
"""Maps OpenAI SDK errors to ostruct-specific errors with actionable guidance."""
|
562
|
+
|
563
|
+
@staticmethod
|
564
|
+
def map_openai_error(error: OpenAIError) -> CLIError:
|
565
|
+
"""Map OpenAI SDK errors to ostruct errors (validated patterns).
|
566
|
+
|
567
|
+
Args:
|
568
|
+
error: OpenAI SDK error to map
|
569
|
+
|
570
|
+
Returns:
|
571
|
+
Appropriate ostruct error with actionable guidance
|
572
|
+
"""
|
573
|
+
error_msg = str(error).lower()
|
574
|
+
|
575
|
+
# Context window errors (confirmed pattern)
|
576
|
+
if (
|
577
|
+
"context_length_exceeded" in error_msg
|
578
|
+
or "maximum context length" in error_msg
|
579
|
+
):
|
580
|
+
return PromptTooLargeError(
|
581
|
+
f"Prompt exceeds model context window (128,000 token limit). "
|
582
|
+
f"Tip: Use explicit file routing (-fc for code, -fs for docs, -ft for config). "
|
583
|
+
f"Original error: {error}"
|
584
|
+
)
|
585
|
+
|
586
|
+
# Authentication errors (confirmed pattern)
|
587
|
+
if "invalid_api_key" in error_msg or "incorrect api key" in error_msg:
|
588
|
+
return AuthenticationError(
|
589
|
+
f"Invalid OpenAI API key. Please check your OPENAI_API_KEY environment variable. "
|
590
|
+
f"Original error: {error}"
|
591
|
+
)
|
592
|
+
|
593
|
+
# Rate limiting (standard pattern)
|
594
|
+
if "rate_limit" in error_msg:
|
595
|
+
return RateLimitError(
|
596
|
+
f"OpenAI API rate limit exceeded. Please wait and try again. "
|
597
|
+
f"Original error: {error}"
|
598
|
+
)
|
599
|
+
|
600
|
+
# Schema validation errors (Responses API specific)
|
601
|
+
if "invalid schema for response_format" in error_msg:
|
602
|
+
return SchemaValidationError(
|
603
|
+
f"Schema validation failed for Responses API. "
|
604
|
+
f"Ensure your schema is compatible with strict mode. "
|
605
|
+
f"Original error: {error}"
|
606
|
+
)
|
607
|
+
|
608
|
+
# Container expiration errors (Code Interpreter specific)
|
609
|
+
if "container" in error_msg and (
|
610
|
+
"expired" in error_msg or "timeout" in error_msg
|
611
|
+
):
|
612
|
+
return ContainerExpiredError(
|
613
|
+
f"Code Interpreter container expired (20-minute runtime limit, 2-minute idle timeout). "
|
614
|
+
f"Please retry your request. Original error: {error}"
|
615
|
+
)
|
616
|
+
|
617
|
+
# File Search errors
|
618
|
+
if "vector_store" in error_msg or "file_search" in error_msg:
|
619
|
+
return FileSearchError(
|
620
|
+
f"File Search operation failed. This can be intermittent - consider retrying. "
|
621
|
+
f"Original error: {error}"
|
622
|
+
)
|
623
|
+
|
624
|
+
# MCP connection errors
|
625
|
+
if "mcp" in error_msg or "model context protocol" in error_msg:
|
626
|
+
return MCPConnectionError(
|
627
|
+
f"MCP server connection failed. Check server URL and network connectivity. "
|
628
|
+
f"Original error: {error}"
|
629
|
+
)
|
630
|
+
|
631
|
+
# Generic API error
|
632
|
+
return APIError(f"OpenAI API error: {error}")
|
633
|
+
|
634
|
+
@staticmethod
|
635
|
+
def map_tool_error(tool_name: str, error: Exception) -> CLIError:
|
636
|
+
"""Map tool-specific errors to ostruct errors.
|
637
|
+
|
638
|
+
Args:
|
639
|
+
tool_name: Name of the tool that failed
|
640
|
+
error: The original error
|
641
|
+
|
642
|
+
Returns:
|
643
|
+
Appropriate ostruct error with tool-specific guidance
|
644
|
+
"""
|
645
|
+
error_msg = str(error).lower()
|
646
|
+
|
647
|
+
if tool_name == "file-search":
|
648
|
+
if "upload" in error_msg or "vector_store" in error_msg:
|
649
|
+
return FileSearchUploadError(
|
650
|
+
f"File Search upload failed: {error}. "
|
651
|
+
f"This can be intermittent - retry with --file-search-retry-count option."
|
652
|
+
)
|
653
|
+
return FileSearchError(f"File Search error: {error}")
|
654
|
+
|
655
|
+
elif tool_name == "code-interpreter":
|
656
|
+
if "container" in error_msg:
|
657
|
+
return ContainerExpiredError(
|
658
|
+
f"Code Interpreter container error: {error}. "
|
659
|
+
f"Container has 20-minute runtime and 2-minute idle limits."
|
660
|
+
)
|
661
|
+
return APIError(f"Code Interpreter error: {error}")
|
662
|
+
|
663
|
+
elif tool_name == "mcp":
|
664
|
+
return MCPConnectionError(
|
665
|
+
f"MCP server error: {error}. "
|
666
|
+
f"Check server connectivity and require_approval='never' setting."
|
667
|
+
)
|
668
|
+
|
669
|
+
return APIError(f"{tool_name} error: {error}")
|
670
|
+
|
671
|
+
|
445
672
|
class SchemaValidationError(ModelCreationError):
|
446
673
|
"""Raised when schema validation fails."""
|
447
674
|
|
@@ -503,9 +730,86 @@ class SchemaValidationError(ModelCreationError):
|
|
503
730
|
super().__init__(final_message, context=context, exit_code=exit_code)
|
504
731
|
|
505
732
|
|
733
|
+
def handle_error(e: Exception) -> None:
|
734
|
+
"""Handle CLI errors and display appropriate messages.
|
735
|
+
|
736
|
+
Maintains specific error type handling while reducing duplication.
|
737
|
+
Provides enhanced debug logging for CLI errors.
|
738
|
+
"""
|
739
|
+
import sys
|
740
|
+
|
741
|
+
import click
|
742
|
+
|
743
|
+
# 1. Determine error type and message
|
744
|
+
if isinstance(e, SchemaValidationError):
|
745
|
+
msg = str(e) # Already formatted in SchemaValidationError
|
746
|
+
exit_code = e.exit_code
|
747
|
+
elif isinstance(e, ModelCreationError):
|
748
|
+
# Unwrap ModelCreationError that might wrap SchemaValidationError
|
749
|
+
if isinstance(e.__cause__, SchemaValidationError):
|
750
|
+
return handle_error(e.__cause__)
|
751
|
+
msg = f"Model creation error: {str(e)}"
|
752
|
+
exit_code = ExitCode.SCHEMA_ERROR
|
753
|
+
elif isinstance(e, click.UsageError):
|
754
|
+
msg = f"Usage error: {str(e)}"
|
755
|
+
exit_code = ExitCode.USAGE_ERROR
|
756
|
+
elif isinstance(e, SchemaFileError):
|
757
|
+
msg = str(e) # Use existing __str__ formatting
|
758
|
+
exit_code = ExitCode.SCHEMA_ERROR
|
759
|
+
elif isinstance(e, (InvalidJSONError, json.JSONDecodeError)):
|
760
|
+
msg = f"Invalid JSON error: {str(e)}"
|
761
|
+
exit_code = ExitCode.DATA_ERROR
|
762
|
+
elif isinstance(e, CLIError):
|
763
|
+
msg = str(e) # Use existing __str__ formatting
|
764
|
+
exit_code = ExitCode(e.exit_code) # Convert int to ExitCode
|
765
|
+
else:
|
766
|
+
msg = f"Unexpected error: {str(e)}"
|
767
|
+
exit_code = ExitCode.INTERNAL_ERROR
|
768
|
+
|
769
|
+
# 2. Debug logging
|
770
|
+
if isinstance(e, CLIError) and logger.isEnabledFor(logging.DEBUG):
|
771
|
+
# Format context fields with lowercase keys and simple values
|
772
|
+
context_str = ""
|
773
|
+
if hasattr(e, "context") and e.context:
|
774
|
+
for key, value in sorted(e.context.items()):
|
775
|
+
if key not in {
|
776
|
+
"timestamp",
|
777
|
+
"host",
|
778
|
+
"version",
|
779
|
+
"python_version",
|
780
|
+
}:
|
781
|
+
if isinstance(value, dict):
|
782
|
+
context_str += (
|
783
|
+
f"{key.lower()}:\n{json.dumps(value, indent=2)}\n"
|
784
|
+
)
|
785
|
+
else:
|
786
|
+
context_str += f"{key.lower()}: {value}\n"
|
787
|
+
|
788
|
+
logger.debug(
|
789
|
+
f"Error details:\nType: {type(e).__name__}\n{context_str.rstrip()}"
|
790
|
+
)
|
791
|
+
elif not isinstance(
|
792
|
+
e,
|
793
|
+
(
|
794
|
+
click.UsageError,
|
795
|
+
DuplicateFileMappingError,
|
796
|
+
VariableNameError,
|
797
|
+
VariableValueError,
|
798
|
+
),
|
799
|
+
):
|
800
|
+
logger.error(msg, exc_info=True)
|
801
|
+
|
802
|
+
# 3. User output
|
803
|
+
click.secho(msg, fg="red", err=True)
|
804
|
+
sys.exit(exit_code)
|
805
|
+
|
806
|
+
|
506
807
|
# Export public API
|
507
808
|
__all__ = [
|
508
809
|
"VariableError",
|
810
|
+
"VariableNameError",
|
811
|
+
"VariableValueError",
|
812
|
+
"DuplicateFileMappingError",
|
509
813
|
"PathError",
|
510
814
|
"PathSecurityError",
|
511
815
|
"OstructFileNotFoundError",
|
@@ -522,5 +826,5 @@ __all__ = [
|
|
522
826
|
"APIResponseError",
|
523
827
|
"EmptyResponseError",
|
524
828
|
"InvalidResponseFormatError",
|
525
|
-
"
|
829
|
+
"handle_error",
|
526
830
|
]
|
ostruct/cli/exit_codes.py
CHANGED