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.
Files changed (42) hide show
  1. ccproxy/_version.py +2 -2
  2. ccproxy/adapters/codex/__init__.py +11 -0
  3. ccproxy/adapters/openai/models.py +1 -1
  4. ccproxy/adapters/openai/response_adapter.py +355 -0
  5. ccproxy/adapters/openai/response_models.py +178 -0
  6. ccproxy/api/app.py +31 -3
  7. ccproxy/api/dependencies.py +1 -8
  8. ccproxy/api/middleware/errors.py +15 -7
  9. ccproxy/api/routes/codex.py +1251 -0
  10. ccproxy/api/routes/health.py +228 -3
  11. ccproxy/auth/openai/__init__.py +13 -0
  12. ccproxy/auth/openai/credentials.py +166 -0
  13. ccproxy/auth/openai/oauth_client.py +334 -0
  14. ccproxy/auth/openai/storage.py +184 -0
  15. ccproxy/claude_sdk/options.py +1 -1
  16. ccproxy/cli/commands/auth.py +398 -1
  17. ccproxy/cli/commands/serve.py +3 -1
  18. ccproxy/config/claude.py +1 -1
  19. ccproxy/config/codex.py +100 -0
  20. ccproxy/config/scheduler.py +8 -8
  21. ccproxy/config/settings.py +19 -0
  22. ccproxy/core/codex_transformers.py +389 -0
  23. ccproxy/core/http_transformers.py +153 -2
  24. ccproxy/data/claude_headers_fallback.json +37 -0
  25. ccproxy/data/codex_headers_fallback.json +14 -0
  26. ccproxy/models/detection.py +82 -0
  27. ccproxy/models/requests.py +22 -0
  28. ccproxy/models/responses.py +16 -0
  29. ccproxy/scheduler/manager.py +2 -2
  30. ccproxy/scheduler/tasks.py +105 -65
  31. ccproxy/services/claude_detection_service.py +7 -33
  32. ccproxy/services/codex_detection_service.py +252 -0
  33. ccproxy/services/proxy_service.py +530 -0
  34. ccproxy/utils/model_mapping.py +7 -5
  35. ccproxy/utils/startup_helpers.py +205 -12
  36. ccproxy/utils/version_checker.py +6 -0
  37. ccproxy_api-0.1.7.dist-info/METADATA +615 -0
  38. {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/RECORD +41 -28
  39. ccproxy_api-0.1.5.dist-info/METADATA +0 -396
  40. {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/WHEEL +0 -0
  41. {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/entry_points.txt +0 -0
  42. {ccproxy_api-0.1.5.dist-info → ccproxy_api-0.1.7.dist-info}/licenses/LICENSE +0 -0
@@ -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 validate_authentication_startup(app: FastAPI, settings: Settings) -> None:
38
- """Validate authentication credentials at startup.
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
- "auth_token_valid",
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("auth_token_valid", credentials_path=str(validation.path))
70
+ logger.debug(
71
+ "claude_token_valid", credentials_path=str(validation.path)
72
+ )
67
73
  elif validation.expired:
68
74
  logger.warning(
69
- "auth_token_expired",
70
- message="Authentication token has expired. Please run 'ccproxy auth login' to refresh.",
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
- "auth_token_invalid",
76
- message="Authentication token is invalid. Please run 'ccproxy auth login'.",
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
- "auth_token_not_found",
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
- "auth_token_validation_error",
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
 
@@ -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(