ccproxy-api 0.1.4__py3-none-any.whl → 0.1.5__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/adapter.py +1 -1
- ccproxy/adapters/openai/streaming.py +1 -0
- ccproxy/api/app.py +134 -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/proxy.py +25 -6
- ccproxy/api/services/permission_service.py +2 -2
- 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 +5 -0
- 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/serve.py +96 -0
- ccproxy/cli/options/claude_options.py +47 -0
- ccproxy/config/__init__.py +0 -3
- ccproxy/config/claude.py +171 -23
- ccproxy/config/discovery.py +10 -1
- ccproxy/config/scheduler.py +4 -4
- ccproxy/config/settings.py +19 -1
- ccproxy/core/http_transformers.py +305 -73
- ccproxy/core/logging.py +108 -12
- ccproxy/core/transformers.py +5 -0
- ccproxy/models/claude_sdk.py +57 -0
- ccproxy/models/detection.py +126 -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/proxy_service.py +91 -200
- ccproxy/utils/__init__.py +9 -1
- ccproxy/utils/disconnection_monitor.py +83 -0
- ccproxy/utils/id_generator.py +12 -0
- ccproxy/utils/startup_helpers.py +408 -0
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.5.dist-info}/METADATA +29 -2
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.5.dist-info}/RECORD +53 -41
- ccproxy/config/loader.py +0 -105
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.5.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.5.dist-info}/entry_points.txt +0 -0
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.5.dist-info}/licenses/LICENSE +0 -0
ccproxy/scheduler/manager.py
CHANGED
|
@@ -7,6 +7,7 @@ from ccproxy.config.settings import Settings
|
|
|
7
7
|
from .core import Scheduler
|
|
8
8
|
from .registry import register_task
|
|
9
9
|
from .tasks import (
|
|
10
|
+
PoolStatsTask,
|
|
10
11
|
PricingCacheUpdateTask,
|
|
11
12
|
PushgatewayTask,
|
|
12
13
|
StatsPrintingTask,
|
|
@@ -31,6 +32,19 @@ async def setup_scheduler_tasks(scheduler: Scheduler, settings: Settings) -> Non
|
|
|
31
32
|
logger.info("scheduler_disabled")
|
|
32
33
|
return
|
|
33
34
|
|
|
35
|
+
# Log network features status
|
|
36
|
+
logger.info(
|
|
37
|
+
"network_features_status",
|
|
38
|
+
pricing_updates_enabled=scheduler_config.pricing_update_enabled,
|
|
39
|
+
version_check_enabled=scheduler_config.version_check_enabled,
|
|
40
|
+
message=(
|
|
41
|
+
"Network features disabled by default for privacy"
|
|
42
|
+
if not scheduler_config.pricing_update_enabled
|
|
43
|
+
and not scheduler_config.version_check_enabled
|
|
44
|
+
else "Some network features are enabled"
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
34
48
|
# Add pushgateway task if enabled
|
|
35
49
|
if scheduler_config.pushgateway_enabled:
|
|
36
50
|
try:
|
|
@@ -123,21 +137,32 @@ async def setup_scheduler_tasks(scheduler: Scheduler, settings: Settings) -> Non
|
|
|
123
137
|
)
|
|
124
138
|
|
|
125
139
|
|
|
126
|
-
def _register_default_tasks() -> None:
|
|
127
|
-
"""Register default task types in the global registry."""
|
|
140
|
+
def _register_default_tasks(settings: Settings) -> None:
|
|
141
|
+
"""Register default task types in the global registry based on configuration."""
|
|
128
142
|
from .registry import get_task_registry
|
|
129
143
|
|
|
130
144
|
registry = get_task_registry()
|
|
145
|
+
scheduler_config = settings.scheduler
|
|
131
146
|
|
|
132
|
-
# Only register
|
|
133
|
-
if not registry.is_registered(
|
|
147
|
+
# Only register pushgateway task if enabled
|
|
148
|
+
if scheduler_config.pushgateway_enabled and not registry.is_registered(
|
|
149
|
+
"pushgateway"
|
|
150
|
+
):
|
|
134
151
|
register_task("pushgateway", PushgatewayTask)
|
|
135
|
-
|
|
152
|
+
|
|
153
|
+
# Only register stats printing task if enabled
|
|
154
|
+
if scheduler_config.stats_printing_enabled and not registry.is_registered(
|
|
155
|
+
"stats_printing"
|
|
156
|
+
):
|
|
136
157
|
register_task("stats_printing", StatsPrintingTask)
|
|
158
|
+
|
|
159
|
+
# Always register core tasks (not metrics-related)
|
|
137
160
|
if not registry.is_registered("pricing_cache_update"):
|
|
138
161
|
register_task("pricing_cache_update", PricingCacheUpdateTask)
|
|
139
162
|
if not registry.is_registered("version_update_check"):
|
|
140
163
|
register_task("version_update_check", VersionUpdateCheckTask)
|
|
164
|
+
if not registry.is_registered("pool_stats"):
|
|
165
|
+
register_task("pool_stats", PoolStatsTask)
|
|
141
166
|
|
|
142
167
|
|
|
143
168
|
async def start_scheduler(settings: Settings) -> Scheduler | None:
|
|
@@ -156,7 +181,7 @@ async def start_scheduler(settings: Settings) -> Scheduler | None:
|
|
|
156
181
|
return None
|
|
157
182
|
|
|
158
183
|
# Register task types (only when actually starting scheduler)
|
|
159
|
-
_register_default_tasks()
|
|
184
|
+
_register_default_tasks(settings)
|
|
160
185
|
|
|
161
186
|
# Create scheduler with settings
|
|
162
187
|
scheduler = Scheduler(
|
ccproxy/scheduler/tasks.py
CHANGED
|
@@ -483,6 +483,128 @@ class PricingCacheUpdateTask(BaseScheduledTask):
|
|
|
483
483
|
return False
|
|
484
484
|
|
|
485
485
|
|
|
486
|
+
class PoolStatsTask(BaseScheduledTask):
|
|
487
|
+
"""Task for displaying pool statistics periodically."""
|
|
488
|
+
|
|
489
|
+
def __init__(
|
|
490
|
+
self,
|
|
491
|
+
name: str,
|
|
492
|
+
interval_seconds: float,
|
|
493
|
+
enabled: bool = True,
|
|
494
|
+
pool_manager: Any | None = None,
|
|
495
|
+
):
|
|
496
|
+
"""
|
|
497
|
+
Initialize pool stats task.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
name: Task name
|
|
501
|
+
interval_seconds: Interval between stats display
|
|
502
|
+
enabled: Whether task is enabled
|
|
503
|
+
pool_manager: Injected pool manager instance
|
|
504
|
+
"""
|
|
505
|
+
super().__init__(
|
|
506
|
+
name=name,
|
|
507
|
+
interval_seconds=interval_seconds,
|
|
508
|
+
enabled=enabled,
|
|
509
|
+
)
|
|
510
|
+
self._pool_manager = pool_manager
|
|
511
|
+
|
|
512
|
+
async def setup(self) -> None:
|
|
513
|
+
"""Initialize pool manager instance if not injected."""
|
|
514
|
+
if self._pool_manager is None:
|
|
515
|
+
logger.warning(
|
|
516
|
+
"pool_stats_task_no_manager",
|
|
517
|
+
task_name=self.name,
|
|
518
|
+
message="Pool manager not injected, task will be disabled",
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
async def run(self) -> bool:
|
|
522
|
+
"""Display pool statistics."""
|
|
523
|
+
try:
|
|
524
|
+
if not self._pool_manager:
|
|
525
|
+
return True # Not an error, just no pool manager available
|
|
526
|
+
|
|
527
|
+
# Get general pool stats (if available)
|
|
528
|
+
general_pool = getattr(self._pool_manager, "_pool", None)
|
|
529
|
+
general_stats = None
|
|
530
|
+
if general_pool:
|
|
531
|
+
general_stats = general_pool.get_stats()
|
|
532
|
+
|
|
533
|
+
# Get session pool stats
|
|
534
|
+
session_pool = getattr(self._pool_manager, "_session_pool", None)
|
|
535
|
+
session_stats = None
|
|
536
|
+
if session_pool:
|
|
537
|
+
session_stats = await session_pool.get_stats()
|
|
538
|
+
|
|
539
|
+
# Log pool statistics
|
|
540
|
+
logger.debug(
|
|
541
|
+
"pool_stats_report",
|
|
542
|
+
task_name=self.name,
|
|
543
|
+
general_pool={
|
|
544
|
+
"enabled": bool(general_pool),
|
|
545
|
+
"total_clients": general_stats.total_clients
|
|
546
|
+
if general_stats
|
|
547
|
+
else 0,
|
|
548
|
+
"available_clients": general_stats.available_clients
|
|
549
|
+
if general_stats
|
|
550
|
+
else 0,
|
|
551
|
+
"active_clients": general_stats.active_clients
|
|
552
|
+
if general_stats
|
|
553
|
+
else 0,
|
|
554
|
+
"connections_created": general_stats.connections_created
|
|
555
|
+
if general_stats
|
|
556
|
+
else 0,
|
|
557
|
+
"connections_closed": general_stats.connections_closed
|
|
558
|
+
if general_stats
|
|
559
|
+
else 0,
|
|
560
|
+
"acquire_count": general_stats.acquire_count
|
|
561
|
+
if general_stats
|
|
562
|
+
else 0,
|
|
563
|
+
"release_count": general_stats.release_count
|
|
564
|
+
if general_stats
|
|
565
|
+
else 0,
|
|
566
|
+
"health_check_failures": general_stats.health_check_failures
|
|
567
|
+
if general_stats
|
|
568
|
+
else 0,
|
|
569
|
+
}
|
|
570
|
+
if general_pool
|
|
571
|
+
else None,
|
|
572
|
+
session_pool={
|
|
573
|
+
"enabled": session_stats.get("enabled", False)
|
|
574
|
+
if session_stats
|
|
575
|
+
else False,
|
|
576
|
+
"total_sessions": session_stats.get("total_sessions", 0)
|
|
577
|
+
if session_stats
|
|
578
|
+
else 0,
|
|
579
|
+
"active_sessions": session_stats.get("active_sessions", 0)
|
|
580
|
+
if session_stats
|
|
581
|
+
else 0,
|
|
582
|
+
"max_sessions": session_stats.get("max_sessions", 0)
|
|
583
|
+
if session_stats
|
|
584
|
+
else 0,
|
|
585
|
+
"total_messages": session_stats.get("total_messages", 0)
|
|
586
|
+
if session_stats
|
|
587
|
+
else 0,
|
|
588
|
+
"session_ttl": session_stats.get("session_ttl", 0)
|
|
589
|
+
if session_stats
|
|
590
|
+
else 0,
|
|
591
|
+
}
|
|
592
|
+
if session_pool
|
|
593
|
+
else None,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
return True
|
|
597
|
+
|
|
598
|
+
except Exception as e:
|
|
599
|
+
logger.error(
|
|
600
|
+
"pool_stats_task_error",
|
|
601
|
+
task_name=self.name,
|
|
602
|
+
error=str(e),
|
|
603
|
+
error_type=type(e).__name__,
|
|
604
|
+
)
|
|
605
|
+
return False
|
|
606
|
+
|
|
607
|
+
|
|
486
608
|
class VersionUpdateCheckTask(BaseScheduledTask):
|
|
487
609
|
"""Task for checking version updates periodically."""
|
|
488
610
|
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Service for automatically detecting Claude CLI headers at startup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import socket
|
|
9
|
+
import subprocess
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
from fastapi import FastAPI, Request, Response
|
|
14
|
+
|
|
15
|
+
from ccproxy.config.discovery import get_ccproxy_cache_dir
|
|
16
|
+
from ccproxy.config.settings import Settings
|
|
17
|
+
from ccproxy.models.detection import (
|
|
18
|
+
ClaudeCacheData,
|
|
19
|
+
ClaudeCodeHeaders,
|
|
20
|
+
SystemPromptData,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = structlog.get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClaudeDetectionService:
|
|
28
|
+
"""Service for automatically detecting Claude CLI headers at startup."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, settings: Settings) -> None:
|
|
31
|
+
"""Initialize Claude detection service."""
|
|
32
|
+
self.settings = settings
|
|
33
|
+
self.cache_dir = get_ccproxy_cache_dir()
|
|
34
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
self._cached_data: ClaudeCacheData | None = None
|
|
36
|
+
|
|
37
|
+
async def initialize_detection(self) -> ClaudeCacheData:
|
|
38
|
+
"""Initialize Claude detection at startup."""
|
|
39
|
+
try:
|
|
40
|
+
# Get current Claude version
|
|
41
|
+
current_version = await self._get_claude_version()
|
|
42
|
+
|
|
43
|
+
# Try to load from cache first
|
|
44
|
+
detected_data = self._load_from_cache(current_version)
|
|
45
|
+
cached = detected_data is not None
|
|
46
|
+
if cached:
|
|
47
|
+
logger.debug("detection_claude_headers_debug", version=current_version)
|
|
48
|
+
else:
|
|
49
|
+
# No cache or version changed - detect fresh
|
|
50
|
+
detected_data = await self._detect_claude_headers(current_version)
|
|
51
|
+
# Cache the results
|
|
52
|
+
self._save_to_cache(detected_data)
|
|
53
|
+
|
|
54
|
+
self._cached_data = detected_data
|
|
55
|
+
|
|
56
|
+
logger.info(
|
|
57
|
+
"detection_claude_headers_completed",
|
|
58
|
+
version=current_version,
|
|
59
|
+
cached=cached,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# TODO: add proper testing without claude cli installed
|
|
63
|
+
if detected_data is None:
|
|
64
|
+
raise ValueError("Claude detection failed")
|
|
65
|
+
return detected_data
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.warning("detection_claude_headers_failed", fallback=True, error=e)
|
|
69
|
+
# Return fallback data
|
|
70
|
+
fallback_data = self._get_fallback_data()
|
|
71
|
+
self._cached_data = fallback_data
|
|
72
|
+
return fallback_data
|
|
73
|
+
|
|
74
|
+
def get_cached_data(self) -> ClaudeCacheData | None:
|
|
75
|
+
"""Get currently cached detection data."""
|
|
76
|
+
return self._cached_data
|
|
77
|
+
|
|
78
|
+
async def _get_claude_version(self) -> str:
|
|
79
|
+
"""Get Claude CLI version."""
|
|
80
|
+
try:
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
["claude", "--version"],
|
|
83
|
+
capture_output=True,
|
|
84
|
+
text=True,
|
|
85
|
+
timeout=10,
|
|
86
|
+
)
|
|
87
|
+
if result.returncode == 0:
|
|
88
|
+
# Extract version from output like "1.0.60 (Claude Code)"
|
|
89
|
+
version_line = result.stdout.strip()
|
|
90
|
+
if "/" in version_line:
|
|
91
|
+
# Handle "claude-cli/1.0.60" format
|
|
92
|
+
version_line = version_line.split("/")[-1]
|
|
93
|
+
if "(" in version_line:
|
|
94
|
+
# Handle "1.0.60 (Claude Code)" format - extract just the version number
|
|
95
|
+
return version_line.split("(")[0].strip()
|
|
96
|
+
return version_line
|
|
97
|
+
else:
|
|
98
|
+
raise RuntimeError(f"Claude version command failed: {result.stderr}")
|
|
99
|
+
|
|
100
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
|
|
101
|
+
logger.warning("claude_version_detection_failed", error=str(e))
|
|
102
|
+
return "unknown"
|
|
103
|
+
|
|
104
|
+
async def _detect_claude_headers(self, version: str) -> ClaudeCacheData:
|
|
105
|
+
"""Execute Claude CLI with proxy to capture headers and system prompt."""
|
|
106
|
+
# Data captured from the request
|
|
107
|
+
captured_data: dict[str, Any] = {}
|
|
108
|
+
|
|
109
|
+
async def capture_handler(request: Request) -> Response:
|
|
110
|
+
"""Capture the Claude CLI request."""
|
|
111
|
+
captured_data["headers"] = dict(request.headers)
|
|
112
|
+
captured_data["body"] = await request.body()
|
|
113
|
+
# Return a mock response to satisfy Claude CLI
|
|
114
|
+
return Response(
|
|
115
|
+
content='{"type": "message", "content": [{"type": "text", "text": "Test response"}]}',
|
|
116
|
+
media_type="application/json",
|
|
117
|
+
status_code=200,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Create temporary FastAPI app
|
|
121
|
+
temp_app = FastAPI()
|
|
122
|
+
temp_app.post("/v1/messages")(capture_handler)
|
|
123
|
+
|
|
124
|
+
# Find available port
|
|
125
|
+
sock = socket.socket()
|
|
126
|
+
sock.bind(("", 0))
|
|
127
|
+
port = sock.getsockname()[1]
|
|
128
|
+
sock.close()
|
|
129
|
+
|
|
130
|
+
# Start server in background
|
|
131
|
+
from uvicorn import Config, Server
|
|
132
|
+
|
|
133
|
+
config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
|
|
134
|
+
server = Server(config)
|
|
135
|
+
|
|
136
|
+
server_task = asyncio.create_task(server.serve())
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# Wait for server to start
|
|
140
|
+
await asyncio.sleep(0.5)
|
|
141
|
+
|
|
142
|
+
# Execute Claude CLI with proxy
|
|
143
|
+
env = {**dict(os.environ), "ANTHROPIC_BASE_URL": f"http://127.0.0.1:{port}"}
|
|
144
|
+
|
|
145
|
+
process = await asyncio.create_subprocess_exec(
|
|
146
|
+
"claude",
|
|
147
|
+
"test",
|
|
148
|
+
env=env,
|
|
149
|
+
stdout=asyncio.subprocess.PIPE,
|
|
150
|
+
stderr=asyncio.subprocess.PIPE,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Wait for process with timeout
|
|
154
|
+
try:
|
|
155
|
+
await asyncio.wait_for(process.wait(), timeout=30)
|
|
156
|
+
except TimeoutError:
|
|
157
|
+
process.kill()
|
|
158
|
+
await process.wait()
|
|
159
|
+
|
|
160
|
+
# Stop server
|
|
161
|
+
server.should_exit = True
|
|
162
|
+
await server_task
|
|
163
|
+
|
|
164
|
+
if not captured_data:
|
|
165
|
+
raise RuntimeError("Failed to capture Claude CLI request")
|
|
166
|
+
|
|
167
|
+
# Extract headers and system prompt
|
|
168
|
+
headers = self._extract_headers(captured_data["headers"])
|
|
169
|
+
system_prompt = self._extract_system_prompt(captured_data["body"])
|
|
170
|
+
|
|
171
|
+
return ClaudeCacheData(
|
|
172
|
+
claude_version=version, headers=headers, system_prompt=system_prompt
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
# Ensure server is stopped
|
|
177
|
+
server.should_exit = True
|
|
178
|
+
if not server_task.done():
|
|
179
|
+
await server_task
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
def _load_from_cache(self, version: str) -> ClaudeCacheData | None:
|
|
183
|
+
"""Load cached data for specific Claude version."""
|
|
184
|
+
cache_file = self.cache_dir / f"claude_headers_{version}.json"
|
|
185
|
+
|
|
186
|
+
if not cache_file.exists():
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
with cache_file.open("r") as f:
|
|
191
|
+
data = json.load(f)
|
|
192
|
+
return ClaudeCacheData.model_validate(data)
|
|
193
|
+
except Exception:
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def _save_to_cache(self, data: ClaudeCacheData) -> None:
|
|
197
|
+
"""Save detection data to cache."""
|
|
198
|
+
cache_file = self.cache_dir / f"claude_headers_{data.claude_version}.json"
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
with cache_file.open("w") as f:
|
|
202
|
+
json.dump(data.model_dump(), f, indent=2, default=str)
|
|
203
|
+
logger.debug(
|
|
204
|
+
"cache_saved", file=str(cache_file), version=data.claude_version
|
|
205
|
+
)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.warning("cache_save_failed", file=str(cache_file), error=str(e))
|
|
208
|
+
|
|
209
|
+
def _extract_headers(self, headers: dict[str, str]) -> ClaudeCodeHeaders:
|
|
210
|
+
"""Extract Claude CLI headers from captured request."""
|
|
211
|
+
try:
|
|
212
|
+
return ClaudeCodeHeaders.model_validate(headers)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error("header_extraction_failed", error=str(e))
|
|
215
|
+
raise ValueError(f"Failed to extract required headers: {e}") from e
|
|
216
|
+
|
|
217
|
+
def _extract_system_prompt(self, body: bytes) -> SystemPromptData:
|
|
218
|
+
"""Extract system prompt from captured request body."""
|
|
219
|
+
try:
|
|
220
|
+
data = json.loads(body.decode("utf-8"))
|
|
221
|
+
system_content = data.get("system")
|
|
222
|
+
|
|
223
|
+
if system_content is None:
|
|
224
|
+
raise ValueError("No system field found in request body")
|
|
225
|
+
|
|
226
|
+
return SystemPromptData(system_field=system_content)
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error("system_prompt_extraction_failed", error=str(e))
|
|
230
|
+
raise ValueError(f"Failed to extract system prompt: {e}") from e
|
|
231
|
+
|
|
232
|
+
def _get_fallback_data(self) -> ClaudeCacheData:
|
|
233
|
+
"""Get fallback data when detection fails."""
|
|
234
|
+
logger.warning("using_fallback_claude_data")
|
|
235
|
+
|
|
236
|
+
# Use existing hardcoded values as fallback
|
|
237
|
+
fallback_headers = ClaudeCodeHeaders(
|
|
238
|
+
**{
|
|
239
|
+
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
|
240
|
+
"anthropic-version": "2023-06-01",
|
|
241
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
242
|
+
"x-app": "cli",
|
|
243
|
+
"User-Agent": "claude-cli/1.0.60 (external, cli)",
|
|
244
|
+
"X-Stainless-Lang": "js",
|
|
245
|
+
"X-Stainless-Retry-Count": "0",
|
|
246
|
+
"X-Stainless-Timeout": "60",
|
|
247
|
+
"X-Stainless-Package-Version": "0.55.1",
|
|
248
|
+
"X-Stainless-OS": "Linux",
|
|
249
|
+
"X-Stainless-Arch": "x64",
|
|
250
|
+
"X-Stainless-Runtime": "node",
|
|
251
|
+
"X-Stainless-Runtime-Version": "v24.3.0",
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
fallback_prompt = SystemPromptData(
|
|
256
|
+
system_field=[
|
|
257
|
+
{
|
|
258
|
+
"type": "text",
|
|
259
|
+
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
|
|
260
|
+
"cache_control": {"type": "ephemeral"},
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return ClaudeCacheData(
|
|
266
|
+
claude_version="fallback",
|
|
267
|
+
headers=fallback_headers,
|
|
268
|
+
system_prompt=fallback_prompt,
|
|
269
|
+
)
|