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
@@ -1,18 +1,21 @@
1
1
  """Pricing updater for managing periodic refresh of pricing data."""
2
2
 
3
- from decimal import Decimal
3
+ import json
4
+ import time
4
5
  from typing import Any
5
6
 
6
- from structlog import get_logger
7
+ import httpx
8
+ from pydantic import ValidationError
7
9
 
8
- from ccproxy.config.pricing import PricingSettings
10
+ from ccproxy.core.logging import get_plugin_logger
9
11
 
10
12
  from .cache import PricingCache
13
+ from .config import PricingConfig
11
14
  from .loader import PricingLoader
12
15
  from .models import PricingData
13
16
 
14
17
 
15
- logger = get_logger(__name__)
18
+ logger = get_plugin_logger(__name__)
16
19
 
17
20
 
18
21
  class PricingUpdater:
@@ -21,7 +24,7 @@ class PricingUpdater:
21
24
  def __init__(
22
25
  self,
23
26
  cache: PricingCache,
24
- settings: PricingSettings,
27
+ settings: PricingConfig,
25
28
  ) -> None:
26
29
  """Initialize pricing updater.
27
30
 
@@ -47,8 +50,6 @@ class PricingUpdater:
47
50
  Returns:
48
51
  Current pricing data as PricingData model
49
52
  """
50
- import time
51
-
52
53
  current_time = time.time()
53
54
 
54
55
  # Return cached pricing if recent and not forced
@@ -76,7 +77,7 @@ class PricingUpdater:
76
77
  )
77
78
 
78
79
  if should_refresh:
79
- logger.info("pricing_refresh_start")
80
+ logger.debug("pricing_refresh_start")
80
81
  await self._refresh_pricing()
81
82
 
82
83
  # Load pricing data
@@ -115,7 +116,7 @@ class PricingUpdater:
115
116
  True if refresh was successful
116
117
  """
117
118
  try:
118
- logger.info("pricing_refresh_start")
119
+ logger.debug("pricing_refresh_start")
119
120
 
120
121
  # Download fresh data
121
122
  raw_data = await self.cache.download_pricing_data()
@@ -128,11 +129,26 @@ class PricingUpdater:
128
129
  logger.error("cache_save_failed")
129
130
  return False
130
131
 
131
- logger.info("pricing_refresh_completed")
132
+ logger.debug("pricing_refresh_completed")
132
133
  return True
133
134
 
135
+ except httpx.TimeoutException as e:
136
+ logger.error("pricing_refresh_timeout", error=str(e), exc_info=e)
137
+ return False
138
+ except httpx.HTTPError as e:
139
+ logger.error("pricing_refresh_http_error", error=str(e), exc_info=e)
140
+ return False
141
+ except json.JSONDecodeError as e:
142
+ logger.error("pricing_refresh_json_error", error=str(e), exc_info=e)
143
+ return False
144
+ except ValidationError as e:
145
+ logger.error("pricing_refresh_validation_error", error=str(e), exc_info=e)
146
+ return False
147
+ except OSError as e:
148
+ logger.error("pricing_refresh_io_error", error=str(e), exc_info=e)
149
+ return False
134
150
  except Exception as e:
135
- logger.error("pricing_refresh_failed", error=str(e))
151
+ logger.error("pricing_refresh_failed", error=str(e), exc_info=e)
136
152
  return False
137
153
 
138
154
  async def _load_pricing_data(self) -> PricingData | None:
@@ -146,7 +162,13 @@ class PricingUpdater:
146
162
 
147
163
  if raw_data is not None:
148
164
  # Load and validate pricing data using Pydantic
149
- pricing_data = PricingLoader.load_pricing_from_data(raw_data, verbose=False)
165
+ # Use the configured provider setting (defaults to "all")
166
+ pricing_data = PricingLoader.load_pricing_from_data(
167
+ raw_data,
168
+ provider=self.settings.pricing_provider,
169
+ map_to_claude=False, # Don't map OpenAI models to Claude
170
+ verbose=False,
171
+ )
150
172
 
151
173
  if pricing_data:
152
174
  # Get cache info to display age
@@ -154,69 +176,21 @@ class PricingUpdater:
154
176
  age_hours = cache_info.get("age_hours")
155
177
 
156
178
  if age_hours is not None:
157
- logger.info(
179
+ logger.debug(
158
180
  "pricing_loaded_from_external",
159
181
  model_count=len(pricing_data),
160
182
  cache_age_hours=round(age_hours, 2),
161
183
  )
162
184
  else:
163
- logger.info(
185
+ logger.debug(
164
186
  "pricing_loaded_from_external", model_count=len(pricing_data)
165
187
  )
166
188
  return pricing_data
167
189
  else:
168
190
  logger.warning("external_pricing_validation_failed")
169
191
 
170
- # Fallback to embedded pricing
171
- if self.settings.fallback_to_embedded:
172
- logger.info("using_embedded_pricing_fallback")
173
- return self._get_embedded_pricing()
174
- else:
175
- logger.error("pricing_unavailable_no_fallback")
176
- return None
177
-
178
- def _get_embedded_pricing(self) -> PricingData:
179
- """Get embedded (hardcoded) pricing as fallback.
180
-
181
- Returns:
182
- Embedded pricing data as PricingData model
183
- """
184
- # This is the current hardcoded pricing from CostCalculator
185
- embedded_data = {
186
- "claude-3-5-sonnet-20241022": {
187
- "input": Decimal("3.00"),
188
- "output": Decimal("15.00"),
189
- "cache_read": Decimal("0.30"),
190
- "cache_write": Decimal("3.75"),
191
- },
192
- "claude-3-5-haiku-20241022": {
193
- "input": Decimal("0.25"),
194
- "output": Decimal("1.25"),
195
- "cache_read": Decimal("0.03"),
196
- "cache_write": Decimal("0.30"),
197
- },
198
- "claude-3-opus-20240229": {
199
- "input": Decimal("15.00"),
200
- "output": Decimal("75.00"),
201
- "cache_read": Decimal("1.50"),
202
- "cache_write": Decimal("18.75"),
203
- },
204
- "claude-3-sonnet-20240229": {
205
- "input": Decimal("3.00"),
206
- "output": Decimal("15.00"),
207
- "cache_read": Decimal("0.30"),
208
- "cache_write": Decimal("3.75"),
209
- },
210
- "claude-3-haiku-20240307": {
211
- "input": Decimal("0.25"),
212
- "output": Decimal("1.25"),
213
- "cache_read": Decimal("0.03"),
214
- "cache_write": Decimal("0.30"),
215
- },
216
- }
217
-
218
- # Create PricingData from embedded data
219
- return PricingData.from_dict(embedded_data)
192
+ logger.error("pricing_unavailable_no_fallback")
193
+ return None
220
194
 
221
195
  async def force_refresh(self) -> bool:
222
196
  """Force a refresh of pricing data.
@@ -268,7 +242,6 @@ class PricingUpdater:
268
242
  "models_loaded": len(pricing_data) if pricing_data else 0,
269
243
  "model_names": pricing_data.model_names() if pricing_data else [],
270
244
  "auto_update": self.settings.auto_update,
271
- "fallback_to_embedded": self.settings.fallback_to_embedded,
272
245
  "has_cached_pricing": self._cached_pricing is not None,
273
246
  }
274
247
 
@@ -286,14 +259,30 @@ class PricingUpdater:
286
259
  if raw_data is None:
287
260
  return False
288
261
 
289
- # Try to parse Claude models
290
- claude_models = PricingLoader.extract_claude_models(raw_data)
291
- if not claude_models:
292
- logger.warning("claude_models_not_found_in_external")
293
- return False
262
+ # Try to extract models based on configured provider
263
+ if self.settings.pricing_provider == "claude":
264
+ models = PricingLoader.extract_claude_models(raw_data)
265
+ if not models:
266
+ logger.warning("claude_models_not_found_in_external")
267
+ return False
268
+ else:
269
+ models = PricingLoader.extract_models_by_provider(
270
+ raw_data, provider=self.settings.pricing_provider
271
+ )
272
+ if not models:
273
+ logger.warning(
274
+ "models_not_found_in_external",
275
+ provider=self.settings.pricing_provider,
276
+ )
277
+ return False
294
278
 
295
279
  # Try to load and validate using Pydantic
296
- pricing_data = PricingLoader.load_pricing_from_data(raw_data, verbose=False)
280
+ pricing_data = PricingLoader.load_pricing_from_data(
281
+ raw_data,
282
+ provider=self.settings.pricing_provider,
283
+ map_to_claude=False,
284
+ verbose=False,
285
+ )
297
286
  if not pricing_data:
298
287
  logger.warning("external_pricing_load_failed")
299
288
  return False
@@ -303,6 +292,31 @@ class PricingUpdater:
303
292
  )
304
293
  return True
305
294
 
295
+ except httpx.TimeoutException as e:
296
+ logger.error(
297
+ "external_pricing_validation_timeout", error=str(e), exc_info=e
298
+ )
299
+ return False
300
+ except httpx.HTTPError as e:
301
+ logger.error(
302
+ "external_pricing_validation_http_error", error=str(e), exc_info=e
303
+ )
304
+ return False
305
+ except json.JSONDecodeError as e:
306
+ logger.error(
307
+ "external_pricing_validation_json_error", error=str(e), exc_info=e
308
+ )
309
+ return False
310
+ except ValidationError as e:
311
+ logger.error(
312
+ "external_pricing_validation_validation_error", error=str(e), exc_info=e
313
+ )
314
+ return False
315
+ except OSError as e:
316
+ logger.error(
317
+ "external_pricing_validation_io_error", error=str(e), exc_info=e
318
+ )
319
+ return False
306
320
  except Exception as e:
307
- logger.error("external_pricing_validation_failed", error=str(e))
321
+ logger.error("external_pricing_validation_failed", error=str(e), exc_info=e)
308
322
  return False
@@ -0,0 +1,99 @@
1
+ """Cost calculation utilities for token-based pricing (plugin-owned).
2
+
3
+ These helpers live inside the pricing plugin to avoid coupling core to
4
+ pricing logic. They accept an optional PricingService instance for callers
5
+ that already have one; otherwise they create a default service on demand.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .config import PricingConfig
11
+ from .service import PricingService
12
+
13
+
14
+ async def calculate_token_cost(
15
+ tokens_input: int | None,
16
+ tokens_output: int | None,
17
+ model: str | None,
18
+ cache_read_tokens: int | None = None,
19
+ cache_write_tokens: int | None = None,
20
+ pricing_service: PricingService | None = None,
21
+ ) -> float | None:
22
+ """Calculate total cost in USD for the given token usage.
23
+
24
+ If no pricing_service is provided, a default PricingService is created
25
+ using PricingConfig(). Returns None if model or tokens are missing or if
26
+ pricing information is unavailable.
27
+ """
28
+ if not model or (
29
+ not tokens_input
30
+ and not tokens_output
31
+ and not cache_read_tokens
32
+ and not cache_write_tokens
33
+ ):
34
+ return None
35
+
36
+ service = pricing_service or PricingService(PricingConfig())
37
+
38
+ try:
39
+ cost_decimal = await service.calculate_cost(
40
+ model_name=model,
41
+ input_tokens=tokens_input or 0,
42
+ output_tokens=tokens_output or 0,
43
+ cache_read_tokens=cache_read_tokens or 0,
44
+ cache_write_tokens=cache_write_tokens or 0,
45
+ )
46
+ return float(cost_decimal) if cost_decimal is not None else None
47
+ except Exception:
48
+ return None
49
+
50
+
51
+ async def calculate_cost_breakdown(
52
+ tokens_input: int | None,
53
+ tokens_output: int | None,
54
+ model: str | None,
55
+ cache_read_tokens: int | None = None,
56
+ cache_write_tokens: int | None = None,
57
+ pricing_service: PricingService | None = None,
58
+ ) -> dict[str, float | str] | None:
59
+ """Return a detailed cost breakdown using current pricing data.
60
+
61
+ If no pricing_service is provided, a default PricingService is created.
62
+ Returns None if inputs are insufficient or model pricing is unavailable.
63
+ """
64
+ if not model or (
65
+ not tokens_input
66
+ and not tokens_output
67
+ and not cache_read_tokens
68
+ and not cache_write_tokens
69
+ ):
70
+ return None
71
+
72
+ service = pricing_service or PricingService(PricingConfig())
73
+
74
+ try:
75
+ model_pricing = await service.get_model_pricing(model)
76
+ if not model_pricing:
77
+ return None
78
+
79
+ input_cost = ((tokens_input or 0) / 1_000_000) * float(model_pricing.input)
80
+ output_cost = ((tokens_output or 0) / 1_000_000) * float(model_pricing.output)
81
+ cache_read_cost = ((cache_read_tokens or 0) / 1_000_000) * float(
82
+ model_pricing.cache_read
83
+ )
84
+ cache_write_cost = ((cache_write_tokens or 0) / 1_000_000) * float(
85
+ model_pricing.cache_write
86
+ )
87
+
88
+ total_cost = input_cost + output_cost + cache_read_cost + cache_write_cost
89
+
90
+ return {
91
+ "input_cost": input_cost,
92
+ "output_cost": output_cost,
93
+ "cache_read_cost": cache_read_cost,
94
+ "cache_write_cost": cache_write_cost,
95
+ "total_cost": total_cost,
96
+ "model": model,
97
+ }
98
+ except Exception:
99
+ return None
@@ -0,0 +1,40 @@
1
+ # Request Tracer Plugin
2
+
3
+ Captures detailed request and response traces for troubleshooting.
4
+
5
+ ## Highlights
6
+ - Registers both the core HTTP tracer and contextual request hook handlers
7
+ - Writes JSON or raw HTTP logs with size and path filtering controls
8
+ - Validates configuration and reports issues through structured logs
9
+
10
+ ## Configuration
11
+ - `RequestTracerConfig` toggles enablement, formats, filters, and log targets
12
+ - Hook registration occurs only when a hook registry is available
13
+ - Generate defaults with `python3 scripts/generate_config_from_model.py \
14
+ --format toml --plugin request_tracer --config-class RequestTracerConfig`
15
+
16
+ ```toml
17
+ [plugins.request_tracer]
18
+ # enabled = true
19
+ # verbose_api = true
20
+ # json_logs_enabled = true
21
+ # raw_http_enabled = true
22
+ # trace_oauth = true
23
+ # log_dir = "/tmp/ccproxy/traces"
24
+ # exclude_paths = ["/health", "/metrics", "/readyz", "/livez"]
25
+ # include_paths = []
26
+ # exclude_headers = ["authorization", "x-api-key", "cookie", "x-auth-token"]
27
+ # redact_sensitive = true
28
+ # max_body_size = 10485760
29
+ # truncate_body_preview = 1024
30
+ # log_client_request = true
31
+ # log_client_response = true
32
+ # log_provider_request = true
33
+ # log_provider_response = true
34
+ # log_streaming_chunks = false
35
+ ```
36
+
37
+ ## Related Components
38
+ - `hook.py`: handles request lifecycle events and formatting
39
+ - `plugin.py`: runtime validation and hook wiring
40
+ - `config.py`: settings model for log output options
@@ -0,0 +1,7 @@
1
+ """Request Tracer plugin for request tracing."""
2
+
3
+ from .config import RequestTracerConfig
4
+ from .hook import RequestTracerHook
5
+
6
+
7
+ __all__ = ["RequestTracerConfig", "RequestTracerHook"]
@@ -0,0 +1,120 @@
1
+ """Configuration for the RequestTracer plugin."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class RequestTracerConfig(BaseModel):
7
+ """Unified configuration for request tracing.
8
+
9
+ Combines structured JSON tracing (from core_tracer) and raw HTTP logging
10
+ (from raw_http_logger) into a single configuration.
11
+ """
12
+
13
+ # Enable/disable entire plugin
14
+ enabled: bool = Field(
15
+ default=True, description="Enable or disable the request tracer plugin"
16
+ )
17
+
18
+ # Structured tracing (from core_tracer)
19
+ verbose_api: bool = Field(
20
+ default=True,
21
+ description="Enable verbose API logging with structured JSON output",
22
+ )
23
+ json_logs_enabled: bool = Field(
24
+ default=True, description="Enable structured JSON logging to files"
25
+ )
26
+
27
+ # Raw HTTP logging (from raw_http_logger)
28
+ raw_http_enabled: bool = Field(
29
+ default=True, description="Enable raw HTTP protocol logging"
30
+ )
31
+
32
+ # OAuth tracing
33
+ trace_oauth: bool = Field(
34
+ default=True,
35
+ description="Enable OAuth request/response tracing for CLI operations",
36
+ )
37
+
38
+ # Directory configuration
39
+ log_dir: str = Field(
40
+ default="/tmp/ccproxy/traces", description="Base directory for all trace logs"
41
+ )
42
+ request_log_dir: str | None = Field(
43
+ default=None,
44
+ description="Override directory for structured JSON logs (defaults to log_dir)",
45
+ )
46
+ raw_log_dir: str | None = Field(
47
+ default=None,
48
+ description="Override directory for raw HTTP logs (defaults to log_dir/raw)",
49
+ )
50
+
51
+ # Request filtering
52
+ exclude_paths: list[str] = Field(
53
+ default_factory=lambda: ["/health", "/metrics", "/readyz", "/livez"],
54
+ description="Request paths to exclude from tracing",
55
+ )
56
+ include_paths: list[str] = Field(
57
+ default_factory=list, description="If specified, only trace these paths"
58
+ )
59
+
60
+ # Privacy & security
61
+ exclude_headers: list[str] = Field(
62
+ default_factory=lambda: [
63
+ "authorization",
64
+ "x-api-key",
65
+ "cookie",
66
+ "x-auth-token",
67
+ ],
68
+ description="Headers to redact in raw logs",
69
+ )
70
+ redact_sensitive: bool = Field(
71
+ default=True, description="Redact sensitive data in structured logs"
72
+ )
73
+
74
+ # Performance settings
75
+ max_body_size: int = Field(
76
+ default=10485760, # 10MB
77
+ description="Maximum body size to log (bytes)",
78
+ )
79
+ truncate_body_preview: int = Field(
80
+ default=1024,
81
+ description="Maximum body preview size for structured logs (chars)",
82
+ )
83
+
84
+ # Granular control
85
+ log_client_request: bool = Field(default=True, description="Log client requests")
86
+ log_client_response: bool = Field(default=True, description="Log client responses")
87
+ log_provider_request: bool = Field(
88
+ default=True, description="Log provider requests"
89
+ )
90
+ log_provider_response: bool = Field(
91
+ default=True, description="Log provider responses"
92
+ )
93
+
94
+ # Streaming configuration
95
+ log_streaming_chunks: bool = Field(
96
+ default=False, description="Log individual streaming chunks (verbose)"
97
+ )
98
+
99
+ model_config = ConfigDict()
100
+
101
+ def get_json_log_dir(self) -> str:
102
+ """Get directory for structured JSON logs."""
103
+ return self.request_log_dir or self.log_dir
104
+
105
+ def get_raw_log_dir(self) -> str:
106
+ """Get directory for raw HTTP logs."""
107
+ return self.raw_log_dir or self.log_dir
108
+
109
+ def should_trace_path(self, path: str) -> bool:
110
+ """Check if a path should be traced based on include/exclude rules."""
111
+ # First check exclude_paths (takes precedence)
112
+ if any(path.startswith(exclude) for exclude in self.exclude_paths):
113
+ return False
114
+
115
+ # Then check include_paths (if specified, only log included paths)
116
+ if self.include_paths:
117
+ return any(path.startswith(include) for include in self.include_paths)
118
+
119
+ # Default: trace all paths not explicitly excluded
120
+ return True