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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.4.dist-info}/METADATA +63 -2
  102. ccproxy_api-0.1.4.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.4.dist-info}/WHEEL +0 -0
  107. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/entry_points.txt +0 -0
  108. {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/licenses/LICENSE +0 -0
ccproxy/cli/helpers.py CHANGED
@@ -1,12 +1,10 @@
1
1
  """CLI helper utilities for CCProxy API."""
2
2
 
3
- import logging
4
3
  from pathlib import Path
5
4
  from typing import Any
6
5
 
7
6
  from rich_toolkit import RichToolkit, RichToolkitTheme
8
7
  from rich_toolkit.styles import TaggedStyle
9
- from uvicorn.logging import DefaultFormatter
10
8
 
11
9
  from ccproxy.core.async_utils import patched_typing
12
10
 
ccproxy/cli/main.py CHANGED
@@ -1,33 +1,19 @@
1
1
  """Main entry point for CCProxy API Server."""
2
2
 
3
- import os
4
- import secrets
5
3
  from pathlib import Path
6
- from typing import Annotated, Any, Optional, cast
4
+ from typing import Annotated
7
5
 
8
6
  import typer
9
- from click import get_current_context
10
7
  from structlog import get_logger
11
- from typer import Typer
12
8
 
13
9
  from ccproxy._version import __version__
14
- from ccproxy.api.middleware.cors import setup_cors_middleware
15
10
  from ccproxy.cli.helpers import (
16
11
  get_rich_toolkit,
17
- is_running_in_docker,
18
- warning,
19
12
  )
20
- from ccproxy.config.settings import (
21
- ConfigurationError,
22
- Settings,
23
- config_manager,
24
- get_settings,
25
- )
26
- from ccproxy.core.async_utils import get_package_dir, get_root_package_name
27
- from ccproxy.core.logging import setup_logging
28
13
 
29
14
  from .commands.auth import app as auth_app
30
15
  from .commands.config import app as config_app
16
+ from .commands.permission_handler import app as permission_handler_app
31
17
  from .commands.serve import api
32
18
 
33
19
 
@@ -98,6 +84,9 @@ app.add_typer(config_app)
98
84
  # Register auth command
99
85
  app.add_typer(auth_app)
100
86
 
87
+ # Register permission handler command
88
+ app.add_typer(permission_handler_app)
89
+
101
90
 
102
91
  # Register imported commands
103
92
  app.command(name="serve")(api)
@@ -1,7 +1,6 @@
1
1
  """Claude-specific CLI options."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any
5
4
 
6
5
  import typer
7
6
 
@@ -78,6 +77,22 @@ def validate_cwd(
78
77
  return value
79
78
 
80
79
 
80
+ def validate_sdk_message_mode(
81
+ ctx: typer.Context, param: typer.CallbackParam, value: str | None
82
+ ) -> str | None:
83
+ """Validate SDK message mode."""
84
+ if value is None:
85
+ return None
86
+
87
+ valid_modes = {"forward", "ignore", "formatted"}
88
+ if value not in valid_modes:
89
+ raise typer.BadParameter(
90
+ f"SDK message mode must be one of: {', '.join(valid_modes)}"
91
+ )
92
+
93
+ return value
94
+
95
+
81
96
  # Factory functions removed - use Annotated syntax directly in commands
82
97
 
83
98
 
@@ -99,6 +114,7 @@ class ClaudeOptions:
99
114
  max_turns: int | None = None,
100
115
  cwd: str | None = None,
101
116
  permission_prompt_tool_name: str | None = None,
117
+ sdk_message_mode: str | None = None,
102
118
  ):
103
119
  """Initialize Claude options.
104
120
 
@@ -112,6 +128,7 @@ class ClaudeOptions:
112
128
  max_turns: Maximum conversation turns
113
129
  cwd: Working directory path
114
130
  permission_prompt_tool_name: Permission prompt tool name
131
+ sdk_message_mode: SDK message handling mode
115
132
  """
116
133
  self.max_thinking_tokens = max_thinking_tokens
117
134
  self.allowed_tools = allowed_tools
@@ -122,3 +139,4 @@ class ClaudeOptions:
122
139
  self.max_turns = max_turns
123
140
  self.cwd = cwd
124
141
  self.permission_prompt_tool_name = permission_prompt_tool_name
142
+ self.sdk_message_mode = sdk_message_mode
@@ -1,9 +1,6 @@
1
1
  """Core CLI options for configuration and global settings."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any
5
-
6
- import typer
7
4
 
8
5
 
9
6
  # Factory functions removed - use Annotated syntax directly in commands
@@ -1,7 +1,5 @@
1
1
  """Security-related CLI options."""
2
2
 
3
- from typing import Any
4
-
5
3
  import typer
6
4
 
7
5
 
@@ -1,7 +1,5 @@
1
1
  """Server-related CLI options."""
2
2
 
3
- from typing import Any
4
-
5
3
  import typer
6
4
 
7
5
 
@@ -49,6 +47,7 @@ class ServerOptions:
49
47
  reload: bool | None = None,
50
48
  log_level: str | None = None,
51
49
  log_file: str | None = None,
50
+ use_terminal_confirmation_handler: bool | None = None,
52
51
  ):
53
52
  """Initialize server options.
54
53
 
@@ -58,9 +57,11 @@ class ServerOptions:
58
57
  reload: Enable auto-reload for development
59
58
  log_level: Logging level
60
59
  log_file: Path to JSON log file
60
+ use_terminal_confirmation_handler: Enable terminal UI for confirmation prompts
61
61
  """
62
62
  self.port = port
63
63
  self.host = host
64
64
  self.reload = reload
65
65
  self.log_level = log_level
66
66
  self.log_file = log_file
67
+ self.use_terminal_confirmation_handler = use_terminal_confirmation_handler
ccproxy/config/auth.py CHANGED
@@ -1,6 +1,5 @@
1
1
  """Authentication and credentials configuration."""
2
2
 
3
- import os
4
3
  from pathlib import Path
5
4
  from typing import Any
6
5
 
ccproxy/config/claude.py CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  import os
4
4
  import shutil
5
+ from enum import Enum
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
9
+ import structlog
8
10
  from pydantic import BaseModel, Field, field_validator
9
11
 
10
12
  from ccproxy.core.async_utils import get_package_dir, patched_typing
@@ -14,6 +16,31 @@ from ccproxy.core.async_utils import get_package_dir, patched_typing
14
16
  with patched_typing():
15
17
  from claude_code_sdk import ClaudeCodeOptions # noqa: E402
16
18
 
19
+ logger = structlog.get_logger(__name__)
20
+
21
+
22
+ def _create_default_claude_code_options() -> ClaudeCodeOptions:
23
+ """Create ClaudeCodeOptions with default values."""
24
+ return ClaudeCodeOptions(
25
+ mcp_servers={
26
+ "confirmation": {"type": "sse", "url": "http://127.0.0.1:8000/mcp"}
27
+ },
28
+ permission_prompt_tool_name="mcp__confirmation__check_permission",
29
+ )
30
+
31
+
32
+ class SDKMessageMode(str, Enum):
33
+ """Modes for handling SDK messages from Claude SDK.
34
+
35
+ - forward: Forward SDK content blocks directly with original types and metadata
36
+ - ignore: Skip SDK messages and blocks completely
37
+ - formatted: Format as XML tags with JSON data in text deltas
38
+ """
39
+
40
+ FORWARD = "forward"
41
+ IGNORE = "ignore"
42
+ FORMATTED = "formatted"
43
+
17
44
 
18
45
  class ClaudeSettings(BaseModel):
19
46
  """Claude-specific configuration settings."""
@@ -24,10 +51,20 @@ class ClaudeSettings(BaseModel):
24
51
  )
25
52
 
26
53
  code_options: ClaudeCodeOptions = Field(
27
- default_factory=lambda: ClaudeCodeOptions(),
54
+ default_factory=_create_default_claude_code_options,
28
55
  description="Claude Code SDK options configuration",
29
56
  )
30
57
 
58
+ sdk_message_mode: SDKMessageMode = Field(
59
+ default=SDKMessageMode.FORWARD,
60
+ description="Mode for handling SDK messages from Claude SDK. Options: forward (direct SDK blocks), ignore (skip blocks), formatted (XML tags with JSON data)",
61
+ )
62
+
63
+ pretty_format: bool = Field(
64
+ default=True,
65
+ description="Whether to use pretty formatting (indented JSON, newlines after XML tags, unescaped content). When false: compact JSON, no newlines, escaped content between XML tags",
66
+ )
67
+
31
68
  @field_validator("cli_path")
32
69
  @classmethod
33
70
  def validate_claude_cli_path(cls, v: str | None) -> str | None:
@@ -47,12 +84,51 @@ class ClaudeSettings(BaseModel):
47
84
  def validate_claude_code_options(cls, v: Any) -> Any:
48
85
  """Validate and convert Claude Code options."""
49
86
  if v is None:
50
- return ClaudeCodeOptions()
87
+ # Create instance with default values (same as default_factory)
88
+ return _create_default_claude_code_options()
51
89
 
52
90
  # If it's already a ClaudeCodeOptions instance, return as-is
53
91
  if isinstance(v, ClaudeCodeOptions):
54
92
  return v
55
93
 
94
+ # If it's an empty dict, treat it like None and use defaults
95
+ if isinstance(v, dict) and not v:
96
+ return _create_default_claude_code_options()
97
+
98
+ # For non-empty dicts, merge with defaults instead of replacing them
99
+ if isinstance(v, dict):
100
+ # Start with default values
101
+ defaults = _create_default_claude_code_options()
102
+
103
+ # Extract default values as a dict for merging
104
+ default_values = {
105
+ "mcp_servers": defaults.mcp_servers.copy(),
106
+ "permission_prompt_tool_name": defaults.permission_prompt_tool_name,
107
+ }
108
+
109
+ # Add other default attributes if they exist
110
+ for attr in [
111
+ "max_thinking_tokens",
112
+ "allowed_tools",
113
+ "disallowed_tools",
114
+ "cwd",
115
+ "append_system_prompt",
116
+ "max_turns",
117
+ "continue_conversation",
118
+ "permission_mode",
119
+ "model",
120
+ "system_prompt",
121
+ ]:
122
+ if hasattr(defaults, attr):
123
+ default_value = getattr(defaults, attr, None)
124
+ if default_value is not None:
125
+ default_values[attr] = default_value
126
+
127
+ # Merge CLI overrides with defaults (CLI overrides take precedence)
128
+ merged_values = {**default_values, **v}
129
+
130
+ return ClaudeCodeOptions(**merged_values)
131
+
56
132
  # Try to convert to dict if possible
57
133
  if hasattr(v, "model_dump"):
58
134
  return v.model_dump()
@@ -1,5 +1,4 @@
1
1
  from pathlib import Path
2
- from typing import Optional
3
2
 
4
3
  from ccproxy.core.system import get_xdg_config_home
5
4
 
@@ -1,7 +1,6 @@
1
1
  """Docker settings configuration for CCProxy API."""
2
2
 
3
3
  import os
4
- from typing import Any
5
4
 
6
5
  from pydantic import BaseModel, Field, field_validator, model_validator
7
6
 
ccproxy/config/loader.py CHANGED
@@ -1,10 +1,7 @@
1
1
  """Configuration file loader for ccproxy."""
2
2
 
3
- import os
4
3
  from pathlib import Path
5
- from typing import Any, Optional
6
-
7
- from pydantic import BaseModel
4
+ from typing import Any
8
5
 
9
6
  from ccproxy.config.discovery import find_toml_config_file
10
7
  from ccproxy.config.settings import Settings
@@ -82,6 +82,26 @@ class SchedulerSettings(BaseSettings):
82
82
  description="Interval in seconds between stats printing",
83
83
  )
84
84
 
85
+ # Version checking task settings
86
+ version_check_enabled: bool = Field(
87
+ default=True,
88
+ description="Whether version update checking is enabled",
89
+ )
90
+
91
+ version_check_interval_hours: int = Field(
92
+ default=12,
93
+ ge=1,
94
+ le=168, # Max 1 week
95
+ description="Interval in hours between version checks",
96
+ )
97
+
98
+ version_check_startup_max_age_hours: float = Field(
99
+ default=1.0,
100
+ ge=0.1,
101
+ le=24.0,
102
+ description="Maximum age in hours since last check before running startup check",
103
+ )
104
+
85
105
  model_config = SettingsConfigDict(
86
106
  env_prefix="SCHEDULER__",
87
107
  case_sensitive=False,
@@ -1,7 +1,5 @@
1
1
  """Security configuration settings."""
2
2
 
3
- from typing import Literal
4
-
5
3
  from pydantic import BaseModel, Field
6
4
 
7
5
 
@@ -12,3 +10,10 @@ class SecuritySettings(BaseModel):
12
10
  default=None,
13
11
  description="Bearer token for API authentication (optional)",
14
12
  )
13
+
14
+ confirmation_timeout_seconds: int = Field(
15
+ default=30,
16
+ ge=5,
17
+ le=300,
18
+ description="Timeout in seconds for permission confirmation requests (5-300)",
19
+ )
ccproxy/config/server.py CHANGED
@@ -60,6 +60,11 @@ class ServerSettings(BaseModel):
60
60
  description="Path to JSON log file. If specified, logs will be written to this file in JSON format",
61
61
  )
62
62
 
63
+ use_terminal_permission_handler: bool = Field(
64
+ default=False,
65
+ description="Enable terminal UI for permission prompts. Set to False to use external handler via SSE (not implemented)",
66
+ )
67
+
63
68
  @field_validator("log_level")
64
69
  @classmethod
65
70
  def validate_log_level(cls, v: str) -> str:
@@ -3,17 +3,15 @@
3
3
  import contextlib
4
4
  import json
5
5
  import os
6
- import shutil
7
6
  import tomllib
8
7
  from pathlib import Path
9
- from typing import Any, Literal
8
+ from typing import Any
10
9
 
11
- from pydantic import BaseModel, Field, field_validator, model_validator
10
+ import structlog
11
+ from pydantic import Field, field_validator, model_validator
12
12
  from pydantic_settings import BaseSettings, SettingsConfigDict
13
13
 
14
- from ccproxy import __version__
15
- from ccproxy.config.discovery import find_toml_config_file, get_claude_cli_config_dir
16
- from ccproxy.core.async_utils import format_version, get_package_dir, patched_typing
14
+ from ccproxy.config.discovery import find_toml_config_file
17
15
 
18
16
  from .auth import AuthSettings
19
17
  from .claude import ClaudeSettings
@@ -463,6 +461,11 @@ class ConfigurationManager:
463
461
  if cli_args.get("claude_cli_path") is not None:
464
462
  claude_settings["cli_path"] = cli_args["claude_cli_path"]
465
463
 
464
+ # Direct Claude settings (not nested in code_options)
465
+ for key in ["sdk_message_mode"]:
466
+ if cli_args.get(key) is not None:
467
+ claude_settings[key] = cli_args[key]
468
+
466
469
  # Claude Code options
467
470
  claude_opts = {}
468
471
  for key in [
@@ -508,6 +511,8 @@ class ConfigurationManager:
508
511
  # Global configuration manager instance
509
512
  config_manager = ConfigurationManager()
510
513
 
514
+ logger = structlog.get_logger(__name__)
515
+
511
516
 
512
517
  def get_settings(config_path: Path | str | None = None) -> Settings:
513
518
  """Get the global settings instance with configuration file support.
@@ -527,7 +532,8 @@ def get_settings(config_path: Path | str | None = None) -> Settings:
527
532
  with contextlib.suppress(json.JSONDecodeError):
528
533
  cli_overrides = json.loads(cli_overrides_json)
529
534
 
530
- return Settings.from_config(config_path=config_path, **cli_overrides)
535
+ settings = Settings.from_config(config_path=config_path, **cli_overrides)
536
+ return settings
531
537
  except Exception as e:
532
538
  # If settings can't be loaded (e.g., missing API key),
533
539
  # this will be handled by the caller
@@ -2,7 +2,7 @@
2
2
 
3
3
  import re
4
4
  from pathlib import Path
5
- from typing import Any, Optional, Union
5
+ from typing import Any
6
6
  from urllib.parse import urlparse
7
7
 
8
8
 
@@ -465,15 +465,12 @@ def validate_config_with_schema(
465
465
  import json
466
466
  import subprocess
467
467
  import tempfile
468
- from typing import Any
469
468
 
470
469
  # Import tomllib for Python 3.11+ or fallback to tomli
471
470
  try:
472
471
  import tomllib
473
472
  except ImportError:
474
- import tomli as tomllib # type: ignore[import-not-found,no-redef]
475
-
476
- from ccproxy.config.settings import Settings
473
+ import tomli as tomllib # type: ignore[no-redef]
477
474
 
478
475
  config_path = Path()
479
476
 
ccproxy/core/errors.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Core error types for the proxy system."""
2
2
 
3
- from typing import Any, Optional
3
+ from typing import Any
4
4
 
5
5
  from fastapi import HTTPException
6
6
 
@@ -234,6 +234,45 @@ class DockerError(ClaudeProxyError):
234
234
  )
235
235
 
236
236
 
237
+ class PermissionRequestError(ClaudeProxyError):
238
+ """Base exception for permission request-related errors."""
239
+
240
+ pass
241
+
242
+
243
+ class PermissionNotFoundError(PermissionRequestError):
244
+ """Raised when permission request is not found."""
245
+
246
+ def __init__(self, confirmation_id: str) -> None:
247
+ super().__init__(
248
+ message=f"Permission request '{confirmation_id}' not found",
249
+ error_type="not_found_error",
250
+ status_code=404,
251
+ )
252
+
253
+
254
+ class PermissionExpiredError(PermissionRequestError):
255
+ """Raised when permission request has expired."""
256
+
257
+ def __init__(self, confirmation_id: str) -> None:
258
+ super().__init__(
259
+ message=f"Permission request '{confirmation_id}' has expired",
260
+ error_type="expired_error",
261
+ status_code=410,
262
+ )
263
+
264
+
265
+ class PermissionAlreadyResolvedError(PermissionRequestError):
266
+ """Raised when trying to resolve an already resolved request."""
267
+
268
+ def __init__(self, confirmation_id: str, status: str) -> None:
269
+ super().__init__(
270
+ message=f"Permission request '{confirmation_id}' already resolved with status: {status}",
271
+ error_type="conflict_error",
272
+ status_code=409,
273
+ )
274
+
275
+
237
276
  __all__ = [
238
277
  # Core proxy errors
239
278
  "ProxyError",
@@ -253,4 +292,9 @@ __all__ = [
253
292
  "TimeoutError",
254
293
  "ServiceUnavailableError",
255
294
  "DockerError",
295
+ # Permission errors
296
+ "PermissionRequestError",
297
+ "PermissionNotFoundError",
298
+ "PermissionExpiredError",
299
+ "PermissionAlreadyResolvedError",
256
300
  ]
@@ -1,6 +1,5 @@
1
1
  """HTTP-level transformers for proxy service."""
2
2
 
3
- import json
4
3
  from typing import TYPE_CHECKING, Any
5
4
 
6
5
  import structlog
@@ -18,6 +17,8 @@ logger = structlog.get_logger(__name__)
18
17
  # Claude Code system prompt constants
19
18
  claude_code_prompt = "You are Claude Code, Anthropic's official CLI for Claude."
20
19
 
20
+ # claude_code_prompt = "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# important-instruction-reminders\nDo what has been asked; nothing more, nothing less.\nNEVER create files unless they're absolutely necessary for achieving your goal.\nALWAYS prefer editing an existing file to creating a new one.\nNEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n\n \n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n</system-reminder>\n"
21
+
21
22
 
22
23
  def get_claude_code_prompt() -> dict[str, Any]:
23
24
  """Get the Claude Code system prompt with cache control."""
@@ -179,7 +180,7 @@ class HTTPRequestTransformer(RequestTransformer):
179
180
 
180
181
  # Claude CLI identity headers
181
182
  proxy_headers["x-app"] = "cli"
182
- proxy_headers["User-Agent"] = "claude-cli/1.0.43 (external, cli)"
183
+ proxy_headers["User-Agent"] = "claude-cli/1.0.60 (external, cli)"
183
184
 
184
185
  # Stainless SDK compatibility headers
185
186
  proxy_headers["X-Stainless-Lang"] = "js"
@@ -189,7 +190,7 @@ class HTTPRequestTransformer(RequestTransformer):
189
190
  proxy_headers["X-Stainless-OS"] = "Linux"
190
191
  proxy_headers["X-Stainless-Arch"] = "x64"
191
192
  proxy_headers["X-Stainless-Runtime"] = "node"
192
- proxy_headers["X-Stainless-Runtime-Version"] = "v22.14.0"
193
+ proxy_headers["X-Stainless-Runtime-Version"] = "v24.3.0"
193
194
 
194
195
  # Standard HTTP headers for proper API interaction
195
196
  proxy_headers["accept-language"] = "*"
@@ -6,10 +6,10 @@ providing a single location for defining contracts and protocols.
6
6
 
7
7
  from abc import ABC, abstractmethod
8
8
  from collections.abc import AsyncIterator
9
- from typing import Any, Optional, Protocol, TypeVar, runtime_checkable
9
+ from typing import Any, Protocol, TypeVar, runtime_checkable
10
10
 
11
11
  from ccproxy.auth.models import ClaudeCredentials
12
- from ccproxy.core.types import ProxyRequest, ProxyResponse, TransformContext
12
+ from ccproxy.core.types import TransformContext
13
13
 
14
14
 
15
15
  __all__ = [