ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +434 -219
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +144 -168
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +388 -524
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +540 -19
  97. ccproxy/data/codex_headers_fallback.json +114 -7
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +61 -105
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +268 -276
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +68 -446
  387. ccproxy/utils/version_checker.py +273 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1251
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -243
  458. ccproxy/services/codex_detection_service.py +0 -252
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.7.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.7.dist-info/RECORD +0 -191
  473. ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,255 @@
1
+ """GitHub CLI detection service for Copilot plugin."""
2
+
3
+ import asyncio
4
+ import shutil
5
+ from datetime import datetime, timedelta
6
+ from typing import TYPE_CHECKING, Any, cast
7
+
8
+ from ccproxy.config.settings import Settings
9
+ from ccproxy.core.logging import get_plugin_logger
10
+
11
+ from .models import CopilotCacheData, CopilotCliInfo
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from ccproxy.services.cli_detection import CLIDetectionService
16
+
17
+
18
+ logger = get_plugin_logger()
19
+
20
+
21
+ class CopilotDetectionService:
22
+ """GitHub CLI detection and capability discovery service."""
23
+
24
+ def __init__(self, settings: Settings, cli_service: "CLIDetectionService"):
25
+ """Initialize detection service.
26
+
27
+ Args:
28
+ settings: Application settings
29
+ cli_service: Core CLI detection service
30
+ """
31
+ self.settings = settings
32
+ self._cli_service = cli_service
33
+ self._cache: CopilotCacheData | None = None
34
+ self._cache_ttl = timedelta(minutes=5) # Cache for 5 minutes
35
+
36
+ async def initialize_detection(self) -> CopilotCacheData:
37
+ """Initialize GitHub CLI detection and cache results.
38
+
39
+ Returns:
40
+ Cached detection data
41
+ """
42
+ if self._cache and not self._is_cache_expired():
43
+ logger.debug(
44
+ "using_cached_detection_data",
45
+ cache_age=(datetime.now() - self._cache.last_check).total_seconds(),
46
+ )
47
+ return self._cache
48
+
49
+ logger.debug("initializing_github_cli_detection")
50
+
51
+ # Check if GitHub CLI is available
52
+ cli_path = self.get_cli_path()
53
+ cli_available = cli_path is not None
54
+
55
+ cli_version = None
56
+ auth_status = None
57
+ username = None
58
+
59
+ if cli_available and cli_path:
60
+ try:
61
+ # Get CLI version
62
+ version_result = await asyncio.create_subprocess_exec(
63
+ *cli_path,
64
+ "--version",
65
+ stdout=asyncio.subprocess.PIPE,
66
+ stderr=asyncio.subprocess.PIPE,
67
+ )
68
+ stdout, stderr = await version_result.communicate()
69
+
70
+ if version_result.returncode == 0:
71
+ version_output = stdout.decode().strip()
72
+ # Parse version from "gh version 2.x.x" format
73
+ for line in version_output.split("\n"):
74
+ if line.startswith("gh version"):
75
+ cli_version = (
76
+ line.split()[2] if len(line.split()) >= 3 else None
77
+ )
78
+ break
79
+
80
+ # Check authentication status
81
+ auth_result = await asyncio.create_subprocess_exec(
82
+ *cli_path,
83
+ "auth",
84
+ "status",
85
+ stdout=asyncio.subprocess.PIPE,
86
+ stderr=asyncio.subprocess.PIPE,
87
+ )
88
+ stdout, stderr = await auth_result.communicate()
89
+
90
+ if auth_result.returncode == 0:
91
+ auth_status = "authenticated"
92
+ auth_output = (
93
+ stderr.decode() + stdout.decode()
94
+ ) # gh auth status uses stderr
95
+
96
+ # Extract username from output
97
+ for line in auth_output.split("\n"):
98
+ if "Logged in to github.com as" in line:
99
+ parts = line.split()
100
+ if len(parts) >= 6:
101
+ username = parts[5].strip()
102
+ break
103
+ else:
104
+ auth_status = "not_authenticated"
105
+
106
+ except Exception as e:
107
+ logger.warning(
108
+ "github_cli_check_failed",
109
+ error=str(e),
110
+ exc_info=e,
111
+ )
112
+ auth_status = "check_failed"
113
+
114
+ # Update cache
115
+ self._cache = CopilotCacheData(
116
+ cli_available=cli_available,
117
+ cli_version=cli_version,
118
+ auth_status=auth_status,
119
+ username=username,
120
+ last_check=datetime.now(),
121
+ )
122
+
123
+ logger.debug(
124
+ "github_cli_detection_completed",
125
+ cli_available=cli_available,
126
+ cli_version=cli_version,
127
+ auth_status=auth_status,
128
+ username=username,
129
+ )
130
+
131
+ return self._cache
132
+
133
+ def get_cli_path(self) -> list[str] | None:
134
+ """Get GitHub CLI command path.
135
+
136
+ Returns:
137
+ CLI command as list of strings, or None if not available
138
+ """
139
+ # Try to find GitHub CLI
140
+ cli_binary = shutil.which("gh")
141
+ if cli_binary:
142
+ return [cli_binary]
143
+
144
+ logger.debug("github_cli_not_found")
145
+ return None
146
+
147
+ def get_cli_health_info(self) -> CopilotCliInfo:
148
+ """Get GitHub CLI health information.
149
+
150
+ Returns:
151
+ CLI health information
152
+ """
153
+ if not self._cache:
154
+ return CopilotCliInfo(
155
+ available=False,
156
+ version=None,
157
+ authenticated=False,
158
+ username=None,
159
+ error="Detection not initialized - call initialize_detection() first",
160
+ )
161
+
162
+ return CopilotCliInfo(
163
+ available=self._cache.cli_available,
164
+ version=self._cache.cli_version,
165
+ authenticated=self._cache.auth_status == "authenticated",
166
+ username=self._cache.username,
167
+ error=None if self._cache.cli_available else "GitHub CLI not found in PATH",
168
+ )
169
+
170
+ def _is_cache_expired(self) -> bool:
171
+ """Check if detection cache has expired.
172
+
173
+ Returns:
174
+ True if cache is expired
175
+ """
176
+ if not self._cache:
177
+ return True
178
+
179
+ return datetime.now() - self._cache.last_check > self._cache_ttl
180
+
181
+ async def refresh_cache(self) -> CopilotCacheData:
182
+ """Force refresh of detection cache.
183
+
184
+ Returns:
185
+ Fresh detection data
186
+ """
187
+ logger.debug("forcing_detection_cache_refresh")
188
+ self._cache = None
189
+ return await self.initialize_detection()
190
+
191
+ def get_recommended_headers(self) -> dict[str, str]:
192
+ """Get recommended headers for Copilot API requests.
193
+
194
+ Returns:
195
+ Dictionary of headers
196
+ """
197
+ headers = {
198
+ "Content-Type": "application/json",
199
+ "Copilot-Integration-Id": "vscode-chat",
200
+ "Editor-Version": "vscode/1.85.0",
201
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
202
+ "User-Agent": "GitHubCopilotChat/0.26.7",
203
+ "X-GitHub-Api-Version": "2025-04-01",
204
+ }
205
+
206
+ # Add CLI version if available
207
+ if self._cache and self._cache.cli_version:
208
+ headers["X-GitHub-CLI-Version"] = self._cache.cli_version
209
+
210
+ return headers
211
+
212
+ async def validate_environment(self) -> dict[str, Any]:
213
+ """Validate the environment for Copilot usage.
214
+
215
+ Returns:
216
+ Validation results with status and details
217
+ """
218
+ await self.initialize_detection()
219
+
220
+ validation = {
221
+ "status": "healthy",
222
+ "details": {
223
+ "github_cli": {
224
+ "available": self._cache.cli_available if self._cache else False,
225
+ "version": self._cache.cli_version if self._cache else None,
226
+ "authenticated": (
227
+ self._cache.auth_status == "authenticated"
228
+ if self._cache
229
+ else False
230
+ ),
231
+ "username": self._cache.username if self._cache else None,
232
+ },
233
+ "last_check": self._cache.last_check.isoformat()
234
+ if self._cache
235
+ else None,
236
+ },
237
+ }
238
+
239
+ # Determine overall health
240
+ issues: list[str] = []
241
+ details = cast(dict[str, Any], validation["details"])
242
+ github_cli = cast(dict[str, Any], details["github_cli"])
243
+
244
+ if not github_cli["available"]:
245
+ issues.append("GitHub CLI not available")
246
+ if not github_cli["authenticated"]:
247
+ issues.append("GitHub CLI not authenticated")
248
+ if not details["copilot_access"]:
249
+ issues.append("No Copilot access detected")
250
+
251
+ if issues:
252
+ validation["status"] = "unhealthy"
253
+ validation["issues"] = issues
254
+
255
+ return validation
@@ -0,0 +1,275 @@
1
+ """Copilot token manager implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from time import time
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from ccproxy.auth.managers.base import BaseTokenManager
12
+ from ccproxy.auth.managers.token_snapshot import TokenSnapshot
13
+ from ccproxy.auth.oauth.protocol import StandardProfileFields
14
+ from ccproxy.auth.storage.base import TokenStorage
15
+ from ccproxy.core.logging import get_plugin_logger
16
+
17
+ from .config import CopilotOAuthConfig
18
+ from .oauth.client import CopilotOAuthClient
19
+ from .oauth.models import CopilotCredentials
20
+ from .oauth.storage import CopilotOAuthStorage
21
+
22
+
23
+ logger = get_plugin_logger()
24
+
25
+
26
+ class CopilotTokenManager(BaseTokenManager[CopilotCredentials]):
27
+ """Manager for GitHub Copilot credential lifecycle."""
28
+
29
+ def __init__(
30
+ self,
31
+ storage: TokenStorage[CopilotCredentials] | None = None,
32
+ *,
33
+ config: CopilotOAuthConfig | None = None,
34
+ http_client: httpx.AsyncClient | None = None,
35
+ hook_manager: Any | None = None,
36
+ detection_service: Any | None = None,
37
+ ) -> None:
38
+ storage = storage or CopilotOAuthStorage()
39
+ super().__init__(storage)
40
+ self.config = config or CopilotOAuthConfig()
41
+ self._client = CopilotOAuthClient(
42
+ self.config,
43
+ storage
44
+ if isinstance(storage, CopilotOAuthStorage)
45
+ else CopilotOAuthStorage(),
46
+ http_client=http_client,
47
+ hook_manager=hook_manager,
48
+ detection_service=detection_service,
49
+ )
50
+ self._profile_cache: StandardProfileFields | None = None
51
+
52
+ @classmethod
53
+ async def create(
54
+ cls,
55
+ storage: TokenStorage[CopilotCredentials] | None = None,
56
+ *,
57
+ config: CopilotOAuthConfig | None = None,
58
+ http_client: httpx.AsyncClient | None = None,
59
+ hook_manager: Any | None = None,
60
+ detection_service: Any | None = None,
61
+ ) -> CopilotTokenManager:
62
+ """Async factory for parity with other managers."""
63
+ return cls(
64
+ storage=storage,
65
+ config=config,
66
+ http_client=http_client,
67
+ hook_manager=hook_manager,
68
+ detection_service=detection_service,
69
+ )
70
+
71
+ def _build_token_snapshot(self, credentials: CopilotCredentials) -> TokenSnapshot:
72
+ """Construct a token snapshot for Copilot credentials."""
73
+ access_token: str | None = None
74
+ copilot_token = credentials.copilot_token
75
+ if copilot_token and copilot_token.token:
76
+ access_token = copilot_token.token.get_secret_value()
77
+
78
+ refresh_token: str | None = None
79
+ oauth_token = credentials.oauth_token
80
+ if oauth_token.refresh_token:
81
+ refresh_token = oauth_token.refresh_token.get_secret_value()
82
+
83
+ expires_at = None
84
+ if copilot_token and copilot_token.expires_at:
85
+ expires_at = copilot_token.expires_at
86
+ else:
87
+ if oauth_token.expires_in and oauth_token.created_at:
88
+ expires_at = oauth_token.expires_at_datetime
89
+
90
+ scope_value = oauth_token.scope or ""
91
+ scopes = tuple(
92
+ scope
93
+ for scope in (item.strip() for item in scope_value.split(" "))
94
+ if scope
95
+ )
96
+
97
+ extras = {
98
+ "account_type": credentials.account_type,
99
+ "has_copilot_token": bool(credentials.copilot_token),
100
+ }
101
+
102
+ logger.debug(
103
+ "copilot_token_snapshot",
104
+ scopes=scopes,
105
+ expires_at=expires_at,
106
+ credentials=credentials,
107
+ access_token=access_token,
108
+ refresh_token=refresh_token,
109
+ )
110
+ return TokenSnapshot(
111
+ provider="copilot",
112
+ access_token=access_token,
113
+ refresh_token=refresh_token,
114
+ expires_at=expires_at,
115
+ scopes=scopes,
116
+ extras=extras,
117
+ )
118
+
119
+ # ==================================================================
120
+ # BaseTokenManager protocol implementations
121
+ # ==================================================================
122
+
123
+ async def refresh_token(self) -> CopilotCredentials | None:
124
+ credentials = await self.load_credentials()
125
+ if not credentials:
126
+ logger.error("copilot_refresh_no_credentials", category="auth")
127
+ return None
128
+
129
+ try:
130
+ refreshed = await self._client.refresh_copilot_token(credentials)
131
+ # Client already persisted credentials; refresh in-memory caches.
132
+ self._credentials_cache = refreshed
133
+ self._credentials_loaded_at = time()
134
+ self._auth_cache.clear()
135
+ self._profile_cache = None
136
+ return refreshed
137
+ except Exception as exc: # pragma: no cover - defensive logging
138
+ logger.error(
139
+ "copilot_refresh_failed",
140
+ error=str(exc),
141
+ exc_info=exc,
142
+ category="auth",
143
+ )
144
+ return None
145
+
146
+ def is_expired(self, credentials: CopilotCredentials) -> bool:
147
+ token = credentials.copilot_token
148
+ if not token:
149
+ return True
150
+
151
+ now = datetime.now(UTC)
152
+ if token.expires_at and now >= token.expires_at:
153
+ return True
154
+
155
+ refresh_deadline = self._compute_refresh_deadline(credentials)
156
+ if refresh_deadline and now >= refresh_deadline:
157
+ return True
158
+
159
+ return credentials.oauth_token.is_expired
160
+
161
+ def get_account_id(self, credentials: CopilotCredentials) -> str | None:
162
+ # GitHub account information is part of profile, not raw credentials.
163
+ return None
164
+
165
+ def get_expiration_time(self, credentials: CopilotCredentials) -> datetime | None:
166
+ candidates: list[datetime] = []
167
+
168
+ token = credentials.copilot_token
169
+ if token:
170
+ if token.expires_at:
171
+ candidates.append(token.expires_at)
172
+
173
+ refresh_deadline = self._compute_refresh_deadline(credentials)
174
+ if refresh_deadline:
175
+ candidates.append(refresh_deadline)
176
+
177
+ if credentials.oauth_token.expires_in and credentials.oauth_token.created_at:
178
+ candidates.append(credentials.oauth_token.expires_at_datetime)
179
+
180
+ if not candidates:
181
+ return None
182
+
183
+ return min(candidates)
184
+
185
+ # ==================================================================
186
+ # Token access helpers used by adapters/routes
187
+ # ==================================================================
188
+
189
+ async def ensure_copilot_token(self) -> str:
190
+ credentials = await self.load_credentials()
191
+ if not credentials:
192
+ raise ValueError("No Copilot credentials available")
193
+
194
+ if credentials.oauth_token.is_expired:
195
+ raise ValueError("OAuth token expired; re-authentication required")
196
+
197
+ if not credentials.copilot_token or credentials.copilot_token.is_expired:
198
+ logger.debug("copilot_token_refresh_needed", category="auth")
199
+ credentials = await self._client.refresh_copilot_token(credentials)
200
+ self._credentials_cache = credentials
201
+ self._credentials_loaded_at = time()
202
+ self._auth_cache.clear()
203
+ self._profile_cache = None
204
+
205
+ token = credentials.copilot_token
206
+ if not token:
207
+ raise ValueError("Unable to obtain Copilot service token")
208
+ return token.token.get_secret_value()
209
+
210
+ async def ensure_oauth_token(self) -> str:
211
+ credentials = await self.load_credentials()
212
+ if not credentials:
213
+ raise ValueError("No Copilot credentials available")
214
+ if credentials.oauth_token.is_expired:
215
+ raise ValueError("OAuth token expired; re-authentication required")
216
+ return credentials.oauth_token.access_token.get_secret_value()
217
+
218
+ async def get_access_token(self) -> str | None:
219
+ try:
220
+ return await self.ensure_copilot_token()
221
+ except Exception as exc:
222
+ logger.error(
223
+ "copilot_access_token_failed",
224
+ error=str(exc),
225
+ category="auth",
226
+ )
227
+ return None
228
+
229
+ async def get_access_token_with_refresh(self) -> str | None:
230
+ return await self.get_access_token()
231
+
232
+ async def get_profile(self) -> StandardProfileFields | None:
233
+ if self._profile_cache:
234
+ return self._profile_cache
235
+ credentials = await self.load_credentials()
236
+ if not credentials:
237
+ return None
238
+ try:
239
+ profile = await self._client.get_standard_profile(credentials.oauth_token)
240
+ except Exception as exc: # pragma: no cover - defensive logging
241
+ logger.debug("copilot_profile_fetch_failed", error=str(exc))
242
+ return None
243
+ self._profile_cache = profile
244
+ return profile
245
+
246
+ async def get_profile_quick(self) -> StandardProfileFields | None:
247
+ return await self.get_profile()
248
+
249
+ async def aclose(self) -> None:
250
+ await self._client.close()
251
+
252
+ def _compute_refresh_deadline(
253
+ self, credentials: CopilotCredentials
254
+ ) -> datetime | None:
255
+ token = credentials.copilot_token
256
+ if not token or not token.refresh_in:
257
+ return None
258
+
259
+ try:
260
+ updated_at = int(credentials.updated_at)
261
+ except (TypeError, ValueError):
262
+ return None
263
+
264
+ try:
265
+ refresh_in = int(token.refresh_in)
266
+ except (TypeError, ValueError):
267
+ return None
268
+
269
+ if refresh_in <= 0:
270
+ return datetime.now(UTC)
271
+
272
+ return datetime.fromtimestamp(updated_at + refresh_in, tz=UTC)
273
+
274
+
275
+ __all__ = ["CopilotTokenManager"]