ccproxy-api 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/_version.py +2 -2
- ccproxy/adapters/codex/__init__.py +11 -0
- ccproxy/adapters/openai/models.py +1 -1
- ccproxy/adapters/openai/response_adapter.py +355 -0
- ccproxy/adapters/openai/response_models.py +178 -0
- ccproxy/api/app.py +31 -3
- ccproxy/api/dependencies.py +1 -8
- ccproxy/api/middleware/errors.py +15 -7
- ccproxy/api/routes/codex.py +1251 -0
- ccproxy/api/routes/health.py +228 -3
- ccproxy/auth/openai/__init__.py +13 -0
- ccproxy/auth/openai/credentials.py +166 -0
- ccproxy/auth/openai/oauth_client.py +334 -0
- ccproxy/auth/openai/storage.py +184 -0
- ccproxy/claude_sdk/options.py +1 -1
- ccproxy/cli/commands/auth.py +398 -1
- ccproxy/cli/commands/serve.py +3 -1
- ccproxy/config/claude.py +1 -1
- ccproxy/config/codex.py +100 -0
- ccproxy/config/scheduler.py +8 -8
- ccproxy/config/settings.py +19 -0
- ccproxy/core/codex_transformers.py +389 -0
- ccproxy/core/http_transformers.py +153 -2
- ccproxy/data/claude_headers_fallback.json +37 -0
- ccproxy/data/codex_headers_fallback.json +14 -0
- ccproxy/models/detection.py +82 -0
- ccproxy/models/requests.py +22 -0
- ccproxy/models/responses.py +16 -0
- ccproxy/scheduler/manager.py +2 -2
- ccproxy/scheduler/tasks.py +105 -65
- ccproxy/services/claude_detection_service.py +7 -33
- ccproxy/services/codex_detection_service.py +252 -0
- ccproxy/services/proxy_service.py +530 -0
- ccproxy/utils/model_mapping.py +7 -5
- ccproxy/utils/startup_helpers.py +205 -12
- ccproxy/utils/version_checker.py +6 -0
- ccproxy_api-0.1.7.dist-info/METADATA +615 -0
- {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/RECORD +41 -28
- ccproxy_api-0.1.5.dist-info/METADATA +0 -396
- {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/entry_points.txt +0 -0
- {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/licenses/LICENSE +0 -0
ccproxy/utils/startup_helpers.py
CHANGED
|
@@ -15,6 +15,7 @@ from fastapi import FastAPI
|
|
|
15
15
|
|
|
16
16
|
from ccproxy.auth.credentials_adapter import CredentialsAuthManager
|
|
17
17
|
from ccproxy.auth.exceptions import CredentialsNotFoundError
|
|
18
|
+
from ccproxy.auth.openai.credentials import OpenAITokenManager
|
|
18
19
|
from ccproxy.observability import get_metrics
|
|
19
20
|
|
|
20
21
|
# Note: get_claude_cli_info is imported locally to avoid circular imports
|
|
@@ -23,6 +24,7 @@ from ccproxy.scheduler.errors import SchedulerError
|
|
|
23
24
|
from ccproxy.scheduler.manager import start_scheduler, stop_scheduler
|
|
24
25
|
from ccproxy.services.claude_detection_service import ClaudeDetectionService
|
|
25
26
|
from ccproxy.services.claude_sdk_service import ClaudeSDKService
|
|
27
|
+
from ccproxy.services.codex_detection_service import CodexDetectionService
|
|
26
28
|
from ccproxy.services.credentials.manager import CredentialsManager
|
|
27
29
|
|
|
28
30
|
|
|
@@ -34,8 +36,10 @@ if TYPE_CHECKING:
|
|
|
34
36
|
logger = structlog.get_logger(__name__)
|
|
35
37
|
|
|
36
38
|
|
|
37
|
-
async def
|
|
38
|
-
|
|
39
|
+
async def validate_claude_authentication_startup(
|
|
40
|
+
app: FastAPI, settings: Settings
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Validate Claude authentication credentials at startup.
|
|
39
43
|
|
|
40
44
|
Args:
|
|
41
45
|
app: FastAPI application instance
|
|
@@ -57,40 +61,145 @@ async def validate_authentication_startup(app: FastAPI, settings: Settings) -> N
|
|
|
57
61
|
/ 3600
|
|
58
62
|
)
|
|
59
63
|
logger.debug(
|
|
60
|
-
"
|
|
64
|
+
"claude_token_valid",
|
|
61
65
|
expires_in_hours=hours_until_expiry,
|
|
62
66
|
subscription_type=oauth_token.subscription_type,
|
|
63
67
|
credentials_path=str(validation.path) if validation.path else None,
|
|
64
68
|
)
|
|
65
69
|
else:
|
|
66
|
-
logger.debug(
|
|
70
|
+
logger.debug(
|
|
71
|
+
"claude_token_valid", credentials_path=str(validation.path)
|
|
72
|
+
)
|
|
67
73
|
elif validation.expired:
|
|
68
74
|
logger.warning(
|
|
69
|
-
"
|
|
70
|
-
message="
|
|
75
|
+
"claude_token_expired",
|
|
76
|
+
message="Claude authentication token has expired. Please run 'ccproxy auth login' to refresh.",
|
|
71
77
|
credentials_path=str(validation.path) if validation.path else None,
|
|
72
78
|
)
|
|
73
79
|
else:
|
|
74
80
|
logger.warning(
|
|
75
|
-
"
|
|
76
|
-
message="
|
|
81
|
+
"claude_token_invalid",
|
|
82
|
+
message="Claude authentication token is invalid. Please run 'ccproxy auth login'.",
|
|
77
83
|
credentials_path=str(validation.path) if validation.path else None,
|
|
78
84
|
)
|
|
79
85
|
except CredentialsNotFoundError:
|
|
80
86
|
logger.warning(
|
|
81
|
-
"
|
|
82
|
-
message="No authentication credentials found. Please run 'ccproxy auth login' to authenticate.",
|
|
87
|
+
"claude_token_not_found",
|
|
88
|
+
message="No Claude authentication credentials found. Please run 'ccproxy auth login' to authenticate.",
|
|
83
89
|
searched_paths=settings.auth.storage.storage_paths,
|
|
84
90
|
)
|
|
85
91
|
except Exception as e:
|
|
86
92
|
logger.error(
|
|
87
|
-
"
|
|
93
|
+
"claude_token_validation_error",
|
|
88
94
|
error=str(e),
|
|
89
|
-
message="Failed to validate authentication token. The server will continue without authentication.",
|
|
95
|
+
message="Failed to validate Claude authentication token. The server will continue without Claude authentication.",
|
|
90
96
|
exc_info=True,
|
|
91
97
|
)
|
|
92
98
|
|
|
93
99
|
|
|
100
|
+
async def validate_codex_authentication_startup(
|
|
101
|
+
app: FastAPI, settings: Settings
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Validate Codex (OpenAI) authentication credentials at startup.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
app: FastAPI application instance
|
|
107
|
+
settings: Application settings
|
|
108
|
+
"""
|
|
109
|
+
# Skip codex authentication validation if codex is disabled
|
|
110
|
+
if not settings.codex.enabled:
|
|
111
|
+
logger.debug("codex_token_validation_skipped", reason="codex_disabled")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
token_manager = OpenAITokenManager()
|
|
116
|
+
credentials = await token_manager.load_credentials()
|
|
117
|
+
|
|
118
|
+
if not credentials:
|
|
119
|
+
logger.warning(
|
|
120
|
+
"codex_token_not_found",
|
|
121
|
+
message="No Codex authentication credentials found. Please run 'ccproxy auth login-openai' to authenticate.",
|
|
122
|
+
location=token_manager.get_storage_location(),
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
if not credentials.active:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"codex_token_inactive",
|
|
129
|
+
message="Codex authentication credentials are inactive. Please run 'ccproxy auth login-openai' to refresh.",
|
|
130
|
+
location=token_manager.get_storage_location(),
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if credentials.is_expired():
|
|
135
|
+
logger.warning(
|
|
136
|
+
"codex_token_expired",
|
|
137
|
+
message="Codex authentication token has expired. Please run 'ccproxy auth login-openai' to refresh.",
|
|
138
|
+
location=token_manager.get_storage_location(),
|
|
139
|
+
expires_at=credentials.expires_at.isoformat(),
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
hours_until_expiry = int(credentials.expires_in_seconds() / 3600)
|
|
143
|
+
logger.debug(
|
|
144
|
+
"codex_token_valid",
|
|
145
|
+
expires_in_hours=hours_until_expiry,
|
|
146
|
+
account_id=credentials.account_id,
|
|
147
|
+
location=token_manager.get_storage_location(),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(
|
|
152
|
+
"codex_token_validation_error",
|
|
153
|
+
error=str(e),
|
|
154
|
+
message="Failed to validate Codex authentication token. The server will continue without Codex authentication.",
|
|
155
|
+
exc_info=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def check_version_updates_startup(app: FastAPI, settings: Settings) -> None:
|
|
160
|
+
"""Trigger version update check at startup.
|
|
161
|
+
|
|
162
|
+
Manually runs the version check task once during application startup,
|
|
163
|
+
before the scheduler starts managing periodic checks.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
app: FastAPI application instance
|
|
167
|
+
settings: Application settings
|
|
168
|
+
"""
|
|
169
|
+
# Skip version check if disabled by settings
|
|
170
|
+
if not settings.scheduler.version_check_enabled:
|
|
171
|
+
logger.debug("version_check_startup_disabled")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
# Import locally to avoid circular imports and create task instance
|
|
176
|
+
from ccproxy.scheduler.tasks import VersionUpdateCheckTask
|
|
177
|
+
|
|
178
|
+
# Create a temporary task instance for startup check
|
|
179
|
+
version_task = VersionUpdateCheckTask(
|
|
180
|
+
name="version_check_startup",
|
|
181
|
+
interval_seconds=settings.scheduler.version_check_interval_hours * 3600,
|
|
182
|
+
enabled=True,
|
|
183
|
+
version_check_cache_ttl_hours=settings.scheduler.version_check_cache_ttl_hours,
|
|
184
|
+
skip_first_scheduled_run=False,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Run the version check once and wait for it to complete
|
|
188
|
+
success = await version_task.run()
|
|
189
|
+
|
|
190
|
+
if success:
|
|
191
|
+
logger.debug("version_check_startup_completed")
|
|
192
|
+
else:
|
|
193
|
+
logger.debug("version_check_startup_failed")
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.debug(
|
|
197
|
+
"version_check_startup_error",
|
|
198
|
+
error=str(e),
|
|
199
|
+
error_type=type(e).__name__,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
94
203
|
async def check_claude_cli_startup(app: FastAPI, settings: Settings) -> None:
|
|
95
204
|
"""Check Claude CLI availability at startup.
|
|
96
205
|
|
|
@@ -126,6 +235,41 @@ async def check_claude_cli_startup(app: FastAPI, settings: Settings) -> None:
|
|
|
126
235
|
)
|
|
127
236
|
|
|
128
237
|
|
|
238
|
+
async def check_codex_cli_startup(app: FastAPI, settings: Settings) -> None:
|
|
239
|
+
"""Check Codex CLI availability at startup.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
app: FastAPI application instance
|
|
243
|
+
settings: Application settings
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
from ccproxy.api.routes.health import get_codex_cli_info
|
|
247
|
+
|
|
248
|
+
codex_info = await get_codex_cli_info()
|
|
249
|
+
|
|
250
|
+
if codex_info.status == "available":
|
|
251
|
+
logger.info(
|
|
252
|
+
"codex_cli_available",
|
|
253
|
+
status=codex_info.status,
|
|
254
|
+
version=codex_info.version,
|
|
255
|
+
binary_path=codex_info.binary_path,
|
|
256
|
+
)
|
|
257
|
+
else:
|
|
258
|
+
logger.warning(
|
|
259
|
+
"codex_cli_unavailable",
|
|
260
|
+
status=codex_info.status,
|
|
261
|
+
error=codex_info.error,
|
|
262
|
+
binary_path=codex_info.binary_path,
|
|
263
|
+
message=f"Codex CLI status: {codex_info.status}",
|
|
264
|
+
)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(
|
|
267
|
+
"codex_cli_check_failed",
|
|
268
|
+
error=str(e),
|
|
269
|
+
message="Failed to check Codex CLI status during startup",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
129
273
|
async def initialize_log_storage_startup(app: FastAPI, settings: Settings) -> None:
|
|
130
274
|
"""Initialize log storage if needed and backend is DuckDB.
|
|
131
275
|
|
|
@@ -261,6 +405,55 @@ async def initialize_claude_detection_startup(app: FastAPI, settings: Settings)
|
|
|
261
405
|
app.state.claude_detection_service = detection_service
|
|
262
406
|
|
|
263
407
|
|
|
408
|
+
async def initialize_codex_detection_startup(app: FastAPI, settings: Settings) -> None:
|
|
409
|
+
"""Initialize Codex detection service.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
app: FastAPI application instance
|
|
413
|
+
settings: Application settings
|
|
414
|
+
"""
|
|
415
|
+
# Skip codex detection if codex is disabled
|
|
416
|
+
if not settings.codex.enabled:
|
|
417
|
+
logger.debug("codex_detection_skipped", reason="codex_disabled")
|
|
418
|
+
detection_service = CodexDetectionService(settings)
|
|
419
|
+
app.state.codex_detection_data = detection_service._get_fallback_data()
|
|
420
|
+
app.state.codex_detection_service = detection_service
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
# Check if Codex CLI is available before attempting header detection
|
|
424
|
+
from ccproxy.api.routes.health import get_codex_cli_info
|
|
425
|
+
|
|
426
|
+
codex_info = await get_codex_cli_info()
|
|
427
|
+
if codex_info.status != "available":
|
|
428
|
+
logger.debug(
|
|
429
|
+
"codex_detection_skipped",
|
|
430
|
+
reason="codex_cli_not_available",
|
|
431
|
+
status=codex_info.status,
|
|
432
|
+
)
|
|
433
|
+
detection_service = CodexDetectionService(settings)
|
|
434
|
+
app.state.codex_detection_data = detection_service._get_fallback_data()
|
|
435
|
+
app.state.codex_detection_service = detection_service
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
logger.debug("initializing_codex_detection")
|
|
440
|
+
detection_service = CodexDetectionService(settings)
|
|
441
|
+
codex_data = await detection_service.initialize_detection()
|
|
442
|
+
app.state.codex_detection_data = codex_data
|
|
443
|
+
app.state.codex_detection_service = detection_service
|
|
444
|
+
logger.debug(
|
|
445
|
+
"codex_detection_completed",
|
|
446
|
+
version=codex_data.codex_version,
|
|
447
|
+
cached_at=codex_data.cached_at.isoformat(),
|
|
448
|
+
)
|
|
449
|
+
except Exception as e:
|
|
450
|
+
logger.error("codex_detection_startup_failed", error=str(e))
|
|
451
|
+
# Continue startup with fallback - detection service will provide fallback data
|
|
452
|
+
detection_service = CodexDetectionService(settings)
|
|
453
|
+
app.state.codex_detection_data = detection_service._get_fallback_data()
|
|
454
|
+
app.state.codex_detection_service = detection_service
|
|
455
|
+
|
|
456
|
+
|
|
264
457
|
async def initialize_claude_sdk_startup(app: FastAPI, settings: Settings) -> None:
|
|
265
458
|
"""Initialize ClaudeSDKService and store in app state.
|
|
266
459
|
|
ccproxy/utils/version_checker.py
CHANGED
|
@@ -94,6 +94,12 @@ def compare_versions(current: str, latest: str) -> bool:
|
|
|
94
94
|
try:
|
|
95
95
|
current_parsed = pkg_version.parse(current)
|
|
96
96
|
latest_parsed = pkg_version.parse(latest)
|
|
97
|
+
|
|
98
|
+
# For dev versions, compare base version instead
|
|
99
|
+
if current_parsed.is_devrelease:
|
|
100
|
+
current_base = pkg_version.parse(current_parsed.base_version)
|
|
101
|
+
return latest_parsed > current_base
|
|
102
|
+
|
|
97
103
|
return latest_parsed > current_parsed
|
|
98
104
|
except Exception as e:
|
|
99
105
|
logger.error(
|