ccproxy-api 0.1.2__py3-none-any.whl → 0.1.4__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/openai/__init__.py +1 -2
- ccproxy/adapters/openai/adapter.py +218 -180
- ccproxy/adapters/openai/streaming.py +247 -65
- ccproxy/api/__init__.py +0 -3
- ccproxy/api/app.py +173 -40
- ccproxy/api/dependencies.py +62 -3
- ccproxy/api/middleware/errors.py +3 -7
- ccproxy/api/middleware/headers.py +0 -2
- ccproxy/api/middleware/logging.py +4 -3
- ccproxy/api/middleware/request_content_logging.py +297 -0
- ccproxy/api/middleware/request_id.py +5 -0
- ccproxy/api/middleware/server_header.py +0 -4
- ccproxy/api/routes/__init__.py +9 -1
- ccproxy/api/routes/claude.py +23 -32
- ccproxy/api/routes/health.py +58 -4
- ccproxy/api/routes/mcp.py +171 -0
- ccproxy/api/routes/metrics.py +4 -8
- ccproxy/api/routes/permissions.py +217 -0
- ccproxy/api/routes/proxy.py +0 -53
- ccproxy/api/services/__init__.py +6 -0
- ccproxy/api/services/permission_service.py +368 -0
- ccproxy/api/ui/__init__.py +6 -0
- ccproxy/api/ui/permission_handler_protocol.py +33 -0
- ccproxy/api/ui/terminal_permission_handler.py +593 -0
- ccproxy/auth/conditional.py +2 -2
- ccproxy/auth/dependencies.py +1 -1
- ccproxy/auth/oauth/models.py +0 -1
- ccproxy/auth/oauth/routes.py +1 -3
- ccproxy/auth/storage/json_file.py +0 -1
- ccproxy/auth/storage/keyring.py +0 -3
- ccproxy/claude_sdk/__init__.py +2 -0
- ccproxy/claude_sdk/client.py +91 -8
- ccproxy/claude_sdk/converter.py +405 -210
- ccproxy/claude_sdk/options.py +76 -29
- ccproxy/claude_sdk/parser.py +200 -0
- ccproxy/claude_sdk/streaming.py +286 -0
- ccproxy/cli/commands/__init__.py +5 -2
- ccproxy/cli/commands/auth.py +2 -4
- ccproxy/cli/commands/permission_handler.py +553 -0
- ccproxy/cli/commands/serve.py +30 -12
- ccproxy/cli/docker/params.py +0 -4
- ccproxy/cli/helpers.py +0 -2
- ccproxy/cli/main.py +5 -16
- ccproxy/cli/options/claude_options.py +19 -1
- ccproxy/cli/options/core_options.py +0 -3
- ccproxy/cli/options/security_options.py +0 -2
- ccproxy/cli/options/server_options.py +3 -2
- ccproxy/config/auth.py +0 -1
- ccproxy/config/claude.py +78 -2
- ccproxy/config/discovery.py +0 -1
- ccproxy/config/docker_settings.py +0 -1
- ccproxy/config/loader.py +1 -4
- ccproxy/config/scheduler.py +20 -0
- ccproxy/config/security.py +7 -2
- ccproxy/config/server.py +5 -0
- ccproxy/config/settings.py +13 -7
- ccproxy/config/validators.py +1 -1
- ccproxy/core/async_utils.py +1 -4
- ccproxy/core/errors.py +45 -1
- ccproxy/core/http_transformers.py +4 -3
- ccproxy/core/interfaces.py +2 -2
- ccproxy/core/logging.py +97 -95
- ccproxy/core/middleware.py +1 -1
- ccproxy/core/proxy.py +1 -1
- ccproxy/core/transformers.py +1 -1
- ccproxy/core/types.py +1 -1
- ccproxy/docker/models.py +1 -1
- ccproxy/docker/protocol.py +0 -3
- ccproxy/models/__init__.py +41 -0
- ccproxy/models/claude_sdk.py +420 -0
- ccproxy/models/messages.py +45 -18
- ccproxy/models/permissions.py +115 -0
- ccproxy/models/requests.py +1 -1
- ccproxy/models/responses.py +29 -2
- ccproxy/observability/access_logger.py +1 -2
- ccproxy/observability/context.py +17 -1
- ccproxy/observability/metrics.py +1 -3
- ccproxy/observability/pushgateway.py +0 -2
- ccproxy/observability/stats_printer.py +2 -4
- ccproxy/observability/storage/duckdb_simple.py +1 -1
- ccproxy/observability/storage/models.py +0 -1
- ccproxy/pricing/cache.py +0 -1
- ccproxy/pricing/loader.py +5 -21
- ccproxy/pricing/updater.py +0 -1
- ccproxy/scheduler/__init__.py +1 -0
- ccproxy/scheduler/core.py +6 -6
- ccproxy/scheduler/manager.py +35 -7
- ccproxy/scheduler/registry.py +1 -1
- ccproxy/scheduler/tasks.py +127 -2
- ccproxy/services/claude_sdk_service.py +220 -328
- ccproxy/services/credentials/manager.py +0 -1
- ccproxy/services/credentials/oauth_client.py +1 -2
- ccproxy/services/proxy_service.py +93 -222
- ccproxy/testing/config.py +1 -1
- ccproxy/testing/mock_responses.py +0 -1
- ccproxy/utils/model_mapping.py +197 -0
- ccproxy/utils/models_provider.py +150 -0
- ccproxy/utils/simple_request_logger.py +284 -0
- ccproxy/utils/version_checker.py +184 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/METADATA +63 -2
- ccproxy_api-0.1.4.dist-info/RECORD +166 -0
- ccproxy/cli/commands/permission.py +0 -128
- ccproxy_api-0.1.2.dist-info/RECORD +0 -150
- /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/entry_points.txt +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.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
|
|
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
|
-
|
|
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
|
-
#
|
|
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=
|
|
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(
|
|
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
|
-
#
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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,
|
|
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
|
-
#
|
|
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
|
-
|
|
709
|
-
|
|
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
|
-
|
|
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