ccproxy-api 0.1.1__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.
- 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 +65 -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 +88 -19
- ccproxy/claude_sdk/parser.py +200 -0
- ccproxy/claude_sdk/streaming.py +286 -0
- ccproxy/cli/commands/__init__.py +5 -1
- ccproxy/cli/commands/auth.py +2 -4
- ccproxy/cli/commands/permission_handler.py +553 -0
- ccproxy/cli/commands/serve.py +52 -12
- ccproxy/cli/docker/params.py +0 -4
- ccproxy/cli/helpers.py +0 -2
- ccproxy/cli/main.py +6 -17
- ccproxy/cli/options/claude_options.py +41 -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 +15 -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 +64 -1
- 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 +225 -329
- 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.1.dist-info → ccproxy_api-0.1.3.dist-info}/METADATA +63 -2
- ccproxy_api-0.1.3.dist-info/RECORD +166 -0
- {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/entry_points.txt +1 -0
- ccproxy_api-0.1.1.dist-info/RECORD +0 -149
- /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
- {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.1.dist-info → ccproxy_api-0.1.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Strongly-typed Pydantic models for Claude SDK types.
|
|
2
|
+
|
|
3
|
+
This module provides Pydantic models that mirror the Claude SDK types from the
|
|
4
|
+
official claude-code-sdk-python repository. These models enable strong typing
|
|
5
|
+
throughout the proxy system and provide runtime validation.
|
|
6
|
+
|
|
7
|
+
Based on: https://github.com/anthropics/claude-code-sdk-python/blob/main/src/claude_code_sdk/types.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Annotated, Any, Literal, TypeVar, cast
|
|
13
|
+
|
|
14
|
+
# Import Claude SDK types for isinstance checks
|
|
15
|
+
from claude_code_sdk import TextBlock as SDKTextBlock
|
|
16
|
+
from claude_code_sdk import ToolResultBlock as SDKToolResultBlock
|
|
17
|
+
from claude_code_sdk import ToolUseBlock as SDKToolUseBlock
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
19
|
+
|
|
20
|
+
from ccproxy.models.requests import Usage
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Type variables for generic functions
|
|
24
|
+
T = TypeVar("T", bound=BaseModel)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Generic conversion function
|
|
28
|
+
def to_sdk_variant(base_model: BaseModel, sdk_class: type[T]) -> T:
|
|
29
|
+
"""Convert a base model to its SDK variant using model_validate().
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
base_model: The base model instance to convert
|
|
33
|
+
sdk_class: The target SDK class to convert to
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Instance of the SDK class with data from the base model
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> text_block = TextBlock(text="message")
|
|
40
|
+
>>> text_block_sdk = to_sdk_variant(text_block, TextBlockSDK)
|
|
41
|
+
"""
|
|
42
|
+
return sdk_class.model_validate(base_model.model_dump())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Core Content Block Types
|
|
46
|
+
class TextBlock(BaseModel):
|
|
47
|
+
"""Text content block from Claude SDK."""
|
|
48
|
+
|
|
49
|
+
type: Literal["text"] = "text"
|
|
50
|
+
text: str = Field(..., description="Text content")
|
|
51
|
+
|
|
52
|
+
model_config = ConfigDict(extra="allow")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ToolUseBlock(BaseModel):
|
|
56
|
+
"""Tool use content block from Claude SDK."""
|
|
57
|
+
|
|
58
|
+
type: Literal["tool_use"] = "tool_use"
|
|
59
|
+
id: str = Field(..., description="Unique identifier for the tool use")
|
|
60
|
+
name: str = Field(..., description="Name of the tool being used")
|
|
61
|
+
input: dict[str, Any] = Field(..., description="Input parameters for the tool")
|
|
62
|
+
|
|
63
|
+
model_config = ConfigDict(extra="allow")
|
|
64
|
+
|
|
65
|
+
def to_sdk_block(self) -> dict[str, Any]:
|
|
66
|
+
"""Convert to ToolUseSDKBlock format for streaming."""
|
|
67
|
+
return {
|
|
68
|
+
"type": "tool_use_sdk",
|
|
69
|
+
"id": self.id,
|
|
70
|
+
"name": self.name,
|
|
71
|
+
"input": self.input,
|
|
72
|
+
"source": "claude_code_sdk",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ToolResultBlock(BaseModel):
|
|
77
|
+
"""Tool result content block from Claude SDK."""
|
|
78
|
+
|
|
79
|
+
type: Literal["tool_result"] = "tool_result"
|
|
80
|
+
tool_use_id: str = Field(
|
|
81
|
+
..., description="ID of the tool use this result corresponds to"
|
|
82
|
+
)
|
|
83
|
+
content: str | list[dict[str, Any]] | None = Field(
|
|
84
|
+
None, description="Result content from the tool"
|
|
85
|
+
)
|
|
86
|
+
is_error: bool | None = Field(
|
|
87
|
+
None, description="Whether this result represents an error"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
model_config = ConfigDict(extra="allow")
|
|
91
|
+
|
|
92
|
+
def to_sdk_block(self) -> dict[str, Any]:
|
|
93
|
+
"""Convert to ToolResultSDKBlock format for streaming."""
|
|
94
|
+
return {
|
|
95
|
+
"type": "tool_result_sdk",
|
|
96
|
+
"tool_use_id": self.tool_use_id,
|
|
97
|
+
"content": self.content,
|
|
98
|
+
"is_error": self.is_error,
|
|
99
|
+
"source": "claude_code_sdk",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ThinkingBlock(BaseModel):
|
|
104
|
+
"""Thinking content block from Claude SDK.
|
|
105
|
+
|
|
106
|
+
Note: Thinking blocks are not normally sent by Claude Code SDK, but this model
|
|
107
|
+
is included for defensive programming to handle any future SDK changes or edge cases
|
|
108
|
+
where thinking content might be included in SDK responses.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
type: Literal["thinking"] = "thinking"
|
|
112
|
+
thinking: str = Field(..., description="Thinking content text")
|
|
113
|
+
signature: str | None = Field(None, description="Optional thinking signature")
|
|
114
|
+
|
|
115
|
+
model_config = ConfigDict(extra="allow")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Union type for basic content blocks
|
|
119
|
+
ContentBlock = Annotated[
|
|
120
|
+
TextBlock | ToolUseBlock | ToolResultBlock | ThinkingBlock,
|
|
121
|
+
Field(discriminator="type"),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Message Types
|
|
126
|
+
class UserMessage(BaseModel):
|
|
127
|
+
"""User message from Claude SDK."""
|
|
128
|
+
|
|
129
|
+
content: list[ContentBlock] = Field(
|
|
130
|
+
..., description="List of content blocks in the message"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
model_config = ConfigDict(extra="allow")
|
|
134
|
+
|
|
135
|
+
@field_validator("content", mode="before")
|
|
136
|
+
@classmethod
|
|
137
|
+
def convert_content_blocks(cls, v: Any) -> list[Any]:
|
|
138
|
+
"""Convert Claude SDK dataclass blocks to Pydantic models."""
|
|
139
|
+
if not isinstance(v, list):
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
converted_blocks = []
|
|
143
|
+
for block in v:
|
|
144
|
+
if isinstance(block, SDKTextBlock | SDKToolUseBlock | SDKToolResultBlock):
|
|
145
|
+
# Convert Claude SDK dataclass to dict and add type field
|
|
146
|
+
if isinstance(block, SDKTextBlock):
|
|
147
|
+
converted_blocks.append({"type": "text", "text": block.text})
|
|
148
|
+
elif isinstance(block, SDKToolUseBlock):
|
|
149
|
+
converted_blocks.append(
|
|
150
|
+
cast(
|
|
151
|
+
Any,
|
|
152
|
+
{
|
|
153
|
+
"type": "tool_use",
|
|
154
|
+
"id": str(block.id),
|
|
155
|
+
"name": str(block.name),
|
|
156
|
+
"input": dict(block.input),
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
elif isinstance(block, SDKToolResultBlock):
|
|
161
|
+
converted_blocks.append(
|
|
162
|
+
cast(
|
|
163
|
+
Any,
|
|
164
|
+
{
|
|
165
|
+
"type": "tool_result",
|
|
166
|
+
"tool_use_id": str(block.tool_use_id),
|
|
167
|
+
"content": block.content,
|
|
168
|
+
"is_error": block.is_error,
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
converted_blocks.append(block)
|
|
174
|
+
|
|
175
|
+
return converted_blocks
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class AssistantMessage(BaseModel):
|
|
179
|
+
"""Assistant message from Claude SDK."""
|
|
180
|
+
|
|
181
|
+
content: list[ContentBlock] = Field(
|
|
182
|
+
..., description="List of content blocks in the message"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
model_config = ConfigDict(extra="allow")
|
|
186
|
+
|
|
187
|
+
@field_validator("content", mode="before")
|
|
188
|
+
@classmethod
|
|
189
|
+
def convert_content_blocks(cls, v: Any) -> list[Any]:
|
|
190
|
+
"""Convert Claude SDK dataclass blocks to Pydantic models."""
|
|
191
|
+
if not isinstance(v, list):
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
converted_blocks = []
|
|
195
|
+
for block in v:
|
|
196
|
+
if isinstance(block, SDKTextBlock | SDKToolUseBlock | SDKToolResultBlock):
|
|
197
|
+
# Convert Claude SDK dataclass to dict and add type field
|
|
198
|
+
if isinstance(block, SDKTextBlock):
|
|
199
|
+
converted_blocks.append({"type": "text", "text": block.text})
|
|
200
|
+
elif isinstance(block, SDKToolUseBlock):
|
|
201
|
+
converted_blocks.append(
|
|
202
|
+
cast(
|
|
203
|
+
Any,
|
|
204
|
+
{
|
|
205
|
+
"type": "tool_use",
|
|
206
|
+
"id": str(block.id),
|
|
207
|
+
"name": str(block.name),
|
|
208
|
+
"input": dict(block.input),
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
elif isinstance(block, SDKToolResultBlock):
|
|
213
|
+
converted_blocks.append(
|
|
214
|
+
cast(
|
|
215
|
+
Any,
|
|
216
|
+
{
|
|
217
|
+
"type": "tool_result",
|
|
218
|
+
"tool_use_id": str(block.tool_use_id),
|
|
219
|
+
"content": block.content,
|
|
220
|
+
"is_error": block.is_error,
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
converted_blocks.append(block)
|
|
226
|
+
|
|
227
|
+
return converted_blocks
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class SystemMessage(BaseModel):
|
|
231
|
+
"""System message from Claude SDK."""
|
|
232
|
+
|
|
233
|
+
type: Literal["system_message"] = "system_message"
|
|
234
|
+
|
|
235
|
+
subtype: str = Field(default="", description="Subtype of the system message")
|
|
236
|
+
data: dict[str, Any] = Field(
|
|
237
|
+
default_factory=dict, description="System message data"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
model_config = ConfigDict(extra="allow")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class ResultMessage(BaseModel):
|
|
244
|
+
"""Result message from Claude SDK."""
|
|
245
|
+
|
|
246
|
+
type: Literal["result_message"] = "result_message"
|
|
247
|
+
|
|
248
|
+
subtype: str = Field(default="", description="Subtype of the result message")
|
|
249
|
+
duration_ms: int = Field(default=0, description="Total duration in milliseconds")
|
|
250
|
+
duration_api_ms: int = Field(default=0, description="API duration in milliseconds")
|
|
251
|
+
is_error: bool = Field(
|
|
252
|
+
default=False, description="Whether this result represents an error"
|
|
253
|
+
)
|
|
254
|
+
num_turns: int = Field(default=0, description="Number of conversation turns")
|
|
255
|
+
session_id: str = Field(default="", description="Session ID for the result")
|
|
256
|
+
total_cost_usd: float | None = Field(None, description="Total cost in USD")
|
|
257
|
+
usage: dict[str, Any] | None = Field(
|
|
258
|
+
None, description="Usage information dictionary"
|
|
259
|
+
)
|
|
260
|
+
result: str | None = Field(None, description="Result string if available")
|
|
261
|
+
|
|
262
|
+
# Add computed properties for backward compatibility
|
|
263
|
+
@property
|
|
264
|
+
def stop_reason(self) -> str:
|
|
265
|
+
"""Get stop reason from result or default to end_turn."""
|
|
266
|
+
if self.is_error:
|
|
267
|
+
return "error"
|
|
268
|
+
return "end_turn"
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def usage_model(self) -> Usage:
|
|
272
|
+
"""Get usage information as a Usage model for backward compatibility."""
|
|
273
|
+
if self.usage is None:
|
|
274
|
+
return Usage()
|
|
275
|
+
return Usage.model_validate(self.usage)
|
|
276
|
+
|
|
277
|
+
model_config = ConfigDict(extra="allow")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# Custom Content Block Types for Internal Use
|
|
281
|
+
class SDKMessageMode(SystemMessage):
|
|
282
|
+
"""Custom content block for system messages with source attribution."""
|
|
283
|
+
|
|
284
|
+
type: Literal["system_message"] = "system_message"
|
|
285
|
+
source: str = "claude_code_sdk"
|
|
286
|
+
|
|
287
|
+
model_config = ConfigDict(extra="allow")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class ToolUseSDKBlock(BaseModel):
|
|
291
|
+
"""Custom content block for tool use with SDK metadata."""
|
|
292
|
+
|
|
293
|
+
type: Literal["tool_use_sdk"] = "tool_use_sdk"
|
|
294
|
+
id: str = Field(..., description="Unique identifier for the tool use")
|
|
295
|
+
name: str = Field(..., description="Name of the tool being used")
|
|
296
|
+
input: dict[str, Any] = Field(..., description="Input parameters for the tool")
|
|
297
|
+
source: str = "claude_code_sdk"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class ToolResultSDKBlock(BaseModel):
|
|
301
|
+
"""Custom content block for tool results with SDK metadata."""
|
|
302
|
+
|
|
303
|
+
type: Literal["tool_result_sdk"] = "tool_result_sdk"
|
|
304
|
+
tool_use_id: str = Field(
|
|
305
|
+
..., description="ID of the tool use this result corresponds to"
|
|
306
|
+
)
|
|
307
|
+
content: str | list[dict[str, Any]] | None = Field(
|
|
308
|
+
None, description="Result content from the tool"
|
|
309
|
+
)
|
|
310
|
+
is_error: bool | None = Field(
|
|
311
|
+
None, description="Whether this result represents an error"
|
|
312
|
+
)
|
|
313
|
+
source: str = "claude_code_sdk"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class ResultMessageBlock(ResultMessage):
|
|
317
|
+
"""Custom content block for result messages with session data."""
|
|
318
|
+
|
|
319
|
+
type: Literal["result_message"] = "result_message"
|
|
320
|
+
source: str = "claude_code_sdk"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# Union type for all custom content blocks
|
|
324
|
+
SDKContentBlock = Annotated[
|
|
325
|
+
TextBlock
|
|
326
|
+
| ToolUseBlock
|
|
327
|
+
| ToolResultBlock
|
|
328
|
+
| ThinkingBlock
|
|
329
|
+
| SDKMessageMode
|
|
330
|
+
| ToolUseSDKBlock
|
|
331
|
+
| ToolResultSDKBlock
|
|
332
|
+
| ResultMessageBlock,
|
|
333
|
+
Field(discriminator="type"),
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# Extended content block type that includes both SDK and custom blocks
|
|
338
|
+
ExtendedContentBlock = SDKContentBlock
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# Conversion Functions
|
|
342
|
+
def convert_sdk_text_block(text_content: str) -> TextBlock:
|
|
343
|
+
"""Convert raw text content to TextBlock model."""
|
|
344
|
+
return TextBlock(text=text_content)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def convert_sdk_tool_use_block(
|
|
348
|
+
tool_id: str, tool_name: str, tool_input: dict[str, Any]
|
|
349
|
+
) -> ToolUseBlock:
|
|
350
|
+
"""Convert raw tool use data to ToolUseBlock model."""
|
|
351
|
+
return ToolUseBlock(id=tool_id, name=tool_name, input=tool_input)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def convert_sdk_tool_result_block(
|
|
355
|
+
tool_use_id: str,
|
|
356
|
+
content: str | list[dict[str, Any]] | None = None,
|
|
357
|
+
is_error: bool | None = None,
|
|
358
|
+
) -> ToolResultBlock:
|
|
359
|
+
"""Convert raw tool result data to ToolResultBlock model."""
|
|
360
|
+
return ToolResultBlock(tool_use_id=tool_use_id, content=content, is_error=is_error)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def convert_sdk_system_message(subtype: str, data: dict[str, Any]) -> SystemMessage:
|
|
364
|
+
"""Convert raw system message data to SystemMessage model."""
|
|
365
|
+
return SystemMessage(subtype=subtype, data=data)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def convert_sdk_result_message(
|
|
369
|
+
session_id: str,
|
|
370
|
+
subtype: str = "",
|
|
371
|
+
duration_ms: int = 0,
|
|
372
|
+
duration_api_ms: int = 0,
|
|
373
|
+
is_error: bool = False,
|
|
374
|
+
num_turns: int = 0,
|
|
375
|
+
usage: dict[str, Any] | None = None,
|
|
376
|
+
total_cost_usd: float | None = None,
|
|
377
|
+
result: str | None = None,
|
|
378
|
+
) -> ResultMessage:
|
|
379
|
+
"""Convert raw result message data to ResultMessage model."""
|
|
380
|
+
return ResultMessage(
|
|
381
|
+
session_id=session_id,
|
|
382
|
+
subtype=subtype,
|
|
383
|
+
duration_ms=duration_ms,
|
|
384
|
+
duration_api_ms=duration_api_ms,
|
|
385
|
+
is_error=is_error,
|
|
386
|
+
num_turns=num_turns,
|
|
387
|
+
usage=usage,
|
|
388
|
+
total_cost_usd=total_cost_usd,
|
|
389
|
+
result=result,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
__all__ = [
|
|
394
|
+
# Generic conversion
|
|
395
|
+
"to_sdk_variant",
|
|
396
|
+
# Content blocks
|
|
397
|
+
"TextBlock",
|
|
398
|
+
"ToolUseBlock",
|
|
399
|
+
"ToolResultBlock",
|
|
400
|
+
"ThinkingBlock",
|
|
401
|
+
"ContentBlock",
|
|
402
|
+
# Messages
|
|
403
|
+
"UserMessage",
|
|
404
|
+
"AssistantMessage",
|
|
405
|
+
"SystemMessage",
|
|
406
|
+
"ResultMessage",
|
|
407
|
+
# Custom content blocks
|
|
408
|
+
"SDKMessageMode",
|
|
409
|
+
"ToolUseSDKBlock",
|
|
410
|
+
"ToolResultSDKBlock",
|
|
411
|
+
"ResultMessageBlock",
|
|
412
|
+
"SDKContentBlock",
|
|
413
|
+
"ExtendedContentBlock",
|
|
414
|
+
# Conversion functions
|
|
415
|
+
"convert_sdk_text_block",
|
|
416
|
+
"convert_sdk_tool_use_block",
|
|
417
|
+
"convert_sdk_tool_result_block",
|
|
418
|
+
"convert_sdk_system_message",
|
|
419
|
+
"convert_sdk_result_message",
|
|
420
|
+
]
|
ccproxy/models/messages.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"""Message models for Anthropic Messages API endpoint."""
|
|
2
2
|
|
|
3
|
-
from typing import Annotated, Any, Literal
|
|
3
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
6
6
|
|
|
7
|
+
from .claude_sdk import SDKContentBlock
|
|
7
8
|
from .requests import Message, ToolDefinition, Usage
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
pass
|
|
13
|
+
from .types import ServiceTier, StopReason, ToolChoiceType
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class SystemMessage(BaseModel):
|
|
@@ -111,6 +116,13 @@ class MessageCreateParams(BaseModel):
|
|
|
111
116
|
max_length=4,
|
|
112
117
|
),
|
|
113
118
|
] = None
|
|
119
|
+
stop_reason: Annotated[
|
|
120
|
+
list[str] | None,
|
|
121
|
+
Field(
|
|
122
|
+
description="Custom sequences where the model should stop generating",
|
|
123
|
+
max_length=4,
|
|
124
|
+
),
|
|
125
|
+
] = None
|
|
114
126
|
stream: Annotated[
|
|
115
127
|
bool | None,
|
|
116
128
|
Field(description="Whether to stream the response"),
|
|
@@ -195,22 +207,37 @@ class MessageCreateParams(BaseModel):
|
|
|
195
207
|
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
196
208
|
|
|
197
209
|
|
|
198
|
-
class
|
|
199
|
-
"""
|
|
210
|
+
class TextContentBlock(BaseModel):
|
|
211
|
+
"""Text content block."""
|
|
200
212
|
|
|
201
|
-
type:
|
|
202
|
-
text:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
input:
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
type: Literal["text"]
|
|
214
|
+
text: str
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class ToolUseContentBlock(BaseModel):
|
|
218
|
+
"""Tool use content block."""
|
|
219
|
+
|
|
220
|
+
type: Literal["tool_use"]
|
|
221
|
+
id: str
|
|
222
|
+
name: str
|
|
223
|
+
input: dict[str, Any]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class ThinkingContentBlock(BaseModel):
|
|
227
|
+
"""Thinking content block."""
|
|
228
|
+
|
|
229
|
+
type: Literal["thinking"]
|
|
230
|
+
thinking: str
|
|
231
|
+
signature: str | None = None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
MessageContentBlock = Annotated[
|
|
235
|
+
TextContentBlock | ToolUseContentBlock | ThinkingContentBlock,
|
|
236
|
+
Field(discriminator="type"),
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
CCProxyContentBlock = MessageContentBlock | SDKContentBlock
|
|
214
241
|
|
|
215
242
|
|
|
216
243
|
class MessageResponse(BaseModel):
|
|
@@ -222,7 +249,7 @@ class MessageResponse(BaseModel):
|
|
|
222
249
|
"assistant"
|
|
223
250
|
)
|
|
224
251
|
content: Annotated[
|
|
225
|
-
list[
|
|
252
|
+
list[CCProxyContentBlock],
|
|
226
253
|
Field(description="Array of content blocks in the response"),
|
|
227
254
|
]
|
|
228
255
|
model: Annotated[str, Field(description="The model used for the response")]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Pydantic models for permission system."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PermissionStatus(Enum):
|
|
12
|
+
"""Status of a permission request."""
|
|
13
|
+
|
|
14
|
+
PENDING = "pending"
|
|
15
|
+
ALLOWED = "allowed"
|
|
16
|
+
DENIED = "denied"
|
|
17
|
+
EXPIRED = "expired"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EventType(Enum):
|
|
21
|
+
"""Types of permission events."""
|
|
22
|
+
|
|
23
|
+
PERMISSION_REQUEST = "permission_request"
|
|
24
|
+
PERMISSION_RESOLVED = "permission_resolved"
|
|
25
|
+
PERMISSION_EXPIRED = "permission_expired"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PermissionInput(BaseModel):
|
|
29
|
+
"""Input parameters for a tool permission request."""
|
|
30
|
+
|
|
31
|
+
command: str | None = None
|
|
32
|
+
code: str | None = None
|
|
33
|
+
path: str | None = None
|
|
34
|
+
content: str | None = None
|
|
35
|
+
# Add other common input fields as needed
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PermissionRequest(BaseModel):
|
|
39
|
+
"""Represents a tool permission request."""
|
|
40
|
+
|
|
41
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
42
|
+
tool_name: str
|
|
43
|
+
input: dict[str, str] # More specific than Any
|
|
44
|
+
status: PermissionStatus = PermissionStatus.PENDING
|
|
45
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
46
|
+
expires_at: datetime
|
|
47
|
+
resolved_at: datetime | None = None
|
|
48
|
+
|
|
49
|
+
# Private attribute for event-driven waiting
|
|
50
|
+
_resolved_event: asyncio.Event = PrivateAttr(default_factory=asyncio.Event)
|
|
51
|
+
|
|
52
|
+
def is_expired(self) -> bool:
|
|
53
|
+
"""Check if the request has expired."""
|
|
54
|
+
if self.status != PermissionStatus.PENDING:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
now, expires_at = self._normalize_datetimes(datetime.now(UTC), self.expires_at)
|
|
58
|
+
return now > expires_at
|
|
59
|
+
|
|
60
|
+
def time_remaining(self) -> int:
|
|
61
|
+
"""Get time remaining in seconds."""
|
|
62
|
+
if self.status != PermissionStatus.PENDING:
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
now, expires_at = self._normalize_datetimes(datetime.now(UTC), self.expires_at)
|
|
66
|
+
remaining = (expires_at - now).total_seconds()
|
|
67
|
+
return max(0, int(remaining))
|
|
68
|
+
|
|
69
|
+
def resolve(self, allowed: bool) -> None:
|
|
70
|
+
"""Resolve the request."""
|
|
71
|
+
if self.status != PermissionStatus.PENDING:
|
|
72
|
+
raise ValueError(f"Cannot resolve request in {self.status} status")
|
|
73
|
+
|
|
74
|
+
self.status = PermissionStatus.ALLOWED if allowed else PermissionStatus.DENIED
|
|
75
|
+
self.resolved_at = datetime.now(UTC)
|
|
76
|
+
# Signal waiting coroutines that resolution is complete
|
|
77
|
+
self._resolved_event.set()
|
|
78
|
+
|
|
79
|
+
def _normalize_datetimes(
|
|
80
|
+
self, dt1: datetime, dt2: datetime
|
|
81
|
+
) -> tuple[datetime, datetime]:
|
|
82
|
+
"""Normalize two datetimes to ensure both are timezone-aware.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
dt1: First datetime to normalize
|
|
86
|
+
dt2: Second datetime to normalize
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Tuple of normalized timezone-aware datetimes
|
|
90
|
+
"""
|
|
91
|
+
# If dt1 is timezone-aware, convert dt2 to timezone-aware if needed
|
|
92
|
+
if dt1.tzinfo is not None:
|
|
93
|
+
if dt2.tzinfo is None:
|
|
94
|
+
dt2 = dt2.replace(tzinfo=UTC)
|
|
95
|
+
# If dt2 is timezone-aware, convert dt1 to timezone-aware if needed
|
|
96
|
+
elif dt2.tzinfo is not None:
|
|
97
|
+
dt1 = dt1.replace(tzinfo=UTC)
|
|
98
|
+
|
|
99
|
+
return dt1, dt2
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PermissionEvent(BaseModel):
|
|
103
|
+
"""Event emitted by the permission service."""
|
|
104
|
+
|
|
105
|
+
type: EventType
|
|
106
|
+
request_id: str
|
|
107
|
+
tool_name: str | None = None
|
|
108
|
+
input: dict[str, str] | None = None
|
|
109
|
+
created_at: str | None = None
|
|
110
|
+
expires_at: str | None = None
|
|
111
|
+
timeout_seconds: int | None = None
|
|
112
|
+
allowed: bool | None = None
|
|
113
|
+
resolved_at: str | None = None
|
|
114
|
+
expired_at: str | None = None
|
|
115
|
+
message: str | None = None
|