ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__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.0a4.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0a4.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.0a4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,284 @@
1
+ """Claude API plugin hooks for streaming metrics extraction."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from ccproxy.core.logging import get_plugin_logger
7
+ from ccproxy.core.plugins.hooks import Hook, HookContext, HookEvent
8
+ from ccproxy.streaming.sse_parser import SSEStreamParser
9
+
10
+ from .streaming_metrics import extract_usage_from_streaming_chunk
11
+
12
+
13
+ logger = get_plugin_logger()
14
+
15
+
16
+ class ClaudeAPIStreamingMetricsHook(Hook):
17
+ """Hook to extract and accumulate metrics from Claude API streaming responses."""
18
+
19
+ name = "claude_api_streaming_metrics"
20
+ events = [HookEvent.PROVIDER_STREAM_CHUNK, HookEvent.PROVIDER_STREAM_END]
21
+ priority = 700 # HookLayer.OBSERVATION - Metrics collection layer
22
+
23
+ def __init__(
24
+ self, pricing_service: Any = None, plugin_registry: Any = None
25
+ ) -> None:
26
+ """Initialize with optional pricing service for cost calculation.
27
+
28
+ Args:
29
+ pricing_service: Direct pricing service instance (if available at init)
30
+ plugin_registry: Plugin registry to get pricing service lazily
31
+ """
32
+ self.pricing_service = pricing_service
33
+ self.plugin_registry = plugin_registry
34
+ # Store metrics per request_id
35
+ self._metrics_cache: dict[str, dict[str, Any]] = {}
36
+ # Incremental SSE parsers keyed by request
37
+ self._sse_parsers: dict[str, SSEStreamParser] = {}
38
+
39
+ def _get_pricing_service(self) -> Any:
40
+ """Get pricing service, trying lazy loading if not already available."""
41
+ if self.pricing_service:
42
+ return self.pricing_service
43
+
44
+ if self.plugin_registry:
45
+ try:
46
+ from ccproxy.plugins.pricing.service import PricingService
47
+
48
+ self.pricing_service = self.plugin_registry.get_service(
49
+ "pricing", PricingService
50
+ )
51
+ if self.pricing_service:
52
+ logger.debug(
53
+ "pricing_service_obtained_lazily",
54
+ plugin="claude_api",
55
+ )
56
+ except Exception as e:
57
+ logger.debug(
58
+ "lazy_pricing_service_failed",
59
+ plugin="claude_api",
60
+ error=str(e),
61
+ )
62
+
63
+ return self.pricing_service
64
+
65
+ async def __call__(self, context: HookContext) -> None:
66
+ """Extract metrics from streaming chunks and add to stream end events."""
67
+ # Only process claude_api provider events
68
+ if context.provider != "claude_api":
69
+ return
70
+
71
+ request_id = context.metadata.get("request_id")
72
+ if not request_id:
73
+ return
74
+
75
+ if context.event == HookEvent.PROVIDER_STREAM_CHUNK:
76
+ await self._process_chunk(context, request_id)
77
+ elif context.event == HookEvent.PROVIDER_STREAM_END:
78
+ await self._finalize_metrics(context, request_id)
79
+
80
+ async def _process_chunk(self, context: HookContext, request_id: str) -> None:
81
+ """Process a streaming chunk to extract metrics."""
82
+ chunk_data = context.data.get("chunk")
83
+ if not chunk_data:
84
+ return
85
+
86
+ # Debug: Log chunk type and sample
87
+ logger.debug(
88
+ "chunk_received",
89
+ plugin="claude_api",
90
+ request_id=request_id,
91
+ chunk_type=type(chunk_data).__name__,
92
+ chunk_sample=str(chunk_data)[:200] if chunk_data else None,
93
+ )
94
+
95
+ # Initialize metrics cache for this request if needed
96
+ if request_id not in self._metrics_cache:
97
+ self._metrics_cache[request_id] = {
98
+ "tokens_input": None,
99
+ "tokens_output": None,
100
+ "cache_read_tokens": None,
101
+ "cache_write_tokens": None,
102
+ "cost_usd": None,
103
+ "model": None,
104
+ }
105
+
106
+ try:
107
+ if isinstance(chunk_data, str | bytes):
108
+ parser = self._sse_parsers.setdefault(request_id, SSEStreamParser())
109
+ for payload in parser.feed(chunk_data):
110
+ if isinstance(payload, dict):
111
+ self._extract_and_accumulate(payload, request_id)
112
+ for raw_event, error in parser.consume_errors():
113
+ logger.debug(
114
+ "chunk_metrics_sse_event_skipped",
115
+ plugin="claude_api",
116
+ request_id=request_id,
117
+ error=str(error),
118
+ event_preview=raw_event[:200],
119
+ )
120
+ elif isinstance(chunk_data, dict):
121
+ # Direct dict chunk
122
+ self._extract_and_accumulate(chunk_data, request_id)
123
+
124
+ except (json.JSONDecodeError, KeyError) as e:
125
+ logger.debug(
126
+ "chunk_metrics_parse_failed",
127
+ plugin="claude_api",
128
+ error=str(e),
129
+ request_id=request_id,
130
+ )
131
+
132
+ def _extract_and_accumulate(
133
+ self, event_data: dict[str, Any], request_id: str
134
+ ) -> None:
135
+ """Extract metrics from parsed event data and accumulate."""
136
+ usage_data = extract_usage_from_streaming_chunk(event_data)
137
+
138
+ if not usage_data:
139
+ return
140
+
141
+ cache = self._metrics_cache[request_id]
142
+ event_type = usage_data.get("event_type")
143
+
144
+ # Handle message_start: get input tokens and initial cache tokens
145
+ if event_type == "message_start":
146
+ cache["tokens_input"] = usage_data.get("input_tokens")
147
+ cache["cache_read_tokens"] = (
148
+ usage_data.get("cache_read_input_tokens") or cache["cache_read_tokens"]
149
+ )
150
+ cache["cache_write_tokens"] = (
151
+ usage_data.get("cache_creation_input_tokens")
152
+ or cache["cache_write_tokens"]
153
+ )
154
+
155
+ # Extract model from the message_start event
156
+ if not cache["model"] and usage_data.get("model"):
157
+ cache["model"] = usage_data.get("model")
158
+
159
+ logger.debug(
160
+ "hook_metrics_extracted",
161
+ plugin="claude_api",
162
+ event_type="message_start",
163
+ tokens_input=cache["tokens_input"],
164
+ cache_read_tokens=cache["cache_read_tokens"],
165
+ cache_write_tokens=cache["cache_write_tokens"],
166
+ model=cache["model"],
167
+ request_id=request_id,
168
+ )
169
+
170
+ # Handle message_delta: get final output tokens
171
+ elif event_type == "message_delta":
172
+ cache["tokens_output"] = usage_data.get("output_tokens")
173
+
174
+ # Calculate cost if we have all required data
175
+ pricing_service = self._get_pricing_service()
176
+ logger.debug(
177
+ "hook_calculating_cost",
178
+ plugin="claude_api",
179
+ request_id=request_id,
180
+ pricing_service=bool(pricing_service is not None),
181
+ model=cache["model"],
182
+ )
183
+ if pricing_service and cache["model"]:
184
+ try:
185
+ from ccproxy.plugins.pricing.exceptions import (
186
+ ModelPricingNotFoundError,
187
+ PricingDataNotLoadedError,
188
+ PricingServiceDisabledError,
189
+ )
190
+
191
+ cost_decimal = pricing_service.calculate_cost_sync(
192
+ model_name=cache["model"],
193
+ input_tokens=cache["tokens_input"] or 0,
194
+ output_tokens=cache["tokens_output"] or 0,
195
+ cache_read_tokens=cache["cache_read_tokens"] or 0,
196
+ cache_write_tokens=cache["cache_write_tokens"] or 0,
197
+ )
198
+ cache["cost_usd"] = float(cost_decimal)
199
+
200
+ logger.debug(
201
+ "hook_cost_calculated",
202
+ plugin="claude_api",
203
+ model=cache["model"],
204
+ cost_usd=cache["cost_usd"],
205
+ request_id=request_id,
206
+ )
207
+ except (
208
+ ModelPricingNotFoundError,
209
+ PricingDataNotLoadedError,
210
+ PricingServiceDisabledError,
211
+ ) as e:
212
+ logger.debug(
213
+ "hook_cost_calculation_skipped",
214
+ plugin="claude_api",
215
+ reason=str(e),
216
+ request_id=request_id,
217
+ )
218
+ except Exception as e:
219
+ logger.debug(
220
+ "hook_cost_calculation_failed",
221
+ plugin="claude_api",
222
+ error=str(e),
223
+ request_id=request_id,
224
+ )
225
+
226
+ logger.debug(
227
+ "hook_metrics_extracted",
228
+ plugin="claude_api",
229
+ event_type="message_delta",
230
+ tokens_output=cache["tokens_output"],
231
+ cost_usd=cache.get("cost_usd"),
232
+ request_id=request_id,
233
+ )
234
+
235
+ async def _finalize_metrics(self, context: HookContext, request_id: str) -> None:
236
+ """Add accumulated metrics to the PROVIDER_STREAM_END event."""
237
+ parser = self._sse_parsers.pop(request_id, None)
238
+ if parser:
239
+ for payload in parser.flush():
240
+ if isinstance(payload, dict):
241
+ self._extract_and_accumulate(payload, request_id)
242
+ for raw_event, error in parser.consume_errors():
243
+ logger.debug(
244
+ "chunk_metrics_sse_event_skipped",
245
+ plugin="claude_api",
246
+ request_id=request_id,
247
+ error=str(error),
248
+ event_preview=raw_event[:200],
249
+ )
250
+
251
+ if request_id not in self._metrics_cache:
252
+ return
253
+
254
+ metrics = self._metrics_cache.pop(request_id, {})
255
+
256
+ # Add metrics to the event's usage_metrics field
257
+ if not context.data.get("usage_metrics"):
258
+ context.data["usage_metrics"] = {}
259
+
260
+ # Update with our collected metrics
261
+ if metrics["tokens_input"] is not None:
262
+ context.data["usage_metrics"]["input_tokens"] = metrics["tokens_input"]
263
+ if metrics["tokens_output"] is not None:
264
+ context.data["usage_metrics"]["output_tokens"] = metrics["tokens_output"]
265
+ if metrics["cache_read_tokens"] is not None:
266
+ context.data["usage_metrics"]["cache_read_input_tokens"] = metrics[
267
+ "cache_read_tokens"
268
+ ]
269
+ if metrics["cache_write_tokens"] is not None:
270
+ context.data["usage_metrics"]["cache_creation_input_tokens"] = metrics[
271
+ "cache_write_tokens"
272
+ ]
273
+ if metrics["cost_usd"] is not None:
274
+ context.data["usage_metrics"]["cost_usd"] = metrics["cost_usd"]
275
+ if metrics["model"]:
276
+ context.data["model"] = metrics["model"]
277
+
278
+ logger.info(
279
+ "streaming_metrics_finalized",
280
+ plugin="claude_api",
281
+ request_id=request_id,
282
+ usage_metrics=context.data.get("usage_metrics", {}),
283
+ context_data_keys=list(context.data.keys()) if context.data else [],
284
+ )
@@ -0,0 +1,256 @@
1
+ """Claude API plugin local CLI health models and detection models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from enum import Enum
7
+ from typing import Annotated, Any, TypedDict
8
+
9
+ from pydantic import (
10
+ BaseModel,
11
+ ConfigDict,
12
+ Field,
13
+ field_serializer,
14
+ field_validator,
15
+ model_validator,
16
+ )
17
+
18
+ from ccproxy.models.detection import DetectedHeaders, DetectedPrompts
19
+
20
+
21
+ class ClaudeCliStatus(str, Enum):
22
+ AVAILABLE = "available"
23
+ NOT_INSTALLED = "not_installed"
24
+ BINARY_FOUND_BUT_ERRORS = "binary_found_but_errors"
25
+ TIMEOUT = "timeout"
26
+ ERROR = "error"
27
+
28
+
29
+ class ClaudeCliInfo(BaseModel):
30
+ status: ClaudeCliStatus
31
+ version: str | None = None
32
+ binary_path: str | None = None
33
+ version_output: str | None = None
34
+ error: str | None = None
35
+ return_code: str | None = None
36
+
37
+
38
+ class ClaudeAgentHeaders(BaseModel):
39
+ """Pydantic model for Claude CLI headers extraction with field aliases."""
40
+
41
+ anthropic_beta: str = Field(
42
+ alias="anthropic-beta",
43
+ description="Anthropic beta features",
44
+ default="claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
45
+ )
46
+ anthropic_version: str = Field(
47
+ alias="anthropic-version",
48
+ description="Anthropic API version",
49
+ default="2023-06-01",
50
+ )
51
+ anthropic_dangerous_direct_browser_access: str = Field(
52
+ alias="anthropic-dangerous-direct-browser-access",
53
+ description="Browser access flag",
54
+ default="true",
55
+ )
56
+ x_app: str = Field(
57
+ alias="x-app", description="Application identifier", default="cli"
58
+ )
59
+ user_agent: str = Field(
60
+ alias="user-agent",
61
+ description="User agent string",
62
+ default="claude-cli/1.0.60 (external, cli)",
63
+ )
64
+ x_stainless_lang: str = Field(
65
+ alias="x-stainless-lang", description="SDK language", default="js"
66
+ )
67
+ x_stainless_retry_count: str = Field(
68
+ alias="x-stainless-retry-count", description="Retry count", default="0"
69
+ )
70
+ x_stainless_timeout: str = Field(
71
+ alias="x-stainless-timeout", description="Request timeout", default="60"
72
+ )
73
+ x_stainless_package_version: str = Field(
74
+ alias="x-stainless-package-version",
75
+ description="Package version",
76
+ default="0.55.1",
77
+ )
78
+ x_stainless_os: str = Field(
79
+ alias="x-stainless-os", description="Operating system", default="Linux"
80
+ )
81
+ x_stainless_arch: str = Field(
82
+ alias="x-stainless-arch", description="Architecture", default="x64"
83
+ )
84
+ x_stainless_runtime: str = Field(
85
+ alias="x-stainless-runtime", description="Runtime", default="node"
86
+ )
87
+ x_stainless_runtime_version: str = Field(
88
+ alias="x-stainless-runtime-version",
89
+ description="Runtime version",
90
+ default="v24.3.0",
91
+ )
92
+
93
+ model_config = ConfigDict(extra="ignore", populate_by_name=True)
94
+
95
+ def to_headers_dict(self) -> dict[str, str]:
96
+ """Convert to headers dictionary for HTTP forwarding with proper case."""
97
+ headers = {}
98
+
99
+ # Map field names to proper HTTP header names
100
+ header_mapping = {
101
+ "anthropic_beta": "anthropic-beta",
102
+ "anthropic_version": "anthropic-version",
103
+ "anthropic_dangerous_direct_browser_access": "anthropic-dangerous-direct-browser-access",
104
+ "x_app": "x-app",
105
+ "user_agent": "User-Agent",
106
+ "x_stainless_lang": "X-Stainless-Lang",
107
+ "x_stainless_retry_count": "X-Stainless-Retry-Count",
108
+ "x_stainless_timeout": "X-Stainless-Timeout",
109
+ "x_stainless_package_version": "X-Stainless-Package-Version",
110
+ "x_stainless_os": "X-Stainless-OS",
111
+ "x_stainless_arch": "X-Stainless-Arch",
112
+ "x_stainless_runtime": "X-Stainless-Runtime",
113
+ "x_stainless_runtime_version": "X-Stainless-Runtime-Version",
114
+ }
115
+
116
+ for field_name, header_name in header_mapping.items():
117
+ value = getattr(self, field_name, None)
118
+ if value is not None:
119
+ headers[header_name] = value
120
+
121
+ return headers
122
+
123
+
124
+ class SystemPromptData(BaseModel):
125
+ """Extracted system prompt information."""
126
+
127
+ system_field: Annotated[
128
+ str | list[dict[str, Any]],
129
+ Field(
130
+ description="Complete system field as detected from Claude CLI, preserving exact structure including type, text, and cache_control"
131
+ ),
132
+ ]
133
+
134
+ model_config = ConfigDict(extra="forbid")
135
+
136
+
137
+ class ClaudeCacheData(BaseModel):
138
+ """Cached Claude CLI detection data with version tracking."""
139
+
140
+ claude_version: Annotated[str, Field(description="Claude CLI version")]
141
+ headers: Annotated[
142
+ DetectedHeaders,
143
+ Field(
144
+ description="Captured headers (lowercase keys) in insertion order",
145
+ default_factory=DetectedHeaders,
146
+ ),
147
+ ]
148
+ prompts: Annotated[
149
+ DetectedPrompts,
150
+ Field(description="Captured prompt metadata", default_factory=DetectedPrompts),
151
+ ]
152
+ body_json: Annotated[
153
+ dict[str, Any] | None,
154
+ Field(
155
+ description="Legacy captured request body (deprecated)",
156
+ default=None,
157
+ exclude=True,
158
+ ),
159
+ ] = None
160
+ method: Annotated[
161
+ str | None, Field(description="Captured HTTP method", default=None)
162
+ ] = None
163
+ url: Annotated[str | None, Field(description="Captured full URL", default=None)] = (
164
+ None
165
+ )
166
+ path: Annotated[
167
+ str | None, Field(description="Captured request path", default=None)
168
+ ] = None
169
+ query_params: Annotated[
170
+ dict[str, str] | None,
171
+ Field(description="Captured query parameters", default=None),
172
+ ] = None
173
+ cached_at: datetime = Field(
174
+ description="Cache timestamp",
175
+ default_factory=lambda: datetime.now(UTC),
176
+ )
177
+
178
+ model_config = ConfigDict(extra="forbid")
179
+
180
+ @model_validator(mode="before")
181
+ @classmethod
182
+ def _coerce_legacy_format(cls, values: dict[str, Any]) -> dict[str, Any]:
183
+ if not isinstance(values, dict):
184
+ return values
185
+
186
+ if "prompts" not in values:
187
+ legacy_body = values.get("body_json")
188
+ if legacy_body is not None:
189
+ values["prompts"] = DetectedPrompts.from_body(legacy_body)
190
+ cls._log_legacy_usage("body_json")
191
+
192
+ return values
193
+
194
+ @field_validator("headers", mode="before")
195
+ @classmethod
196
+ def _validate_headers(cls, value: Any) -> DetectedHeaders:
197
+ if isinstance(value, DetectedHeaders):
198
+ return value
199
+ if isinstance(value, dict):
200
+ return DetectedHeaders(value)
201
+ if value is None:
202
+ cls._log_legacy_usage("missing_headers")
203
+ return DetectedHeaders()
204
+ raise TypeError("headers must be a mapping of strings")
205
+
206
+ @field_validator("prompts", mode="before")
207
+ @classmethod
208
+ def _validate_prompts(cls, value: Any) -> DetectedPrompts:
209
+ if isinstance(value, DetectedPrompts):
210
+ return value
211
+ if isinstance(value, dict):
212
+ return DetectedPrompts.from_body(value)
213
+ if value is None:
214
+ return DetectedPrompts()
215
+ raise TypeError("prompts must be derived from a mapping")
216
+
217
+ @field_serializer("headers")
218
+ def _serialize_headers(self, headers: DetectedHeaders) -> dict[str, str]:
219
+ return headers.as_dict()
220
+
221
+ @field_serializer("prompts")
222
+ def _serialize_prompts(self, prompts: DetectedPrompts) -> dict[str, Any]:
223
+ raw = prompts.raw or {}
224
+ if not isinstance(raw, dict):
225
+ raw = {}
226
+ if prompts.instructions and "instructions" not in raw:
227
+ raw = dict(raw)
228
+ raw["instructions"] = prompts.instructions
229
+ if prompts.system is not None and "system" not in raw:
230
+ raw = dict(raw)
231
+ raw["system"] = prompts.system
232
+ return raw
233
+
234
+ @staticmethod
235
+ def _log_legacy_usage(reason: str) -> None:
236
+ try:
237
+ from ccproxy.core.logging import get_plugin_logger
238
+
239
+ logger = get_plugin_logger()
240
+ logger.debug(
241
+ "legacy_detection_cache_format",
242
+ plugin="claude_api",
243
+ reason=reason,
244
+ )
245
+ except Exception: # pragma: no cover - logging best-effort only
246
+ pass
247
+
248
+
249
+ class ClaudeAPIAuthData(TypedDict, total=False):
250
+ """Authentication data for Claude API provider.
251
+
252
+ Attributes:
253
+ access_token: Bearer token for Anthropic Claude API authentication
254
+ """
255
+
256
+ access_token: str | None