ccproxy-api 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. ccproxy/_version.py +2 -2
  2. ccproxy/adapters/openai/__init__.py +1 -2
  3. ccproxy/adapters/openai/adapter.py +218 -180
  4. ccproxy/adapters/openai/streaming.py +247 -65
  5. ccproxy/api/__init__.py +0 -3
  6. ccproxy/api/app.py +173 -40
  7. ccproxy/api/dependencies.py +62 -3
  8. ccproxy/api/middleware/errors.py +3 -7
  9. ccproxy/api/middleware/headers.py +0 -2
  10. ccproxy/api/middleware/logging.py +4 -3
  11. ccproxy/api/middleware/request_content_logging.py +297 -0
  12. ccproxy/api/middleware/request_id.py +5 -0
  13. ccproxy/api/middleware/server_header.py +0 -4
  14. ccproxy/api/routes/__init__.py +9 -1
  15. ccproxy/api/routes/claude.py +23 -32
  16. ccproxy/api/routes/health.py +58 -4
  17. ccproxy/api/routes/mcp.py +171 -0
  18. ccproxy/api/routes/metrics.py +4 -8
  19. ccproxy/api/routes/permissions.py +217 -0
  20. ccproxy/api/routes/proxy.py +0 -53
  21. ccproxy/api/services/__init__.py +6 -0
  22. ccproxy/api/services/permission_service.py +368 -0
  23. ccproxy/api/ui/__init__.py +6 -0
  24. ccproxy/api/ui/permission_handler_protocol.py +33 -0
  25. ccproxy/api/ui/terminal_permission_handler.py +593 -0
  26. ccproxy/auth/conditional.py +2 -2
  27. ccproxy/auth/dependencies.py +1 -1
  28. ccproxy/auth/oauth/models.py +0 -1
  29. ccproxy/auth/oauth/routes.py +1 -3
  30. ccproxy/auth/storage/json_file.py +0 -1
  31. ccproxy/auth/storage/keyring.py +0 -3
  32. ccproxy/claude_sdk/__init__.py +2 -0
  33. ccproxy/claude_sdk/client.py +91 -8
  34. ccproxy/claude_sdk/converter.py +405 -210
  35. ccproxy/claude_sdk/options.py +76 -29
  36. ccproxy/claude_sdk/parser.py +200 -0
  37. ccproxy/claude_sdk/streaming.py +286 -0
  38. ccproxy/cli/commands/__init__.py +5 -2
  39. ccproxy/cli/commands/auth.py +2 -4
  40. ccproxy/cli/commands/permission_handler.py +553 -0
  41. ccproxy/cli/commands/serve.py +30 -12
  42. ccproxy/cli/docker/params.py +0 -4
  43. ccproxy/cli/helpers.py +0 -2
  44. ccproxy/cli/main.py +5 -16
  45. ccproxy/cli/options/claude_options.py +19 -1
  46. ccproxy/cli/options/core_options.py +0 -3
  47. ccproxy/cli/options/security_options.py +0 -2
  48. ccproxy/cli/options/server_options.py +3 -2
  49. ccproxy/config/auth.py +0 -1
  50. ccproxy/config/claude.py +78 -2
  51. ccproxy/config/discovery.py +0 -1
  52. ccproxy/config/docker_settings.py +0 -1
  53. ccproxy/config/loader.py +1 -4
  54. ccproxy/config/scheduler.py +20 -0
  55. ccproxy/config/security.py +7 -2
  56. ccproxy/config/server.py +5 -0
  57. ccproxy/config/settings.py +13 -7
  58. ccproxy/config/validators.py +1 -1
  59. ccproxy/core/async_utils.py +1 -4
  60. ccproxy/core/errors.py +45 -1
  61. ccproxy/core/http_transformers.py +4 -3
  62. ccproxy/core/interfaces.py +2 -2
  63. ccproxy/core/logging.py +97 -95
  64. ccproxy/core/middleware.py +1 -1
  65. ccproxy/core/proxy.py +1 -1
  66. ccproxy/core/transformers.py +1 -1
  67. ccproxy/core/types.py +1 -1
  68. ccproxy/docker/models.py +1 -1
  69. ccproxy/docker/protocol.py +0 -3
  70. ccproxy/models/__init__.py +41 -0
  71. ccproxy/models/claude_sdk.py +420 -0
  72. ccproxy/models/messages.py +45 -18
  73. ccproxy/models/permissions.py +115 -0
  74. ccproxy/models/requests.py +1 -1
  75. ccproxy/models/responses.py +29 -2
  76. ccproxy/observability/access_logger.py +1 -2
  77. ccproxy/observability/context.py +17 -1
  78. ccproxy/observability/metrics.py +1 -3
  79. ccproxy/observability/pushgateway.py +0 -2
  80. ccproxy/observability/stats_printer.py +2 -4
  81. ccproxy/observability/storage/duckdb_simple.py +1 -1
  82. ccproxy/observability/storage/models.py +0 -1
  83. ccproxy/pricing/cache.py +0 -1
  84. ccproxy/pricing/loader.py +5 -21
  85. ccproxy/pricing/updater.py +0 -1
  86. ccproxy/scheduler/__init__.py +1 -0
  87. ccproxy/scheduler/core.py +6 -6
  88. ccproxy/scheduler/manager.py +35 -7
  89. ccproxy/scheduler/registry.py +1 -1
  90. ccproxy/scheduler/tasks.py +127 -2
  91. ccproxy/services/claude_sdk_service.py +220 -328
  92. ccproxy/services/credentials/manager.py +0 -1
  93. ccproxy/services/credentials/oauth_client.py +1 -2
  94. ccproxy/services/proxy_service.py +93 -222
  95. ccproxy/testing/config.py +1 -1
  96. ccproxy/testing/mock_responses.py +0 -1
  97. ccproxy/utils/model_mapping.py +197 -0
  98. ccproxy/utils/models_provider.py +150 -0
  99. ccproxy/utils/simple_request_logger.py +284 -0
  100. ccproxy/utils/version_checker.py +184 -0
  101. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/METADATA +63 -2
  102. ccproxy_api-0.1.3.dist-info/RECORD +166 -0
  103. ccproxy/cli/commands/permission.py +0 -128
  104. ccproxy_api-0.1.2.dist-info/RECORD +0 -150
  105. /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
  106. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/WHEEL +0 -0
  107. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/entry_points.txt +0 -0
  108. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -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
+ ]
@@ -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
- from .types import ContentBlockType, ServiceTier, StopReason, ToolChoiceType
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 MessageContentBlock(BaseModel):
199
- """Content block in a message response."""
210
+ class TextContentBlock(BaseModel):
211
+ """Text content block."""
200
212
 
201
- type: Annotated[ContentBlockType, Field(description="Type of content block")]
202
- text: Annotated[
203
- str | None, Field(description="Text content (for text/thinking blocks)")
204
- ] = None
205
- id: Annotated[str | None, Field(description="Unique ID (for tool_use blocks)")] = (
206
- None
207
- )
208
- name: Annotated[
209
- str | None, Field(description="Tool name (for tool_use blocks)")
210
- ] = None
211
- input: Annotated[
212
- dict[str, Any] | None, Field(description="Tool input (for tool_use blocks)")
213
- ] = None
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[MessageContentBlock],
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Annotated, Any, Literal
4
4
 
5
- from pydantic import BaseModel, ConfigDict, Field, field_validator, validator
5
+ from pydantic import BaseModel, ConfigDict, Field
6
6
 
7
7
 
8
8
  class ImageSource(BaseModel):
@@ -4,7 +4,7 @@ from typing import Annotated, Any, Literal
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, Field
6
6
 
7
- from .requests import MessageContent, Usage
7
+ from .requests import Usage
8
8
 
9
9
 
10
10
  class ToolCall(BaseModel):
@@ -176,7 +176,34 @@ class PermissionToolDenyResponse(BaseModel):
176
176
  model_config = ConfigDict(extra="forbid")
177
177
 
178
178
 
179
- PermissionToolResponse = PermissionToolAllowResponse | PermissionToolDenyResponse
179
+ class PermissionToolPendingResponse(BaseModel):
180
+ """Response model for pending permission tool requests requiring user confirmation."""
181
+
182
+ behavior: Annotated[
183
+ Literal["pending"], Field(description="Permission behavior")
184
+ ] = "pending"
185
+ confirmation_id: Annotated[
186
+ str,
187
+ Field(
188
+ description="Unique identifier for the confirmation request",
189
+ alias="confirmationId",
190
+ ),
191
+ ]
192
+ message: Annotated[
193
+ str,
194
+ Field(
195
+ description="Instructions for retrying the request after user confirmation"
196
+ ),
197
+ ] = "User confirmation required. Please retry with the same confirmation_id."
198
+
199
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
200
+
201
+
202
+ PermissionToolResponse = (
203
+ PermissionToolAllowResponse
204
+ | PermissionToolDenyResponse
205
+ | PermissionToolPendingResponse
206
+ )
180
207
 
181
208
 
182
209
  class RateLimitError(APIError):
@@ -7,9 +7,8 @@ access logs with complete request metadata including token usage and costs.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import asyncio
11
10
  import time
12
- from typing import TYPE_CHECKING, Any, Optional
11
+ from typing import TYPE_CHECKING, Any
13
12
 
14
13
  import structlog
15
14