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.
- 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 +62 -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 +76 -29
- ccproxy/claude_sdk/parser.py +200 -0
- ccproxy/claude_sdk/streaming.py +286 -0
- ccproxy/cli/commands/__init__.py +5 -2
- ccproxy/cli/commands/auth.py +2 -4
- ccproxy/cli/commands/permission_handler.py +553 -0
- ccproxy/cli/commands/serve.py +30 -12
- ccproxy/cli/docker/params.py +0 -4
- ccproxy/cli/helpers.py +0 -2
- ccproxy/cli/main.py +5 -16
- ccproxy/cli/options/claude_options.py +19 -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 +13 -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 +29 -2
- 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 +220 -328
- 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.2.dist-info → ccproxy_api-0.1.4.dist-info}/METADATA +63 -2
- ccproxy_api-0.1.4.dist-info/RECORD +166 -0
- ccproxy/cli/commands/permission.py +0 -128
- ccproxy_api-0.1.2.dist-info/RECORD +0 -150
- /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/entry_points.txt +0 -0
- {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
|
|
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,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
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=
|
|
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
|
-
|
|
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()
|
ccproxy/config/discovery.py
CHANGED
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
|
|
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
|
ccproxy/config/scheduler.py
CHANGED
|
@@ -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,
|
ccproxy/config/security.py
CHANGED
|
@@ -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:
|
ccproxy/config/settings.py
CHANGED
|
@@ -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
|
|
8
|
+
from typing import Any
|
|
10
9
|
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
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
|
ccproxy/config/validators.py
CHANGED
ccproxy/core/async_utils.py
CHANGED
|
@@ -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[
|
|
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
|
|
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.
|
|
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"] = "
|
|
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"] = "*"
|
ccproxy/core/interfaces.py
CHANGED
|
@@ -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,
|
|
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
|
|
12
|
+
from ccproxy.core.types import TransformContext
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
__all__ = [
|