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,235 @@
1
+ """Max tokens adapter implementation using hook system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from ccproxy.core.logging import get_plugin_logger
9
+ from ccproxy.core.plugins.hooks import Hook, HookContext, HookEvent
10
+ from ccproxy.core.plugins.hooks.layers import HookLayer
11
+
12
+ from .config import MaxTokensConfig
13
+ from .service import TokenLimitsService
14
+
15
+
16
+ logger = get_plugin_logger(__name__)
17
+
18
+
19
+ class MaxTokensHook(Hook):
20
+ """Hook that enforces `max_tokens` limits before provider dispatch."""
21
+
22
+ name = "max_tokens"
23
+ events = [HookEvent.PROVIDER_REQUEST_PREPARED]
24
+ priority = HookLayer.PROCESSING # Run before observation hooks
25
+
26
+ def __init__(self, config: MaxTokensConfig, service: TokenLimitsService):
27
+ """Initialize max tokens hook."""
28
+ self.config = config
29
+ self.service = service
30
+
31
+ async def __call__(self, context: HookContext) -> None:
32
+ """Adjust token limits in provider request payload when required."""
33
+ if not self.config.enabled:
34
+ return
35
+
36
+ if context.event is not HookEvent.PROVIDER_REQUEST_PREPARED:
37
+ return
38
+
39
+ provider = context.provider
40
+ if provider and not self.config.should_process_provider(provider):
41
+ return
42
+
43
+ payload = context.data.get("body")
44
+ if not isinstance(payload, dict):
45
+ return
46
+
47
+ model = payload.get("model")
48
+ if not isinstance(model, str):
49
+ return
50
+
51
+ provider_model = context.metadata.get("provider_model")
52
+ if not isinstance(provider_model, str):
53
+ provider_model = model
54
+
55
+ alias_map = context.metadata.get("_model_alias_map")
56
+ client_model = context.metadata.get("client_model")
57
+ if not client_model and isinstance(alias_map, dict):
58
+ client_model = alias_map.get(provider_model) or alias_map.get(model)
59
+
60
+ modifiers = context.data.setdefault("modifiers", {})
61
+ modified = False
62
+
63
+ # Ensure we work with the latest payload returned from the service
64
+ modified_payload, modification = self.service.modify_max_tokens(
65
+ payload, provider_model, provider
66
+ )
67
+
68
+ if modification and modification.was_modified():
69
+ payload = modified_payload
70
+ context.data["body"] = payload
71
+ modifiers["max_tokens"] = {
72
+ "original": modification.original_max_tokens,
73
+ "new": modification.new_max_tokens,
74
+ "reason": modification.reason,
75
+ }
76
+ modified = True
77
+ else:
78
+ payload = modified_payload
79
+ context.data["body"] = payload
80
+
81
+ current_max_output = payload.get("max_output_tokens")
82
+ provider_limit = self.service.get_max_output_tokens(provider_model)
83
+ if provider_limit is None and self.config.fallback_max_tokens:
84
+ provider_limit = self.config.fallback_max_tokens
85
+
86
+ new_max_output: int | None = None
87
+ output_reason: str | None = None
88
+
89
+ if isinstance(current_max_output, int) and current_max_output > 0:
90
+ original_limit = (
91
+ self.service.get_max_output_tokens(client_model)
92
+ if client_model
93
+ else None
94
+ )
95
+
96
+ if (
97
+ provider_limit
98
+ and original_limit
99
+ and client_model
100
+ and client_model != provider_model
101
+ and current_max_output == original_limit
102
+ and provider_limit != original_limit
103
+ ):
104
+ new_max_output = provider_limit
105
+ output_reason = "max_output_tokens_aligned_with_mapped_model"
106
+ elif provider_limit and current_max_output > provider_limit:
107
+ new_max_output = provider_limit
108
+ output_reason = "max_output_tokens_capped_to_provider_limit"
109
+
110
+ if new_max_output is not None and new_max_output != current_max_output:
111
+ payload["max_output_tokens"] = new_max_output
112
+ modifiers["max_output_tokens"] = {
113
+ "original": current_max_output,
114
+ "new": new_max_output,
115
+ "reason": output_reason,
116
+ }
117
+ modified = True
118
+
119
+ if self.config.log_modifications:
120
+ logger.info(
121
+ "max_output_tokens_adjusted",
122
+ provider=provider,
123
+ provider_model=provider_model,
124
+ client_model=client_model,
125
+ original=current_max_output,
126
+ new=new_max_output,
127
+ reason=output_reason,
128
+ )
129
+
130
+ if modified:
131
+ context.data["body"] = payload
132
+ context.data["body_kind"] = "json"
133
+ context.data["body_raw"] = json.dumps(payload).encode("utf-8")
134
+
135
+
136
+ class MaxTokensAdapter:
137
+ """Max tokens adapter using hook-based request interception."""
138
+
139
+ def __init__(self, config: MaxTokensConfig):
140
+ """Initialize max tokens adapter."""
141
+ self.config = config
142
+ self.service = TokenLimitsService(config)
143
+ self.hook = MaxTokensHook(config, self.service)
144
+ self._initialized = False
145
+
146
+ async def initialize(self) -> None:
147
+ """Initialize the adapter and register hooks."""
148
+ if self._initialized:
149
+ return
150
+
151
+ if not self.config.enabled:
152
+ logger.debug("max_tokens_adapter_disabled")
153
+ return
154
+
155
+ # Initialize the service
156
+ self.service.initialize()
157
+
158
+ # Register hook with hook manager
159
+ try:
160
+ from ccproxy.services.container import ServiceContainer
161
+
162
+ container = ServiceContainer.get_current(strict=False)
163
+ if container:
164
+ try:
165
+ hook_registry = container.get_hook_registry()
166
+ if hook_registry:
167
+ hook_registry.register(self.hook)
168
+ logger.debug(
169
+ "max_tokens_hook_registered",
170
+ hook_name=self.hook.name,
171
+ events=[event.value for event in self.hook.events],
172
+ priority=self.hook.priority,
173
+ )
174
+ else:
175
+ logger.warning("no_hook_registry_available")
176
+ except Exception as service_error:
177
+ logger.warning(
178
+ "hook_registry_service_not_available",
179
+ error=str(service_error),
180
+ )
181
+ else:
182
+ logger.warning("no_service_container_available")
183
+
184
+ except Exception as e:
185
+ logger.error(
186
+ "failed_to_register_max_tokens_hook",
187
+ error=str(e),
188
+ exc_info=e,
189
+ )
190
+ # Continue without hook registration - plugin will be effectively disabled
191
+
192
+ self._initialized = True
193
+ logger.info("max_tokens_adapter_initialized")
194
+
195
+ async def cleanup(self) -> None:
196
+ """Cleanup resources."""
197
+ if not self._initialized:
198
+ return
199
+
200
+ try:
201
+ # Unregister hook
202
+ from ccproxy.services.container import ServiceContainer
203
+
204
+ container = ServiceContainer.get_current(strict=False)
205
+ if container:
206
+ try:
207
+ hook_registry = container.get_hook_registry()
208
+ if hook_registry:
209
+ hook_registry.unregister(self.hook)
210
+ logger.debug("max_tokens_hook_unregistered")
211
+ except Exception as service_error:
212
+ logger.debug(
213
+ "hook_registry_service_not_available_during_cleanup",
214
+ error=str(service_error),
215
+ )
216
+
217
+ except Exception as e:
218
+ logger.error(
219
+ "failed_to_unregister_max_tokens_hook",
220
+ error=str(e),
221
+ exc_info=e,
222
+ )
223
+
224
+ self._initialized = False
225
+ logger.debug("max_tokens_adapter_cleanup_completed")
226
+
227
+ def get_modification_stats(self) -> dict[str, Any]:
228
+ """Get statistics about max_tokens modifications."""
229
+ # This could be enhanced to track modification statistics
230
+ return {
231
+ "adapter_initialized": self._initialized,
232
+ "config_enabled": self.config.enabled,
233
+ "target_providers": self.config.target_providers,
234
+ "fallback_max_tokens": self.config.fallback_max_tokens,
235
+ }
@@ -0,0 +1,86 @@
1
+ """Configuration for max_tokens plugin."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, Field, model_validator
7
+
8
+
9
+ class MaxTokensConfig(BaseModel):
10
+ """Configuration for the max_tokens plugin."""
11
+
12
+ enabled: bool = Field(
13
+ default=True, description="Whether the max_tokens plugin is enabled"
14
+ )
15
+ default_token_limits_file: str = Field(
16
+ default=str(Path(__file__).parent / "token_limits.json"),
17
+ description="Path to JSON file containing default token limits",
18
+ )
19
+ fallback_max_tokens: int = Field(
20
+ default=4096,
21
+ ge=1,
22
+ description="Fallback max_tokens when model limits are unknown",
23
+ )
24
+ apply_to_all_providers: bool = Field(
25
+ default=True,
26
+ description="Whether to apply to all providers or only specific ones",
27
+ )
28
+ target_providers: list[str] = Field(
29
+ default_factory=lambda: ["claude_api", "claude_sdk", "codex", "copilot"],
30
+ description="List of providers to apply max_tokens modifications to",
31
+ )
32
+ require_pricing_data: bool = Field(
33
+ default=False,
34
+ description=(
35
+ "If True, only modify requests when pricing data is available. "
36
+ "If False, use fallback limits when pricing data is not available."
37
+ ),
38
+ )
39
+ log_modifications: bool = Field(
40
+ default=True, description="Whether to log max_tokens modifications"
41
+ )
42
+ enforce_mode: bool = Field(
43
+ default=False,
44
+ description=(
45
+ "When enabled, always set max_tokens to the model's maximum limit, "
46
+ "ignoring the request's current max_tokens value"
47
+ ),
48
+ )
49
+ prioritize_local_file: bool = Field(
50
+ default=False,
51
+ description=(
52
+ "When enabled, local token_limits.json values take precedence over "
53
+ "pricing cache values. When disabled, local file is only used as fallback "
54
+ "when pricing cache is unavailable or model is not found in cache."
55
+ ),
56
+ )
57
+ modification_reasons: dict[str, str] = Field(
58
+ default_factory=lambda: {
59
+ "missing": "max_tokens was missing from request",
60
+ "invalid": "max_tokens was invalid or too high",
61
+ "exceeded": "max_tokens exceeded model limit",
62
+ "enforced": "max_tokens enforced to model limit (enforce mode)",
63
+ },
64
+ description="Reason templates for modifications",
65
+ )
66
+
67
+ @model_validator(mode="before")
68
+ @classmethod
69
+ def validate_config(cls, data: dict[str, Any]) -> dict[str, Any]:
70
+ """Validate configuration values."""
71
+ # Ensure target_providers is a list
72
+ if isinstance(data.get("target_providers"), str):
73
+ data["target_providers"] = [data["target_providers"]]
74
+ return data
75
+
76
+ def should_process_provider(self, provider: str) -> bool:
77
+ """Check if plugin should process requests for given provider."""
78
+ if self.apply_to_all_providers:
79
+ return True
80
+ return provider in self.target_providers
81
+
82
+ def get_modification_reason(self, reason_type: str) -> str:
83
+ """Get modification reason text for given reason type."""
84
+ return self.modification_reasons.get(
85
+ reason_type, f"Unknown reason: {reason_type}"
86
+ )
@@ -0,0 +1,53 @@
1
+ """Pydantic models for max_tokens plugin."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ModelTokenLimits(BaseModel):
7
+ """Token limits for a specific model."""
8
+
9
+ max_output_tokens: int = Field(
10
+ ..., ge=1, description="Maximum output tokens for the model"
11
+ )
12
+ max_input_tokens: int | None = Field(
13
+ default=None, ge=1, description="Maximum input tokens for the model"
14
+ )
15
+
16
+
17
+ class TokenLimitsData(BaseModel):
18
+ """Complete token limits data for all models."""
19
+
20
+ models: dict[str, ModelTokenLimits] = Field(
21
+ default_factory=dict, description="Model name to token limits mapping"
22
+ )
23
+
24
+ def get_max_output_tokens(self, model_name: str) -> int | None:
25
+ """Get maximum output tokens for a model."""
26
+ model_limits = self.models.get(model_name)
27
+ return model_limits.max_output_tokens if model_limits else None
28
+
29
+ def get_max_input_tokens(self, model_name: str) -> int | None:
30
+ """Get maximum input tokens for a model."""
31
+ model_limits = self.models.get(model_name)
32
+ return model_limits.max_input_tokens if model_limits else None
33
+
34
+ def has_model(self, model_name: str) -> bool:
35
+ """Check if model limits exist for the given model."""
36
+ return model_name in self.models
37
+
38
+
39
+ class MaxTokensModification(BaseModel):
40
+ """Information about max_tokens modification made by the plugin."""
41
+
42
+ original_max_tokens: int | None = Field(
43
+ description="Original max_tokens value from request"
44
+ )
45
+ new_max_tokens: int | None = Field(
46
+ description="New max_tokens value after modification"
47
+ )
48
+ model: str = Field(description="Model name")
49
+ reason: str = Field(description="Reason for modification")
50
+
51
+ def was_modified(self) -> bool:
52
+ """Check if max_tokens was modified."""
53
+ return self.original_max_tokens != self.new_max_tokens
@@ -0,0 +1,200 @@
1
+ """Max tokens plugin implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ccproxy.core.logging import get_plugin_logger
6
+ from ccproxy.core.plugins import (
7
+ PluginManifest,
8
+ SystemPluginFactory,
9
+ SystemPluginRuntime,
10
+ )
11
+ from ccproxy.core.plugins.hooks import HookRegistry
12
+
13
+ from .adapter import MaxTokensHook
14
+ from .config import MaxTokensConfig
15
+ from .service import TokenLimitsService
16
+
17
+
18
+ logger = get_plugin_logger(__name__)
19
+
20
+
21
+ class MaxTokensRuntime(SystemPluginRuntime):
22
+ """Runtime for max_tokens plugin."""
23
+
24
+ def __init__(self, manifest: PluginManifest):
25
+ """Initialize runtime."""
26
+ super().__init__(manifest)
27
+ self.config: MaxTokensConfig | None = None
28
+ self.service: TokenLimitsService | None = None
29
+ self.hook: MaxTokensHook | None = None
30
+ self.hook_registered = False
31
+
32
+ async def _on_initialize(self) -> None:
33
+ """Initialize the max tokens 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, MaxTokensConfig):
40
+ logger.debug("plugin_no_config_using_defaults", category="plugin")
41
+ # Use default config if none provided
42
+ self.config = MaxTokensConfig()
43
+ else:
44
+ self.config = config
45
+
46
+ logger.debug("initializing_max_tokens_plugin", enabled=self.config.enabled)
47
+
48
+ if not self.config.enabled:
49
+ logger.debug("max_tokens_plugin_disabled")
50
+ return
51
+
52
+ # Create and initialize service
53
+ self.service = TokenLimitsService(self.config)
54
+ self.service.initialize()
55
+
56
+ # Create hook instance
57
+ self.hook = MaxTokensHook(self.config, self.service)
58
+
59
+ # Attempt to register hook with available registry sources
60
+ hook_registry: HookRegistry | None = None
61
+
62
+ # 1. Directly from context (preferred when ServiceContainer wires it)
63
+ hook_registry = self.context.get("hook_registry")
64
+
65
+ # 2. Fallback to service container if available
66
+ if not hook_registry:
67
+ container = self.context.get("service_container")
68
+ if container:
69
+ try:
70
+ hook_registry = container.get_hook_registry()
71
+ except Exception as container_error: # pragma: no cover - defensive
72
+ logger.debug(
73
+ "max_tokens_hook_registry_from_container_failed",
74
+ error=str(container_error),
75
+ )
76
+
77
+ # 3. Fallback to app.state when running inside FastAPI app
78
+ if not hook_registry:
79
+ app = self.context.get("app")
80
+ if app and hasattr(app.state, "hook_registry"):
81
+ hook_registry = app.state.hook_registry
82
+
83
+ if hook_registry and isinstance(hook_registry, HookRegistry):
84
+ hook_registry.register(self.hook)
85
+ self.hook_registered = True
86
+ logger.debug(
87
+ "max_tokens_hook_registered",
88
+ providers="*"
89
+ if self.config.apply_to_all_providers
90
+ else self.config.target_providers,
91
+ fallback=self.config.fallback_max_tokens,
92
+ )
93
+ else:
94
+ logger.warning(
95
+ "max_tokens_hook_registry_unavailable",
96
+ message="max_tokens adjustments disabled",
97
+ )
98
+
99
+ logger.info(
100
+ "max_tokens_plugin_initialized",
101
+ target_providers=self.config.target_providers,
102
+ fallback_max_tokens=self.config.fallback_max_tokens,
103
+ )
104
+
105
+ async def _on_shutdown(self) -> None:
106
+ """Shutdown the plugin and cleanup resources."""
107
+ logger.debug("shutting_down_max_tokens_plugin")
108
+
109
+ # Unregister hook if we registered one
110
+ if self.hook:
111
+ hook_registry: HookRegistry | None = None
112
+ if self.context:
113
+ hook_registry = self.context.get("hook_registry")
114
+ if not hook_registry:
115
+ container = self.context.get("service_container")
116
+ if container:
117
+ try:
118
+ hook_registry = container.get_hook_registry()
119
+ except Exception as container_error: # pragma: no cover
120
+ logger.debug(
121
+ "max_tokens_hook_registry_from_container_failed_shutdown",
122
+ error=str(container_error),
123
+ )
124
+ if not hook_registry:
125
+ app = self.context.get("app")
126
+ if app and hasattr(app.state, "hook_registry"):
127
+ hook_registry = app.state.hook_registry
128
+
129
+ if hook_registry and isinstance(hook_registry, HookRegistry):
130
+ hook_registry.unregister(self.hook)
131
+ self.hook_registered = False
132
+ logger.debug("max_tokens_hook_unregistered")
133
+
134
+ self.hook = None
135
+
136
+ if self.service:
137
+ self.service = None
138
+
139
+ logger.debug("max_tokens_plugin_shutdown_complete")
140
+
141
+ async def _get_health_details(self) -> dict[str, Any]:
142
+ """Get health check details."""
143
+ try:
144
+ base_health = {
145
+ "type": "system",
146
+ "initialized": self.initialized,
147
+ "enabled": self.config.enabled if self.config else False,
148
+ "hook_registered": self.hook_registered,
149
+ }
150
+
151
+ if not self.config or not self.config.enabled:
152
+ return base_health
153
+
154
+ # Add service health info
155
+ health_details = base_health.copy()
156
+
157
+ if self.service:
158
+ health_details["models_count"] = len(
159
+ self.service.token_limits_data.models
160
+ )
161
+ health_details["fallback_max_tokens"] = self.config.fallback_max_tokens
162
+
163
+ return health_details
164
+
165
+ except Exception as e:
166
+ logger.error("health_check_failed", error=str(e))
167
+ return {
168
+ "type": "system",
169
+ "initialized": self.initialized,
170
+ "enabled": self.config.enabled if self.config else False,
171
+ "error": str(e),
172
+ "hook_registered": self.hook_registered,
173
+ }
174
+
175
+
176
+ class MaxTokensFactory(SystemPluginFactory):
177
+ """Factory for max_tokens plugin."""
178
+
179
+ def __init__(self) -> None:
180
+ """Initialize factory with manifest."""
181
+ # Create manifest - max_tokens logic is now integrated into HTTP adapter
182
+ manifest = PluginManifest(
183
+ name="max_tokens",
184
+ version="0.1.0",
185
+ description="Automatically sets max_tokens based on model limits when missing or invalid",
186
+ is_provider=False,
187
+ config_class=MaxTokensConfig,
188
+ provides=["max_tokens"], # This plugin provides the max_tokens service
189
+ )
190
+
191
+ # Initialize with manifest
192
+ super().__init__(manifest)
193
+
194
+ def create_runtime(self) -> MaxTokensRuntime:
195
+ """Create runtime instance."""
196
+ return MaxTokensRuntime(self.manifest)
197
+
198
+
199
+ # Export the factory instance
200
+ factory = MaxTokensFactory()