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.
Files changed (72) hide show
  1. ccproxy/_version.py +2 -2
  2. ccproxy/adapters/codex/__init__.py +11 -0
  3. ccproxy/adapters/openai/adapter.py +1 -1
  4. ccproxy/adapters/openai/models.py +1 -1
  5. ccproxy/adapters/openai/response_adapter.py +355 -0
  6. ccproxy/adapters/openai/response_models.py +178 -0
  7. ccproxy/adapters/openai/streaming.py +1 -0
  8. ccproxy/api/app.py +150 -224
  9. ccproxy/api/dependencies.py +22 -2
  10. ccproxy/api/middleware/errors.py +27 -3
  11. ccproxy/api/middleware/logging.py +4 -0
  12. ccproxy/api/responses.py +6 -1
  13. ccproxy/api/routes/claude.py +222 -17
  14. ccproxy/api/routes/codex.py +1231 -0
  15. ccproxy/api/routes/health.py +228 -3
  16. ccproxy/api/routes/proxy.py +25 -6
  17. ccproxy/api/services/permission_service.py +2 -2
  18. ccproxy/auth/openai/__init__.py +13 -0
  19. ccproxy/auth/openai/credentials.py +166 -0
  20. ccproxy/auth/openai/oauth_client.py +334 -0
  21. ccproxy/auth/openai/storage.py +184 -0
  22. ccproxy/claude_sdk/__init__.py +4 -8
  23. ccproxy/claude_sdk/client.py +661 -131
  24. ccproxy/claude_sdk/exceptions.py +16 -0
  25. ccproxy/claude_sdk/manager.py +219 -0
  26. ccproxy/claude_sdk/message_queue.py +342 -0
  27. ccproxy/claude_sdk/options.py +6 -1
  28. ccproxy/claude_sdk/session_client.py +546 -0
  29. ccproxy/claude_sdk/session_pool.py +550 -0
  30. ccproxy/claude_sdk/stream_handle.py +538 -0
  31. ccproxy/claude_sdk/stream_worker.py +392 -0
  32. ccproxy/claude_sdk/streaming.py +53 -11
  33. ccproxy/cli/commands/auth.py +398 -1
  34. ccproxy/cli/commands/serve.py +99 -1
  35. ccproxy/cli/options/claude_options.py +47 -0
  36. ccproxy/config/__init__.py +0 -3
  37. ccproxy/config/claude.py +171 -23
  38. ccproxy/config/codex.py +100 -0
  39. ccproxy/config/discovery.py +10 -1
  40. ccproxy/config/scheduler.py +2 -2
  41. ccproxy/config/settings.py +38 -1
  42. ccproxy/core/codex_transformers.py +389 -0
  43. ccproxy/core/http_transformers.py +458 -75
  44. ccproxy/core/logging.py +108 -12
  45. ccproxy/core/transformers.py +5 -0
  46. ccproxy/models/claude_sdk.py +57 -0
  47. ccproxy/models/detection.py +208 -0
  48. ccproxy/models/requests.py +22 -0
  49. ccproxy/models/responses.py +16 -0
  50. ccproxy/observability/access_logger.py +72 -14
  51. ccproxy/observability/metrics.py +151 -0
  52. ccproxy/observability/storage/duckdb_simple.py +12 -0
  53. ccproxy/observability/storage/models.py +16 -0
  54. ccproxy/observability/streaming_response.py +107 -0
  55. ccproxy/scheduler/manager.py +31 -6
  56. ccproxy/scheduler/tasks.py +122 -0
  57. ccproxy/services/claude_detection_service.py +269 -0
  58. ccproxy/services/claude_sdk_service.py +333 -130
  59. ccproxy/services/codex_detection_service.py +263 -0
  60. ccproxy/services/proxy_service.py +618 -197
  61. ccproxy/utils/__init__.py +9 -1
  62. ccproxy/utils/disconnection_monitor.py +83 -0
  63. ccproxy/utils/id_generator.py +12 -0
  64. ccproxy/utils/model_mapping.py +7 -5
  65. ccproxy/utils/startup_helpers.py +470 -0
  66. ccproxy_api-0.1.6.dist-info/METADATA +615 -0
  67. {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.dist-info}/RECORD +70 -47
  68. ccproxy/config/loader.py +0 -105
  69. ccproxy_api-0.1.4.dist-info/METADATA +0 -369
  70. {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.dist-info}/WHEEL +0 -0
  71. {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.dist-info}/entry_points.txt +0 -0
  72. {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.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,
@@ -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
@@ -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",