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
@@ -22,7 +22,6 @@ from ccproxy.auth.models import (
22
22
  from ccproxy.auth.storage import JsonFileTokenStorage as JsonFileStorage
23
23
  from ccproxy.auth.storage import TokenStorage as CredentialsStorageBackend
24
24
  from ccproxy.config.auth import AuthSettings
25
- from ccproxy.services.credentials.config import CredentialsConfig
26
25
  from ccproxy.services.credentials.oauth_client import OAuthClient
27
26
 
28
27
 
@@ -4,13 +4,12 @@ import asyncio
4
4
  import base64
5
5
  import hashlib
6
6
  import secrets
7
- import time
8
7
  import urllib.parse
9
8
  import webbrowser
10
9
  from datetime import UTC, datetime
11
10
  from http.server import BaseHTTPRequestHandler, HTTPServer
12
11
  from threading import Thread
13
- from typing import Any, Optional
12
+ from typing import Any
14
13
  from urllib.parse import parse_qs, urlparse
15
14
 
16
15
  import httpx
@@ -2,7 +2,6 @@
2
2
 
3
3
  import asyncio
4
4
  import json
5
- import logging
6
5
  import os
7
6
  import random
8
7
  import time
@@ -15,7 +14,6 @@ import httpx
15
14
  import structlog
16
15
  from fastapi import HTTPException, Request
17
16
  from fastapi.responses import StreamingResponse
18
- from pydantic import BaseModel
19
17
  from typing_extensions import TypedDict
20
18
 
21
19
  from ccproxy.config.settings import Settings
@@ -33,6 +31,10 @@ from ccproxy.observability import (
33
31
  from ccproxy.observability.access_logger import log_request_access
34
32
  from ccproxy.services.credentials.manager import CredentialsManager
35
33
  from ccproxy.testing import RealisticMockResponseGenerator
34
+ from ccproxy.utils.simple_request_logger import (
35
+ append_streaming_log,
36
+ write_request_log,
37
+ )
36
38
 
37
39
 
38
40
  if TYPE_CHECKING:
@@ -120,14 +122,10 @@ class ProxyService:
120
122
  self._verbose_api = (
121
123
  os.environ.get("CCPROXY_VERBOSE_API", "false").lower() == "true"
122
124
  )
123
- self._request_log_dir = os.environ.get("CCPROXY_REQUEST_LOG_DIR")
124
-
125
- # Create request log directory if specified
126
- if self._request_log_dir and self._verbose_api:
127
- Path(self._request_log_dir).mkdir(parents=True, exist_ok=True)
125
+ # Note: Request logging is now handled by simple_request_logger utility
126
+ # which checks CCPROXY_LOG_REQUESTS and CCPROXY_REQUEST_LOG_DIR independently
128
127
 
129
- # Track current request ID for logging
130
- self._current_request_id: str | None = None
128
+ # Request context is now passed as parameters to methods
131
129
 
132
130
  def _init_proxy_url(self) -> str | None:
133
131
  """Initialize proxy URL from environment variables."""
@@ -195,10 +193,6 @@ class ProxyService:
195
193
  model, streaming = self._extract_request_metadata(body)
196
194
  endpoint = path.split("/")[-1] if path else "unknown"
197
195
 
198
- # Handle /v1/models endpoint specially
199
- if path == "/v1/models":
200
- return await self.handle_models_request(headers, timeout)
201
-
202
196
  # Use existing context from request if available, otherwise create new one
203
197
  if request and hasattr(request, "state") and hasattr(request.state, "context"):
204
198
  # Use existing context from middleware
@@ -237,9 +231,6 @@ class ProxyService:
237
231
  )
238
232
 
239
233
  async with context_manager as ctx:
240
- # Store the current request ID for file logging
241
- self._current_request_id = ctx.request_id
242
-
243
234
  try:
244
235
  # 1. Authentication - get access token
245
236
  async with timed_operation("oauth_token", ctx.request_id):
@@ -299,7 +290,7 @@ class ProxyService:
299
290
  logger.debug("non_streaming_response_detected")
300
291
 
301
292
  # Log the outgoing request if verbose API logging is enabled
302
- self._log_verbose_api_request(transformed_request)
293
+ await self._log_verbose_api_request(transformed_request, ctx)
303
294
 
304
295
  # Handle regular request
305
296
  async with timed_operation("api_call", ctx.request_id) as api_op:
@@ -322,8 +313,8 @@ class ProxyService:
322
313
  api_op["duration_seconds"] = api_duration
323
314
 
324
315
  # Log the received response if verbose API logging is enabled
325
- self._log_verbose_api_response(
326
- status_code, response_headers, response_body
316
+ await self._log_verbose_api_response(
317
+ status_code, response_headers, response_body, ctx
327
318
  )
328
319
 
329
320
  # 4. Response transformation
@@ -439,9 +430,6 @@ class ProxyService:
439
430
  # Re-raise the exception without transformation
440
431
  # Let higher layers handle specific error types
441
432
  raise
442
- finally:
443
- # Reset current request ID
444
- self._current_request_id = None
445
433
 
446
434
  async def _get_access_token(self) -> str:
447
435
  """Get access token for upstream authentication.
@@ -483,7 +471,7 @@ class ProxyService:
483
471
  logger.debug(
484
472
  "credential_check_failed",
485
473
  error=str(e),
486
- exc_info=logger.isEnabledFor(logging.DEBUG),
474
+ exc_info=True,
487
475
  )
488
476
 
489
477
  raise HTTPException(
@@ -624,7 +612,9 @@ class ProxyService:
624
612
  for k, v in headers.items()
625
613
  }
626
614
 
627
- def _log_verbose_api_request(self, request_data: RequestData) -> None:
615
+ async def _log_verbose_api_request(
616
+ self, request_data: RequestData, ctx: "RequestContext"
617
+ ) -> None:
628
618
  """Log details of an outgoing API request if verbose logging is enabled."""
629
619
  if not self._verbose_api:
630
620
  return
@@ -656,21 +646,27 @@ class ProxyService:
656
646
  body_preview=body_preview,
657
647
  )
658
648
 
659
- # Write to individual file if directory is specified
660
- # Note: We cannot get request ID here since this is called from multiple places
661
- # Request ID will be determined within _write_request_to_file method
662
- self._write_request_to_file(
663
- "request",
664
- {
649
+ # Use new request logging system
650
+ request_id = ctx.request_id
651
+ timestamp = ctx.get_log_timestamp_prefix()
652
+ await write_request_log(
653
+ request_id=request_id,
654
+ log_type="upstream_request",
655
+ data={
665
656
  "method": request_data["method"],
666
657
  "url": request_data["url"],
667
658
  "headers": dict(request_data["headers"]), # Don't redact in file
668
659
  "body": full_body,
669
660
  },
661
+ timestamp=timestamp,
670
662
  )
671
663
 
672
- def _log_verbose_api_response(
673
- self, status_code: int, headers: dict[str, str], body: bytes
664
+ async def _log_verbose_api_response(
665
+ self,
666
+ status_code: int,
667
+ headers: dict[str, str],
668
+ body: bytes,
669
+ ctx: "RequestContext",
674
670
  ) -> None:
675
671
  """Log details of a received API response if verbose logging is enabled."""
676
672
  if not self._verbose_api:
@@ -692,7 +688,7 @@ class ProxyService:
692
688
  body_preview=body_preview,
693
689
  )
694
690
 
695
- # Write to individual file if directory is specified
691
+ # Use new request logging system
696
692
  full_body = None
697
693
  if body:
698
694
  try:
@@ -705,13 +701,18 @@ class ProxyService:
705
701
  except Exception:
706
702
  full_body = f"<binary data of length {len(body)}>"
707
703
 
708
- self._write_request_to_file(
709
- "response",
710
- {
704
+ # Use new request logging system
705
+ request_id = ctx.request_id
706
+ timestamp = ctx.get_log_timestamp_prefix()
707
+ await write_request_log(
708
+ request_id=request_id,
709
+ log_type="upstream_response",
710
+ data={
711
711
  "status_code": status_code,
712
712
  "headers": dict(headers), # Don't redact in file
713
713
  "body": full_body,
714
714
  },
715
+ timestamp=timestamp,
715
716
  )
716
717
 
717
718
  def _should_stream_response(self, headers: dict[str, str]) -> bool:
@@ -774,7 +775,7 @@ class ProxyService:
774
775
  StreamingResponse or error response tuple
775
776
  """
776
777
  # Log the outgoing request if verbose API logging is enabled
777
- self._log_verbose_api_request(request_data)
778
+ await self._log_verbose_api_request(request_data, ctx)
778
779
 
779
780
  # First, make the request and check for errors before streaming
780
781
  proxy_url = self._proxy_url
@@ -799,8 +800,8 @@ class ProxyService:
799
800
  error_content = await response.aread()
800
801
 
801
802
  # Log the full error response body
802
- self._log_verbose_api_response(
803
- response.status_code, dict(response.headers), error_content
803
+ await self._log_verbose_api_response(
804
+ response.status_code, dict(response.headers), error_content, ctx
804
805
  )
805
806
 
806
807
  logger.info(
@@ -899,6 +900,25 @@ class ProxyService:
899
900
  response_status = response.status_code
900
901
  response_headers = dict(response.headers)
901
902
 
903
+ # Log upstream response headers for streaming
904
+ if self._verbose_api:
905
+ request_id = ctx.request_id
906
+ timestamp = ctx.get_log_timestamp_prefix()
907
+ await write_request_log(
908
+ request_id=request_id,
909
+ log_type="upstream_response_headers",
910
+ data={
911
+ "status_code": response.status_code,
912
+ "headers": dict(response.headers),
913
+ "stream_type": "anthropic_sse"
914
+ if not self.response_transformer._is_openai_request(
915
+ original_path
916
+ )
917
+ else "openai_sse",
918
+ },
919
+ timestamp=timestamp,
920
+ )
921
+
902
922
  # Transform streaming response
903
923
  is_openai = self.response_transformer._is_openai_request(
904
924
  original_path
@@ -911,11 +931,23 @@ class ProxyService:
911
931
  # Transform Anthropic SSE to OpenAI SSE format using adapter
912
932
  logger.debug("sse_transform_start", path=original_path)
913
933
 
934
+ # Get timestamp once for all streaming chunks
935
+ request_id = ctx.request_id
936
+ timestamp = ctx.get_log_timestamp_prefix()
937
+
914
938
  async for (
915
939
  transformed_chunk
916
940
  ) in self._transform_anthropic_to_openai_stream(
917
941
  response, original_path
918
942
  ):
943
+ # Log transformed streaming chunk
944
+ await append_streaming_log(
945
+ request_id=request_id,
946
+ log_type="upstream_streaming",
947
+ data=transformed_chunk,
948
+ timestamp=timestamp,
949
+ )
950
+
919
951
  logger.debug(
920
952
  "transformed_chunk_yielded",
921
953
  chunk_size=len(transformed_chunk),
@@ -930,10 +962,22 @@ class ProxyService:
930
962
  # Use cached verbose streaming configuration
931
963
  verbose_streaming = self._verbose_streaming
932
964
 
965
+ # Get timestamp once for all streaming chunks
966
+ request_id = ctx.request_id
967
+ timestamp = ctx.get_log_timestamp_prefix()
968
+
933
969
  async for chunk in response.aiter_bytes():
934
970
  if chunk:
935
971
  chunk_count += 1
936
972
 
973
+ # Log raw streaming chunk
974
+ await append_streaming_log(
975
+ request_id=request_id,
976
+ log_type="upstream_streaming",
977
+ data=chunk,
978
+ timestamp=timestamp,
979
+ )
980
+
937
981
  # Compact logging for content_block_delta events
938
982
  chunk_str = chunk.decode("utf-8", errors="replace")
939
983
 
@@ -1048,12 +1092,21 @@ class ProxyService:
1048
1092
 
1049
1093
  # Parse SSE chunks from response into dict stream
1050
1094
  async def sse_to_dict_stream() -> AsyncGenerator[dict[str, object], None]:
1095
+ chunk_count = 0
1051
1096
  async for line in response.aiter_lines():
1052
1097
  if line.startswith("data: "):
1053
1098
  data_str = line[6:].strip()
1054
1099
  if data_str and data_str != "[DONE]":
1055
1100
  try:
1056
- yield json.loads(data_str)
1101
+ chunk_data = json.loads(data_str)
1102
+ chunk_count += 1
1103
+ logger.debug(
1104
+ "proxy_anthropic_chunk_received",
1105
+ chunk_count=chunk_count,
1106
+ chunk_type=chunk_data.get("type"),
1107
+ chunk=chunk_data,
1108
+ )
1109
+ yield chunk_data
1057
1110
  except json.JSONDecodeError:
1058
1111
  logger.warning("sse_parse_failed", data=data_str)
1059
1112
  continue
@@ -1065,43 +1118,6 @@ class ProxyService:
1065
1118
  sse_line = f"data: {json.dumps(openai_chunk)}\n\n"
1066
1119
  yield sse_line.encode("utf-8")
1067
1120
 
1068
- def _write_request_to_file(self, data_type: str, data: dict[str, Any]) -> None:
1069
- """Write request or response data to individual file if logging directory is configured.
1070
-
1071
- Args:
1072
- data_type: Type of data ("request" or "response")
1073
- data: The data to write
1074
- """
1075
- if not self._request_log_dir or not self._verbose_api:
1076
- return
1077
-
1078
- # Use the current request ID stored during request handling
1079
- request_id = self._current_request_id or "unknown"
1080
-
1081
- # Create filename with request ID and data type
1082
- filename = f"{request_id}_{data_type}.json"
1083
- file_path = Path(self._request_log_dir) / filename
1084
-
1085
- try:
1086
- # Write JSON data to file
1087
- with file_path.open("w", encoding="utf-8") as f:
1088
- json.dump(data, f, indent=2, default=str)
1089
-
1090
- logger.debug(
1091
- "request_data_logged_to_file",
1092
- request_id=request_id,
1093
- data_type=data_type,
1094
- file_path=str(file_path),
1095
- )
1096
-
1097
- except Exception as e:
1098
- logger.error(
1099
- "failed_to_write_request_log_file",
1100
- request_id=request_id,
1101
- data_type=data_type,
1102
- error=str(e),
1103
- )
1104
-
1105
1121
  def _extract_message_type_from_body(self, body: bytes | None) -> str:
1106
1122
  """Extract message type from request body for realistic response generation."""
1107
1123
  if not body:
@@ -1382,151 +1398,6 @@ class ProxyService:
1382
1398
 
1383
1399
  return openai_chunks
1384
1400
 
1385
- async def handle_models_request(
1386
- self,
1387
- headers: dict[str, str],
1388
- timeout: float = 240.0,
1389
- ) -> tuple[int, dict[str, str], bytes]:
1390
- """Handle a /v1/models request to list available models.
1391
-
1392
- Since Anthropic API doesn't support /v1/models endpoint,
1393
- returns a hardcoded list of Anthropic models and recent OpenAI models.
1394
-
1395
- Args:
1396
- headers: Request headers
1397
- timeout: Request timeout in seconds
1398
-
1399
- Returns:
1400
- Tuple of (status_code, headers, body)
1401
- """
1402
- # Define hardcoded Anthropic models
1403
- anthropic_models = [
1404
- {
1405
- "type": "model",
1406
- "id": "claude-opus-4-20250514",
1407
- "display_name": "Claude Opus 4",
1408
- "created_at": 1747526400, # 2025-05-22
1409
- },
1410
- {
1411
- "type": "model",
1412
- "id": "claude-sonnet-4-20250514",
1413
- "display_name": "Claude Sonnet 4",
1414
- "created_at": 1747526400, # 2025-05-22
1415
- },
1416
- {
1417
- "type": "model",
1418
- "id": "claude-3-7-sonnet-20250219",
1419
- "display_name": "Claude Sonnet 3.7",
1420
- "created_at": 1740268800, # 2025-02-24
1421
- },
1422
- {
1423
- "type": "model",
1424
- "id": "claude-3-5-sonnet-20241022",
1425
- "display_name": "Claude Sonnet 3.5 (New)",
1426
- "created_at": 1729555200, # 2024-10-22
1427
- },
1428
- {
1429
- "type": "model",
1430
- "id": "claude-3-5-haiku-20241022",
1431
- "display_name": "Claude Haiku 3.5",
1432
- "created_at": 1729555200, # 2024-10-22
1433
- },
1434
- {
1435
- "type": "model",
1436
- "id": "claude-3-5-sonnet-20240620",
1437
- "display_name": "Claude Sonnet 3.5 (Old)",
1438
- "created_at": 1718841600, # 2024-06-20
1439
- },
1440
- {
1441
- "type": "model",
1442
- "id": "claude-3-haiku-20240307",
1443
- "display_name": "Claude Haiku 3",
1444
- "created_at": 1709769600, # 2024-03-07
1445
- },
1446
- {
1447
- "type": "model",
1448
- "id": "claude-3-opus-20240229",
1449
- "display_name": "Claude Opus 3",
1450
- "created_at": 1709164800, # 2024-02-29
1451
- },
1452
- ]
1453
-
1454
- # Define recent OpenAI models to include (GPT-4 variants and O1 models)
1455
- openai_models = [
1456
- {
1457
- "id": "gpt-4o",
1458
- "object": "model",
1459
- "created": 1715367049,
1460
- "owned_by": "openai",
1461
- },
1462
- {
1463
- "id": "gpt-4o-mini",
1464
- "object": "model",
1465
- "created": 1721172741,
1466
- "owned_by": "openai",
1467
- },
1468
- {
1469
- "id": "gpt-4-turbo",
1470
- "object": "model",
1471
- "created": 1712361441,
1472
- "owned_by": "openai",
1473
- },
1474
- {
1475
- "id": "gpt-4-turbo-preview",
1476
- "object": "model",
1477
- "created": 1706037777,
1478
- "owned_by": "openai",
1479
- },
1480
- {
1481
- "id": "o1",
1482
- "object": "model",
1483
- "created": 1734375816,
1484
- "owned_by": "openai",
1485
- },
1486
- {
1487
- "id": "o1-mini",
1488
- "object": "model",
1489
- "created": 1725649008,
1490
- "owned_by": "openai",
1491
- },
1492
- {
1493
- "id": "o1-preview",
1494
- "object": "model",
1495
- "created": 1725648897,
1496
- "owned_by": "openai",
1497
- },
1498
- {
1499
- "id": "o3",
1500
- "object": "model",
1501
- "created": 1744225308,
1502
- "owned_by": "openai",
1503
- },
1504
- {
1505
- "id": "o3-mini",
1506
- "object": "model",
1507
- "created": 1737146383,
1508
- "owned_by": "openai",
1509
- },
1510
- ]
1511
-
1512
- # Combine models - mixed format with both Anthropic and OpenAI fields
1513
- combined_response = {
1514
- "data": anthropic_models + openai_models,
1515
- "has_more": False,
1516
- "object": "list", # Add OpenAI-style field
1517
- }
1518
-
1519
- # Serialize response
1520
- response_body = json.dumps(combined_response).encode("utf-8")
1521
-
1522
- # Create response headers
1523
- response_headers = {
1524
- "content-type": "application/json",
1525
- "content-length": str(len(response_body)),
1526
- }
1527
-
1528
- return 200, response_headers, response_body
1529
-
1530
1401
  async def close(self) -> None:
1531
1402
  """Close any resources held by the proxy service."""
1532
1403
  if self.proxy_client:
ccproxy/testing/config.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Configuration models for testing utilities."""
2
2
 
3
- from datetime import UTC, datetime
3
+ from datetime import datetime
4
4
  from pathlib import Path
5
5
  from typing import Any, Literal
6
6
 
@@ -1,6 +1,5 @@
1
1
  """Mock response generation for realistic testing."""
2
2
 
3
- import json
4
3
  import random
5
4
  import time
6
5
  from typing import Any