ccproxy-api 0.1.5__py3-none-any.whl → 0.1.7__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 (42) hide show
  1. ccproxy/_version.py +2 -2
  2. ccproxy/adapters/codex/__init__.py +11 -0
  3. ccproxy/adapters/openai/models.py +1 -1
  4. ccproxy/adapters/openai/response_adapter.py +355 -0
  5. ccproxy/adapters/openai/response_models.py +178 -0
  6. ccproxy/api/app.py +31 -3
  7. ccproxy/api/dependencies.py +1 -8
  8. ccproxy/api/middleware/errors.py +15 -7
  9. ccproxy/api/routes/codex.py +1251 -0
  10. ccproxy/api/routes/health.py +228 -3
  11. ccproxy/auth/openai/__init__.py +13 -0
  12. ccproxy/auth/openai/credentials.py +166 -0
  13. ccproxy/auth/openai/oauth_client.py +334 -0
  14. ccproxy/auth/openai/storage.py +184 -0
  15. ccproxy/claude_sdk/options.py +1 -1
  16. ccproxy/cli/commands/auth.py +398 -1
  17. ccproxy/cli/commands/serve.py +3 -1
  18. ccproxy/config/claude.py +1 -1
  19. ccproxy/config/codex.py +100 -0
  20. ccproxy/config/scheduler.py +8 -8
  21. ccproxy/config/settings.py +19 -0
  22. ccproxy/core/codex_transformers.py +389 -0
  23. ccproxy/core/http_transformers.py +153 -2
  24. ccproxy/data/claude_headers_fallback.json +37 -0
  25. ccproxy/data/codex_headers_fallback.json +14 -0
  26. ccproxy/models/detection.py +82 -0
  27. ccproxy/models/requests.py +22 -0
  28. ccproxy/models/responses.py +16 -0
  29. ccproxy/scheduler/manager.py +2 -2
  30. ccproxy/scheduler/tasks.py +105 -65
  31. ccproxy/services/claude_detection_service.py +7 -33
  32. ccproxy/services/codex_detection_service.py +252 -0
  33. ccproxy/services/proxy_service.py +530 -0
  34. ccproxy/utils/model_mapping.py +7 -5
  35. ccproxy/utils/startup_helpers.py +205 -12
  36. ccproxy/utils/version_checker.py +6 -0
  37. ccproxy_api-0.1.7.dist-info/METADATA +615 -0
  38. {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/RECORD +41 -28
  39. ccproxy_api-0.1.5.dist-info/METADATA +0 -396
  40. {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/WHEEL +0 -0
  41. {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/entry_points.txt +0 -0
  42. {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/licenses/LICENSE +0 -0
@@ -3,7 +3,12 @@
3
3
  import asyncio
4
4
  from datetime import UTC, datetime
5
5
  from pathlib import Path
6
- from typing import Annotated
6
+ from typing import TYPE_CHECKING, Annotated
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from ccproxy.auth.openai import OpenAIOAuthClient, OpenAITokenManager
11
+ from ccproxy.config.codex import CodexSettings
7
12
 
8
13
  import typer
9
14
  from rich import box
@@ -563,5 +568,397 @@ def renew(
563
568
  raise typer.Exit(1) from e
564
569
 
565
570
 
571
+ # OpenAI Codex Authentication Commands
572
+
573
+
574
+ def get_openai_token_manager() -> "OpenAITokenManager":
575
+ """Get OpenAI token manager dependency."""
576
+ from ccproxy.auth.openai import OpenAITokenManager
577
+
578
+ return OpenAITokenManager()
579
+
580
+
581
+ def get_openai_oauth_client(settings: "CodexSettings") -> "OpenAIOAuthClient":
582
+ """Get OpenAI OAuth client dependency."""
583
+ from ccproxy.auth.openai import OpenAIOAuthClient
584
+
585
+ token_manager = get_openai_token_manager()
586
+ return OpenAIOAuthClient(settings, token_manager)
587
+
588
+
589
+ @app.command(name="login-openai")
590
+ def login_openai_command(
591
+ no_browser: Annotated[
592
+ bool,
593
+ typer.Option(
594
+ "--no-browser",
595
+ help="Don't automatically open browser for authentication",
596
+ ),
597
+ ] = False,
598
+ ) -> None:
599
+ """Login to OpenAI using OAuth authentication.
600
+
601
+ This command will start a local callback server and open your web browser
602
+ to authenticate with OpenAI. The credentials will be saved to ~/.codex/auth.json.
603
+
604
+ Examples:
605
+ ccproxy auth login-openai
606
+ ccproxy auth login-openai --no-browser
607
+ """
608
+ import asyncio
609
+
610
+ from ccproxy.config.codex import CodexSettings
611
+
612
+ toolkit = get_rich_toolkit()
613
+ toolkit.print("[bold cyan]OpenAI OAuth Login[/bold cyan]", centered=True)
614
+ toolkit.print_line()
615
+
616
+ try:
617
+ # Get Codex settings
618
+ settings = CodexSettings()
619
+
620
+ # Check if already logged in
621
+ token_manager = get_openai_token_manager()
622
+ existing_creds = asyncio.run(token_manager.load_credentials())
623
+
624
+ if existing_creds and not existing_creds.is_expired():
625
+ console.print(
626
+ "[yellow]You are already logged in with valid OpenAI credentials.[/yellow]"
627
+ )
628
+ console.print(
629
+ "Use [cyan]ccproxy auth openai-info[/cyan] to view current credentials."
630
+ )
631
+
632
+ overwrite = typer.confirm(
633
+ "Do you want to login again and overwrite existing credentials?"
634
+ )
635
+ if not overwrite:
636
+ console.print("Login cancelled.")
637
+ return
638
+
639
+ # Create OAuth client and perform login
640
+ oauth_client = get_openai_oauth_client(settings)
641
+
642
+ console.print("Starting OpenAI OAuth login process...")
643
+ console.print(
644
+ "A temporary server will start on port 1455 for the OAuth callback..."
645
+ )
646
+
647
+ if no_browser:
648
+ console.print("Browser will NOT be opened automatically.")
649
+ else:
650
+ console.print("Your browser will open for authentication.")
651
+
652
+ try:
653
+ credentials = asyncio.run(
654
+ oauth_client.authenticate(open_browser=not no_browser)
655
+ )
656
+
657
+ toolkit.print("Successfully logged in to OpenAI!", tag="success")
658
+
659
+ # Show credential info
660
+ console.print("\n[dim]Credential information:[/dim]")
661
+ console.print(f" Account ID: {credentials.account_id}")
662
+ console.print(
663
+ f" Expires: {credentials.expires_at.strftime('%Y-%m-%d %H:%M:%S UTC')}"
664
+ )
665
+ console.print(f" Active: {'Yes' if credentials.active else 'No'}")
666
+
667
+ except Exception as e:
668
+ logger.error(f"OpenAI login failed: {e}")
669
+ toolkit.print(f"Login failed: {e}", tag="error")
670
+ raise typer.Exit(1) from e
671
+
672
+ except KeyboardInterrupt:
673
+ console.print("\n[yellow]Login cancelled by user.[/yellow]")
674
+ raise typer.Exit(1) from None
675
+ except Exception as e:
676
+ toolkit.print(f"Error during OpenAI login: {e}", tag="error")
677
+ raise typer.Exit(1) from e
678
+
679
+
680
+ @app.command(name="logout-openai")
681
+ def logout_openai_command() -> None:
682
+ """Logout from OpenAI and remove saved credentials.
683
+
684
+ This command will remove the OpenAI credentials file (~/.codex/auth.json)
685
+ and invalidate the current session.
686
+
687
+ Examples:
688
+ ccproxy auth logout-openai
689
+ """
690
+ import asyncio
691
+
692
+ toolkit = get_rich_toolkit()
693
+ toolkit.print("[bold cyan]OpenAI Logout[/bold cyan]", centered=True)
694
+ toolkit.print_line()
695
+
696
+ try:
697
+ token_manager = get_openai_token_manager()
698
+
699
+ # Check if credentials exist
700
+ existing_creds = asyncio.run(token_manager.load_credentials())
701
+ if not existing_creds:
702
+ console.print(
703
+ "[yellow]No OpenAI credentials found. Already logged out.[/yellow]"
704
+ )
705
+ return
706
+
707
+ # Confirm logout
708
+ confirm = typer.confirm(
709
+ "Are you sure you want to logout and remove OpenAI credentials?"
710
+ )
711
+ if not confirm:
712
+ console.print("Logout cancelled.")
713
+ return
714
+
715
+ # Delete credentials
716
+ success = asyncio.run(token_manager.delete_credentials())
717
+
718
+ if success:
719
+ toolkit.print("Successfully logged out from OpenAI!", tag="success")
720
+ console.print("OpenAI credentials have been removed.")
721
+ else:
722
+ toolkit.print("Failed to remove OpenAI credentials", tag="error")
723
+ raise typer.Exit(1)
724
+
725
+ except Exception as e:
726
+ toolkit.print(f"Error during OpenAI logout: {e}", tag="error")
727
+ raise typer.Exit(1) from e
728
+
729
+
730
+ @app.command(name="openai-info")
731
+ def openai_info_command() -> None:
732
+ """Display OpenAI credential information.
733
+
734
+ Shows detailed information about the current OpenAI credentials including
735
+ account ID, token expiration, and storage location.
736
+
737
+ Examples:
738
+ ccproxy auth openai-info
739
+ """
740
+ import asyncio
741
+ import base64
742
+ import json
743
+ from datetime import UTC, datetime
744
+
745
+ from rich import box
746
+ from rich.table import Table
747
+
748
+ toolkit = get_rich_toolkit()
749
+ toolkit.print("[bold cyan]OpenAI Credential Information[/bold cyan]", centered=True)
750
+ toolkit.print_line()
751
+
752
+ try:
753
+ token_manager = get_openai_token_manager()
754
+ credentials = asyncio.run(token_manager.load_credentials())
755
+
756
+ if not credentials:
757
+ toolkit.print("No OpenAI credentials found", tag="error")
758
+ console.print("\n[dim]Expected location:[/dim]")
759
+ storage_location = token_manager.storage.get_location()
760
+ console.print(f" - {storage_location}")
761
+ console.print("\n[dim]To login:[/dim]")
762
+ console.print(" ccproxy auth login-openai")
763
+ raise typer.Exit(1)
764
+
765
+ # Decode JWT token to extract additional information
766
+ jwt_payload = {}
767
+ jwt_header = {}
768
+ if credentials.access_token:
769
+ try:
770
+ # Split JWT into parts
771
+ parts = credentials.access_token.split(".")
772
+ if len(parts) == 3:
773
+ # Decode header and payload (add padding if needed)
774
+ header_b64 = parts[0] + "=" * (4 - len(parts[0]) % 4)
775
+ payload_b64 = parts[1] + "=" * (4 - len(parts[1]) % 4)
776
+
777
+ jwt_header = json.loads(base64.urlsafe_b64decode(header_b64))
778
+ jwt_payload = json.loads(base64.urlsafe_b64decode(payload_b64))
779
+ except Exception as decode_error:
780
+ logger.debug(f"Failed to decode JWT token: {decode_error}")
781
+
782
+ # Display account section
783
+ console.print("\n[bold]OpenAI Account[/bold]")
784
+ console.print(f" L Account ID: {credentials.account_id}")
785
+ console.print(f" L Status: {'Active' if credentials.active else 'Inactive'}")
786
+
787
+ # Extract additional info from JWT payload
788
+ if jwt_payload:
789
+ # Get OpenAI auth info from the JWT
790
+ openai_auth = jwt_payload.get("https://api.openai.com/auth", {})
791
+ if openai_auth:
792
+ if "email" in jwt_payload:
793
+ console.print(f" L Email: {jwt_payload['email']}")
794
+ if jwt_payload.get("email_verified"):
795
+ console.print(" L Email Verified: Yes")
796
+
797
+ if openai_auth.get("chatgpt_plan_type"):
798
+ console.print(
799
+ f" L Plan Type: {openai_auth['chatgpt_plan_type'].upper()}"
800
+ )
801
+
802
+ if openai_auth.get("chatgpt_user_id"):
803
+ console.print(f" L User ID: {openai_auth['chatgpt_user_id']}")
804
+
805
+ # Subscription info
806
+ if openai_auth.get("chatgpt_subscription_active_start"):
807
+ console.print(
808
+ f" L Subscription Start: {openai_auth['chatgpt_subscription_active_start']}"
809
+ )
810
+ if openai_auth.get("chatgpt_subscription_active_until"):
811
+ console.print(
812
+ f" L Subscription Until: {openai_auth['chatgpt_subscription_active_until']}"
813
+ )
814
+
815
+ # Organizations
816
+ orgs = openai_auth.get("organizations", [])
817
+ if orgs:
818
+ for org in orgs:
819
+ if org.get("is_default"):
820
+ console.print(
821
+ f" L Organization: {org.get('title', 'Unknown')} ({org.get('role', 'member')})"
822
+ )
823
+ console.print(f" L Org ID: {org.get('id', 'Unknown')}")
824
+
825
+ # Create details table
826
+ console.print()
827
+ table = Table(
828
+ show_header=True,
829
+ header_style="bold cyan",
830
+ box=box.ROUNDED,
831
+ title="Token Details",
832
+ title_style="bold white",
833
+ )
834
+ table.add_column("Property", style="cyan")
835
+ table.add_column("Value", style="white")
836
+
837
+ # File location
838
+ storage_location = token_manager.storage.get_location()
839
+ table.add_row("Storage Location", storage_location)
840
+
841
+ # Token algorithm and type from JWT header
842
+ if jwt_header:
843
+ table.add_row("Algorithm", jwt_header.get("alg", "Unknown"))
844
+ table.add_row("Token Type", jwt_header.get("typ", "Unknown"))
845
+ if jwt_header.get("kid"):
846
+ table.add_row("Key ID", jwt_header["kid"])
847
+
848
+ # Token status
849
+ table.add_row(
850
+ "Token Expired",
851
+ "[red]Yes[/red]" if credentials.is_expired() else "[green]No[/green]",
852
+ )
853
+
854
+ # Expiration details
855
+ exp_dt = credentials.expires_at
856
+ table.add_row("Expires At", exp_dt.strftime("%Y-%m-%d %H:%M:%S UTC"))
857
+
858
+ # Time until expiration
859
+ now = datetime.now(UTC)
860
+ time_diff = exp_dt - now
861
+ if time_diff.total_seconds() > 0:
862
+ days = time_diff.days
863
+ hours = (time_diff.seconds % 86400) // 3600
864
+ minutes = (time_diff.seconds % 3600) // 60
865
+ table.add_row(
866
+ "Time Remaining", f"{days} days, {hours} hours, {minutes} minutes"
867
+ )
868
+ else:
869
+ table.add_row("Time Remaining", "[red]Expired[/red]")
870
+
871
+ # JWT timestamps if available
872
+ if jwt_payload:
873
+ if "iat" in jwt_payload:
874
+ iat_dt = datetime.fromtimestamp(jwt_payload["iat"], tz=UTC)
875
+ table.add_row("Issued At", iat_dt.strftime("%Y-%m-%d %H:%M:%S UTC"))
876
+
877
+ if "auth_time" in jwt_payload:
878
+ auth_dt = datetime.fromtimestamp(jwt_payload["auth_time"], tz=UTC)
879
+ table.add_row("Auth Time", auth_dt.strftime("%Y-%m-%d %H:%M:%S UTC"))
880
+
881
+ # JWT issuer and audience
882
+ if jwt_payload:
883
+ if "iss" in jwt_payload:
884
+ table.add_row("Issuer", jwt_payload["iss"])
885
+ if "aud" in jwt_payload:
886
+ audience = jwt_payload["aud"]
887
+ if isinstance(audience, list):
888
+ audience = ", ".join(audience)
889
+ table.add_row("Audience", audience)
890
+ if "jti" in jwt_payload:
891
+ table.add_row("JWT ID", jwt_payload["jti"])
892
+ if "sid" in jwt_payload:
893
+ table.add_row("Session ID", jwt_payload["sid"])
894
+
895
+ # Token preview (first and last 8 chars)
896
+ if credentials.access_token:
897
+ token_preview = (
898
+ f"{credentials.access_token[:12]}...{credentials.access_token[-8:]}"
899
+ )
900
+ table.add_row("Access Token", f"[dim]{token_preview}[/dim]")
901
+
902
+ # Refresh token status
903
+ has_refresh = bool(credentials.refresh_token)
904
+ table.add_row(
905
+ "Refresh Token",
906
+ "[green]Available[/green]"
907
+ if has_refresh
908
+ else "[yellow]Not available[/yellow]",
909
+ )
910
+
911
+ console.print(table)
912
+
913
+ # Show usage instructions
914
+ console.print("\n[dim]Commands:[/dim]")
915
+ console.print(" ccproxy auth login-openai - Re-authenticate")
916
+ console.print(" ccproxy auth logout-openai - Remove credentials")
917
+
918
+ except Exception as e:
919
+ toolkit.print(f"Error getting OpenAI credential info: {e}", tag="error")
920
+ raise typer.Exit(1) from e
921
+
922
+
923
+ @app.command(name="openai-status")
924
+ def openai_status_command() -> None:
925
+ """Check OpenAI authentication status.
926
+
927
+ Quick status check for OpenAI credentials without detailed information.
928
+ Useful for scripts and automation.
929
+
930
+ Examples:
931
+ ccproxy auth openai-status
932
+ """
933
+ import asyncio
934
+
935
+ try:
936
+ token_manager = get_openai_token_manager()
937
+ credentials = asyncio.run(token_manager.load_credentials())
938
+
939
+ if not credentials:
940
+ console.print("[red]✗[/red] Not logged in to OpenAI")
941
+ raise typer.Exit(1)
942
+
943
+ if credentials.is_expired():
944
+ console.print("[yellow]⚠[/yellow] OpenAI credentials expired")
945
+ console.print(
946
+ f" Expired: {credentials.expires_at.strftime('%Y-%m-%d %H:%M:%S UTC')}"
947
+ )
948
+ raise typer.Exit(1)
949
+
950
+ console.print("[green]✓[/green] OpenAI credentials valid")
951
+ console.print(f" Account: {credentials.account_id}")
952
+ console.print(
953
+ f" Expires: {credentials.expires_at.strftime('%Y-%m-%d %H:%M:%S UTC')}"
954
+ )
955
+
956
+ except SystemExit:
957
+ raise
958
+ except Exception as e:
959
+ console.print(f"[red]✗[/red] Error checking OpenAI status: {e}")
960
+ raise typer.Exit(1) from e
961
+
962
+
566
963
  if __name__ == "__main__":
567
964
  app()
@@ -10,6 +10,7 @@ import uvicorn
10
10
  from click import get_current_context
11
11
  from structlog import get_logger
12
12
 
13
+ from ccproxy._version import __version__
13
14
  from ccproxy.cli.helpers import (
14
15
  get_rich_toolkit,
15
16
  is_running_in_docker,
@@ -709,7 +710,6 @@ def api(
709
710
 
710
711
  # Always reconfigure logging to ensure log level changes are picked up
711
712
  # Use JSON logs if explicitly requested via env var
712
- print(f"{settings.server.log_level} {settings.server.log_file}")
713
713
  setup_logging(
714
714
  json_logs=settings.server.log_format == "json",
715
715
  log_level_name=settings.server.log_level,
@@ -729,6 +729,7 @@ def api(
729
729
  logger.info(
730
730
  "cli_command_starting",
731
731
  command="serve",
732
+ version=__version__,
732
733
  docker=docker,
733
734
  port=server_options.port,
734
735
  host=server_options.host,
@@ -896,6 +897,7 @@ def claude(
896
897
  logger.info(
897
898
  "cli_command_starting",
898
899
  command="claude",
900
+ version=__version__,
899
901
  docker=docker,
900
902
  args=args if args else [],
901
903
  )
ccproxy/config/claude.py CHANGED
@@ -226,7 +226,7 @@ class ClaudeSettings(BaseModel):
226
226
  # Extract default values as a dict for merging
227
227
  default_values = {
228
228
  "mcp_servers": dict(defaults.mcp_servers)
229
- if defaults.mcp_servers
229
+ if isinstance(defaults.mcp_servers, dict)
230
230
  else {},
231
231
  "permission_prompt_tool_name": defaults.permission_prompt_tool_name,
232
232
  }
@@ -0,0 +1,100 @@
1
+ """OpenAI Codex-specific configuration settings."""
2
+
3
+ from pydantic import BaseModel, Field, field_validator
4
+
5
+
6
+ class OAuthSettings(BaseModel):
7
+ """OAuth configuration for OpenAI authentication."""
8
+
9
+ base_url: str = Field(
10
+ default="https://auth.openai.com",
11
+ description="OpenAI OAuth base URL",
12
+ )
13
+
14
+ client_id: str = Field(
15
+ default="app_EMoamEEZ73f0CkXaXp7hrann",
16
+ description="OpenAI OAuth client ID",
17
+ )
18
+
19
+ scopes: list[str] = Field(
20
+ default_factory=lambda: ["openid", "profile", "email", "offline_access"],
21
+ description="OAuth scopes to request",
22
+ )
23
+
24
+ @field_validator("base_url")
25
+ @classmethod
26
+ def validate_base_url(cls, v: str) -> str:
27
+ """Validate OAuth base URL format."""
28
+ if not v.startswith(("http://", "https://")):
29
+ raise ValueError("OAuth base URL must start with http:// or https://")
30
+ return v.rstrip("/")
31
+
32
+
33
+ class CodexSettings(BaseModel):
34
+ """OpenAI Codex-specific configuration settings."""
35
+
36
+ enabled: bool = Field(
37
+ default=True,
38
+ description="Enable OpenAI Codex provider support",
39
+ )
40
+
41
+ base_url: str = Field(
42
+ default="https://chatgpt.com/backend-api/codex",
43
+ description="OpenAI Codex API base URL",
44
+ )
45
+
46
+ oauth: OAuthSettings = Field(
47
+ default_factory=OAuthSettings,
48
+ description="OAuth configuration settings",
49
+ )
50
+
51
+ callback_port: int = Field(
52
+ default=1455,
53
+ ge=1024,
54
+ le=65535,
55
+ description="Port for OAuth callback server (1024-65535)",
56
+ )
57
+
58
+ redirect_uri: str = Field(
59
+ default="http://localhost:1455/auth/callback",
60
+ description="OAuth redirect URI (auto-generated from callback_port if not set)",
61
+ )
62
+
63
+ verbose_logging: bool = Field(
64
+ default=False,
65
+ description="Enable verbose logging for Codex operations",
66
+ )
67
+
68
+ @field_validator("base_url")
69
+ @classmethod
70
+ def validate_base_url(cls, v: str) -> str:
71
+ """Validate Codex base URL format."""
72
+ if not v.startswith(("http://", "https://")):
73
+ raise ValueError("Codex base URL must start with http:// or https://")
74
+ return v.rstrip("/")
75
+
76
+ @field_validator("redirect_uri")
77
+ @classmethod
78
+ def validate_redirect_uri(cls, v: str) -> str:
79
+ """Validate redirect URI format."""
80
+ if not v.startswith(("http://", "https://")):
81
+ raise ValueError("Redirect URI must start with http:// or https://")
82
+ return v
83
+
84
+ @field_validator("callback_port")
85
+ @classmethod
86
+ def validate_callback_port(cls, v: int) -> int:
87
+ """Validate callback port range."""
88
+ if not (1024 <= v <= 65535):
89
+ raise ValueError("Callback port must be between 1024 and 65535")
90
+ return v
91
+
92
+ def get_redirect_uri(self) -> str:
93
+ """Get the redirect URI, auto-generating if needed."""
94
+ if (
95
+ self.redirect_uri
96
+ and self.redirect_uri
97
+ != f"http://localhost:{self.callback_port}/auth/callback"
98
+ ):
99
+ return self.redirect_uri
100
+ return f"http://localhost:{self.callback_port}/auth/callback"
@@ -34,8 +34,8 @@ class SchedulerSettings(BaseSettings):
34
34
 
35
35
  # Pricing updater task settings
36
36
  pricing_update_enabled: bool = Field(
37
- default=False,
38
- description="Whether pricing cache update task is enabled. Disabled by default for privacy - downloads from GitHub when enabled",
37
+ default=True,
38
+ description="Whether pricing cache update task is enabled. Enabled by default for privacy - downloads from GitHub when enabled",
39
39
  )
40
40
 
41
41
  pricing_update_interval_hours: int = Field(
@@ -84,22 +84,22 @@ class SchedulerSettings(BaseSettings):
84
84
 
85
85
  # Version checking task settings
86
86
  version_check_enabled: bool = Field(
87
- default=False,
88
- description="Whether version update checking is enabled. Disabled by default for privacy - checks GitHub API when enabled",
87
+ default=True,
88
+ description="Whether version update checking is enabled. Enabled by default for privacy - checks GitHub API when enabled",
89
89
  )
90
90
 
91
91
  version_check_interval_hours: int = Field(
92
- default=12,
92
+ default=6,
93
93
  ge=1,
94
94
  le=168, # Max 1 week
95
95
  description="Interval in hours between version checks",
96
96
  )
97
97
 
98
- version_check_startup_max_age_hours: float = Field(
99
- default=1.0,
98
+ version_check_cache_ttl_hours: float = Field(
99
+ default=6,
100
100
  ge=0.1,
101
101
  le=24.0,
102
- description="Maximum age in hours since last check before running startup check",
102
+ description="Maximum age in hours since last check version check",
103
103
  )
104
104
 
105
105
  model_config = SettingsConfigDict(
@@ -15,6 +15,7 @@ from ccproxy.config.discovery import find_toml_config_file
15
15
 
16
16
  from .auth import AuthSettings
17
17
  from .claude import ClaudeSettings
18
+ from .codex import CodexSettings
18
19
  from .cors import CORSSettings
19
20
  from .docker_settings import DockerSettings
20
21
  from .observability import ObservabilitySettings
@@ -85,6 +86,12 @@ class Settings(BaseSettings):
85
86
  description="Claude-specific configuration settings",
86
87
  )
87
88
 
89
+ # Codex-specific settings
90
+ codex: CodexSettings = Field(
91
+ default_factory=CodexSettings,
92
+ description="OpenAI Codex-specific configuration settings",
93
+ )
94
+
88
95
  # Proxy and authentication
89
96
  reverse_proxy: ReverseProxySettings = Field(
90
97
  default_factory=ReverseProxySettings,
@@ -168,6 +175,18 @@ class Settings(BaseSettings):
168
175
  return ClaudeSettings(**v)
169
176
  return v
170
177
 
178
+ @field_validator("codex", mode="before")
179
+ @classmethod
180
+ def validate_codex(cls, v: Any) -> Any:
181
+ """Validate and convert Codex settings."""
182
+ if v is None:
183
+ return CodexSettings()
184
+ if isinstance(v, CodexSettings):
185
+ return v
186
+ if isinstance(v, dict):
187
+ return CodexSettings(**v)
188
+ return v
189
+
171
190
  @field_validator("reverse_proxy", mode="before")
172
191
  @classmethod
173
192
  def validate_reverse_proxy(cls, v: Any) -> Any: