ccproxy-api 0.1.7__py3-none-any.whl → 0.2.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 (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
ccproxy/core/logging.py CHANGED
@@ -1,24 +1,277 @@
1
+ import inspect
1
2
  import logging
3
+ import os
4
+ import re
2
5
  import shutil
3
6
  import sys
4
7
  from collections.abc import MutableMapping
5
8
  from pathlib import Path
6
- from typing import Any, TextIO
9
+ from typing import Any, Protocol, TextIO
7
10
 
8
11
  import structlog
9
12
  from rich.console import Console
10
13
  from rich.traceback import Traceback
14
+ from structlog.contextvars import bind_contextvars
11
15
  from structlog.stdlib import BoundLogger
12
16
  from structlog.typing import ExcInfo, Processor
13
17
 
18
+ from ccproxy.core.id_utils import generate_short_id
19
+
20
+
21
+ DEFAULT_LOG_LEVEL_NAME = "WARNING"
22
+
23
+
24
+ # Custom protocol for BoundLogger with trace method
25
+ class TraceBoundLogger(Protocol):
26
+ """Protocol defining BoundLogger with trace method."""
27
+
28
+ def trace(self, msg: str, *args: Any, **kwargs: Any) -> Any:
29
+ """Log at TRACE level."""
30
+ ...
31
+
32
+ def debug(self, msg: str, *args: Any, **kwargs: Any) -> Any:
33
+ """Log at DEBUG level."""
34
+ ...
35
+
36
+ def info(self, msg: str, *args: Any, **kwargs: Any) -> Any:
37
+ """Log at INFO level."""
38
+ ...
39
+
40
+ def warning(self, msg: str, *args: Any, **kwargs: Any) -> Any:
41
+ """Log at WARNING level."""
42
+ ...
43
+
44
+ def error(self, msg: str, *args: Any, **kwargs: Any) -> Any:
45
+ """Log at ERROR level."""
46
+ ...
47
+
48
+ def bind(self, **kwargs: Any) -> "TraceBoundLogger":
49
+ """Bind additional context to logger."""
50
+ ...
51
+
52
+ def log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> Any:
53
+ """Log at specific level."""
54
+ ...
55
+
56
+
57
+ # Import LogCategory locally to avoid circular import
58
+
59
+
60
+ # Add TRACE level below DEBUG
61
+ TRACE_LEVEL = 5
62
+ logging.addLevelName(TRACE_LEVEL, "TRACE")
63
+
64
+ # Register TRACE level with structlog
65
+ structlog.stdlib.LEVEL_TO_NAME[TRACE_LEVEL] = "trace" # type: ignore[attr-defined]
66
+ structlog.stdlib.NAME_TO_LEVEL["trace"] = TRACE_LEVEL # type: ignore[attr-defined]
67
+
68
+
69
+ # Monkey-patch trace method to Logger class
70
+ def trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None:
71
+ """Log at TRACE level (below DEBUG)."""
72
+ if self.isEnabledFor(TRACE_LEVEL):
73
+ self._log(TRACE_LEVEL, message, args, **kwargs)
74
+
75
+
76
+ logging.Logger.trace = trace # type: ignore[attr-defined]
77
+
78
+
79
+ # Custom BoundLogger that includes trace method
80
+ class TraceBoundLoggerImpl(BoundLogger):
81
+ """BoundLogger with trace method support."""
82
+
83
+ def trace(self, msg: str, *args: Any, **kwargs: Any) -> Any:
84
+ """Log at TRACE level."""
85
+ return self.log(TRACE_LEVEL, msg, *args, **kwargs)
86
+
14
87
 
15
88
  suppress_debug = [
16
89
  "ccproxy.scheduler",
17
- "ccproxy.observability.context",
18
- "ccproxy.utils.simple_request_logger",
19
90
  ]
20
91
 
21
92
 
93
+ def category_filter(
94
+ logger: Any, method_name: str, event_dict: MutableMapping[str, Any]
95
+ ) -> MutableMapping[str, Any]:
96
+ """Filter logs by category based on environment configuration."""
97
+ # Get filter settings from environment
98
+ included_channels = os.getenv("CCPROXY_LOG_CHANNELS", "").strip()
99
+ excluded_channels = os.getenv("CCPROXY_LOG_EXCLUDE_CHANNELS", "").strip()
100
+
101
+ if not included_channels and not excluded_channels:
102
+ return event_dict # No filtering
103
+
104
+ included = (
105
+ [c.strip() for c in included_channels.split(",") if c.strip()]
106
+ if included_channels
107
+ else []
108
+ )
109
+ excluded = (
110
+ [c.strip() for c in excluded_channels.split(",") if c.strip()]
111
+ if excluded_channels
112
+ else []
113
+ )
114
+
115
+ category = event_dict.get("category")
116
+
117
+ # For foreign (stdlib) logs without category, check if logger name suggests a category
118
+ if category is None:
119
+ logger_name = event_dict.get("logger", "")
120
+ # Map common logger names to categories
121
+ if logger_name.startswith(("uvicorn", "fastapi", "starlette")):
122
+ category = "general" # Allow uvicorn/fastapi logs through as general
123
+ elif logger_name.startswith("httpx"):
124
+ category = "http"
125
+ else:
126
+ category = "general" # Default fallback
127
+
128
+ # Add the category to the event dict for consistent handling
129
+ event_dict["category"] = category
130
+
131
+ # Apply filters - be more permissive with foreign logs that got "general" as fallback
132
+ # and ALWAYS allow errors and warnings through regardless of category filtering
133
+ log_level = event_dict.get("level", "").lower()
134
+ is_critical_message = log_level in ("error", "warning", "critical")
135
+
136
+ if included and category not in included:
137
+ # Always allow critical messages through regardless of category filtering
138
+ if is_critical_message:
139
+ return event_dict
140
+
141
+ # If it's a foreign log with "general" fallback, and "general" is not in included channels,
142
+ # still allow it through to prevent breaking stdlib logging
143
+ logger_name = event_dict.get("logger", "")
144
+ is_foreign_log = not logger_name.startswith(
145
+ "ccproxy"
146
+ ) and not logger_name.startswith("plugins")
147
+
148
+ if not (is_foreign_log and category == "general"):
149
+ raise structlog.DropEvent
150
+
151
+ if excluded and category in excluded:
152
+ # Always allow critical messages through even if their category is explicitly excluded
153
+ if is_critical_message:
154
+ return event_dict
155
+ raise structlog.DropEvent
156
+
157
+ return event_dict
158
+
159
+
160
+ def format_category_for_console(
161
+ logger: Any, method_name: str, event_dict: MutableMapping[str, Any]
162
+ ) -> MutableMapping[str, Any]:
163
+ """Format category field for better visibility in console output."""
164
+ logger_name = event_dict.get("logger", "") or ""
165
+ category = event_dict.get("category")
166
+ event = event_dict.get("event", "")
167
+
168
+ # Treat non-ccproxy/plugin loggers as external for display purposes.
169
+ is_external_logger = not (
170
+ logger_name.startswith("ccproxy") or logger_name.startswith("plugins")
171
+ )
172
+
173
+ if category:
174
+ category_upper = str(category).upper()
175
+
176
+ # Avoid echoing redundant [GENERAL] prefixes for external libraries.
177
+ if not (category_upper == "GENERAL" and is_external_logger):
178
+ event_dict["event"] = f"[{category_upper}] {event}"
179
+ else:
180
+ # Add default category if missing.
181
+ event_dict["category"] = "general"
182
+ if not is_external_logger:
183
+ event_dict["event"] = f"[GENERAL] {event}"
184
+
185
+ return event_dict
186
+
187
+
188
+ class CategoryConsoleRenderer:
189
+ """Custom console renderer that formats categories as a separate padded column."""
190
+
191
+ def __init__(self, base_renderer: Any):
192
+ self.base_renderer = base_renderer
193
+
194
+ def __call__(
195
+ self, logger: Any, method_name: str, event_dict: MutableMapping[str, Any]
196
+ ) -> str:
197
+ # Extract category and plugin_name, remove from event dict to prevent duplicate display
198
+ category = event_dict.pop("category", "general")
199
+ plugin_name = event_dict.pop("plugin_name", None)
200
+
201
+ # Get the rendered output from base renderer (without category/plugin_name in key-value pairs)
202
+ rendered = self.base_renderer(logger, method_name, event_dict)
203
+
204
+ # Color mapping for different categories
205
+ category_colors = {
206
+ "lifecycle": "\033[92m", # bright green
207
+ "plugin": "\033[94m", # bright blue
208
+ "http": "\033[95m", # bright magenta
209
+ "streaming": "\033[96m", # bright cyan
210
+ "auth": "\033[93m", # bright yellow
211
+ "transform": "\033[91m", # bright red
212
+ "cache": "\033[97m", # bright white
213
+ "middleware": "\033[35m", # magenta
214
+ "config": "\033[34m", # blue
215
+ "metrics": "\033[32m", # green
216
+ "access": "\033[33m", # yellow
217
+ "request": "\033[36m", # cyan
218
+ "general": "\033[37m", # white
219
+ }
220
+
221
+ # Plugin name colors (distinct from categories)
222
+ plugin_colors = {
223
+ "claude_api": "\033[38;5;33m", # blue
224
+ "claude_sdk": "\033[38;5;39m", # bright blue
225
+ "codex": "\033[38;5;214m", # orange
226
+ "permissions": "\033[38;5;165m", # purple
227
+ "raw_http_logger": "\033[38;5;150m", # light green
228
+ }
229
+
230
+ # Get colors
231
+ category_color = category_colors.get(category.lower(), "\033[37m")
232
+ plugin_color = (
233
+ plugin_colors.get(plugin_name, "\033[38;5;242m") if plugin_name else None
234
+ )
235
+
236
+ # Build the display fields
237
+ # Truncate long category names to fit the field width
238
+ truncated_category = (
239
+ category.lower()[:10] if len(category) > 10 else category.lower()
240
+ )
241
+ category_field = f"{category_color}\033[1m[{truncated_category:<10}]\033[0m"
242
+
243
+ # Always show a plugin field - either plugin name or "core"
244
+ if plugin_name:
245
+ # Truncate long plugin names to fit the field width
246
+ truncated_name = plugin_name[:12] if len(plugin_name) > 12 else plugin_name
247
+ plugin_field = f"{plugin_color}\033[1m[{truncated_name:<12}]\033[0m "
248
+ else:
249
+ # Show "core" for non-plugin logs with a distinct color
250
+ core_color = "\033[38;5;8m" # dark gray
251
+ plugin_field = f"{core_color}\033[1m[{'core':<12}]\033[0m "
252
+
253
+ # Insert fields after the level field in the rendered output
254
+ # Find the position right after the level field closes with "] "
255
+ level_end_pattern = r"(\[[^\]]*\[[^\]]*m[^\]]*\[[^\]]*m\])\s+"
256
+ match = re.search(level_end_pattern, rendered)
257
+
258
+ if match:
259
+ # Insert plugin_field and category_field after the level field
260
+ insert_pos = match.end()
261
+ rendered = (
262
+ rendered[:insert_pos]
263
+ + plugin_field
264
+ + category_field
265
+ + " "
266
+ + rendered[insert_pos:]
267
+ )
268
+ else:
269
+ # Fallback: prepend fields to the beginning
270
+ rendered = plugin_field + category_field + " " + rendered
271
+
272
+ return str(rendered)
273
+
274
+
22
275
  def configure_structlog(log_level: int = logging.INFO) -> None:
23
276
  """Configure structlog with shared processors following canonical pattern."""
24
277
  # Shared processors for all structlog loggers
@@ -27,6 +280,7 @@ def configure_structlog(log_level: int = logging.INFO) -> None:
27
280
  structlog.stdlib.filter_by_level,
28
281
  structlog.stdlib.add_log_level,
29
282
  structlog.stdlib.add_logger_name,
283
+ category_filter, # Add category filtering
30
284
  ]
31
285
 
32
286
  # Add debug-specific processors
@@ -75,7 +329,7 @@ def configure_structlog(log_level: int = logging.INFO) -> None:
75
329
  processors=processors,
76
330
  context_class=dict,
77
331
  logger_factory=structlog.stdlib.LoggerFactory(),
78
- wrapper_class=structlog.stdlib.BoundLogger,
332
+ wrapper_class=TraceBoundLoggerImpl,
79
333
  cache_logger_on_first_use=True,
80
334
  )
81
335
 
@@ -112,12 +366,17 @@ def setup_logging(
112
366
  json_logs: bool = False,
113
367
  log_level_name: str = "DEBUG",
114
368
  log_file: str | None = None,
115
- ) -> BoundLogger:
369
+ ) -> TraceBoundLogger:
116
370
  """
117
371
  Setup logging for the entire application using canonical structlog pattern.
118
372
  Returns a structlog logger instance.
119
373
  """
120
- log_level = getattr(logging, log_level_name.upper(), logging.INFO)
374
+ # Handle custom TRACE level explicitly
375
+ log_level_upper = log_level_name.upper()
376
+ if log_level_upper == "TRACE":
377
+ log_level = TRACE_LEVEL
378
+ else:
379
+ log_level = getattr(logging, log_level_upper, logging.INFO)
121
380
 
122
381
  # Install rich traceback handler globally with frame limit
123
382
  # install_rich_traceback(
@@ -149,6 +408,7 @@ def setup_logging(
149
408
  structlog.contextvars.merge_contextvars,
150
409
  structlog.stdlib.add_log_level,
151
410
  structlog.stdlib.add_logger_name,
411
+ category_filter, # Apply category filtering to all logs
152
412
  structlog.dev.set_exc_info,
153
413
  ]
154
414
 
@@ -189,16 +449,25 @@ def setup_logging(
189
449
  # 4. Setup console handler with ConsoleRenderer
190
450
  console_handler = logging.StreamHandler(sys.stdout)
191
451
  console_handler.setLevel(log_level)
452
+ base_console_renderer = structlog.dev.ConsoleRenderer(
453
+ exception_formatter=rich_traceback, # Use rich for better formatting
454
+ colors=True,
455
+ pad_event=30,
456
+ )
457
+
192
458
  console_renderer = (
193
459
  structlog.processors.JSONRenderer()
194
460
  if json_logs
195
- else structlog.dev.ConsoleRenderer(
196
- exception_formatter=rich_traceback # structlog.dev.rich_traceback, # Use rich for better formatting
197
- )
461
+ else CategoryConsoleRenderer(base_console_renderer)
198
462
  )
199
463
 
200
464
  # Console gets human-readable timestamps for both structlog and stdlib logs
201
- console_processors = shared_processors + [console_timestamper, format_timestamp_ms]
465
+ # Note: format_category_for_console must come after category_filter
466
+ console_processors = shared_processors + [
467
+ console_timestamper,
468
+ format_timestamp_ms,
469
+ format_category_for_console,
470
+ ]
202
471
  console_handler.setFormatter(
203
472
  structlog.stdlib.ProcessorFormatter(
204
473
  foreign_pre_chain=console_processors, # type: ignore[arg-type]
@@ -259,29 +528,215 @@ def setup_logging(
259
528
  "urllib3",
260
529
  "urllib3.connectionpool",
261
530
  "requests",
262
- "aiohttp",
263
531
  "httpcore",
264
532
  "httpcore.http11",
265
533
  "fastapi_mcp",
266
534
  "sse_starlette",
267
535
  "mcp",
536
+ "hpack",
268
537
  ]:
269
538
  noisy_logger = logging.getLogger(noisy_logger_name)
270
539
  noisy_logger.handlers = []
271
540
  noisy_logger.propagate = True
272
541
  noisy_logger.setLevel(noisy_log_level)
273
542
 
274
- [
543
+ for logger_name in suppress_debug:
275
544
  logging.getLogger(logger_name).setLevel(
276
545
  logging.INFO if log_level <= logging.DEBUG else log_level
277
- ) # type: ignore[func-returns-value]
278
- for logger_name in suppress_debug
279
- ]
546
+ )
280
547
 
281
548
  return structlog.get_logger() # type: ignore[no-any-return]
282
549
 
283
550
 
284
551
  # Create a convenience function for getting loggers
285
- def get_logger(name: str | None = None) -> BoundLogger:
286
- """Get a structlog logger instance."""
287
- return structlog.get_logger(name) # type: ignore[no-any-return]
552
+ def get_logger(name: str | None = None) -> TraceBoundLogger:
553
+ """Get a structlog logger instance with request context automatically bound.
554
+
555
+ This function checks for an active RequestContext and automatically binds
556
+ the request_id to the logger if available, ensuring all logs are correlated
557
+ with the current request.
558
+
559
+ Args:
560
+ name: Logger name (typically __name__)
561
+
562
+ Returns:
563
+ TraceBoundLogger with request_id bound if available
564
+ """
565
+ logger = structlog.get_logger(name)
566
+
567
+ # Try to get request context and bind request_id if available
568
+ try:
569
+ from ccproxy.core.request_context import RequestContext
570
+
571
+ context = RequestContext.get_current()
572
+ if context and context.request_id:
573
+ logger = logger.bind(request_id=context.request_id)
574
+ except Exception:
575
+ # If anything fails, just return the regular logger
576
+ # This ensures backward compatibility
577
+ pass
578
+
579
+ return logger # type: ignore[no-any-return]
580
+
581
+
582
+ def get_plugin_logger(name: str | None = None) -> TraceBoundLogger:
583
+ """Get a plugin-aware logger with plugin_name automatically bound.
584
+
585
+ This function auto-detects the plugin name from the caller's module path
586
+ and binds it to the logger. Preserves all existing functionality including
587
+ request_id binding and trace method.
588
+
589
+ Args:
590
+ name: Logger name (auto-detected from caller if None)
591
+
592
+ Returns:
593
+ TraceBoundLogger with plugin_name and request_id bound if available
594
+ """
595
+ if name is None:
596
+ # Auto-detect caller's module name
597
+ frame = inspect.currentframe()
598
+ if frame and frame.f_back:
599
+ name = frame.f_back.f_globals.get("__name__", "unknown")
600
+ else:
601
+ name = "unknown"
602
+
603
+ # Use existing get_logger (preserves request_id binding & trace method)
604
+ logger = get_logger(name)
605
+
606
+ # Extract and bind plugin name for plugin modules
607
+ if name and name.startswith("plugins."):
608
+ parts = name.split(".", 2)
609
+ if len(parts) > 1:
610
+ plugin_name = parts[1] # e.g., "claude_api", "codex"
611
+ logger = logger.bind(plugin_name=plugin_name)
612
+
613
+ return logger
614
+
615
+
616
+ def _parse_arg_value(argv: list[str], flag: str) -> str | None:
617
+ """Parse a simple CLI flag value from argv.
618
+
619
+ Supports "--flag value" and "--flag=value" forms. Returns None if not present.
620
+ """
621
+ if not argv:
622
+ return None
623
+ try:
624
+ for i, token in enumerate(argv):
625
+ if token == flag and i + 1 < len(argv):
626
+ return argv[i + 1]
627
+ if token.startswith(flag + "="):
628
+ return token.split("=", 1)[1]
629
+ except Exception:
630
+ # Be forgiving in bootstrap parsing
631
+ return None
632
+ return None
633
+
634
+
635
+ def bootstrap_cli_logging(argv: list[str] | None = None) -> None:
636
+ """Best-effort early logging setup from env and CLI args.
637
+
638
+ - Parses `--log-level` and `--log-file` from argv (if provided).
639
+ - Honors env overrides `LOGGING__LEVEL`, `LOGGING__FILE`.
640
+ - Enables JSON logs if explicitly requested via `LOGGING__FORMAT=json` or `CCPROXY_JSON_LOGS=true`.
641
+ - No-op if structlog is already configured, letting later setup prevail.
642
+
643
+ This is intentionally lightweight and is followed by a full `setup_logging`
644
+ call after settings are loaded (e.g., in the serve command), so runtime
645
+ changes from config are still applied.
646
+ """
647
+ try:
648
+ if structlog.is_configured():
649
+ return
650
+
651
+ if argv is None:
652
+ argv = sys.argv[1:]
653
+
654
+ # Env-based defaults
655
+ env_level = os.getenv("LOGGING__LEVEL") or os.getenv("CCPROXY_LOG_LEVEL")
656
+ env_file = os.getenv("LOGGING__FILE")
657
+ env_format = os.getenv("LOGGING__FORMAT")
658
+
659
+ # CLI overrides
660
+ arg_level = _parse_arg_value(argv, "--log-level")
661
+ arg_file = _parse_arg_value(argv, "--log-file")
662
+
663
+ # We always want a predictable, quiet baseline before full config.
664
+ # Default to INFO unless an explicit override requests another level.
665
+ # Resolve effective values (CLI > env)
666
+ level = (arg_level or env_level or DEFAULT_LOG_LEVEL_NAME).upper()
667
+ log_file = arg_file or env_file
668
+
669
+ # JSON if explicitly requested via env
670
+ json_logs = False
671
+ if env_format:
672
+ json_logs = env_format.lower() == "json"
673
+
674
+ # Apply early setup. Safe to run again later with final settings.
675
+ # Never escalate to DEBUG/TRACE unless explicitly requested via env/argv.
676
+ if level not in {"TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}:
677
+ level = DEFAULT_LOG_LEVEL_NAME
678
+ setup_logging(json_logs=json_logs, log_level_name=level, log_file=log_file)
679
+ except Exception:
680
+ # Never break CLI due to bootstrap; final setup will run later.
681
+ return
682
+
683
+
684
+ def set_command_context(cmd_id: str | None = None) -> str:
685
+ """Bind a command-wide correlation ID to structlog context.
686
+
687
+ Uses structlog.contextvars so all logs (including from plugins) will carry
688
+ `cmd_id` once logging is configured with `merge_contextvars`.
689
+
690
+ Args:
691
+ cmd_id: Optional explicit command ID. If None, a UUID4 is generated.
692
+
693
+ Returns:
694
+ The command ID that was bound.
695
+ """
696
+ try:
697
+ if not cmd_id:
698
+ cmd_id = generate_short_id()
699
+ # Bind only cmd_id to avoid colliding with per-request request_id fields
700
+ bind_contextvars(cmd_id=cmd_id)
701
+ return cmd_id
702
+ except Exception:
703
+ # Be defensive: never break CLI startup due to context binding
704
+ return cmd_id or ""
705
+
706
+
707
+ # --- Lightweight test-time bootstrap ---------------------------------------
708
+ # Ensure structlog logs are capturable by pytest's caplog without requiring
709
+ # full application setup. When running under pytest (PYTEST_CURRENT_TEST),
710
+ # configure structlog to emit through stdlib logging with a simple renderer
711
+ # and set the root level to INFO so info logs are not filtered.
712
+ def _bootstrap_test_logging_if_needed() -> None:
713
+ try:
714
+ if os.getenv("PYTEST_CURRENT_TEST") and not structlog.is_configured():
715
+ # Ensure INFO-level logs are visible to caplog
716
+ logging.getLogger().setLevel(logging.INFO)
717
+
718
+ # Configure structlog to hand off to stdlib with extra fields so that
719
+ # pytest's caplog sees attributes like `record.category`.
720
+ structlog.configure(
721
+ processors=[
722
+ structlog.stdlib.filter_by_level,
723
+ structlog.stdlib.add_log_level,
724
+ structlog.stdlib.add_logger_name,
725
+ category_filter,
726
+ structlog.processors.TimeStamper(fmt="iso"),
727
+ structlog.processors.format_exc_info,
728
+ # Pass fields as LogRecord.extra for caplog
729
+ structlog.stdlib.render_to_log_kwargs,
730
+ ],
731
+ context_class=dict,
732
+ logger_factory=structlog.stdlib.LoggerFactory(),
733
+ wrapper_class=TraceBoundLoggerImpl,
734
+ cache_logger_on_first_use=True,
735
+ )
736
+ except Exception:
737
+ # Never fail test imports due to logging bootstrap
738
+ pass
739
+
740
+
741
+ # Invoke test bootstrap on import if appropriate
742
+ _bootstrap_test_logging_if_needed()
@@ -0,0 +1,77 @@
1
+ """CCProxy Plugin System public API (minimal re-exports).
2
+
3
+ This module exposes the common symbols used by plugins and app code while
4
+ keeping imports straightforward to avoid circular dependencies.
5
+ """
6
+
7
+ from .declaration import (
8
+ AuthCommandSpec,
9
+ FormatAdapterSpec,
10
+ FormatPair,
11
+ HookSpec,
12
+ MiddlewareLayer,
13
+ MiddlewareSpec,
14
+ PluginContext,
15
+ PluginManifest,
16
+ PluginRuntimeProtocol,
17
+ RouteSpec,
18
+ TaskSpec,
19
+ )
20
+ from .factories import (
21
+ BaseProviderPluginFactory,
22
+ PluginRegistry,
23
+ )
24
+ from .interfaces import (
25
+ AuthProviderPluginFactory,
26
+ BasePluginFactory,
27
+ PluginFactory,
28
+ ProviderPluginFactory,
29
+ SystemPluginFactory,
30
+ factory_type_name,
31
+ )
32
+ from .loader import load_cli_plugins, load_plugin_system
33
+ from .middleware import CoreMiddlewareSpec, MiddlewareManager, setup_default_middleware
34
+ from .runtime import (
35
+ AuthProviderPluginRuntime,
36
+ BasePluginRuntime,
37
+ ProviderPluginRuntime,
38
+ SystemPluginRuntime,
39
+ )
40
+
41
+
42
+ __all__ = [
43
+ # Declarations
44
+ "PluginManifest",
45
+ "PluginContext",
46
+ "PluginRuntimeProtocol",
47
+ "MiddlewareSpec",
48
+ "MiddlewareLayer",
49
+ "RouteSpec",
50
+ "TaskSpec",
51
+ "HookSpec",
52
+ "AuthCommandSpec",
53
+ "FormatAdapterSpec",
54
+ "FormatPair",
55
+ # Runtime
56
+ "BasePluginRuntime",
57
+ "SystemPluginRuntime",
58
+ "ProviderPluginRuntime",
59
+ "AuthProviderPluginRuntime",
60
+ # Base factory
61
+ "BaseProviderPluginFactory",
62
+ # Factory and registry
63
+ "PluginFactory",
64
+ "BasePluginFactory",
65
+ "SystemPluginFactory",
66
+ "ProviderPluginFactory",
67
+ "AuthProviderPluginFactory",
68
+ "PluginRegistry",
69
+ "factory_type_name",
70
+ # Middleware
71
+ "MiddlewareManager",
72
+ "CoreMiddlewareSpec",
73
+ "setup_default_middleware",
74
+ # Loader functions
75
+ "load_plugin_system",
76
+ "load_cli_plugins",
77
+ ]