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/config/claude.py
CHANGED
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
import structlog
|
|
10
|
-
from pydantic import BaseModel, Field, field_validator
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
11
11
|
|
|
12
12
|
from ccproxy.core.async_utils import get_package_dir, patched_typing
|
|
13
13
|
|
|
@@ -19,14 +19,29 @@ with patched_typing():
|
|
|
19
19
|
logger = structlog.get_logger(__name__)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def _create_default_claude_code_options(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
def _create_default_claude_code_options(
|
|
23
|
+
builtin_permissions: bool = True,
|
|
24
|
+
continue_conversation: bool = False,
|
|
25
|
+
) -> ClaudeCodeOptions:
|
|
26
|
+
"""Create ClaudeCodeOptions with default values.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
builtin_permissions: Whether to include built-in permission handling defaults
|
|
30
|
+
"""
|
|
31
|
+
if builtin_permissions:
|
|
32
|
+
return ClaudeCodeOptions(
|
|
33
|
+
continue_conversation=continue_conversation,
|
|
34
|
+
mcp_servers={
|
|
35
|
+
"confirmation": {"type": "sse", "url": "http://127.0.0.1:8000/mcp"}
|
|
36
|
+
},
|
|
37
|
+
permission_prompt_tool_name="mcp__confirmation__check_permission",
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
return ClaudeCodeOptions(
|
|
41
|
+
mcp_servers={},
|
|
42
|
+
permission_prompt_tool_name=None,
|
|
43
|
+
continue_conversation=continue_conversation,
|
|
44
|
+
)
|
|
30
45
|
|
|
31
46
|
|
|
32
47
|
class SDKMessageMode(str, Enum):
|
|
@@ -42,6 +57,94 @@ class SDKMessageMode(str, Enum):
|
|
|
42
57
|
FORMATTED = "formatted"
|
|
43
58
|
|
|
44
59
|
|
|
60
|
+
class SystemPromptInjectionMode(str, Enum):
|
|
61
|
+
"""Modes for system prompt injection.
|
|
62
|
+
|
|
63
|
+
- minimal: Only inject Claude Code identification prompt
|
|
64
|
+
- full: Inject all detected system messages from Claude CLI
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
MINIMAL = "minimal"
|
|
68
|
+
FULL = "full"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SessionPoolSettings(BaseModel):
|
|
72
|
+
"""Session pool configuration settings."""
|
|
73
|
+
|
|
74
|
+
enabled: bool = Field(
|
|
75
|
+
default=True, description="Enable session-aware persistent pooling"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
session_ttl: int = Field(
|
|
79
|
+
default=3600,
|
|
80
|
+
ge=60,
|
|
81
|
+
le=86400,
|
|
82
|
+
description="Session time-to-live in seconds (1 minute to 24 hours)",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
max_sessions: int = Field(
|
|
86
|
+
default=1000,
|
|
87
|
+
ge=1,
|
|
88
|
+
le=10000,
|
|
89
|
+
description="Maximum number of concurrent sessions",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
cleanup_interval: int = Field(
|
|
93
|
+
default=300,
|
|
94
|
+
ge=30,
|
|
95
|
+
le=3600,
|
|
96
|
+
description="Session cleanup interval in seconds (30 seconds to 1 hour)",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
idle_threshold: int = Field(
|
|
100
|
+
default=600,
|
|
101
|
+
ge=60,
|
|
102
|
+
le=7200,
|
|
103
|
+
description="Session idle threshold in seconds (1 minute to 2 hours)",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
connection_recovery: bool = Field(
|
|
107
|
+
default=True,
|
|
108
|
+
description="Enable automatic connection recovery for unhealthy sessions",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
stream_first_chunk_timeout: int = Field(
|
|
112
|
+
default=3,
|
|
113
|
+
ge=1,
|
|
114
|
+
le=30,
|
|
115
|
+
description="Stream first chunk timeout in seconds (1-30 seconds)",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
stream_ongoing_timeout: int = Field(
|
|
119
|
+
default=60,
|
|
120
|
+
ge=10,
|
|
121
|
+
le=600,
|
|
122
|
+
description="Stream ongoing timeout in seconds after first chunk (10 seconds to 10 minutes)",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
stream_interrupt_timeout: int = Field(
|
|
126
|
+
default=10,
|
|
127
|
+
ge=2,
|
|
128
|
+
le=60,
|
|
129
|
+
description="Stream interrupt timeout in seconds for SDK and worker operations (2-60 seconds)",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@model_validator(mode="after")
|
|
133
|
+
def validate_timeout_hierarchy(self) -> "SessionPoolSettings":
|
|
134
|
+
"""Ensure stream timeouts are less than session TTL."""
|
|
135
|
+
if self.stream_ongoing_timeout >= self.session_ttl:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"stream_ongoing_timeout ({self.stream_ongoing_timeout}s) must be less than session_ttl ({self.session_ttl}s)"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if self.stream_first_chunk_timeout >= self.stream_ongoing_timeout:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"stream_first_chunk_timeout ({self.stream_first_chunk_timeout}s) must be less than stream_ongoing_timeout ({self.stream_ongoing_timeout}s)"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
|
|
45
148
|
class ClaudeSettings(BaseModel):
|
|
46
149
|
"""Claude-specific configuration settings."""
|
|
47
150
|
|
|
@@ -50,8 +153,13 @@ class ClaudeSettings(BaseModel):
|
|
|
50
153
|
description="Path to Claude CLI executable",
|
|
51
154
|
)
|
|
52
155
|
|
|
53
|
-
|
|
54
|
-
|
|
156
|
+
builtin_permissions: bool = Field(
|
|
157
|
+
default=True,
|
|
158
|
+
description="Whether to enable built-in permission handling infrastructure (MCP server and SSE endpoints). When disabled, users can still configure custom MCP servers and permission tools.",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
code_options: ClaudeCodeOptions | None = Field(
|
|
162
|
+
default=None,
|
|
55
163
|
description="Claude Code SDK options configuration",
|
|
56
164
|
)
|
|
57
165
|
|
|
@@ -60,11 +168,21 @@ class ClaudeSettings(BaseModel):
|
|
|
60
168
|
description="Mode for handling SDK messages from Claude SDK. Options: forward (direct SDK blocks), ignore (skip blocks), formatted (XML tags with JSON data)",
|
|
61
169
|
)
|
|
62
170
|
|
|
171
|
+
system_prompt_injection_mode: SystemPromptInjectionMode = Field(
|
|
172
|
+
default=SystemPromptInjectionMode.MINIMAL,
|
|
173
|
+
description="Mode for system prompt injection. Options: minimal (Claude Code ID only), full (all detected system messages)",
|
|
174
|
+
)
|
|
175
|
+
|
|
63
176
|
pretty_format: bool = Field(
|
|
64
177
|
default=True,
|
|
65
178
|
description="Whether to use pretty formatting (indented JSON, newlines after XML tags, unescaped content). When false: compact JSON, no newlines, escaped content between XML tags",
|
|
66
179
|
)
|
|
67
180
|
|
|
181
|
+
sdk_session_pool: SessionPoolSettings = Field(
|
|
182
|
+
default_factory=SessionPoolSettings,
|
|
183
|
+
description="Configuration settings for session-aware SDK client pooling",
|
|
184
|
+
)
|
|
185
|
+
|
|
68
186
|
@field_validator("cli_path")
|
|
69
187
|
@classmethod
|
|
70
188
|
def validate_claude_cli_path(cls, v: str | None) -> str | None:
|
|
@@ -81,11 +199,16 @@ class ClaudeSettings(BaseModel):
|
|
|
81
199
|
|
|
82
200
|
@field_validator("code_options", mode="before")
|
|
83
201
|
@classmethod
|
|
84
|
-
def validate_claude_code_options(cls, v: Any) -> Any:
|
|
202
|
+
def validate_claude_code_options(cls, v: Any, info: Any) -> Any:
|
|
85
203
|
"""Validate and convert Claude Code options."""
|
|
204
|
+
# Get builtin_permissions setting from the model data
|
|
205
|
+
builtin_permissions = True # default
|
|
206
|
+
if info.data and "builtin_permissions" in info.data:
|
|
207
|
+
builtin_permissions = info.data["builtin_permissions"]
|
|
208
|
+
|
|
86
209
|
if v is None:
|
|
87
|
-
# Create instance with default values
|
|
88
|
-
return _create_default_claude_code_options()
|
|
210
|
+
# Create instance with default values based on builtin_permissions
|
|
211
|
+
return _create_default_claude_code_options(builtin_permissions)
|
|
89
212
|
|
|
90
213
|
# If it's already a ClaudeCodeOptions instance, return as-is
|
|
91
214
|
if isinstance(v, ClaudeCodeOptions):
|
|
@@ -93,16 +216,18 @@ class ClaudeSettings(BaseModel):
|
|
|
93
216
|
|
|
94
217
|
# If it's an empty dict, treat it like None and use defaults
|
|
95
218
|
if isinstance(v, dict) and not v:
|
|
96
|
-
return _create_default_claude_code_options()
|
|
219
|
+
return _create_default_claude_code_options(builtin_permissions)
|
|
97
220
|
|
|
98
221
|
# For non-empty dicts, merge with defaults instead of replacing them
|
|
99
222
|
if isinstance(v, dict):
|
|
100
|
-
# Start with default values
|
|
101
|
-
defaults = _create_default_claude_code_options()
|
|
223
|
+
# Start with default values based on builtin_permissions
|
|
224
|
+
defaults = _create_default_claude_code_options(builtin_permissions)
|
|
102
225
|
|
|
103
226
|
# Extract default values as a dict for merging
|
|
104
227
|
default_values = {
|
|
105
|
-
"mcp_servers": defaults.mcp_servers
|
|
228
|
+
"mcp_servers": dict(defaults.mcp_servers)
|
|
229
|
+
if isinstance(defaults.mcp_servers, dict)
|
|
230
|
+
else {},
|
|
106
231
|
"permission_prompt_tool_name": defaults.permission_prompt_tool_name,
|
|
107
232
|
}
|
|
108
233
|
|
|
@@ -124,18 +249,41 @@ class ClaudeSettings(BaseModel):
|
|
|
124
249
|
if default_value is not None:
|
|
125
250
|
default_values[attr] = default_value
|
|
126
251
|
|
|
252
|
+
# Handle MCP server merging when builtin_permissions is enabled
|
|
253
|
+
if builtin_permissions and "mcp_servers" in v:
|
|
254
|
+
user_mcp_servers = v["mcp_servers"]
|
|
255
|
+
if isinstance(user_mcp_servers, dict):
|
|
256
|
+
# Merge user MCP servers with built-in ones (user takes precedence)
|
|
257
|
+
default_mcp = default_values["mcp_servers"]
|
|
258
|
+
if isinstance(default_mcp, dict):
|
|
259
|
+
merged_mcp_servers = {
|
|
260
|
+
**default_mcp,
|
|
261
|
+
**user_mcp_servers,
|
|
262
|
+
}
|
|
263
|
+
v = {**v, "mcp_servers": merged_mcp_servers}
|
|
264
|
+
|
|
127
265
|
# Merge CLI overrides with defaults (CLI overrides take precedence)
|
|
128
266
|
merged_values = {**default_values, **v}
|
|
129
267
|
|
|
130
268
|
return ClaudeCodeOptions(**merged_values)
|
|
131
269
|
|
|
132
|
-
# Try to convert to
|
|
270
|
+
# Try to convert to ClaudeCodeOptions if possible
|
|
133
271
|
if hasattr(v, "model_dump"):
|
|
134
|
-
return v.model_dump()
|
|
272
|
+
return ClaudeCodeOptions(**v.model_dump())
|
|
135
273
|
elif hasattr(v, "__dict__"):
|
|
136
|
-
return v.__dict__
|
|
137
|
-
|
|
138
|
-
|
|
274
|
+
return ClaudeCodeOptions(**v.__dict__)
|
|
275
|
+
|
|
276
|
+
# Fallback: use default values
|
|
277
|
+
return _create_default_claude_code_options(builtin_permissions)
|
|
278
|
+
|
|
279
|
+
@model_validator(mode="after")
|
|
280
|
+
def validate_code_options_after(self) -> "ClaudeSettings":
|
|
281
|
+
"""Ensure code_options is properly initialized after field validation."""
|
|
282
|
+
if self.code_options is None:
|
|
283
|
+
self.code_options = _create_default_claude_code_options(
|
|
284
|
+
self.builtin_permissions
|
|
285
|
+
)
|
|
286
|
+
return self
|
|
139
287
|
|
|
140
288
|
def find_claude_cli(self) -> tuple[str | None, bool]:
|
|
141
289
|
"""Find Claude CLI executable in PATH or specified location.
|
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/discovery.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
from ccproxy.core.system import get_xdg_config_home
|
|
3
|
+
from ccproxy.core.system import get_xdg_cache_home, get_xdg_config_home
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def find_toml_config_file() -> Path | None:
|
|
@@ -84,3 +84,12 @@ def get_claude_docker_home_dir() -> Path:
|
|
|
84
84
|
Path to the Claude Docker home directory within XDG_DATA_HOME.
|
|
85
85
|
"""
|
|
86
86
|
return get_ccproxy_config_dir() / "home"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_ccproxy_cache_dir() -> Path:
|
|
90
|
+
"""Get the ccproxy cache directory.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Path to the ccproxy cache directory within XDG_CACHE_HOME.
|
|
94
|
+
"""
|
|
95
|
+
return get_xdg_cache_home() / "ccproxy"
|
ccproxy/config/scheduler.py
CHANGED
|
@@ -35,7 +35,7 @@ class SchedulerSettings(BaseSettings):
|
|
|
35
35
|
# Pricing updater task settings
|
|
36
36
|
pricing_update_enabled: bool = Field(
|
|
37
37
|
default=True,
|
|
38
|
-
description="Whether pricing cache update task is enabled",
|
|
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(
|
|
@@ -85,7 +85,7 @@ class SchedulerSettings(BaseSettings):
|
|
|
85
85
|
# Version checking task settings
|
|
86
86
|
version_check_enabled: bool = Field(
|
|
87
87
|
default=True,
|
|
88
|
-
description="Whether version update checking is enabled",
|
|
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:
|
|
@@ -462,10 +481,28 @@ class ConfigurationManager:
|
|
|
462
481
|
claude_settings["cli_path"] = cli_args["claude_cli_path"]
|
|
463
482
|
|
|
464
483
|
# Direct Claude settings (not nested in code_options)
|
|
465
|
-
for key in [
|
|
484
|
+
for key in [
|
|
485
|
+
"sdk_message_mode",
|
|
486
|
+
"system_prompt_injection_mode",
|
|
487
|
+
"builtin_permissions",
|
|
488
|
+
]:
|
|
466
489
|
if cli_args.get(key) is not None:
|
|
467
490
|
claude_settings[key] = cli_args[key]
|
|
468
491
|
|
|
492
|
+
# Handle pool configuration
|
|
493
|
+
if cli_args.get("sdk_pool") is not None:
|
|
494
|
+
claude_settings["sdk_pool"] = {"enabled": cli_args["sdk_pool"]}
|
|
495
|
+
|
|
496
|
+
if cli_args.get("sdk_pool_size") is not None:
|
|
497
|
+
if "sdk_pool" not in claude_settings:
|
|
498
|
+
claude_settings["sdk_pool"] = {}
|
|
499
|
+
claude_settings["sdk_pool"]["pool_size"] = cli_args["sdk_pool_size"]
|
|
500
|
+
|
|
501
|
+
if cli_args.get("sdk_session_pool") is not None:
|
|
502
|
+
claude_settings["sdk_session_pool"] = {
|
|
503
|
+
"enabled": cli_args["sdk_session_pool"]
|
|
504
|
+
}
|
|
505
|
+
|
|
469
506
|
# Claude Code options
|
|
470
507
|
claude_opts = {}
|
|
471
508
|
for key in [
|