ccproxy-api 0.1.0__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 (148) hide show
  1. ccproxy/__init__.py +4 -0
  2. ccproxy/__main__.py +7 -0
  3. ccproxy/_version.py +21 -0
  4. ccproxy/adapters/__init__.py +11 -0
  5. ccproxy/adapters/base.py +80 -0
  6. ccproxy/adapters/openai/__init__.py +43 -0
  7. ccproxy/adapters/openai/adapter.py +915 -0
  8. ccproxy/adapters/openai/models.py +412 -0
  9. ccproxy/adapters/openai/streaming.py +449 -0
  10. ccproxy/api/__init__.py +28 -0
  11. ccproxy/api/app.py +225 -0
  12. ccproxy/api/dependencies.py +140 -0
  13. ccproxy/api/middleware/__init__.py +11 -0
  14. ccproxy/api/middleware/auth.py +0 -0
  15. ccproxy/api/middleware/cors.py +55 -0
  16. ccproxy/api/middleware/errors.py +703 -0
  17. ccproxy/api/middleware/headers.py +51 -0
  18. ccproxy/api/middleware/logging.py +175 -0
  19. ccproxy/api/middleware/request_id.py +69 -0
  20. ccproxy/api/middleware/server_header.py +62 -0
  21. ccproxy/api/responses.py +84 -0
  22. ccproxy/api/routes/__init__.py +16 -0
  23. ccproxy/api/routes/claude.py +181 -0
  24. ccproxy/api/routes/health.py +489 -0
  25. ccproxy/api/routes/metrics.py +1033 -0
  26. ccproxy/api/routes/proxy.py +238 -0
  27. ccproxy/auth/__init__.py +75 -0
  28. ccproxy/auth/bearer.py +68 -0
  29. ccproxy/auth/credentials_adapter.py +93 -0
  30. ccproxy/auth/dependencies.py +229 -0
  31. ccproxy/auth/exceptions.py +79 -0
  32. ccproxy/auth/manager.py +102 -0
  33. ccproxy/auth/models.py +118 -0
  34. ccproxy/auth/oauth/__init__.py +26 -0
  35. ccproxy/auth/oauth/models.py +49 -0
  36. ccproxy/auth/oauth/routes.py +396 -0
  37. ccproxy/auth/oauth/storage.py +0 -0
  38. ccproxy/auth/storage/__init__.py +12 -0
  39. ccproxy/auth/storage/base.py +57 -0
  40. ccproxy/auth/storage/json_file.py +159 -0
  41. ccproxy/auth/storage/keyring.py +192 -0
  42. ccproxy/claude_sdk/__init__.py +20 -0
  43. ccproxy/claude_sdk/client.py +169 -0
  44. ccproxy/claude_sdk/converter.py +331 -0
  45. ccproxy/claude_sdk/options.py +120 -0
  46. ccproxy/cli/__init__.py +14 -0
  47. ccproxy/cli/commands/__init__.py +8 -0
  48. ccproxy/cli/commands/auth.py +553 -0
  49. ccproxy/cli/commands/config/__init__.py +14 -0
  50. ccproxy/cli/commands/config/commands.py +766 -0
  51. ccproxy/cli/commands/config/schema_commands.py +119 -0
  52. ccproxy/cli/commands/serve.py +630 -0
  53. ccproxy/cli/docker/__init__.py +34 -0
  54. ccproxy/cli/docker/adapter_factory.py +157 -0
  55. ccproxy/cli/docker/params.py +278 -0
  56. ccproxy/cli/helpers.py +144 -0
  57. ccproxy/cli/main.py +193 -0
  58. ccproxy/cli/options/__init__.py +14 -0
  59. ccproxy/cli/options/claude_options.py +216 -0
  60. ccproxy/cli/options/core_options.py +40 -0
  61. ccproxy/cli/options/security_options.py +48 -0
  62. ccproxy/cli/options/server_options.py +117 -0
  63. ccproxy/config/__init__.py +40 -0
  64. ccproxy/config/auth.py +154 -0
  65. ccproxy/config/claude.py +124 -0
  66. ccproxy/config/cors.py +79 -0
  67. ccproxy/config/discovery.py +87 -0
  68. ccproxy/config/docker_settings.py +265 -0
  69. ccproxy/config/loader.py +108 -0
  70. ccproxy/config/observability.py +158 -0
  71. ccproxy/config/pricing.py +88 -0
  72. ccproxy/config/reverse_proxy.py +31 -0
  73. ccproxy/config/scheduler.py +89 -0
  74. ccproxy/config/security.py +14 -0
  75. ccproxy/config/server.py +81 -0
  76. ccproxy/config/settings.py +534 -0
  77. ccproxy/config/validators.py +231 -0
  78. ccproxy/core/__init__.py +274 -0
  79. ccproxy/core/async_utils.py +675 -0
  80. ccproxy/core/constants.py +97 -0
  81. ccproxy/core/errors.py +256 -0
  82. ccproxy/core/http.py +328 -0
  83. ccproxy/core/http_transformers.py +428 -0
  84. ccproxy/core/interfaces.py +247 -0
  85. ccproxy/core/logging.py +189 -0
  86. ccproxy/core/middleware.py +114 -0
  87. ccproxy/core/proxy.py +143 -0
  88. ccproxy/core/system.py +38 -0
  89. ccproxy/core/transformers.py +259 -0
  90. ccproxy/core/types.py +129 -0
  91. ccproxy/core/validators.py +288 -0
  92. ccproxy/docker/__init__.py +67 -0
  93. ccproxy/docker/adapter.py +588 -0
  94. ccproxy/docker/docker_path.py +207 -0
  95. ccproxy/docker/middleware.py +103 -0
  96. ccproxy/docker/models.py +228 -0
  97. ccproxy/docker/protocol.py +192 -0
  98. ccproxy/docker/stream_process.py +264 -0
  99. ccproxy/docker/validators.py +173 -0
  100. ccproxy/models/__init__.py +123 -0
  101. ccproxy/models/errors.py +42 -0
  102. ccproxy/models/messages.py +243 -0
  103. ccproxy/models/requests.py +85 -0
  104. ccproxy/models/responses.py +227 -0
  105. ccproxy/models/types.py +102 -0
  106. ccproxy/observability/__init__.py +51 -0
  107. ccproxy/observability/access_logger.py +400 -0
  108. ccproxy/observability/context.py +447 -0
  109. ccproxy/observability/metrics.py +539 -0
  110. ccproxy/observability/pushgateway.py +366 -0
  111. ccproxy/observability/sse_events.py +303 -0
  112. ccproxy/observability/stats_printer.py +755 -0
  113. ccproxy/observability/storage/__init__.py +1 -0
  114. ccproxy/observability/storage/duckdb_simple.py +665 -0
  115. ccproxy/observability/storage/models.py +55 -0
  116. ccproxy/pricing/__init__.py +19 -0
  117. ccproxy/pricing/cache.py +212 -0
  118. ccproxy/pricing/loader.py +267 -0
  119. ccproxy/pricing/models.py +106 -0
  120. ccproxy/pricing/updater.py +309 -0
  121. ccproxy/scheduler/__init__.py +39 -0
  122. ccproxy/scheduler/core.py +335 -0
  123. ccproxy/scheduler/exceptions.py +34 -0
  124. ccproxy/scheduler/manager.py +186 -0
  125. ccproxy/scheduler/registry.py +150 -0
  126. ccproxy/scheduler/tasks.py +484 -0
  127. ccproxy/services/__init__.py +10 -0
  128. ccproxy/services/claude_sdk_service.py +614 -0
  129. ccproxy/services/credentials/__init__.py +55 -0
  130. ccproxy/services/credentials/config.py +105 -0
  131. ccproxy/services/credentials/manager.py +562 -0
  132. ccproxy/services/credentials/oauth_client.py +482 -0
  133. ccproxy/services/proxy_service.py +1536 -0
  134. ccproxy/static/.keep +0 -0
  135. ccproxy/testing/__init__.py +34 -0
  136. ccproxy/testing/config.py +148 -0
  137. ccproxy/testing/content_generation.py +197 -0
  138. ccproxy/testing/mock_responses.py +262 -0
  139. ccproxy/testing/response_handlers.py +161 -0
  140. ccproxy/testing/scenarios.py +241 -0
  141. ccproxy/utils/__init__.py +6 -0
  142. ccproxy/utils/cost_calculator.py +210 -0
  143. ccproxy/utils/streaming_metrics.py +199 -0
  144. ccproxy_api-0.1.0.dist-info/METADATA +253 -0
  145. ccproxy_api-0.1.0.dist-info/RECORD +148 -0
  146. ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
  147. ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
  148. ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
ccproxy/static/.keep ADDED
File without changes
@@ -0,0 +1,34 @@
1
+ """Testing utilities and mock response generation for CCProxy.
2
+
3
+ This package provides comprehensive testing utilities including:
4
+ - Mock response generation for bypass mode
5
+ - Request payload builders for dual-format testing
6
+ - Response processing and metrics collection
7
+ - Traffic pattern generation and scenario management
8
+ """
9
+
10
+ from ccproxy.testing.config import (
11
+ MockResponseConfig,
12
+ RequestScenario,
13
+ TrafficConfig,
14
+ TrafficMetrics,
15
+ )
16
+ from ccproxy.testing.content_generation import MessageContentGenerator, PayloadBuilder
17
+ from ccproxy.testing.mock_responses import RealisticMockResponseGenerator
18
+ from ccproxy.testing.response_handlers import MetricsExtractor, ResponseHandler
19
+ from ccproxy.testing.scenarios import ScenarioGenerator, TrafficPatternAnalyzer
20
+
21
+
22
+ __all__ = [
23
+ "MockResponseConfig",
24
+ "RequestScenario",
25
+ "TrafficConfig",
26
+ "TrafficMetrics",
27
+ "MessageContentGenerator",
28
+ "PayloadBuilder",
29
+ "RealisticMockResponseGenerator",
30
+ "MetricsExtractor",
31
+ "ResponseHandler",
32
+ "ScenarioGenerator",
33
+ "TrafficPatternAnalyzer",
34
+ ]
@@ -0,0 +1,148 @@
1
+ """Configuration models for testing utilities."""
2
+
3
+ from datetime import UTC, datetime
4
+ from pathlib import Path
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import BaseModel
8
+
9
+
10
+ # Type aliases for traffic patterns and response types
11
+ TrafficPattern = Literal["constant", "burst", "ramping", "realistic"]
12
+ ResponseType = Literal["success", "error", "mixed", "unavailable"]
13
+ AuthType = Literal["none", "bearer", "configured", "credentials"]
14
+
15
+
16
+ class MockResponseConfig(BaseModel):
17
+ """Configuration for realistic mock responses."""
18
+
19
+ # Token range configurations
20
+ input_token_range: tuple[int, int] = (10, 500) # Min/max input tokens
21
+ output_token_range: tuple[int, int] = (5, 1000) # Min/max output tokens
22
+ cache_token_probability: float = 0.3 # Chance of cache tokens
23
+ cache_read_range: tuple[int, int] = (50, 200) # Cache read token range
24
+ cache_write_range: tuple[int, int] = (20, 100) # Cache write token range
25
+
26
+ # Latency simulation
27
+ base_latency_ms: tuple[int, int] = (100, 2000) # Base response latency
28
+ streaming_chunk_delay_ms: tuple[int, int] = (10, 100) # Per-chunk delay
29
+
30
+ # Content variation
31
+ response_length_variety: bool = True # Vary response length
32
+ short_response_range: tuple[int, int] = (1, 3) # Short response sentences
33
+ long_response_range: tuple[int, int] = (5, 15) # Long response sentences
34
+
35
+ # Error simulation
36
+ simulate_errors: bool = True # Include error scenarios
37
+ error_probability: float = 0.05 # Chance of error response
38
+
39
+ # Realistic timing
40
+ token_generation_rate: float = 50.0 # Tokens per second for streaming
41
+
42
+
43
+ class TrafficConfig(BaseModel):
44
+ """Configuration for traffic generation scenarios."""
45
+
46
+ # Basic settings
47
+ duration_seconds: int = 60
48
+ requests_per_second: float = 1.0
49
+ pattern: TrafficPattern = "constant"
50
+
51
+ # Target Configuration
52
+ target_url: str = "http://localhost:8000" # Proxy server URL
53
+ api_formats: list[str] = ["anthropic", "openai"] # Which formats to test
54
+ format_distribution: dict[str, float] = { # % distribution of formats
55
+ "anthropic": 0.7,
56
+ "openai": 0.3,
57
+ }
58
+
59
+ # Request configuration
60
+ models: list[str] = ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"]
61
+ message_types: list[str] = ["short", "long", "tool_use"]
62
+ streaming_probability: float = 0.3
63
+
64
+ # Advanced Request Types
65
+ advanced_scenarios: bool = False # Enable complex scenarios
66
+ tool_use_probability: float = 0.2 # Specific probability for tool use
67
+
68
+ # Response configuration
69
+ response_type: ResponseType = "mixed"
70
+ error_probability: float = 0.1
71
+ latency_ms_min: int = 100
72
+ latency_ms_max: int = 2000
73
+
74
+ # Authentication and Testing
75
+ bypass_mode: bool = True # Use bypass headers (test mode)
76
+ real_api_keys: dict[str, str] = {} # Real API keys when bypass_mode=False
77
+
78
+ # Timeframe simulation
79
+ simulate_historical: bool = False
80
+ start_timestamp: datetime | None = None
81
+ end_timestamp: datetime | None = None
82
+
83
+ # Output configuration
84
+ output_file: Path | None = None
85
+ log_requests: bool = True
86
+ log_responses: bool = False
87
+ log_format_conversions: bool = True # Log API format transformations
88
+
89
+
90
+ class RequestScenario(BaseModel):
91
+ """Individual request scenario configuration."""
92
+
93
+ model: str
94
+ message_type: str
95
+ streaming: bool
96
+ response_type: ResponseType
97
+ timestamp: datetime
98
+
99
+ # API Format and Endpoint Control
100
+ api_format: Literal["anthropic", "openai"] = "anthropic"
101
+ endpoint_path: str = (
102
+ "/api/v1/messages" # "/api/v1/messages" or "/api/v1/chat/completions"
103
+ )
104
+
105
+ # Request Control
106
+ bypass_upstream: bool = True # Add bypass header to prevent real API calls
107
+ use_real_auth: bool = False # Use real API keys vs test mode
108
+
109
+ # Enhanced Headers
110
+ headers: dict[str, str] = {} # All request headers
111
+
112
+ # Target URL
113
+ target_url: str = "http://localhost:8000" # Full base URL for request
114
+
115
+ # Payload Customization
116
+ custom_payload: dict[str, Any] | None = None # Override default payload generation
117
+
118
+
119
+ class TrafficMetrics(BaseModel):
120
+ """Enhanced metrics for dual-format testing."""
121
+
122
+ total_requests: int = 0
123
+ successful_requests: int = 0
124
+ failed_requests: int = 0
125
+ error_requests: int = 0
126
+ average_latency_ms: float = 0.0
127
+ requests_per_second: float = 0.0
128
+ start_time: datetime
129
+ end_time: datetime | None = None
130
+
131
+ # Format-specific metrics
132
+ anthropic_requests: int = 0
133
+ openai_requests: int = 0
134
+
135
+ # Streaming vs non-streaming
136
+ streaming_requests: int = 0
137
+ standard_requests: int = 0
138
+
139
+ # Format validation
140
+ format_validation_errors: int = 0
141
+
142
+ # Response time by format
143
+ anthropic_avg_latency_ms: float = 0.0
144
+ openai_avg_latency_ms: float = 0.0
145
+
146
+ # Token usage
147
+ total_input_tokens: int = 0
148
+ total_output_tokens: int = 0
@@ -0,0 +1,197 @@
1
+ """Content generation utilities for testing requests and responses."""
2
+
3
+ import random
4
+ from typing import Any
5
+
6
+ from ccproxy.testing.config import RequestScenario
7
+
8
+
9
+ class MessageContentGenerator:
10
+ """Generate realistic message content for testing."""
11
+
12
+ def __init__(self) -> None:
13
+ self.response_templates = self._load_response_templates()
14
+ self.request_templates = self._load_request_templates()
15
+
16
+ def _load_response_templates(self) -> dict[str, list[str]]:
17
+ """Load variety of response templates."""
18
+ return {
19
+ "short": [
20
+ "Hello! How can I help you today?",
21
+ "I'm happy to assist you.",
22
+ "What would you like to know?",
23
+ "I'm here to help!",
24
+ "How may I assist you?",
25
+ ],
26
+ "medium": [
27
+ "I'd be happy to help you with that. Let me provide you with some information that should be useful for your question.",
28
+ "That's an interesting question. Here's what I can tell you about this topic based on my knowledge.",
29
+ "I understand what you're asking about. Let me break this down into a clear explanation for you.",
30
+ ],
31
+ "long": [
32
+ "This is a comprehensive topic that requires a detailed explanation. Let me walk you through the key concepts step by step. First, it's important to understand the foundational principles. Then we can explore the more advanced aspects. Finally, I'll provide some practical examples to illustrate the concepts.",
33
+ "That's an excellent question that touches on several important areas. To give you a complete answer, I need to cover multiple aspects. Let me start with the basic framework, then dive into the specifics, and conclude with some recommendations based on best practices in this field.",
34
+ ],
35
+ "tool_use": [
36
+ "I'll help you with that calculation.",
37
+ "Let me solve that mathematical problem for you.",
38
+ "I can compute that result using the calculator tool.",
39
+ ],
40
+ }
41
+
42
+ def _load_request_templates(self) -> dict[str, list[str]]:
43
+ """Load variety of request message templates."""
44
+ return {
45
+ "short": [
46
+ "Hello!",
47
+ "How are you?",
48
+ "What's the weather like?",
49
+ "Tell me a joke.",
50
+ "What time is it?",
51
+ ],
52
+ "long": [
53
+ "I need help writing a detailed technical document about API design patterns. Can you provide a comprehensive guide covering REST principles, authentication methods, error handling, and best practices for scalable API development?",
54
+ "Please explain the differences between various machine learning algorithms including supervised learning, unsupervised learning, and reinforcement learning. Include examples of when to use each approach and their respective advantages and disadvantages.",
55
+ "I'm planning a complex software architecture for a distributed system. Can you help me understand microservices patterns, database sharding strategies, caching layers, and how to handle eventual consistency in distributed transactions?",
56
+ ],
57
+ "tool_use": [
58
+ "Calculate 23 * 45 + 67 for me",
59
+ "What's the result of (150 / 3) * 2.5?",
60
+ "Help me calculate the compound interest on $1000 at 5% for 3 years",
61
+ ],
62
+ }
63
+
64
+ def get_request_message_content(self, message_type: str) -> str:
65
+ """Get request message content based on type."""
66
+ if message_type in self.request_templates:
67
+ return random.choice(self.request_templates[message_type])
68
+ else:
69
+ # Fallback to short message for unknown types
70
+ return random.choice(self.request_templates["short"])
71
+
72
+ def get_response_content(
73
+ self, message_type: str, model: str
74
+ ) -> tuple[str, int, int]:
75
+ """Generate response content with realistic token counts."""
76
+ # Select base template
77
+ if message_type == "tool_use":
78
+ base_content = random.choice(self.response_templates["tool_use"])
79
+ # Add calculation result
80
+ result = random.randint(1, 1000)
81
+ content = f"{base_content} The result is {result}."
82
+ elif message_type in self.response_templates:
83
+ content = random.choice(self.response_templates[message_type])
84
+ else:
85
+ # Mix of different lengths for unknown types
86
+ template_type = random.choice(["short", "medium", "long"])
87
+ content = random.choice(self.response_templates[template_type])
88
+
89
+ # Calculate realistic token counts based on content
90
+ # Rough estimate: ~4 characters per token
91
+ estimated_output_tokens = max(1, len(content) // 4)
92
+
93
+ # Add some randomness but keep it realistic
94
+ output_tokens = random.randint(
95
+ max(1, estimated_output_tokens - 10), estimated_output_tokens + 20
96
+ )
97
+
98
+ # Input tokens based on typical request sizes (10-500 range)
99
+ input_tokens = random.randint(10, 500)
100
+
101
+ return content, input_tokens, output_tokens
102
+
103
+
104
+ class PayloadBuilder:
105
+ """Build request payloads for different API formats."""
106
+
107
+ def __init__(self) -> None:
108
+ self.content_generator = MessageContentGenerator()
109
+
110
+ def build_anthropic_payload(self, scenario: RequestScenario) -> dict[str, Any]:
111
+ """Build Anthropic format payload."""
112
+ payload = {
113
+ "model": scenario.model,
114
+ "messages": [
115
+ {
116
+ "role": "user",
117
+ "content": self.content_generator.get_request_message_content(
118
+ scenario.message_type
119
+ ),
120
+ }
121
+ ],
122
+ "stream": scenario.streaming,
123
+ "max_tokens": random.randint(100, 4000), # Realistic token limits
124
+ }
125
+
126
+ if scenario.message_type == "tool_use":
127
+ payload["tools"] = [
128
+ {
129
+ "name": "calculator",
130
+ "description": "Perform basic calculations",
131
+ "input_schema": {
132
+ "type": "object",
133
+ "properties": {
134
+ "expression": {
135
+ "type": "string",
136
+ "description": "Math expression to evaluate",
137
+ }
138
+ },
139
+ "required": ["expression"],
140
+ },
141
+ }
142
+ ]
143
+
144
+ return payload
145
+
146
+ def build_openai_payload(self, scenario: RequestScenario) -> dict[str, Any]:
147
+ """Build OpenAI format payload."""
148
+ messages = [
149
+ {
150
+ "role": "user",
151
+ "content": self.content_generator.get_request_message_content(
152
+ scenario.message_type
153
+ ),
154
+ }
155
+ ]
156
+
157
+ payload = {
158
+ "model": scenario.model,
159
+ "messages": messages,
160
+ "stream": scenario.streaming,
161
+ "max_tokens": random.randint(100, 4000), # Realistic token limits
162
+ }
163
+
164
+ if scenario.message_type == "tool_use":
165
+ payload["tools"] = [
166
+ {
167
+ "type": "function",
168
+ "function": {
169
+ "name": "calculator",
170
+ "description": "Perform basic calculations",
171
+ "parameters": {
172
+ "type": "object",
173
+ "properties": {
174
+ "expression": {
175
+ "type": "string",
176
+ "description": "Math expression to evaluate",
177
+ }
178
+ },
179
+ "required": ["expression"],
180
+ },
181
+ },
182
+ }
183
+ ]
184
+
185
+ return payload
186
+
187
+ def build_payload(self, scenario: RequestScenario) -> dict[str, Any]:
188
+ """Build request payload based on scenario format."""
189
+ # Use custom payload if provided
190
+ if scenario.custom_payload:
191
+ return scenario.custom_payload
192
+
193
+ # Build format-specific payload
194
+ if scenario.api_format == "openai":
195
+ return self.build_openai_payload(scenario)
196
+ else:
197
+ return self.build_anthropic_payload(scenario)
@@ -0,0 +1,262 @@
1
+ """Mock response generation for realistic testing."""
2
+
3
+ import json
4
+ import random
5
+ import time
6
+ from typing import Any
7
+
8
+ from ccproxy.testing.config import MockResponseConfig
9
+ from ccproxy.testing.content_generation import MessageContentGenerator
10
+
11
+
12
+ class RealisticMockResponseGenerator:
13
+ """Generate realistic mock responses with proper randomization."""
14
+
15
+ def __init__(self, config: MockResponseConfig | None = None):
16
+ self.config = config or MockResponseConfig()
17
+ self.content_generator: MessageContentGenerator = MessageContentGenerator()
18
+
19
+ def generate_response_content(
20
+ self, message_type: str, model: str
21
+ ) -> tuple[str, int, int]:
22
+ """Generate response content with realistic token counts."""
23
+ return self.content_generator.get_response_content(message_type, model)
24
+
25
+ def generate_cache_tokens(self) -> tuple[int, int]:
26
+ """Generate realistic cache token counts."""
27
+ if random.random() < self.config.cache_token_probability:
28
+ cache_read = random.randint(*self.config.cache_read_range)
29
+ cache_write = random.randint(*self.config.cache_write_range)
30
+ return cache_read, cache_write
31
+ return 0, 0
32
+
33
+ def should_simulate_error(self) -> bool:
34
+ """Determine if this response should be an error."""
35
+ return (
36
+ self.config.simulate_errors
37
+ and random.random() < self.config.error_probability
38
+ )
39
+
40
+ def generate_error_response(self, api_format: str) -> tuple[dict[str, Any], int]:
41
+ """Generate realistic error response."""
42
+ error_types = [
43
+ {
44
+ "type": "rate_limit_error",
45
+ "message": "Rate limit exceeded. Please try again later.",
46
+ "status_code": 429,
47
+ },
48
+ {
49
+ "type": "invalid_request_error",
50
+ "message": "Invalid request format.",
51
+ "status_code": 400,
52
+ },
53
+ {
54
+ "type": "overloaded_error",
55
+ "message": "Service temporarily overloaded.",
56
+ "status_code": 503,
57
+ },
58
+ ]
59
+
60
+ error = random.choice(error_types)
61
+ status_code: int = error["status_code"] # type: ignore[assignment]
62
+
63
+ if api_format == "openai":
64
+ return {
65
+ "error": {
66
+ "message": error["message"],
67
+ "type": error["type"],
68
+ "code": error["type"],
69
+ }
70
+ }, status_code
71
+ else:
72
+ return {
73
+ "type": "error",
74
+ "error": {"type": error["type"], "message": error["message"]},
75
+ }, status_code
76
+
77
+ def generate_realistic_anthropic_stream(
78
+ self,
79
+ request_id: str,
80
+ model: str,
81
+ content: str,
82
+ input_tokens: int,
83
+ output_tokens: int,
84
+ cache_read_tokens: int,
85
+ cache_write_tokens: int,
86
+ ) -> list[dict[str, Any]]:
87
+ """Generate realistic Anthropic streaming chunks."""
88
+
89
+ chunks = []
90
+
91
+ # Message start
92
+ chunks.append(
93
+ {
94
+ "type": "message_start",
95
+ "message": {
96
+ "id": request_id,
97
+ "type": "message",
98
+ "role": "assistant",
99
+ "content": [],
100
+ "model": model,
101
+ "stop_reason": None,
102
+ "stop_sequence": None,
103
+ "usage": {"input_tokens": input_tokens, "output_tokens": 0},
104
+ },
105
+ }
106
+ )
107
+
108
+ # Content block start
109
+ chunk_start: dict[str, Any] = {
110
+ "type": "content_block_start",
111
+ "index": 0,
112
+ "content_block": {"type": "text", "text": ""},
113
+ }
114
+ chunks.append(chunk_start)
115
+
116
+ # Split content into realistic chunks (by words)
117
+ words = content.split()
118
+ chunk_sizes = []
119
+
120
+ # Generate realistic chunk sizes
121
+ i = 0
122
+ while i < len(words):
123
+ # Random chunk size between 1-5 words
124
+ chunk_size = random.randint(1, min(5, len(words) - i))
125
+ chunk_sizes.append(chunk_size)
126
+ i += chunk_size
127
+
128
+ # Generate content deltas
129
+ word_index = 0
130
+ for chunk_size in chunk_sizes:
131
+ chunk_words = words[word_index : word_index + chunk_size]
132
+ chunk_text = (
133
+ " " + " ".join(chunk_words) if word_index > 0 else " ".join(chunk_words)
134
+ )
135
+
136
+ chunk_delta: dict[str, Any] = {
137
+ "type": "content_block_delta",
138
+ "index": 0,
139
+ "delta": {"type": "text_delta", "text": chunk_text},
140
+ }
141
+ chunks.append(chunk_delta)
142
+ word_index += chunk_size
143
+
144
+ # Content block stop
145
+ chunk_stop: dict[str, Any] = {"type": "content_block_stop", "index": 0}
146
+ chunks.append(chunk_stop)
147
+
148
+ # Message delta with final usage
149
+ chunks.append(
150
+ {
151
+ "type": "message_delta",
152
+ "delta": {"stop_reason": "end_turn", "stop_sequence": None},
153
+ "usage": {
154
+ "output_tokens": output_tokens,
155
+ "cache_creation_input_tokens": cache_write_tokens,
156
+ "cache_read_input_tokens": cache_read_tokens,
157
+ },
158
+ }
159
+ )
160
+
161
+ # Message stop
162
+ chunks.append({"type": "message_stop"})
163
+
164
+ return chunks
165
+
166
+ def generate_realistic_openai_stream(
167
+ self,
168
+ request_id: str,
169
+ model: str,
170
+ content: str,
171
+ input_tokens: int,
172
+ output_tokens: int,
173
+ ) -> list[dict[str, Any]]:
174
+ """Generate realistic OpenAI streaming chunks by converting Anthropic format."""
175
+
176
+ # Generate Anthropic chunks first
177
+ anthropic_chunks = self.generate_realistic_anthropic_stream(
178
+ request_id, model, content, input_tokens, output_tokens, 0, 0
179
+ )
180
+
181
+ # Convert to OpenAI format
182
+ openai_chunks = []
183
+ for chunk in anthropic_chunks:
184
+ # Use simplified conversion logic
185
+ if chunk.get("type") == "message_start":
186
+ openai_chunks.append(
187
+ {
188
+ "id": f"chatcmpl-{request_id}",
189
+ "object": "chat.completion.chunk",
190
+ "created": int(time.time()),
191
+ "model": model,
192
+ "choices": [
193
+ {
194
+ "index": 0,
195
+ "delta": {"role": "assistant", "content": ""},
196
+ "finish_reason": None,
197
+ }
198
+ ],
199
+ }
200
+ )
201
+ elif chunk.get("type") == "content_block_delta":
202
+ delta_text = chunk.get("delta", {}).get("text", "")
203
+ openai_chunks.append(
204
+ {
205
+ "id": f"chatcmpl-{request_id}",
206
+ "object": "chat.completion.chunk",
207
+ "created": int(time.time()),
208
+ "model": model,
209
+ "choices": [
210
+ {
211
+ "index": 0,
212
+ "delta": {"content": delta_text},
213
+ "finish_reason": None,
214
+ }
215
+ ],
216
+ }
217
+ )
218
+ elif chunk.get("type") == "message_stop":
219
+ openai_chunks.append(
220
+ {
221
+ "id": f"chatcmpl-{request_id}",
222
+ "object": "chat.completion.chunk",
223
+ "created": int(time.time()),
224
+ "model": model,
225
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
226
+ }
227
+ )
228
+
229
+ return openai_chunks
230
+
231
+ def calculate_realistic_cost(
232
+ self,
233
+ input_tokens: int,
234
+ output_tokens: int,
235
+ model: str,
236
+ cache_read_tokens: int,
237
+ cache_write_tokens: int,
238
+ ) -> float:
239
+ """Calculate realistic cost based on current Claude pricing."""
240
+
241
+ # Simplified pricing (should use actual cost calculator)
242
+ if "sonnet" in model.lower():
243
+ input_cost_per_token = 0.000003 # $3 per million tokens
244
+ output_cost_per_token = 0.000015 # $15 per million tokens
245
+ elif "haiku" in model.lower():
246
+ input_cost_per_token = 0.00000025 # $0.25 per million tokens
247
+ output_cost_per_token = 0.00000125 # $1.25 per million tokens
248
+ else:
249
+ input_cost_per_token = 0.000003
250
+ output_cost_per_token = 0.000015
251
+
252
+ base_cost = (
253
+ input_tokens * input_cost_per_token + output_tokens * output_cost_per_token
254
+ )
255
+
256
+ # Cache costs (typically lower)
257
+ cache_cost = (
258
+ cache_read_tokens * input_cost_per_token * 0.1 # 10% of input cost
259
+ + cache_write_tokens * input_cost_per_token * 0.5 # 50% of input cost
260
+ )
261
+
262
+ return round(base_cost + cache_cost, 6)