ccproxy-api 0.1.5__py3-none-any.whl → 0.1.6__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.
- ccproxy/_version.py +2 -2
- ccproxy/adapters/codex/__init__.py +11 -0
- ccproxy/adapters/openai/models.py +1 -1
- ccproxy/adapters/openai/response_adapter.py +355 -0
- ccproxy/adapters/openai/response_models.py +178 -0
- ccproxy/api/app.py +16 -0
- ccproxy/api/routes/codex.py +1231 -0
- ccproxy/api/routes/health.py +228 -3
- ccproxy/auth/openai/__init__.py +13 -0
- ccproxy/auth/openai/credentials.py +166 -0
- ccproxy/auth/openai/oauth_client.py +334 -0
- ccproxy/auth/openai/storage.py +184 -0
- ccproxy/claude_sdk/options.py +1 -1
- ccproxy/cli/commands/auth.py +398 -1
- ccproxy/cli/commands/serve.py +3 -1
- ccproxy/config/claude.py +1 -1
- ccproxy/config/codex.py +100 -0
- ccproxy/config/scheduler.py +4 -4
- ccproxy/config/settings.py +19 -0
- ccproxy/core/codex_transformers.py +389 -0
- ccproxy/core/http_transformers.py +153 -2
- ccproxy/models/detection.py +82 -0
- ccproxy/models/requests.py +22 -0
- ccproxy/models/responses.py +16 -0
- ccproxy/services/codex_detection_service.py +263 -0
- ccproxy/services/proxy_service.py +530 -0
- ccproxy/utils/model_mapping.py +7 -5
- ccproxy/utils/startup_helpers.py +62 -0
- ccproxy_api-0.1.6.dist-info/METADATA +615 -0
- {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.6.dist-info}/RECORD +33 -22
- ccproxy_api-0.1.5.dist-info/METADATA +0 -396
- {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.6.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.6.dist-info}/entry_points.txt +0 -0
- {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.6.dist-info}/licenses/LICENSE +0 -0
ccproxy/cli/commands/auth.py
CHANGED
|
@@ -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()
|
ccproxy/cli/commands/serve.py
CHANGED
|
@@ -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
|
}
|
ccproxy/config/codex.py
ADDED
|
@@ -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"
|
ccproxy/config/scheduler.py
CHANGED
|
@@ -34,8 +34,8 @@ class SchedulerSettings(BaseSettings):
|
|
|
34
34
|
|
|
35
35
|
# Pricing updater task settings
|
|
36
36
|
pricing_update_enabled: bool = Field(
|
|
37
|
-
default=
|
|
38
|
-
description="Whether pricing cache update task is 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,8 +84,8 @@ class SchedulerSettings(BaseSettings):
|
|
|
84
84
|
|
|
85
85
|
# Version checking task settings
|
|
86
86
|
version_check_enabled: bool = Field(
|
|
87
|
-
default=
|
|
88
|
-
description="Whether version update checking is 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(
|
ccproxy/config/settings.py
CHANGED
|
@@ -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:
|