ccproxy-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. ccproxy/__init__.py +4 -0
  2. ccproxy/__main__.py +7 -0
  3. ccproxy/_version.py +21 -0
  4. ccproxy/adapters/__init__.py +11 -0
  5. ccproxy/adapters/base.py +80 -0
  6. ccproxy/adapters/openai/__init__.py +43 -0
  7. ccproxy/adapters/openai/adapter.py +915 -0
  8. ccproxy/adapters/openai/models.py +412 -0
  9. ccproxy/adapters/openai/streaming.py +449 -0
  10. ccproxy/api/__init__.py +28 -0
  11. ccproxy/api/app.py +225 -0
  12. ccproxy/api/dependencies.py +140 -0
  13. ccproxy/api/middleware/__init__.py +11 -0
  14. ccproxy/api/middleware/auth.py +0 -0
  15. ccproxy/api/middleware/cors.py +55 -0
  16. ccproxy/api/middleware/errors.py +703 -0
  17. ccproxy/api/middleware/headers.py +51 -0
  18. ccproxy/api/middleware/logging.py +175 -0
  19. ccproxy/api/middleware/request_id.py +69 -0
  20. ccproxy/api/middleware/server_header.py +62 -0
  21. ccproxy/api/responses.py +84 -0
  22. ccproxy/api/routes/__init__.py +16 -0
  23. ccproxy/api/routes/claude.py +181 -0
  24. ccproxy/api/routes/health.py +489 -0
  25. ccproxy/api/routes/metrics.py +1033 -0
  26. ccproxy/api/routes/proxy.py +238 -0
  27. ccproxy/auth/__init__.py +75 -0
  28. ccproxy/auth/bearer.py +68 -0
  29. ccproxy/auth/credentials_adapter.py +93 -0
  30. ccproxy/auth/dependencies.py +229 -0
  31. ccproxy/auth/exceptions.py +79 -0
  32. ccproxy/auth/manager.py +102 -0
  33. ccproxy/auth/models.py +118 -0
  34. ccproxy/auth/oauth/__init__.py +26 -0
  35. ccproxy/auth/oauth/models.py +49 -0
  36. ccproxy/auth/oauth/routes.py +396 -0
  37. ccproxy/auth/oauth/storage.py +0 -0
  38. ccproxy/auth/storage/__init__.py +12 -0
  39. ccproxy/auth/storage/base.py +57 -0
  40. ccproxy/auth/storage/json_file.py +159 -0
  41. ccproxy/auth/storage/keyring.py +192 -0
  42. ccproxy/claude_sdk/__init__.py +20 -0
  43. ccproxy/claude_sdk/client.py +169 -0
  44. ccproxy/claude_sdk/converter.py +331 -0
  45. ccproxy/claude_sdk/options.py +120 -0
  46. ccproxy/cli/__init__.py +14 -0
  47. ccproxy/cli/commands/__init__.py +8 -0
  48. ccproxy/cli/commands/auth.py +553 -0
  49. ccproxy/cli/commands/config/__init__.py +14 -0
  50. ccproxy/cli/commands/config/commands.py +766 -0
  51. ccproxy/cli/commands/config/schema_commands.py +119 -0
  52. ccproxy/cli/commands/serve.py +630 -0
  53. ccproxy/cli/docker/__init__.py +34 -0
  54. ccproxy/cli/docker/adapter_factory.py +157 -0
  55. ccproxy/cli/docker/params.py +278 -0
  56. ccproxy/cli/helpers.py +144 -0
  57. ccproxy/cli/main.py +193 -0
  58. ccproxy/cli/options/__init__.py +14 -0
  59. ccproxy/cli/options/claude_options.py +216 -0
  60. ccproxy/cli/options/core_options.py +40 -0
  61. ccproxy/cli/options/security_options.py +48 -0
  62. ccproxy/cli/options/server_options.py +117 -0
  63. ccproxy/config/__init__.py +40 -0
  64. ccproxy/config/auth.py +154 -0
  65. ccproxy/config/claude.py +124 -0
  66. ccproxy/config/cors.py +79 -0
  67. ccproxy/config/discovery.py +87 -0
  68. ccproxy/config/docker_settings.py +265 -0
  69. ccproxy/config/loader.py +108 -0
  70. ccproxy/config/observability.py +158 -0
  71. ccproxy/config/pricing.py +88 -0
  72. ccproxy/config/reverse_proxy.py +31 -0
  73. ccproxy/config/scheduler.py +89 -0
  74. ccproxy/config/security.py +14 -0
  75. ccproxy/config/server.py +81 -0
  76. ccproxy/config/settings.py +534 -0
  77. ccproxy/config/validators.py +231 -0
  78. ccproxy/core/__init__.py +274 -0
  79. ccproxy/core/async_utils.py +675 -0
  80. ccproxy/core/constants.py +97 -0
  81. ccproxy/core/errors.py +256 -0
  82. ccproxy/core/http.py +328 -0
  83. ccproxy/core/http_transformers.py +428 -0
  84. ccproxy/core/interfaces.py +247 -0
  85. ccproxy/core/logging.py +189 -0
  86. ccproxy/core/middleware.py +114 -0
  87. ccproxy/core/proxy.py +143 -0
  88. ccproxy/core/system.py +38 -0
  89. ccproxy/core/transformers.py +259 -0
  90. ccproxy/core/types.py +129 -0
  91. ccproxy/core/validators.py +288 -0
  92. ccproxy/docker/__init__.py +67 -0
  93. ccproxy/docker/adapter.py +588 -0
  94. ccproxy/docker/docker_path.py +207 -0
  95. ccproxy/docker/middleware.py +103 -0
  96. ccproxy/docker/models.py +228 -0
  97. ccproxy/docker/protocol.py +192 -0
  98. ccproxy/docker/stream_process.py +264 -0
  99. ccproxy/docker/validators.py +173 -0
  100. ccproxy/models/__init__.py +123 -0
  101. ccproxy/models/errors.py +42 -0
  102. ccproxy/models/messages.py +243 -0
  103. ccproxy/models/requests.py +85 -0
  104. ccproxy/models/responses.py +227 -0
  105. ccproxy/models/types.py +102 -0
  106. ccproxy/observability/__init__.py +51 -0
  107. ccproxy/observability/access_logger.py +400 -0
  108. ccproxy/observability/context.py +447 -0
  109. ccproxy/observability/metrics.py +539 -0
  110. ccproxy/observability/pushgateway.py +366 -0
  111. ccproxy/observability/sse_events.py +303 -0
  112. ccproxy/observability/stats_printer.py +755 -0
  113. ccproxy/observability/storage/__init__.py +1 -0
  114. ccproxy/observability/storage/duckdb_simple.py +665 -0
  115. ccproxy/observability/storage/models.py +55 -0
  116. ccproxy/pricing/__init__.py +19 -0
  117. ccproxy/pricing/cache.py +212 -0
  118. ccproxy/pricing/loader.py +267 -0
  119. ccproxy/pricing/models.py +106 -0
  120. ccproxy/pricing/updater.py +309 -0
  121. ccproxy/scheduler/__init__.py +39 -0
  122. ccproxy/scheduler/core.py +335 -0
  123. ccproxy/scheduler/exceptions.py +34 -0
  124. ccproxy/scheduler/manager.py +186 -0
  125. ccproxy/scheduler/registry.py +150 -0
  126. ccproxy/scheduler/tasks.py +484 -0
  127. ccproxy/services/__init__.py +10 -0
  128. ccproxy/services/claude_sdk_service.py +614 -0
  129. ccproxy/services/credentials/__init__.py +55 -0
  130. ccproxy/services/credentials/config.py +105 -0
  131. ccproxy/services/credentials/manager.py +562 -0
  132. ccproxy/services/credentials/oauth_client.py +482 -0
  133. ccproxy/services/proxy_service.py +1536 -0
  134. ccproxy/static/.keep +0 -0
  135. ccproxy/testing/__init__.py +34 -0
  136. ccproxy/testing/config.py +148 -0
  137. ccproxy/testing/content_generation.py +197 -0
  138. ccproxy/testing/mock_responses.py +262 -0
  139. ccproxy/testing/response_handlers.py +161 -0
  140. ccproxy/testing/scenarios.py +241 -0
  141. ccproxy/utils/__init__.py +6 -0
  142. ccproxy/utils/cost_calculator.py +210 -0
  143. ccproxy/utils/streaming_metrics.py +199 -0
  144. ccproxy_api-0.1.0.dist-info/METADATA +253 -0
  145. ccproxy_api-0.1.0.dist-info/RECORD +148 -0
  146. ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
  147. ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
  148. ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,157 @@
1
+ """Docker adapter factory for CLI commands.
2
+
3
+ This module provides functions to create Docker adapters from CLI settings
4
+ and command-line arguments.
5
+ """
6
+
7
+ import getpass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from ccproxy.config.settings import Settings
12
+ from ccproxy.docker import (
13
+ DockerEnv,
14
+ DockerPath,
15
+ DockerUserContext,
16
+ DockerVolume,
17
+ )
18
+
19
+
20
+ def _create_docker_adapter_from_settings(
21
+ settings: Settings,
22
+ docker_image: str | None = None,
23
+ docker_env: list[str] | None = None,
24
+ docker_volume: list[str] | None = None,
25
+ docker_arg: list[str] | None = None,
26
+ docker_home: str | None = None,
27
+ docker_workspace: str | None = None,
28
+ user_mapping_enabled: bool | None = None,
29
+ user_uid: int | None = None,
30
+ user_gid: int | None = None,
31
+ command: list[str] | None = None,
32
+ cmd_args: list[str] | None = None,
33
+ **kwargs: Any,
34
+ ) -> tuple[
35
+ str,
36
+ list[DockerVolume],
37
+ DockerEnv,
38
+ list[str] | None,
39
+ DockerUserContext | None,
40
+ list[str],
41
+ ]:
42
+ """Convert settings and overrides to Docker adapter parameters.
43
+
44
+ Args:
45
+ settings: Application settings
46
+ docker_image: Override Docker image
47
+ docker_env: Additional environment variables
48
+ docker_volume: Additional volume mappings
49
+ docker_arg: Additional Docker arguments
50
+ docker_home: Override home directory
51
+ docker_workspace: Override workspace directory
52
+ user_mapping_enabled: Override user mapping setting
53
+ user_uid: Override user ID
54
+ user_gid: Override group ID
55
+ command: Command to run in container
56
+ cmd_args: Arguments for the command
57
+ **kwargs: Additional keyword arguments (ignored)
58
+
59
+ Returns:
60
+ Tuple of (image, volumes, environment, command, user_context, additional_args)
61
+ """
62
+ docker_settings = settings.docker
63
+
64
+ # Determine effective image
65
+ image = docker_image or docker_settings.docker_image
66
+
67
+ # Process volumes
68
+ volumes: list[DockerVolume] = []
69
+
70
+ # Add home/workspace volumes with effective directories
71
+ home_dir = docker_home or docker_settings.docker_home_directory
72
+ workspace_dir = docker_workspace or docker_settings.docker_workspace_directory
73
+
74
+ if home_dir:
75
+ volumes.append((str(Path(home_dir)), "/data/home"))
76
+ if workspace_dir:
77
+ volumes.append((str(Path(workspace_dir)), "/data/workspace"))
78
+
79
+ # Add base volumes from settings
80
+ for vol_str in docker_settings.docker_volumes:
81
+ parts = vol_str.split(":", 2)
82
+ if len(parts) >= 2:
83
+ volumes.append((parts[0], parts[1]))
84
+
85
+ # Add CLI override volumes
86
+ if docker_volume:
87
+ for vol_str in docker_volume:
88
+ parts = vol_str.split(":", 2)
89
+ if len(parts) >= 2:
90
+ volumes.append((parts[0], parts[1]))
91
+
92
+ # Process environment
93
+ environment: DockerEnv = docker_settings.docker_environment.copy()
94
+
95
+ # Add home/workspace environment variables
96
+ if home_dir:
97
+ environment["CLAUDE_HOME"] = "/data/home"
98
+ if workspace_dir:
99
+ environment["CLAUDE_WORKSPACE"] = "/data/workspace"
100
+
101
+ # Add CLI override environment
102
+ if docker_env:
103
+ for env_var in docker_env:
104
+ if "=" in env_var:
105
+ key, value = env_var.split("=", 1)
106
+ environment[key] = value
107
+
108
+ # Create user context
109
+ user_context = None
110
+ effective_mapping_enabled = (
111
+ user_mapping_enabled
112
+ if user_mapping_enabled is not None
113
+ else docker_settings.user_mapping_enabled
114
+ )
115
+
116
+ if effective_mapping_enabled:
117
+ effective_uid = user_uid if user_uid is not None else docker_settings.user_uid
118
+ effective_gid = user_gid if user_gid is not None else docker_settings.user_gid
119
+
120
+ if effective_uid is not None and effective_gid is not None:
121
+ # Create DockerPath instances for user context
122
+ home_path = None
123
+ workspace_path = None
124
+
125
+ if home_dir:
126
+ home_path = DockerPath(
127
+ host_path=Path(home_dir), container_path="/data/home"
128
+ )
129
+ if workspace_dir:
130
+ workspace_path = DockerPath(
131
+ host_path=Path(workspace_dir), container_path="/data/workspace"
132
+ )
133
+
134
+ # Use a default username if not available
135
+ username = getpass.getuser()
136
+
137
+ user_context = DockerUserContext(
138
+ uid=effective_uid,
139
+ gid=effective_gid,
140
+ username=username,
141
+ home_path=home_path,
142
+ workspace_path=workspace_path,
143
+ )
144
+
145
+ # Build command
146
+ final_command = None
147
+ if command:
148
+ final_command = command.copy()
149
+ if cmd_args:
150
+ final_command.extend(cmd_args)
151
+
152
+ # Additional Docker arguments
153
+ additional_args = docker_settings.docker_additional_args.copy()
154
+ if docker_arg:
155
+ additional_args.extend(docker_arg)
156
+
157
+ return image, volumes, environment, final_command, user_context, additional_args
@@ -0,0 +1,278 @@
1
+ """Shared Docker parameter definitions for Typer CLI commands.
2
+
3
+ This module provides reusable Typer Option definitions for Docker-related
4
+ parameters that are used across multiple CLI commands, eliminating duplication.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ import typer
10
+
11
+
12
+ # Docker parameter validation functions moved here to avoid utils dependency
13
+
14
+
15
+ def parse_docker_env(
16
+ ctx: typer.Context, param: typer.CallbackParam, value: list[str] | None
17
+ ) -> list[str]:
18
+ """Parse Docker environment variable string."""
19
+ if not value:
20
+ return []
21
+
22
+ parsed = []
23
+ for env_str in value:
24
+ if not env_str or env_str == "[]":
25
+ raise typer.BadParameter(
26
+ f"Invalid env format: {env_str}. Expected KEY=VALUE"
27
+ )
28
+ if "=" not in env_str:
29
+ raise typer.BadParameter(
30
+ f"Invalid env format: {env_str}. Expected KEY=VALUE"
31
+ )
32
+ parsed.append(env_str)
33
+
34
+ return parsed
35
+
36
+
37
+ def parse_docker_volume(
38
+ ctx: typer.Context, param: typer.CallbackParam, value: list[str] | None
39
+ ) -> list[str]:
40
+ """Parse Docker volume string."""
41
+ if not value:
42
+ return []
43
+
44
+ # Import the validation function from config
45
+ from ccproxy.config.docker_settings import validate_volume_format
46
+
47
+ parsed = []
48
+ for volume_str in value:
49
+ if not volume_str:
50
+ continue
51
+ try:
52
+ validated_volume = validate_volume_format(volume_str)
53
+ parsed.append(validated_volume)
54
+ except ValueError as e:
55
+ raise typer.BadParameter(str(e)) from e
56
+
57
+ return parsed
58
+
59
+
60
+ def validate_docker_arg(
61
+ ctx: typer.Context, param: typer.CallbackParam, value: list[str] | None
62
+ ) -> list[str]:
63
+ """Validate Docker argument."""
64
+ if not value:
65
+ return []
66
+
67
+ # Basic validation - ensure arguments don't contain dangerous patterns
68
+ validated = []
69
+ for arg in value:
70
+ if not arg:
71
+ continue
72
+ # Basic validation - just return the arg for now
73
+ validated.append(arg)
74
+
75
+ return validated
76
+
77
+
78
+ def validate_docker_home(
79
+ ctx: typer.Context, param: typer.CallbackParam, value: str | None
80
+ ) -> str | None:
81
+ """Validate Docker home directory."""
82
+ if value is None:
83
+ return None
84
+
85
+ from pathlib import Path
86
+
87
+ from ccproxy.config.docker_settings import validate_host_path
88
+
89
+ try:
90
+ return validate_host_path(value)
91
+ except ValueError as e:
92
+ raise typer.BadParameter(str(e)) from e
93
+
94
+
95
+ def validate_docker_image(
96
+ ctx: typer.Context, param: typer.CallbackParam, value: str | None
97
+ ) -> str | None:
98
+ """Validate Docker image name."""
99
+ if value is None:
100
+ return None
101
+
102
+ if not value:
103
+ raise typer.BadParameter("Docker image cannot be empty")
104
+
105
+ # Basic validation - no spaces allowed in image names
106
+ if " " in value:
107
+ raise typer.BadParameter(f"Docker image name cannot contain spaces: {value}")
108
+
109
+ return value
110
+
111
+
112
+ def validate_docker_workspace(
113
+ ctx: typer.Context, param: typer.CallbackParam, value: str | None
114
+ ) -> str | None:
115
+ """Validate Docker workspace directory."""
116
+ if value is None:
117
+ return None
118
+
119
+ from pathlib import Path
120
+
121
+ from ccproxy.config.docker_settings import validate_host_path
122
+
123
+ try:
124
+ return validate_host_path(value)
125
+ except ValueError as e:
126
+ raise typer.BadParameter(str(e)) from e
127
+
128
+
129
+ def validate_user_gid(
130
+ ctx: typer.Context, param: typer.CallbackParam, value: int | None
131
+ ) -> int | None:
132
+ """Validate user GID."""
133
+ if value is None:
134
+ return None
135
+
136
+ if value < 0:
137
+ raise typer.BadParameter("GID must be non-negative")
138
+
139
+ return value
140
+
141
+
142
+ def validate_user_uid(
143
+ ctx: typer.Context, param: typer.CallbackParam, value: int | None
144
+ ) -> int | None:
145
+ """Validate user UID."""
146
+ if value is None:
147
+ return None
148
+
149
+ if value < 0:
150
+ raise typer.BadParameter("UID must be non-negative")
151
+
152
+ return value
153
+
154
+
155
+ def docker_image_option() -> Any:
156
+ """Docker image parameter."""
157
+ return typer.Option(
158
+ None,
159
+ "--docker-image",
160
+ help="Docker image to use (overrides config)",
161
+ )
162
+
163
+
164
+ def docker_env_option() -> Any:
165
+ """Docker environment variables parameter."""
166
+ return typer.Option(
167
+ [],
168
+ "--docker-env",
169
+ help="Environment variables to pass to Docker (KEY=VALUE format, can be used multiple times)",
170
+ )
171
+
172
+
173
+ def docker_volume_option() -> Any:
174
+ """Docker volume mounts parameter."""
175
+ return typer.Option(
176
+ [],
177
+ "--docker-volume",
178
+ help="Volume mounts to add (host:container[:options] format, can be used multiple times)",
179
+ )
180
+
181
+
182
+ def docker_arg_option() -> Any:
183
+ """Docker arguments parameter."""
184
+ return typer.Option(
185
+ [],
186
+ "--docker-arg",
187
+ help="Additional Docker run arguments (can be used multiple times)",
188
+ )
189
+
190
+
191
+ def docker_home_option() -> Any:
192
+ """Docker home directory parameter."""
193
+ return typer.Option(
194
+ None,
195
+ "--docker-home",
196
+ help="Home directory inside Docker container (overrides config)",
197
+ )
198
+
199
+
200
+ def docker_workspace_option() -> Any:
201
+ """Docker workspace directory parameter."""
202
+ return typer.Option(
203
+ None,
204
+ "--docker-workspace",
205
+ help="Workspace directory inside Docker container (overrides config)",
206
+ )
207
+
208
+
209
+ def user_mapping_option() -> Any:
210
+ """User mapping parameter."""
211
+ return typer.Option(
212
+ None,
213
+ "--user-mapping/--no-user-mapping",
214
+ help="Enable/disable UID/GID mapping (overrides config)",
215
+ )
216
+
217
+
218
+ def user_uid_option() -> Any:
219
+ """User UID parameter."""
220
+ return typer.Option(
221
+ None,
222
+ "--user-uid",
223
+ help="User ID to run container as (overrides config)",
224
+ min=0,
225
+ )
226
+
227
+
228
+ def user_gid_option() -> Any:
229
+ """User GID parameter."""
230
+ return typer.Option(
231
+ None,
232
+ "--user-gid",
233
+ help="Group ID to run container as (overrides config)",
234
+ min=0,
235
+ )
236
+
237
+
238
+ class DockerOptions:
239
+ """Container for all Docker-related Typer options.
240
+
241
+ This class provides a convenient way to include all Docker-related
242
+ options in a command using typed attributes.
243
+ """
244
+
245
+ def __init__(
246
+ self,
247
+ docker_image: str | None = None,
248
+ docker_env: list[str] | None = None,
249
+ docker_volume: list[str] | None = None,
250
+ docker_arg: list[str] | None = None,
251
+ docker_home: str | None = None,
252
+ docker_workspace: str | None = None,
253
+ user_mapping_enabled: bool | None = None,
254
+ user_uid: int | None = None,
255
+ user_gid: int | None = None,
256
+ ):
257
+ """Initialize Docker options.
258
+
259
+ Args:
260
+ docker_image: Docker image to use
261
+ docker_env: Environment variables list
262
+ docker_volume: Volume mounts list
263
+ docker_arg: Additional Docker arguments
264
+ docker_home: Home directory path
265
+ docker_workspace: Workspace directory path
266
+ user_mapping_enabled: User mapping flag
267
+ user_uid: User ID
268
+ user_gid: Group ID
269
+ """
270
+ self.docker_image = docker_image
271
+ self.docker_env = docker_env or []
272
+ self.docker_volume = docker_volume or []
273
+ self.docker_arg = docker_arg or []
274
+ self.docker_home = docker_home
275
+ self.docker_workspace = docker_workspace
276
+ self.user_mapping_enabled = user_mapping_enabled
277
+ self.user_uid = user_uid
278
+ self.user_gid = user_gid
ccproxy/cli/helpers.py ADDED
@@ -0,0 +1,144 @@
1
+ """CLI helper utilities for CCProxy API."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from rich_toolkit import RichToolkit, RichToolkitTheme
8
+ from rich_toolkit.styles import TaggedStyle
9
+ from uvicorn.logging import DefaultFormatter
10
+
11
+ from ccproxy.core.async_utils import patched_typing
12
+
13
+
14
+ def get_rich_toolkit() -> RichToolkit:
15
+ theme = RichToolkitTheme(
16
+ style=TaggedStyle(tag_width=11),
17
+ theme={
18
+ # Core tags
19
+ "tag.title": "white on #009485",
20
+ "tag": "white on #007166",
21
+ "placeholder": "grey85",
22
+ "text": "white",
23
+ "selected": "#007166",
24
+ "result": "grey85",
25
+ "progress": "on #007166",
26
+ # Status tags
27
+ "error": "bold red",
28
+ "success": "bold green",
29
+ "warning": "bold yellow",
30
+ "info": "blue",
31
+ # CLI specific tags
32
+ "version": "cyan",
33
+ "docker": "blue",
34
+ "local": "green",
35
+ "claude": "magenta",
36
+ "config": "cyan",
37
+ "volume": "yellow",
38
+ "env": "bright_blue",
39
+ "debug": "dim white",
40
+ "command": "bright_cyan",
41
+ # Logging
42
+ "log.info": "black on blue",
43
+ "log.error": "white on red",
44
+ "log.warning": "black on yellow",
45
+ "log.debug": "dim white",
46
+ },
47
+ )
48
+
49
+ return RichToolkit(theme=theme)
50
+
51
+
52
+ def bold(text: str) -> str:
53
+ return f"[bold]{text}[/bold]"
54
+
55
+
56
+ def dim(text: str) -> str:
57
+ return f"[dim]{text}[/dim]"
58
+
59
+
60
+ def italic(text: str) -> str:
61
+ return f"[italic]{text}[/italic]"
62
+
63
+
64
+ def warning(text: str) -> str:
65
+ return f"[yellow]{text}[/yellow]"
66
+
67
+
68
+ def error(text: str) -> str:
69
+ return f"[red]{text}[/red]"
70
+
71
+
72
+ def code(text: str) -> str:
73
+ return f"[cyan]{text}[/cyan]"
74
+
75
+
76
+ def success(text: str) -> str:
77
+ return f"[green]{text}[/green]"
78
+
79
+
80
+ def link(text: str, link: str) -> str:
81
+ return f"[link={link}]{text}[/link]"
82
+
83
+
84
+ def merge_claude_code_options(base_options: Any, **overrides: Any) -> Any:
85
+ """
86
+ Create a new ClaudeCodeOptions instance by merging base options with overrides.
87
+
88
+ Args:
89
+ base_options: Base ClaudeCodeOptions instance to copy from
90
+ **overrides: Dictionary of option overrides
91
+
92
+ Returns:
93
+ New ClaudeCodeOptions instance with merged options
94
+ """
95
+ with patched_typing():
96
+ from claude_code_sdk import ClaudeCodeOptions
97
+
98
+ # Create a new options instance with the base values
99
+ options = ClaudeCodeOptions()
100
+
101
+ # Copy all attributes from base_options
102
+ if base_options:
103
+ for attr in [
104
+ "model",
105
+ "max_thinking_tokens",
106
+ "max_turns",
107
+ "cwd",
108
+ "system_prompt",
109
+ "append_system_prompt",
110
+ "permission_mode",
111
+ "permission_prompt_tool_name",
112
+ "continue_conversation",
113
+ "resume",
114
+ "allowed_tools",
115
+ "disallowed_tools",
116
+ "mcp_servers",
117
+ "mcp_tools",
118
+ # Anthropic API fields
119
+ "temperature",
120
+ "top_p",
121
+ "top_k",
122
+ "stop_sequences",
123
+ "tools",
124
+ "metadata",
125
+ "service_tier",
126
+ ]:
127
+ if hasattr(base_options, attr):
128
+ base_value = getattr(base_options, attr)
129
+ if base_value is not None:
130
+ setattr(options, attr, base_value)
131
+
132
+ # Apply overrides
133
+ for key, value in overrides.items():
134
+ if value is not None and hasattr(options, key):
135
+ # Handle special type conversions for specific fields
136
+ if key == "cwd" and not isinstance(value, str):
137
+ value = str(value)
138
+ setattr(options, key, value)
139
+
140
+ return options
141
+
142
+
143
+ def is_running_in_docker() -> bool:
144
+ return Path("/.dockerenv").exists()