ccproxy-api 0.1.2__py3-none-any.whl → 0.1.3__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 (108) hide show
  1. ccproxy/_version.py +2 -2
  2. ccproxy/adapters/openai/__init__.py +1 -2
  3. ccproxy/adapters/openai/adapter.py +218 -180
  4. ccproxy/adapters/openai/streaming.py +247 -65
  5. ccproxy/api/__init__.py +0 -3
  6. ccproxy/api/app.py +173 -40
  7. ccproxy/api/dependencies.py +62 -3
  8. ccproxy/api/middleware/errors.py +3 -7
  9. ccproxy/api/middleware/headers.py +0 -2
  10. ccproxy/api/middleware/logging.py +4 -3
  11. ccproxy/api/middleware/request_content_logging.py +297 -0
  12. ccproxy/api/middleware/request_id.py +5 -0
  13. ccproxy/api/middleware/server_header.py +0 -4
  14. ccproxy/api/routes/__init__.py +9 -1
  15. ccproxy/api/routes/claude.py +23 -32
  16. ccproxy/api/routes/health.py +58 -4
  17. ccproxy/api/routes/mcp.py +171 -0
  18. ccproxy/api/routes/metrics.py +4 -8
  19. ccproxy/api/routes/permissions.py +217 -0
  20. ccproxy/api/routes/proxy.py +0 -53
  21. ccproxy/api/services/__init__.py +6 -0
  22. ccproxy/api/services/permission_service.py +368 -0
  23. ccproxy/api/ui/__init__.py +6 -0
  24. ccproxy/api/ui/permission_handler_protocol.py +33 -0
  25. ccproxy/api/ui/terminal_permission_handler.py +593 -0
  26. ccproxy/auth/conditional.py +2 -2
  27. ccproxy/auth/dependencies.py +1 -1
  28. ccproxy/auth/oauth/models.py +0 -1
  29. ccproxy/auth/oauth/routes.py +1 -3
  30. ccproxy/auth/storage/json_file.py +0 -1
  31. ccproxy/auth/storage/keyring.py +0 -3
  32. ccproxy/claude_sdk/__init__.py +2 -0
  33. ccproxy/claude_sdk/client.py +91 -8
  34. ccproxy/claude_sdk/converter.py +405 -210
  35. ccproxy/claude_sdk/options.py +76 -29
  36. ccproxy/claude_sdk/parser.py +200 -0
  37. ccproxy/claude_sdk/streaming.py +286 -0
  38. ccproxy/cli/commands/__init__.py +5 -2
  39. ccproxy/cli/commands/auth.py +2 -4
  40. ccproxy/cli/commands/permission_handler.py +553 -0
  41. ccproxy/cli/commands/serve.py +30 -12
  42. ccproxy/cli/docker/params.py +0 -4
  43. ccproxy/cli/helpers.py +0 -2
  44. ccproxy/cli/main.py +5 -16
  45. ccproxy/cli/options/claude_options.py +19 -1
  46. ccproxy/cli/options/core_options.py +0 -3
  47. ccproxy/cli/options/security_options.py +0 -2
  48. ccproxy/cli/options/server_options.py +3 -2
  49. ccproxy/config/auth.py +0 -1
  50. ccproxy/config/claude.py +78 -2
  51. ccproxy/config/discovery.py +0 -1
  52. ccproxy/config/docker_settings.py +0 -1
  53. ccproxy/config/loader.py +1 -4
  54. ccproxy/config/scheduler.py +20 -0
  55. ccproxy/config/security.py +7 -2
  56. ccproxy/config/server.py +5 -0
  57. ccproxy/config/settings.py +13 -7
  58. ccproxy/config/validators.py +1 -1
  59. ccproxy/core/async_utils.py +1 -4
  60. ccproxy/core/errors.py +45 -1
  61. ccproxy/core/http_transformers.py +4 -3
  62. ccproxy/core/interfaces.py +2 -2
  63. ccproxy/core/logging.py +97 -95
  64. ccproxy/core/middleware.py +1 -1
  65. ccproxy/core/proxy.py +1 -1
  66. ccproxy/core/transformers.py +1 -1
  67. ccproxy/core/types.py +1 -1
  68. ccproxy/docker/models.py +1 -1
  69. ccproxy/docker/protocol.py +0 -3
  70. ccproxy/models/__init__.py +41 -0
  71. ccproxy/models/claude_sdk.py +420 -0
  72. ccproxy/models/messages.py +45 -18
  73. ccproxy/models/permissions.py +115 -0
  74. ccproxy/models/requests.py +1 -1
  75. ccproxy/models/responses.py +29 -2
  76. ccproxy/observability/access_logger.py +1 -2
  77. ccproxy/observability/context.py +17 -1
  78. ccproxy/observability/metrics.py +1 -3
  79. ccproxy/observability/pushgateway.py +0 -2
  80. ccproxy/observability/stats_printer.py +2 -4
  81. ccproxy/observability/storage/duckdb_simple.py +1 -1
  82. ccproxy/observability/storage/models.py +0 -1
  83. ccproxy/pricing/cache.py +0 -1
  84. ccproxy/pricing/loader.py +5 -21
  85. ccproxy/pricing/updater.py +0 -1
  86. ccproxy/scheduler/__init__.py +1 -0
  87. ccproxy/scheduler/core.py +6 -6
  88. ccproxy/scheduler/manager.py +35 -7
  89. ccproxy/scheduler/registry.py +1 -1
  90. ccproxy/scheduler/tasks.py +127 -2
  91. ccproxy/services/claude_sdk_service.py +220 -328
  92. ccproxy/services/credentials/manager.py +0 -1
  93. ccproxy/services/credentials/oauth_client.py +1 -2
  94. ccproxy/services/proxy_service.py +93 -222
  95. ccproxy/testing/config.py +1 -1
  96. ccproxy/testing/mock_responses.py +0 -1
  97. ccproxy/utils/model_mapping.py +197 -0
  98. ccproxy/utils/models_provider.py +150 -0
  99. ccproxy/utils/simple_request_logger.py +284 -0
  100. ccproxy/utils/version_checker.py +184 -0
  101. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/METADATA +63 -2
  102. ccproxy_api-0.1.3.dist-info/RECORD +166 -0
  103. ccproxy/cli/commands/permission.py +0 -128
  104. ccproxy_api-0.1.2.dist-info/RECORD +0 -150
  105. /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
  106. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/WHEEL +0 -0
  107. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/entry_points.txt +0 -0
  108. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/licenses/LICENSE +0 -0
ccproxy/core/logging.py CHANGED
@@ -1,45 +1,25 @@
1
1
  import logging
2
2
  import sys
3
- from collections.abc import MutableMapping
4
- from typing import Any
3
+ from pathlib import Path
5
4
 
6
5
  import structlog
7
6
  from structlog.stdlib import BoundLogger
8
7
  from structlog.typing import Processor
9
8
 
10
9
 
11
- def configure_structlog(json_logs: bool = False, log_level: str = "INFO") -> None:
12
- """Configure structlog with your preferred processors."""
13
- # Use different timestamp format based on log level
14
- # Dev mode (DEBUG): only hours without microseconds
15
- # Info mode: full date without microseconds
16
- if log_level.upper() == "DEBUG":
17
- timestamper = structlog.processors.TimeStamper(fmt="%H:%M:%S")
18
- else:
19
- timestamper = structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S")
20
-
21
- # Processors that will be used for structlog loggers
10
+ def configure_structlog(log_level: int = logging.INFO) -> None:
11
+ """Configure structlog with shared processors following canonical pattern."""
12
+ # Shared processors for all structlog loggers
22
13
  processors: list[Processor] = [
14
+ structlog.contextvars.merge_contextvars, # For request context in web apps
23
15
  structlog.stdlib.filter_by_level,
16
+ structlog.stdlib.add_log_level,
17
+ structlog.stdlib.add_logger_name,
24
18
  ]
25
19
 
26
- # Only add logger name if NOT in INFO mode
27
- if log_level.upper() != "INFO":
28
- processors.append(structlog.stdlib.add_logger_name)
29
-
30
- processors.extend(
31
- [
32
- structlog.stdlib.add_log_level,
33
- structlog.stdlib.PositionalArgumentsFormatter(),
34
- timestamper,
35
- structlog.processors.StackInfoRenderer(),
36
- structlog.processors.format_exc_info,
37
- structlog.processors.UnicodeDecoder(),
38
- ]
39
- )
40
-
41
- # Only add CallsiteParameterAdder if NOT in INFO mode
42
- if log_level.upper() != "INFO":
20
+ # Add debug-specific processors
21
+ if log_level < logging.INFO:
22
+ # Dev mode (DEBUG): add callsite information
43
23
  processors.append(
44
24
  structlog.processors.CallsiteParameterAdder(
45
25
  parameters=[
@@ -49,92 +29,115 @@ def configure_structlog(json_logs: bool = False, log_level: str = "INFO") -> Non
49
29
  )
50
30
  )
51
31
 
52
- # This wrapper passes the event dictionary to the ProcessorFormatter
53
- # so we don't double-render
54
- processors.append(structlog.stdlib.ProcessorFormatter.wrap_for_formatter)
32
+ # Common processors for all log levels
33
+ processors.extend(
34
+ [
35
+ # Use human-readable timestamp for structlog logs in debug mode, normal otherwise
36
+ structlog.processors.TimeStamper(
37
+ fmt="%H:%M:%S" if log_level < logging.INFO else "%Y-%m-%d %H:%M:%S"
38
+ ),
39
+ structlog.processors.StackInfoRenderer(),
40
+ structlog.dev.set_exc_info, # Handle exceptions properly
41
+ # This MUST be the last processor - allows different renderers per handler
42
+ structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
43
+ ]
44
+ )
55
45
 
56
46
  structlog.configure(
57
47
  processors=processors,
58
48
  context_class=dict,
59
49
  logger_factory=structlog.stdlib.LoggerFactory(),
60
50
  wrapper_class=structlog.stdlib.BoundLogger,
61
- cache_logger_on_first_use=False, # Don't cache to allow reconfiguration
51
+ cache_logger_on_first_use=True, # Cache for performance
62
52
  )
63
53
 
64
54
 
65
55
  def setup_logging(
66
- json_logs: bool = False, log_level: str = "INFO", log_file: str | None = None
56
+ json_logs: bool = False, log_level_name: str = "DEBUG", log_file: str | None = None
67
57
  ) -> BoundLogger:
68
58
  """
69
- Setup logging for the entire application including uvicorn and fastapi.
59
+ Setup logging for the entire application using canonical structlog pattern.
70
60
  Returns a structlog logger instance.
71
61
  """
72
- # Set the log level for the root logger first so structlog can see it
62
+ log_level = getattr(logging, log_level_name.upper(), logging.INFO)
63
+
64
+ # Get root logger and set level BEFORE configuring structlog
73
65
  root_logger = logging.getLogger()
74
- root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
66
+ root_logger.setLevel(log_level)
75
67
 
76
- # Configure structlog after setting the log level
77
- configure_structlog(json_logs=json_logs, log_level=log_level)
68
+ # 1. Configure structlog with shared processors
69
+ configure_structlog(log_level=log_level)
78
70
 
79
- # Create a handler that will format stdlib logs through structlog
80
- handler = logging.StreamHandler(sys.stdout)
81
- handler.setLevel(getattr(logging, log_level.upper(), logging.INFO))
71
+ # 2. Setup root logger handlers
72
+ root_logger.handlers = [] # Clear any existing handlers
82
73
 
83
- # Use the appropriate renderer based on json_logs setting
84
- renderer = (
85
- structlog.processors.JSONRenderer()
86
- if json_logs
87
- else structlog.dev.ConsoleRenderer()
88
- )
74
+ # 3. Create shared processors for foreign (stdlib) logs
75
+ shared_processors = [
76
+ structlog.contextvars.merge_contextvars,
77
+ structlog.stdlib.add_log_level,
78
+ structlog.stdlib.add_logger_name,
79
+ structlog.dev.set_exc_info,
80
+ ]
89
81
 
90
- # Use ProcessorFormatter to handle both structlog and stdlib logs
91
- # Use the same timestamp format for foreign logs
92
- if log_level.upper() == "DEBUG":
93
- foreign_timestamper = structlog.processors.TimeStamper(fmt="%H:%M:%S")
94
- else:
95
- foreign_timestamper = structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S")
82
+ # Add debug processors if needed
83
+ if log_level < logging.INFO:
84
+ shared_processors.append(
85
+ structlog.processors.CallsiteParameterAdder( # type: ignore[arg-type]
86
+ parameters=[
87
+ structlog.processors.CallsiteParameter.FILENAME,
88
+ structlog.processors.CallsiteParameter.LINENO,
89
+ ]
90
+ )
91
+ )
96
92
 
97
- # Build foreign_pre_chain conditionally
98
- foreign_pre_chain: list[Processor] = [structlog.stdlib.add_log_level]
93
+ # Add appropriate timestamper for console vs file
94
+ console_timestamper = (
95
+ structlog.processors.TimeStamper(fmt="%H:%M:%S")
96
+ if log_level < logging.INFO
97
+ else structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S")
98
+ )
99
99
 
100
- # Only add logger name if NOT in INFO mode
101
- if log_level.upper() != "INFO":
102
- foreign_pre_chain.append(structlog.stdlib.add_logger_name)
100
+ file_timestamper = structlog.processors.TimeStamper(fmt="iso")
103
101
 
104
- foreign_pre_chain.append(foreign_timestamper)
102
+ # 4. Setup console handler with ConsoleRenderer
103
+ console_handler = logging.StreamHandler(sys.stdout)
104
+ console_handler.setLevel(log_level)
105
+ console_renderer = (
106
+ structlog.processors.JSONRenderer()
107
+ if json_logs
108
+ else structlog.dev.ConsoleRenderer()
109
+ )
105
110
 
106
- handler.setFormatter(
111
+ # Console gets human-readable timestamps for both structlog and stdlib logs
112
+ console_processors = shared_processors + [console_timestamper]
113
+ console_handler.setFormatter(
107
114
  structlog.stdlib.ProcessorFormatter(
108
- processor=renderer,
109
- foreign_pre_chain=foreign_pre_chain,
115
+ foreign_pre_chain=console_processors,
116
+ processor=console_renderer,
110
117
  )
111
118
  )
119
+ root_logger.addHandler(console_handler)
112
120
 
113
- # Configure root logger (level already set above)
114
- handlers: list[logging.Handler] = [handler]
115
-
116
- # Add file handler if log_file is specified
121
+ # 5. Setup file handler with JSONRenderer (if log_file provided)
117
122
  if log_file:
118
- from pathlib import Path
119
-
120
123
  # Ensure parent directory exists
121
124
  log_path = Path(log_file)
122
125
  log_path.parent.mkdir(parents=True, exist_ok=True)
123
126
 
124
- # Create a file handler that always outputs JSON
125
- file_handler = logging.FileHandler(log_file, encoding="utf-8")
126
- file_handler.setLevel(getattr(logging, log_level.upper(), logging.INFO))
127
+ file_handler = logging.FileHandler(log_file, encoding="utf-8", delay=True)
128
+ file_handler.setLevel(log_level)
129
+
130
+ # File gets ISO timestamps for both structlog and stdlib logs
131
+ file_processors = shared_processors + [file_timestamper]
127
132
  file_handler.setFormatter(
128
133
  structlog.stdlib.ProcessorFormatter(
134
+ foreign_pre_chain=file_processors,
129
135
  processor=structlog.processors.JSONRenderer(),
130
- foreign_pre_chain=foreign_pre_chain,
131
136
  )
132
137
  )
133
- handlers.append(file_handler)
138
+ root_logger.addHandler(file_handler)
134
139
 
135
- root_logger.handlers = handlers
136
-
137
- # Make sure uvicorn and fastapi loggers use our configuration
140
+ # 6. Configure stdlib loggers to propagate to our handlers
138
141
  for logger_name in [
139
142
  "uvicorn",
140
143
  "uvicorn.access",
@@ -146,27 +149,23 @@ def setup_logging(
146
149
  logger.handlers = [] # Remove default handlers
147
150
  logger.propagate = True # Use root logger's handlers
148
151
 
149
- # Set uvicorn loggers to WARNING when app log level is INFO to reduce noise
150
- if logger_name.startswith("uvicorn") and log_level.upper() == "INFO":
152
+ # In DEBUG mode, let all logs through at DEBUG level
153
+ # Otherwise, reduce uvicorn noise by setting to WARNING
154
+ if log_level == logging.DEBUG:
155
+ logger.setLevel(logging.DEBUG)
156
+ elif logger_name.startswith("uvicorn"):
151
157
  logger.setLevel(logging.WARNING)
152
158
  else:
153
- logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
159
+ logger.setLevel(log_level)
154
160
 
155
161
  # Configure httpx logger separately - INFO when app is DEBUG, WARNING otherwise
156
162
  httpx_logger = logging.getLogger("httpx")
157
- httpx_logger.handlers = [] # Remove default handlers
158
- httpx_logger.propagate = True # Use root logger's handlers
159
- if log_level.upper() == "DEBUG":
160
- httpx_logger.setLevel(logging.INFO)
161
- else:
162
- httpx_logger.setLevel(logging.WARNING)
163
-
164
- # Set noisy HTTP-related loggers to WARNING when app log level >= WARNING, else use app log level
165
- app_log_level = getattr(logging, log_level.upper(), logging.INFO)
166
- noisy_log_level = (
167
- logging.WARNING if app_log_level <= logging.WARNING else app_log_level
168
- )
163
+ httpx_logger.handlers = []
164
+ httpx_logger.propagate = True
165
+ httpx_logger.setLevel(logging.INFO if log_level < logging.INFO else logging.WARNING)
169
166
 
167
+ # Set noisy HTTP-related loggers to WARNING
168
+ noisy_log_level = logging.WARNING if log_level <= logging.WARNING else log_level
170
169
  for noisy_logger_name in [
171
170
  "urllib3",
172
171
  "urllib3.connectionpool",
@@ -174,10 +173,13 @@ def setup_logging(
174
173
  "aiohttp",
175
174
  "httpcore",
176
175
  "httpcore.http11",
176
+ "fastapi_mcp",
177
+ "sse_starlette",
178
+ "mcp",
177
179
  ]:
178
180
  noisy_logger = logging.getLogger(noisy_logger_name)
179
- noisy_logger.handlers = [] # Remove default handlers
180
- noisy_logger.propagate = True # Use root logger's handlers
181
+ noisy_logger.handlers = []
182
+ noisy_logger.propagate = True
181
183
  noisy_logger.setLevel(noisy_log_level)
182
184
 
183
185
  return structlog.get_logger() # type: ignore[no-any-return]
@@ -2,7 +2,7 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from collections.abc import Awaitable, Callable
5
- from typing import Any, Optional, Protocol, runtime_checkable
5
+ from typing import Protocol, runtime_checkable
6
6
 
7
7
  from ccproxy.core.types import ProxyRequest, ProxyResponse
8
8
 
ccproxy/core/proxy.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Core proxy abstractions for handling HTTP and WebSocket connections."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable
4
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
5
5
 
6
6
  from ccproxy.core.types import ProxyRequest, ProxyResponse
7
7
 
@@ -1,7 +1,7 @@
1
1
  """Core transformer abstractions for request/response transformation."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import TYPE_CHECKING, Any, Optional, Protocol, TypeVar, runtime_checkable
4
+ from typing import TYPE_CHECKING, Any, Protocol, TypeVar, runtime_checkable
5
5
 
6
6
  from structlog import get_logger
7
7
 
ccproxy/core/types.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  from dataclasses import dataclass, field
4
4
  from enum import Enum
5
- from typing import Any, Optional, Union
5
+ from typing import Any
6
6
 
7
7
  from pydantic import BaseModel, ConfigDict, Field
8
8
 
ccproxy/docker/models.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  import platform
5
5
  from pathlib import Path
6
- from typing import ClassVar, Literal
6
+ from typing import ClassVar
7
7
 
8
8
  from pydantic import BaseModel, Field, field_validator
9
9
 
@@ -3,11 +3,8 @@
3
3
  from collections.abc import Awaitable
4
4
  from pathlib import Path
5
5
  from typing import (
6
- TYPE_CHECKING,
7
- Any,
8
6
  Protocol,
9
7
  TypeAlias,
10
- TypeVar,
11
8
  runtime_checkable,
12
9
  )
13
10
 
@@ -1,5 +1,26 @@
1
1
  """Pydantic models for Claude Proxy API Server."""
2
2
 
3
+ from .claude_sdk import (
4
+ AssistantMessage,
5
+ ContentBlock,
6
+ ExtendedContentBlock,
7
+ ResultMessage,
8
+ ResultMessageBlock,
9
+ SDKContentBlock,
10
+ SDKMessageMode,
11
+ TextBlock,
12
+ ToolResultBlock,
13
+ ToolResultSDKBlock,
14
+ ToolUseBlock,
15
+ ToolUseSDKBlock,
16
+ UserMessage,
17
+ convert_sdk_result_message,
18
+ convert_sdk_system_message,
19
+ convert_sdk_text_block,
20
+ convert_sdk_tool_result_block,
21
+ convert_sdk_tool_use_block,
22
+ to_sdk_variant,
23
+ )
3
24
  from .messages import (
4
25
  MessageContentBlock,
5
26
  MessageCreateParams,
@@ -67,6 +88,26 @@ __all__ = [
67
88
  "StreamEventType",
68
89
  "ToolChoiceType",
69
90
  "ToolType",
91
+ # Claude SDK models
92
+ "AssistantMessage",
93
+ "ContentBlock",
94
+ "ExtendedContentBlock",
95
+ "ResultMessage",
96
+ "ResultMessageBlock",
97
+ "SDKContentBlock",
98
+ "SDKMessageMode",
99
+ "TextBlock",
100
+ "ToolResultBlock",
101
+ "ToolResultSDKBlock",
102
+ "ToolUseBlock",
103
+ "ToolUseSDKBlock",
104
+ "UserMessage",
105
+ "convert_sdk_result_message",
106
+ "convert_sdk_system_message",
107
+ "convert_sdk_text_block",
108
+ "convert_sdk_tool_result_block",
109
+ "convert_sdk_tool_use_block",
110
+ "to_sdk_variant",
70
111
  # Message models
71
112
  "MessageContentBlock",
72
113
  "MessageCreateParams",