ccproxy-api 0.1.4__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/adapter.py +1 -1
- ccproxy/adapters/openai/models.py +1 -1
- ccproxy/adapters/openai/response_adapter.py +355 -0
- ccproxy/adapters/openai/response_models.py +178 -0
- ccproxy/adapters/openai/streaming.py +1 -0
- ccproxy/api/app.py +150 -224
- ccproxy/api/dependencies.py +22 -2
- ccproxy/api/middleware/errors.py +27 -3
- ccproxy/api/middleware/logging.py +4 -0
- ccproxy/api/responses.py +6 -1
- ccproxy/api/routes/claude.py +222 -17
- ccproxy/api/routes/codex.py +1231 -0
- ccproxy/api/routes/health.py +228 -3
- ccproxy/api/routes/proxy.py +25 -6
- ccproxy/api/services/permission_service.py +2 -2
- 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/__init__.py +4 -8
- ccproxy/claude_sdk/client.py +661 -131
- ccproxy/claude_sdk/exceptions.py +16 -0
- ccproxy/claude_sdk/manager.py +219 -0
- ccproxy/claude_sdk/message_queue.py +342 -0
- ccproxy/claude_sdk/options.py +6 -1
- ccproxy/claude_sdk/session_client.py +546 -0
- ccproxy/claude_sdk/session_pool.py +550 -0
- ccproxy/claude_sdk/stream_handle.py +538 -0
- ccproxy/claude_sdk/stream_worker.py +392 -0
- ccproxy/claude_sdk/streaming.py +53 -11
- ccproxy/cli/commands/auth.py +398 -1
- ccproxy/cli/commands/serve.py +99 -1
- ccproxy/cli/options/claude_options.py +47 -0
- ccproxy/config/__init__.py +0 -3
- ccproxy/config/claude.py +171 -23
- ccproxy/config/codex.py +100 -0
- ccproxy/config/discovery.py +10 -1
- ccproxy/config/scheduler.py +2 -2
- ccproxy/config/settings.py +38 -1
- ccproxy/core/codex_transformers.py +389 -0
- ccproxy/core/http_transformers.py +458 -75
- ccproxy/core/logging.py +108 -12
- ccproxy/core/transformers.py +5 -0
- ccproxy/models/claude_sdk.py +57 -0
- ccproxy/models/detection.py +208 -0
- ccproxy/models/requests.py +22 -0
- ccproxy/models/responses.py +16 -0
- ccproxy/observability/access_logger.py +72 -14
- ccproxy/observability/metrics.py +151 -0
- ccproxy/observability/storage/duckdb_simple.py +12 -0
- ccproxy/observability/storage/models.py +16 -0
- ccproxy/observability/streaming_response.py +107 -0
- ccproxy/scheduler/manager.py +31 -6
- ccproxy/scheduler/tasks.py +122 -0
- ccproxy/services/claude_detection_service.py +269 -0
- ccproxy/services/claude_sdk_service.py +333 -130
- ccproxy/services/codex_detection_service.py +263 -0
- ccproxy/services/proxy_service.py +618 -197
- ccproxy/utils/__init__.py +9 -1
- ccproxy/utils/disconnection_monitor.py +83 -0
- ccproxy/utils/id_generator.py +12 -0
- ccproxy/utils/model_mapping.py +7 -5
- ccproxy/utils/startup_helpers.py +470 -0
- ccproxy_api-0.1.6.dist-info/METADATA +615 -0
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.dist-info}/RECORD +70 -47
- ccproxy/config/loader.py +0 -105
- ccproxy_api-0.1.4.dist-info/METADATA +0 -369
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.dist-info}/entry_points.txt +0 -0
- {ccproxy_api-0.1.4.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,
|
|
@@ -35,7 +36,9 @@ from ..options.claude_options import (
|
|
|
35
36
|
validate_max_thinking_tokens,
|
|
36
37
|
validate_max_turns,
|
|
37
38
|
validate_permission_mode,
|
|
39
|
+
validate_pool_size,
|
|
38
40
|
validate_sdk_message_mode,
|
|
41
|
+
validate_system_prompt_injection_mode,
|
|
39
42
|
)
|
|
40
43
|
from ..options.security_options import SecurityOptions, validate_auth_token
|
|
41
44
|
from ..options.server_options import (
|
|
@@ -442,6 +445,48 @@ def api(
|
|
|
442
445
|
rich_help_panel="Claude Settings",
|
|
443
446
|
),
|
|
444
447
|
] = None,
|
|
448
|
+
sdk_pool: Annotated[
|
|
449
|
+
bool,
|
|
450
|
+
typer.Option(
|
|
451
|
+
"--sdk-pool/--no-sdk-pool",
|
|
452
|
+
help="Enable/disable general Claude SDK client connection pooling",
|
|
453
|
+
rich_help_panel="Claude Settings",
|
|
454
|
+
),
|
|
455
|
+
] = False,
|
|
456
|
+
sdk_pool_size: Annotated[
|
|
457
|
+
int | None,
|
|
458
|
+
typer.Option(
|
|
459
|
+
"--sdk-pool-size",
|
|
460
|
+
help="Number of clients to maintain in the general pool (1-20)",
|
|
461
|
+
callback=validate_pool_size,
|
|
462
|
+
rich_help_panel="Claude Settings",
|
|
463
|
+
),
|
|
464
|
+
] = None,
|
|
465
|
+
sdk_session_pool: Annotated[
|
|
466
|
+
bool,
|
|
467
|
+
typer.Option(
|
|
468
|
+
"--sdk-session-pool/--no-sdk-session-pool",
|
|
469
|
+
help="Enable/disable session-aware Claude SDK client pooling",
|
|
470
|
+
rich_help_panel="Claude Settings",
|
|
471
|
+
),
|
|
472
|
+
] = False,
|
|
473
|
+
system_prompt_injection_mode: Annotated[
|
|
474
|
+
str | None,
|
|
475
|
+
typer.Option(
|
|
476
|
+
"--system-prompt-injection-mode",
|
|
477
|
+
help="System prompt injection mode: minimal (Claude Code ID only), full (all detected system messages)",
|
|
478
|
+
callback=validate_system_prompt_injection_mode,
|
|
479
|
+
rich_help_panel="Claude Settings",
|
|
480
|
+
),
|
|
481
|
+
] = None,
|
|
482
|
+
builtin_permissions: Annotated[
|
|
483
|
+
bool,
|
|
484
|
+
typer.Option(
|
|
485
|
+
"--builtin-permissions/--no-builtin-permissions",
|
|
486
|
+
help="Enable built-in permission handling infrastructure (MCP server and SSE endpoints). When disabled, users can configure custom MCP servers and permission tools.",
|
|
487
|
+
rich_help_panel="Claude Settings",
|
|
488
|
+
),
|
|
489
|
+
] = True,
|
|
445
490
|
# Core settings
|
|
446
491
|
docker: Annotated[
|
|
447
492
|
bool,
|
|
@@ -526,6 +571,31 @@ def api(
|
|
|
526
571
|
rich_help_panel="Docker Settings",
|
|
527
572
|
),
|
|
528
573
|
] = None,
|
|
574
|
+
# Network control flags
|
|
575
|
+
no_network_calls: Annotated[
|
|
576
|
+
bool,
|
|
577
|
+
typer.Option(
|
|
578
|
+
"--no-network-calls",
|
|
579
|
+
help="Disable all network calls (version checks and pricing updates)",
|
|
580
|
+
rich_help_panel="Privacy Settings",
|
|
581
|
+
),
|
|
582
|
+
] = False,
|
|
583
|
+
disable_version_check: Annotated[
|
|
584
|
+
bool,
|
|
585
|
+
typer.Option(
|
|
586
|
+
"--disable-version-check",
|
|
587
|
+
help="Disable version update checks (prevents calls to GitHub API)",
|
|
588
|
+
rich_help_panel="Privacy Settings",
|
|
589
|
+
),
|
|
590
|
+
] = False,
|
|
591
|
+
disable_pricing_updates: Annotated[
|
|
592
|
+
bool,
|
|
593
|
+
typer.Option(
|
|
594
|
+
"--disable-pricing-updates",
|
|
595
|
+
help="Disable pricing data updates (prevents downloads from GitHub)",
|
|
596
|
+
rich_help_panel="Privacy Settings",
|
|
597
|
+
),
|
|
598
|
+
] = False,
|
|
529
599
|
) -> None:
|
|
530
600
|
"""
|
|
531
601
|
Start the CCProxy API server.
|
|
@@ -573,10 +643,28 @@ def api(
|
|
|
573
643
|
cwd=cwd,
|
|
574
644
|
permission_prompt_tool_name=permission_prompt_tool_name,
|
|
575
645
|
sdk_message_mode=sdk_message_mode,
|
|
646
|
+
sdk_pool=sdk_pool,
|
|
647
|
+
sdk_pool_size=sdk_pool_size,
|
|
648
|
+
sdk_session_pool=sdk_session_pool,
|
|
649
|
+
system_prompt_injection_mode=system_prompt_injection_mode,
|
|
650
|
+
builtin_permissions=builtin_permissions,
|
|
576
651
|
)
|
|
577
652
|
|
|
578
653
|
security_options = SecurityOptions(auth_token=auth_token)
|
|
579
654
|
|
|
655
|
+
# Handle network control flags
|
|
656
|
+
scheduler_overrides = {}
|
|
657
|
+
if no_network_calls:
|
|
658
|
+
# Disable both network features
|
|
659
|
+
scheduler_overrides["pricing_update_enabled"] = False
|
|
660
|
+
scheduler_overrides["version_check_enabled"] = False
|
|
661
|
+
else:
|
|
662
|
+
# Handle individual flags
|
|
663
|
+
if disable_pricing_updates:
|
|
664
|
+
scheduler_overrides["pricing_update_enabled"] = False
|
|
665
|
+
if disable_version_check:
|
|
666
|
+
scheduler_overrides["version_check_enabled"] = False
|
|
667
|
+
|
|
580
668
|
# Extract CLI overrides from structured option containers
|
|
581
669
|
cli_overrides = config_manager.get_cli_overrides_from_args(
|
|
582
670
|
# Server options
|
|
@@ -599,8 +687,17 @@ def api(
|
|
|
599
687
|
permission_prompt_tool_name=claude_options.permission_prompt_tool_name,
|
|
600
688
|
cwd=claude_options.cwd,
|
|
601
689
|
sdk_message_mode=claude_options.sdk_message_mode,
|
|
690
|
+
sdk_pool=claude_options.sdk_pool,
|
|
691
|
+
sdk_pool_size=claude_options.sdk_pool_size,
|
|
692
|
+
sdk_session_pool=claude_options.sdk_session_pool,
|
|
693
|
+
system_prompt_injection_mode=claude_options.system_prompt_injection_mode,
|
|
694
|
+
builtin_permissions=claude_options.builtin_permissions,
|
|
602
695
|
)
|
|
603
696
|
|
|
697
|
+
# Add scheduler overrides if any
|
|
698
|
+
if scheduler_overrides:
|
|
699
|
+
cli_overrides["scheduler"] = scheduler_overrides
|
|
700
|
+
|
|
604
701
|
# Load settings with CLI overrides
|
|
605
702
|
settings = config_manager.load_settings(
|
|
606
703
|
config_path=config, cli_overrides=cli_overrides
|
|
@@ -613,7 +710,6 @@ def api(
|
|
|
613
710
|
|
|
614
711
|
# Always reconfigure logging to ensure log level changes are picked up
|
|
615
712
|
# Use JSON logs if explicitly requested via env var
|
|
616
|
-
print(f"{settings.server.log_level} {settings.server.log_file}")
|
|
617
713
|
setup_logging(
|
|
618
714
|
json_logs=settings.server.log_format == "json",
|
|
619
715
|
log_level_name=settings.server.log_level,
|
|
@@ -633,6 +729,7 @@ def api(
|
|
|
633
729
|
logger.info(
|
|
634
730
|
"cli_command_starting",
|
|
635
731
|
command="serve",
|
|
732
|
+
version=__version__,
|
|
636
733
|
docker=docker,
|
|
637
734
|
port=server_options.port,
|
|
638
735
|
host=server_options.host,
|
|
@@ -800,6 +897,7 @@ def claude(
|
|
|
800
897
|
logger.info(
|
|
801
898
|
"cli_command_starting",
|
|
802
899
|
command="claude",
|
|
900
|
+
version=__version__,
|
|
803
901
|
docker=docker,
|
|
804
902
|
args=args if args else [],
|
|
805
903
|
)
|
|
@@ -93,6 +93,38 @@ def validate_sdk_message_mode(
|
|
|
93
93
|
return value
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def validate_pool_size(
|
|
97
|
+
ctx: typer.Context, param: typer.CallbackParam, value: int | None
|
|
98
|
+
) -> int | None:
|
|
99
|
+
"""Validate pool size."""
|
|
100
|
+
if value is None:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
if value < 1:
|
|
104
|
+
raise typer.BadParameter("Pool size must be at least 1")
|
|
105
|
+
|
|
106
|
+
if value > 20:
|
|
107
|
+
raise typer.BadParameter("Pool size must not exceed 20")
|
|
108
|
+
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def validate_system_prompt_injection_mode(
|
|
113
|
+
ctx: typer.Context, param: typer.CallbackParam, value: str | None
|
|
114
|
+
) -> str | None:
|
|
115
|
+
"""Validate system prompt injection mode."""
|
|
116
|
+
if value is None:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
valid_modes = {"minimal", "full"}
|
|
120
|
+
if value not in valid_modes:
|
|
121
|
+
raise typer.BadParameter(
|
|
122
|
+
f"System prompt injection mode must be one of: {', '.join(valid_modes)}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return value
|
|
126
|
+
|
|
127
|
+
|
|
96
128
|
# Factory functions removed - use Annotated syntax directly in commands
|
|
97
129
|
|
|
98
130
|
|
|
@@ -115,6 +147,11 @@ class ClaudeOptions:
|
|
|
115
147
|
cwd: str | None = None,
|
|
116
148
|
permission_prompt_tool_name: str | None = None,
|
|
117
149
|
sdk_message_mode: str | None = None,
|
|
150
|
+
sdk_pool: bool = False,
|
|
151
|
+
sdk_pool_size: int | None = None,
|
|
152
|
+
sdk_session_pool: bool = False,
|
|
153
|
+
system_prompt_injection_mode: str | None = None,
|
|
154
|
+
builtin_permissions: bool = True,
|
|
118
155
|
):
|
|
119
156
|
"""Initialize Claude options.
|
|
120
157
|
|
|
@@ -129,6 +166,11 @@ class ClaudeOptions:
|
|
|
129
166
|
cwd: Working directory path
|
|
130
167
|
permission_prompt_tool_name: Permission prompt tool name
|
|
131
168
|
sdk_message_mode: SDK message handling mode
|
|
169
|
+
sdk_pool: Enable general Claude SDK client connection pooling
|
|
170
|
+
sdk_pool_size: Number of clients to maintain in the general pool
|
|
171
|
+
sdk_session_pool: Enable session-aware Claude SDK client pooling
|
|
172
|
+
system_prompt_injection_mode: System prompt injection mode
|
|
173
|
+
builtin_permissions: Enable built-in permission handling infrastructure
|
|
132
174
|
"""
|
|
133
175
|
self.max_thinking_tokens = max_thinking_tokens
|
|
134
176
|
self.allowed_tools = allowed_tools
|
|
@@ -140,3 +182,8 @@ class ClaudeOptions:
|
|
|
140
182
|
self.cwd = cwd
|
|
141
183
|
self.permission_prompt_tool_name = permission_prompt_tool_name
|
|
142
184
|
self.sdk_message_mode = sdk_message_mode
|
|
185
|
+
self.sdk_pool = sdk_pool
|
|
186
|
+
self.sdk_pool_size = sdk_pool_size
|
|
187
|
+
self.sdk_session_pool = sdk_session_pool
|
|
188
|
+
self.system_prompt_injection_mode = system_prompt_injection_mode
|
|
189
|
+
self.builtin_permissions = builtin_permissions
|
ccproxy/config/__init__.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from .auth import AuthSettings, CredentialStorageSettings, OAuthSettings
|
|
4
4
|
from .docker_settings import DockerSettings
|
|
5
|
-
from .loader import ConfigLoader, load_config
|
|
6
5
|
from .reverse_proxy import ReverseProxySettings
|
|
7
6
|
from .settings import Settings, get_settings
|
|
8
7
|
from .validators import (
|
|
@@ -26,8 +25,6 @@ __all__ = [
|
|
|
26
25
|
"CredentialStorageSettings",
|
|
27
26
|
"ReverseProxySettings",
|
|
28
27
|
"DockerSettings",
|
|
29
|
-
"ConfigLoader",
|
|
30
|
-
"load_config",
|
|
31
28
|
"ConfigValidationError",
|
|
32
29
|
"validate_config_dict",
|
|
33
30
|
"validate_cors_origins",
|