ccproxy-api 0.1.6__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 +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  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 +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  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 +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  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 +95 -342
  387. ccproxy/utils/version_checker.py +279 -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.6.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 -1231
  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 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  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.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.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.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,22 +5,23 @@ Provides MCP server functionality including permission checking tools.
5
5
 
6
6
  from typing import Annotated
7
7
 
8
- from fastapi import FastAPI
9
- from fastapi_mcp import FastApiMCP # type: ignore[import-untyped]
8
+ from fastapi import APIRouter, FastAPI
9
+ from fastapi_mcp import FastApiMCP
10
10
  from pydantic import BaseModel, ConfigDict, Field
11
- from structlog import get_logger
12
11
 
13
12
  from ccproxy.api.dependencies import SettingsDep
14
- from ccproxy.api.services.permission_service import get_permission_service
15
- from ccproxy.models.permissions import PermissionStatus
16
- from ccproxy.models.responses import (
13
+ from ccproxy.core.logging import get_plugin_logger
14
+
15
+ from .models import (
16
+ PermissionStatus,
17
17
  PermissionToolAllowResponse,
18
18
  PermissionToolDenyResponse,
19
19
  PermissionToolPendingResponse,
20
20
  )
21
+ from .service import get_permission_service
21
22
 
22
23
 
23
- logger = get_logger(__name__)
24
+ logger = get_plugin_logger()
24
25
 
25
26
 
26
27
  class PermissionCheckRequest(BaseModel):
@@ -125,6 +126,32 @@ async def check_permission(
125
126
  return PermissionToolDenyResponse(message="Permission request timed out")
126
127
 
127
128
 
129
+ # Create a router for the plugin system
130
+
131
+ mcp_router = APIRouter()
132
+
133
+
134
+ @mcp_router.post(
135
+ "/permission/check",
136
+ operation_id="check_permission",
137
+ summary="Check permissions for a tool call",
138
+ description="Validates whether a tool call should be allowed based on security rules",
139
+ response_model=PermissionToolAllowResponse
140
+ | PermissionToolDenyResponse
141
+ | PermissionToolPendingResponse,
142
+ )
143
+ async def permission_endpoint(
144
+ request: PermissionCheckRequest,
145
+ settings: SettingsDep,
146
+ ) -> (
147
+ PermissionToolAllowResponse
148
+ | PermissionToolDenyResponse
149
+ | PermissionToolPendingResponse
150
+ ):
151
+ """Check permissions for a tool call."""
152
+ return await check_permission(request, settings)
153
+
154
+
128
155
  def setup_mcp(app: FastAPI) -> None:
129
156
  """Set up MCP server on the given FastAPI app.
130
157
 
@@ -4,8 +4,9 @@ import asyncio
4
4
  import uuid
5
5
  from datetime import UTC, datetime
6
6
  from enum import Enum
7
+ from typing import Annotated, Any, Literal
7
8
 
8
- from pydantic import BaseModel, Field, PrivateAttr
9
+ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
9
10
 
10
11
 
11
12
  class PermissionStatus(Enum):
@@ -113,3 +114,66 @@ class PermissionEvent(BaseModel):
113
114
  resolved_at: str | None = None
114
115
  expired_at: str | None = None
115
116
  message: str | None = None
117
+
118
+
119
+ class PermissionToolAllowResponse(BaseModel):
120
+ """Response model for allowed permission tool requests."""
121
+
122
+ behavior: Annotated[Literal["allow"], Field(description="Permission behavior")] = (
123
+ "allow"
124
+ )
125
+ updated_input: Annotated[
126
+ dict[str, Any],
127
+ Field(
128
+ description="Updated input parameters for the tool, or original input if unchanged",
129
+ alias="updatedInput",
130
+ ),
131
+ ]
132
+
133
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
134
+
135
+
136
+ class PermissionToolDenyResponse(BaseModel):
137
+ """Response model for denied permission tool requests."""
138
+
139
+ behavior: Annotated[Literal["deny"], Field(description="Permission behavior")] = (
140
+ "deny"
141
+ )
142
+ message: Annotated[
143
+ str,
144
+ Field(
145
+ description="Human-readable explanation of why the permission was denied"
146
+ ),
147
+ ]
148
+
149
+ model_config = ConfigDict(extra="forbid")
150
+
151
+
152
+ class PermissionToolPendingResponse(BaseModel):
153
+ """Response model for pending permission tool requests requiring user confirmation."""
154
+
155
+ behavior: Annotated[
156
+ Literal["pending"], Field(description="Permission behavior")
157
+ ] = "pending"
158
+ confirmation_id: Annotated[
159
+ str,
160
+ Field(
161
+ description="Unique identifier for the confirmation request",
162
+ alias="confirmationId",
163
+ ),
164
+ ]
165
+ message: Annotated[
166
+ str,
167
+ Field(
168
+ description="Instructions for retrying the request after user confirmation"
169
+ ),
170
+ ] = "User confirmation required. Please retry with the same confirmation_id."
171
+
172
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
173
+
174
+
175
+ PermissionToolResponse = (
176
+ PermissionToolAllowResponse
177
+ | PermissionToolDenyResponse
178
+ | PermissionToolPendingResponse
179
+ )
@@ -0,0 +1,153 @@
1
+ """Permissions plugin v2 implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ccproxy.core.logging import get_plugin_logger
6
+ from ccproxy.core.plugins import (
7
+ PluginContext,
8
+ PluginManifest,
9
+ RouteSpec,
10
+ SystemPluginFactory,
11
+ SystemPluginRuntime,
12
+ )
13
+
14
+ from .config import PermissionsConfig
15
+ from .mcp import mcp_router
16
+ from .routes import router
17
+ from .service import get_permission_service
18
+
19
+
20
+ logger = get_plugin_logger()
21
+
22
+
23
+ class PermissionsRuntime(SystemPluginRuntime):
24
+ """Runtime for permissions plugin."""
25
+
26
+ def __init__(self, manifest: PluginManifest):
27
+ """Initialize runtime."""
28
+ super().__init__(manifest)
29
+ self.config: PermissionsConfig | None = None
30
+ self.service = get_permission_service()
31
+
32
+ async def _on_initialize(self) -> None:
33
+ """Initialize the permissions plugin."""
34
+ if not self.context:
35
+ raise RuntimeError("Context not set")
36
+
37
+ # Get configuration
38
+ config = self.context.get("config")
39
+ if not isinstance(config, PermissionsConfig):
40
+ logger.debug("plugin_no_config")
41
+ # Use default config if none provided
42
+ self.config = PermissionsConfig()
43
+ else:
44
+ self.config = config
45
+
46
+ logger.debug("initializing_permissions_plugin")
47
+
48
+ # Start the permission service if enabled
49
+ if self.config.enabled:
50
+ # Update service timeout from config
51
+ self.service._timeout_seconds = self.config.timeout_seconds
52
+ await self.service.start()
53
+ logger.debug(
54
+ "permission_service_started",
55
+ timeout_seconds=self.config.timeout_seconds,
56
+ terminal_ui=self.config.enable_terminal_ui,
57
+ sse_stream=self.config.enable_sse_stream,
58
+ )
59
+ else:
60
+ logger.debug("permission_service_disabled")
61
+
62
+ async def _on_shutdown(self) -> None:
63
+ """Shutdown the plugin and cleanup resources."""
64
+ logger.debug("shutting_down_permissions_plugin")
65
+
66
+ # Stop the permission service
67
+ await self.service.stop()
68
+
69
+ logger.debug("permissions_plugin_shutdown_complete")
70
+
71
+ async def _get_health_details(self) -> dict[str, Any]:
72
+ """Get health check details."""
73
+ try:
74
+ # Check if service is running
75
+ pending_count = len(await self.service.get_pending_requests())
76
+ return {
77
+ "type": "system",
78
+ "initialized": self.initialized,
79
+ "pending_requests": pending_count,
80
+ "enabled": self.config.enabled if self.config else False,
81
+ "service_running": self.service is not None,
82
+ }
83
+ except Exception as e:
84
+ logger.error("health_check_failed", error=str(e))
85
+ return {
86
+ "type": "system",
87
+ "initialized": self.initialized,
88
+ "enabled": self.config.enabled if self.config else False,
89
+ "error": str(e),
90
+ }
91
+
92
+
93
+ class PermissionsFactory(SystemPluginFactory):
94
+ """Factory for permissions plugin."""
95
+
96
+ def __init__(self) -> None:
97
+ """Initialize factory with manifest."""
98
+ # Create manifest with static declarations
99
+ manifest = PluginManifest(
100
+ name="permissions",
101
+ version="0.1.0",
102
+ description="Permissions plugin providing authorization services for tool calls",
103
+ is_provider=False,
104
+ config_class=PermissionsConfig,
105
+ )
106
+
107
+ # Initialize with manifest
108
+ super().__init__(manifest)
109
+
110
+ def create_runtime(self) -> PermissionsRuntime:
111
+ """Create runtime instance."""
112
+ return PermissionsRuntime(self.manifest)
113
+
114
+ def create_context(self, core_services: Any) -> PluginContext:
115
+ """Create context and update manifest with routes if enabled."""
116
+ # Get base context
117
+ context = super().create_context(core_services)
118
+
119
+ # Check if plugin is enabled
120
+ config = context.get("config")
121
+ if isinstance(config, PermissionsConfig) and config.enabled:
122
+ # Add routes to manifest
123
+ # This is safe because it happens during app creation phase
124
+ if not self.manifest.routes:
125
+ self.manifest.routes = []
126
+
127
+ # Always add MCP routes at /mcp root (they're essential for Claude Code)
128
+ mcp_route_spec = RouteSpec(
129
+ router=mcp_router,
130
+ prefix="/mcp",
131
+ tags=["mcp"],
132
+ )
133
+ self.manifest.routes.append(mcp_route_spec)
134
+
135
+ # Add SSE streaming routes at /permissions if enabled
136
+ if config.enable_sse_stream:
137
+ permissions_route_spec = RouteSpec(
138
+ router=router,
139
+ prefix="/permissions",
140
+ tags=["permissions"],
141
+ )
142
+ self.manifest.routes.append(permissions_route_spec)
143
+
144
+ logger.debug(
145
+ "permissions_routes_added_to_manifest",
146
+ sse_enabled=config.enable_sse_stream,
147
+ )
148
+
149
+ return context
150
+
151
+
152
+ # Export the factory instance
153
+ factory = PermissionsFactory()
@@ -3,26 +3,31 @@
3
3
  import asyncio
4
4
  import json
5
5
  from collections.abc import AsyncGenerator
6
+ from typing import TYPE_CHECKING, Any
6
7
 
7
8
  from fastapi import APIRouter, HTTPException, Request
8
9
  from pydantic import BaseModel
9
- from sse_starlette.sse import EventSourceResponse
10
- from structlog import get_logger
11
10
 
12
- from ccproxy.api.dependencies import SettingsDep
13
- from ccproxy.api.services.permission_service import get_permission_service
14
- from ccproxy.auth.conditional import ConditionalAuthDep
11
+
12
+ if TYPE_CHECKING:
13
+ pass
14
+
15
+ from ccproxy.api.dependencies import OptionalSettingsDep
16
+ from ccproxy.auth.dependencies import ConditionalAuthDep
15
17
  from ccproxy.core.errors import (
16
18
  PermissionAlreadyResolvedError,
17
19
  PermissionNotFoundError,
18
20
  )
19
- from ccproxy.models.permissions import EventType, PermissionEvent, PermissionStatus
21
+ from ccproxy.core.logging import get_plugin_logger
20
22
 
23
+ from .models import EventType, PermissionEvent, PermissionStatus
24
+ from .service import get_permission_service
21
25
 
22
- logger = get_logger(__name__)
23
26
 
27
+ logger = get_plugin_logger()
24
28
 
25
- router = APIRouter(tags=["permissions"])
29
+
30
+ router = APIRouter()
26
31
 
27
32
 
28
33
  class PermissionResponse(BaseModel):
@@ -106,9 +111,9 @@ async def event_generator(
106
111
  @router.get("/stream")
107
112
  async def stream_permissions(
108
113
  request: Request,
109
- settings: SettingsDep,
114
+ settings: OptionalSettingsDep,
110
115
  auth: ConditionalAuthDep,
111
- ) -> EventSourceResponse:
116
+ ) -> Any:
112
117
  """Stream permission requests via Server-Sent Events.
113
118
 
114
119
  This endpoint streams new permission requests as they are created,
@@ -117,19 +122,18 @@ async def stream_permissions(
117
122
  Returns:
118
123
  EventSourceResponse streaming permission events
119
124
  """
125
+ # Import at runtime to avoid type-checker import requirement
126
+ from sse_starlette.sse import EventSourceResponse
127
+
120
128
  return EventSourceResponse(
121
129
  event_generator(request),
122
- headers={
123
- "Cache-Control": "no-cache",
124
- "X-Accel-Buffering": "no", # Disable nginx buffering
125
- },
126
130
  )
127
131
 
128
132
 
129
133
  @router.get("/{permission_id}")
130
134
  async def get_permission(
131
135
  permission_id: str,
132
- settings: SettingsDep,
136
+ settings: OptionalSettingsDep,
133
137
  auth: ConditionalAuthDep,
134
138
  ) -> PermissionRequestInfo:
135
139
  """Get information about a specific permission request.
@@ -168,7 +172,7 @@ async def get_permission(
168
172
  async def respond_to_permission(
169
173
  permission_id: str,
170
174
  response: PermissionResponse,
171
- settings: SettingsDep,
175
+ settings: OptionalSettingsDep,
172
176
  auth: ConditionalAuthDep,
173
177
  ) -> dict[str, str | bool]:
174
178
  """Submit a response to a permission request.
@@ -3,14 +3,15 @@
3
3
  import asyncio
4
4
  import contextlib
5
5
  from datetime import UTC, datetime, timedelta
6
- from typing import Any
7
-
8
- from structlog import get_logger
6
+ from typing import TYPE_CHECKING, Any
9
7
 
8
+ from ccproxy.core.async_task_manager import AsyncTaskManager, create_managed_task
10
9
  from ccproxy.core.errors import (
11
10
  PermissionNotFoundError,
12
11
  )
13
- from ccproxy.models.permissions import (
12
+ from ccproxy.core.logging import get_plugin_logger
13
+
14
+ from .models import (
14
15
  EventType,
15
16
  PermissionEvent,
16
17
  PermissionRequest,
@@ -18,7 +19,11 @@ from ccproxy.models.permissions import (
18
19
  )
19
20
 
20
21
 
21
- logger = get_logger(__name__)
22
+ if TYPE_CHECKING:
23
+ from ccproxy.services.container import ServiceContainer
24
+
25
+
26
+ logger = get_plugin_logger()
22
27
 
23
28
 
24
29
  class PermissionService:
@@ -32,10 +37,38 @@ class PermissionService:
32
37
  self._event_queues: list[asyncio.Queue[dict[str, Any]]] = []
33
38
  self._lock = asyncio.Lock()
34
39
 
35
- async def start(self) -> None:
36
- if self._expiry_task is None:
37
- self._expiry_task = asyncio.create_task(self._expiry_checker())
38
- logger.debug("permission_service_started")
40
+ async def start(
41
+ self,
42
+ *,
43
+ container: "ServiceContainer | None" = None,
44
+ task_manager: AsyncTaskManager | None = None,
45
+ ) -> None:
46
+ if self._expiry_task is not None:
47
+ return
48
+
49
+ self._shutdown = False
50
+
51
+ try:
52
+ self._expiry_task = await create_managed_task(
53
+ self._expiry_checker(),
54
+ name="permission_expiry_checker",
55
+ creator="PermissionService",
56
+ container=container,
57
+ task_manager=task_manager,
58
+ )
59
+ except RuntimeError as exc:
60
+ if not self._should_fallback_to_unmanaged_task(exc):
61
+ raise
62
+
63
+ logger.warning(
64
+ "permission_service_task_manager_unavailable",
65
+ error=str(exc),
66
+ )
67
+ self._expiry_task = asyncio.create_task(
68
+ self._expiry_checker(), name="permission_expiry_checker"
69
+ )
70
+
71
+ logger.debug("permission_service_started")
39
72
 
40
73
  async def stop(self) -> None:
41
74
  self._shutdown = True
@@ -179,7 +212,7 @@ class PermissionService:
179
212
  async def _expiry_checker(self) -> None:
180
213
  while not self._shutdown:
181
214
  try:
182
- await asyncio.sleep(5)
215
+ await asyncio.sleep(self._get_expiry_poll_interval())
183
216
 
184
217
  now = datetime.now(UTC)
185
218
  expired_ids = []
@@ -220,7 +253,7 @@ class PermissionService:
220
253
  logger.error(
221
254
  "expiry_checker_error",
222
255
  error=str(e),
223
- exc_info=True,
256
+ exc_info=e,
224
257
  )
225
258
 
226
259
  def _should_cleanup_request(
@@ -240,6 +273,27 @@ class PermissionService:
240
273
 
241
274
  return False
242
275
 
276
+ def _get_expiry_poll_interval(self) -> float:
277
+ """Determine how frequently to poll for expired requests."""
278
+
279
+ timeout = max(self._timeout_seconds, 0)
280
+ if timeout == 0:
281
+ return 0.5
282
+
283
+ return max(0.5, min(5.0, timeout / 2))
284
+
285
+ @staticmethod
286
+ def _should_fallback_to_unmanaged_task(exc: RuntimeError) -> bool:
287
+ message = str(exc)
288
+ return any(
289
+ hint in message
290
+ for hint in (
291
+ "Task manager is not started",
292
+ "ServiceContainer is not available",
293
+ "AsyncTaskManager is not registered",
294
+ )
295
+ )
296
+
243
297
  async def subscribe_to_events(self) -> asyncio.Queue[dict[str, Any]]:
244
298
  """Subscribe to permission events.
245
299
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Protocol
4
4
 
5
- from ccproxy.api.services.permission_service import PermissionRequest
5
+ from .. import PermissionRequest
6
6
 
7
7
 
8
8
  class ConfirmationHandlerProtocol(Protocol):
@@ -1,23 +1,71 @@
1
1
  """Terminal UI handler for confirmation requests using Textual with request stacking support."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import asyncio
4
6
  import contextlib
5
7
  import time
6
8
  from dataclasses import dataclass
9
+ from typing import TYPE_CHECKING
10
+
11
+ from ccproxy.core.logging import get_plugin_logger
12
+
13
+ from .. import PermissionRequest
14
+
15
+
16
+ # During type checking, import real Textual types; at runtime, provide fallbacks if absent.
17
+ TEXTUAL_AVAILABLE: bool
18
+ if TYPE_CHECKING:
19
+ from textual.app import App, ComposeResult
20
+ from textual.containers import Container, Vertical
21
+ from textual.events import Key
22
+ from textual.reactive import reactive
23
+ from textual.screen import ModalScreen
24
+ from textual.timer import Timer
25
+ from textual.widgets import Label, Static
26
+
27
+ TEXTUAL_AVAILABLE = True
28
+ else: # pragma: no cover - optional dependency
29
+ try:
30
+ from textual.app import App, ComposeResult
31
+ from textual.containers import Container, Vertical
32
+ from textual.events import Key
33
+ from textual.reactive import reactive
34
+ from textual.screen import ModalScreen
35
+ from textual.timer import Timer
36
+ from textual.widgets import Label, Static
37
+
38
+ TEXTUAL_AVAILABLE = True
39
+ except ImportError:
40
+ TEXTUAL_AVAILABLE = False
41
+
42
+ # Minimal runtime stubs to avoid crashes when Textual is not installed
43
+ class App: # type: ignore[no-redef]
44
+ pass
45
+
46
+ class Container: # type: ignore[no-redef]
47
+ pass
7
48
 
8
- from structlog import get_logger
9
- from textual.app import App, ComposeResult
10
- from textual.containers import Container, Vertical
11
- from textual.events import Key
12
- from textual.reactive import reactive
13
- from textual.screen import ModalScreen
14
- from textual.timer import Timer
15
- from textual.widgets import Label, Static
49
+ class Vertical: # type: ignore[no-redef]
50
+ pass
16
51
 
17
- from ccproxy.api.services.permission_service import PermissionRequest
52
+ class ModalScreen: # type: ignore[no-redef]
53
+ pass
18
54
 
55
+ class Label: # type: ignore[no-redef]
56
+ pass
19
57
 
20
- logger = get_logger(__name__)
58
+ class Static: # type: ignore[no-redef]
59
+ pass
60
+
61
+ def reactive(x: float) -> float: # type: ignore[no-redef]
62
+ return x
63
+
64
+ class Timer: # type: ignore[no-redef]
65
+ pass
66
+
67
+
68
+ logger = get_plugin_logger(__name__)
21
69
 
22
70
 
23
71
  @dataclass
@@ -501,6 +549,14 @@ class TerminalPermissionHandler:
501
549
  Returns:
502
550
  bool: True if the user confirmed, False otherwise
503
551
  """
552
+ if not TEXTUAL_AVAILABLE:
553
+ logger.warning(
554
+ "textual_not_available_denying_request",
555
+ request_id=request.id,
556
+ tool_name=request.tool_name,
557
+ )
558
+ return False
559
+
504
560
  try:
505
561
  logger.info(
506
562
  "handling_confirmation_request",
@@ -0,0 +1,34 @@
1
+ # Pricing Plugin
2
+
3
+ Caches model pricing data and exposes it to other plugins for cost awareness.
4
+
5
+ ## Highlights
6
+ - Loads pricing catalogs and keeps them fresh via the update task
7
+ - Publishes a `pricing` service in the plugin registry for dependents
8
+ - Tracks cache health, age, and failures for health reporting
9
+
10
+ ## Configuration
11
+ - `PricingConfig` toggles enablement, refresh cadence, and startup behavior
12
+ - Auto-update schedules can force refresh on launch or run periodically
13
+ - Generate defaults with `python3 scripts/generate_config_from_model.py \
14
+ --format toml --plugin pricing --config-class PricingConfig`
15
+
16
+ ```toml
17
+ [plugins.pricing]
18
+ # enabled = true
19
+ # cache_dir = "~/.cache/ccproxy"
20
+ # cache_ttl_hours = 24
21
+ # source_url = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
22
+ # download_timeout = 30
23
+ # auto_update = true
24
+ # memory_cache_ttl = 300
25
+ # update_interval_hours = 6.0
26
+ # force_refresh_on_startup = false
27
+ # fallback_to_embedded = false
28
+ # pricing_provider = "all"
29
+ ```
30
+
31
+ ## Related Components
32
+ - `service.py`: pricing lookup and cache management
33
+ - `tasks.py`: asynchronous cache refresh task
34
+ - `plugin.py`: runtime lifecycle and service registration
@@ -0,0 +1,6 @@
1
+ """Pricing plugin for dynamic model pricing."""
2
+
3
+ from .plugin import factory
4
+
5
+
6
+ __all__ = ["factory"]
@@ -5,18 +5,19 @@ import time
5
5
  from typing import Any
6
6
 
7
7
  import httpx
8
- from structlog import get_logger
9
8
 
10
- from ccproxy.config.pricing import PricingSettings
9
+ from ccproxy.core.logging import get_plugin_logger
11
10
 
11
+ from .config import PricingConfig
12
12
 
13
- logger = get_logger(__name__)
13
+
14
+ logger = get_plugin_logger(__name__)
14
15
 
15
16
 
16
17
  class PricingCache:
17
18
  """Manages caching of model pricing data from external sources."""
18
19
 
19
- def __init__(self, settings: PricingSettings) -> None:
20
+ def __init__(self, settings: PricingConfig) -> None:
20
21
  """Initialize pricing cache.
21
22
 
22
23
  Args:
@@ -84,14 +85,14 @@ class PricingCache:
84
85
  timeout = self.settings.download_timeout
85
86
 
86
87
  try:
87
- logger.info("pricing_download_start", url=self.settings.source_url)
88
+ logger.debug("pricing_download_start", url=self.settings.source_url)
88
89
 
89
90
  async with httpx.AsyncClient(timeout=timeout) as client:
90
91
  response = await client.get(self.settings.source_url)
91
92
  response.raise_for_status()
92
93
 
93
94
  data = response.json()
94
- logger.info("pricing_download_completed", model_count=len(data))
95
+ logger.debug("pricing_download_completed", model_count=len(data))
95
96
  return data # type: ignore[no-any-return]
96
97
 
97
98
  except (httpx.HTTPError, json.JSONDecodeError) as e: