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,602 @@
1
+ """OAuth provider implementation for GitHub Copilot."""
2
+
3
+ import contextlib
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ import httpx
7
+
8
+ from ccproxy.auth.managers.token_snapshot import TokenSnapshot
9
+ from ccproxy.auth.oauth.protocol import ProfileLoggingMixin, StandardProfileFields
10
+ from ccproxy.auth.oauth.registry import CliAuthConfig, FlowType, OAuthProviderInfo
11
+ from ccproxy.core.logging import get_plugin_logger
12
+
13
+ from ..config import CopilotOAuthConfig
14
+ from .client import CopilotOAuthClient
15
+ from .models import (
16
+ CopilotCredentials,
17
+ CopilotOAuthToken,
18
+ CopilotTokenInfo,
19
+ CopilotTokenResponse,
20
+ )
21
+ from .storage import CopilotOAuthStorage
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from ccproxy.services.cli_detection import CLIDetectionService
26
+
27
+ from ..manager import CopilotTokenManager
28
+
29
+
30
+ logger = get_plugin_logger()
31
+
32
+
33
+ class CopilotOAuthProvider(ProfileLoggingMixin):
34
+ """GitHub Copilot OAuth provider implementation."""
35
+
36
+ def __init__(
37
+ self,
38
+ config: CopilotOAuthConfig | None = None,
39
+ storage: CopilotOAuthStorage | None = None,
40
+ http_client: httpx.AsyncClient | None = None,
41
+ hook_manager: Any | None = None,
42
+ detection_service: "CLIDetectionService | None" = None,
43
+ ):
44
+ """Initialize Copilot OAuth provider.
45
+
46
+ Args:
47
+ config: OAuth configuration
48
+ storage: Token storage
49
+ http_client: Optional HTTP client for request tracing
50
+ hook_manager: Optional hook manager for events
51
+ detection_service: Optional CLI detection service
52
+ """
53
+ self.config = config or CopilotOAuthConfig()
54
+ self.storage = storage or CopilotOAuthStorage()
55
+ self.hook_manager = hook_manager
56
+ self.detection_service = detection_service
57
+ self.http_client = http_client
58
+ self._cached_profile: StandardProfileFields | None = None
59
+
60
+ self.client = CopilotOAuthClient(
61
+ self.config,
62
+ self.storage,
63
+ http_client,
64
+ hook_manager=hook_manager,
65
+ detection_service=detection_service,
66
+ )
67
+
68
+ @property
69
+ def provider_name(self) -> str:
70
+ """Internal provider name."""
71
+ return "copilot"
72
+
73
+ @property
74
+ def provider_display_name(self) -> str:
75
+ """Display name for UI."""
76
+ return "GitHub Copilot"
77
+
78
+ @property
79
+ def supports_pkce(self) -> bool:
80
+ """Whether this provider supports PKCE."""
81
+ return self.config.use_pkce
82
+
83
+ @property
84
+ def supports_refresh(self) -> bool:
85
+ """Whether this provider supports token refresh."""
86
+ return True
87
+
88
+ @property
89
+ def requires_client_secret(self) -> bool:
90
+ """Whether this provider requires a client secret."""
91
+ return False # GitHub Device Code Flow doesn't require client secret
92
+
93
+ async def get_authorization_url(
94
+ self,
95
+ state: str,
96
+ code_verifier: str | None = None,
97
+ redirect_uri: str | None = None,
98
+ ) -> str:
99
+ """Get the authorization URL for GitHub Device Code Flow.
100
+
101
+ For device code flow, this returns the device authorization endpoint.
102
+ The actual user verification happens at the verification_uri returned
103
+ by start_device_flow().
104
+
105
+ Args:
106
+ state: OAuth state parameter (not used in device flow)
107
+ code_verifier: PKCE code verifier (not used in device flow)
108
+
109
+ Returns:
110
+ Device authorization URL
111
+ """
112
+ # For device code flow, we return the device authorization endpoint
113
+ # The actual flow is handled by the device flow methods
114
+ return self.config.authorize_url
115
+
116
+ async def start_device_flow(self) -> tuple[str, str, str, int]:
117
+ """Start the GitHub device code authorization flow.
118
+
119
+ Returns:
120
+ Tuple of (device_code, user_code, verification_uri, expires_in)
121
+ """
122
+ device_response = await self.client.start_device_flow()
123
+
124
+ logger.info(
125
+ "device_flow_started",
126
+ user_code=device_response.user_code,
127
+ verification_uri=device_response.verification_uri,
128
+ expires_in=device_response.expires_in,
129
+ )
130
+
131
+ return (
132
+ device_response.device_code,
133
+ device_response.user_code,
134
+ device_response.verification_uri,
135
+ device_response.expires_in,
136
+ )
137
+
138
+ async def complete_device_flow(
139
+ self, device_code: str, interval: int = 5, expires_in: int = 900
140
+ ) -> CopilotCredentials:
141
+ """Complete the device flow authorization.
142
+
143
+ Args:
144
+ device_code: Device code from start_device_flow
145
+ interval: Polling interval in seconds
146
+ expires_in: Code expiration time in seconds
147
+
148
+ Returns:
149
+ Complete Copilot credentials
150
+ """
151
+ return await self.client.complete_authorization(
152
+ device_code, interval, expires_in
153
+ )
154
+
155
+ async def handle_callback(
156
+ self,
157
+ code: str,
158
+ state: str,
159
+ code_verifier: str | None = None,
160
+ redirect_uri: str | None = None,
161
+ ) -> Any:
162
+ """Handle OAuth callback (not used in device flow).
163
+
164
+ This method is required by the CLI flow protocol but not used for
165
+ device code flow. Use complete_device_flow instead.
166
+
167
+ Args:
168
+ code: Authorization code from OAuth callback
169
+ state: State parameter for validation
170
+ code_verifier: PKCE code verifier (if PKCE is used)
171
+ redirect_uri: Redirect URI used in authorization (optional)
172
+ """
173
+ raise NotImplementedError(
174
+ "Copilot uses device code flow. Browser callback is not supported."
175
+ )
176
+
177
+ async def exchange_code(
178
+ self, code: str, state: str, code_verifier: str | None = None
179
+ ) -> dict[str, Any]:
180
+ """Exchange authorization code for token (not used in device flow).
181
+
182
+ This method is required by the OAuth protocol but not used for
183
+ device code flow. Use complete_device_flow instead.
184
+ """
185
+ raise NotImplementedError(
186
+ "Device code flow doesn't use authorization code exchange. "
187
+ "Use complete_device_flow instead."
188
+ )
189
+
190
+ async def refresh_token(self, refresh_token: str) -> dict[str, Any]:
191
+ """Refresh access token using refresh token.
192
+
193
+ For Copilot, this refreshes the Copilot service token using the
194
+ stored OAuth token.
195
+
196
+ Args:
197
+ refresh_token: Not used for Copilot (uses OAuth token instead)
198
+
199
+ Returns:
200
+ Token information
201
+ """
202
+ credentials = await self.storage.load_credentials()
203
+ if not credentials:
204
+ raise ValueError("No credentials found for refresh")
205
+
206
+ refreshed_credentials = await self.client.refresh_copilot_token(credentials)
207
+
208
+ # Return token info in standard format
209
+ if refreshed_credentials.copilot_token is not None:
210
+ return {
211
+ "access_token": refreshed_credentials.copilot_token.token.get_secret_value(),
212
+ "token_type": "bearer",
213
+ "expires_at": refreshed_credentials.copilot_token.expires_at,
214
+ "provider": self.provider_name,
215
+ }
216
+ else:
217
+ raise ValueError("Failed to refresh Copilot token")
218
+
219
+ async def get_user_profile(
220
+ self, access_token: str | None = None
221
+ ) -> StandardProfileFields:
222
+ """Get user profile information.
223
+
224
+ Args:
225
+ access_token: Optional OAuth access token (not Copilot token)
226
+
227
+ Returns:
228
+ User profile information
229
+ """
230
+ oauth_token: CopilotOAuthToken | None = None
231
+
232
+ if access_token:
233
+ from pydantic import SecretStr
234
+
235
+ oauth_token = CopilotOAuthToken(
236
+ access_token=SecretStr(access_token), expires_in=None, created_at=None
237
+ )
238
+ else:
239
+ credentials = await self.storage.load_credentials()
240
+ if not credentials:
241
+ raise ValueError("No credentials found")
242
+ oauth_token = credentials.oauth_token
243
+
244
+ profile = await self.client.get_standard_profile(oauth_token)
245
+ self._cached_profile = profile
246
+ return profile
247
+
248
+ async def get_standard_profile(
249
+ self, credentials: Any | None = None
250
+ ) -> StandardProfileFields | None:
251
+ """Get standardized profile information from credentials.
252
+
253
+ Args:
254
+ credentials: Copilot credentials object (optional)
255
+
256
+ Returns:
257
+ Standardized profile fields or None if not available
258
+ """
259
+ try:
260
+ # If credentials is None, try to load from storage
261
+ if credentials is None:
262
+ try:
263
+ credentials = await self.storage.load_credentials()
264
+ if not credentials:
265
+ return None
266
+ except Exception:
267
+ return None
268
+
269
+ # If credentials has OAuth token, use it directly
270
+ if hasattr(credentials, "oauth_token") and credentials.oauth_token:
271
+ return await self.client.get_standard_profile(credentials.oauth_token)
272
+ else:
273
+ # Fallback to loading from storage
274
+ return await self.get_user_profile()
275
+ except Exception as e:
276
+ logger.debug(
277
+ "get_standard_profile_failed",
278
+ error=str(e),
279
+ exc_info=e,
280
+ )
281
+ # Return fallback profile using _extract_standard_profile if we have credentials
282
+ if credentials is not None:
283
+ return self._extract_standard_profile(credentials)
284
+ return None
285
+
286
+ async def get_copilot_token_data(self) -> CopilotTokenResponse | None:
287
+ credentials = await self.storage.load_credentials()
288
+ if not credentials:
289
+ return None
290
+
291
+ return credentials.copilot_token
292
+
293
+ async def get_token_info(self) -> CopilotTokenInfo | None:
294
+ """Get current token information.
295
+
296
+ Returns:
297
+ Token information if available
298
+ """
299
+ credentials = await self.storage.load_credentials()
300
+ if not credentials:
301
+ return None
302
+
303
+ oauth_expires_at = credentials.oauth_token.expires_at_datetime
304
+ copilot_expires_at = None
305
+
306
+ if credentials.copilot_token and credentials.copilot_token.expires_at:
307
+ # expires_at is now a datetime object, no need to parse
308
+ copilot_expires_at = credentials.copilot_token.expires_at
309
+
310
+ # Get profile for additional info
311
+ profile = None
312
+ with contextlib.suppress(Exception):
313
+ profile = await self.get_user_profile()
314
+
315
+ copilot_access = False
316
+ if profile is not None:
317
+ features = getattr(profile, "features", {}) or {}
318
+ copilot_access = bool(features.get("copilot_access"))
319
+ if not copilot_access and getattr(profile, "subscription_type", None):
320
+ copilot_access = True
321
+
322
+ if not copilot_access and credentials.copilot_token is not None:
323
+ token = credentials.copilot_token
324
+ indicative_flags = [
325
+ getattr(token, "chat_enabled", None),
326
+ getattr(token, "annotations_enabled", None),
327
+ getattr(token, "individual", None),
328
+ ]
329
+ if any(flag is True for flag in indicative_flags if flag is not None):
330
+ copilot_access = True
331
+ else:
332
+ copilot_access = (
333
+ True # Possession of a copilot token implies active access
334
+ )
335
+
336
+ if not copilot_access:
337
+ copilot_access = credentials.copilot_token is not None
338
+
339
+ return CopilotTokenInfo(
340
+ provider="copilot",
341
+ oauth_expires_at=oauth_expires_at,
342
+ copilot_expires_at=copilot_expires_at,
343
+ account_type=credentials.account_type,
344
+ copilot_access=copilot_access,
345
+ )
346
+
347
+ async def get_token_snapshot(self) -> TokenSnapshot | None:
348
+ """Return a token snapshot built from stored credentials."""
349
+
350
+ try:
351
+ manager = await self.create_token_manager(storage=self.storage)
352
+ snapshot = await manager.get_token_snapshot()
353
+ if snapshot:
354
+ return snapshot
355
+ except Exception as exc: # pragma: no cover - defensive logging
356
+ logger.debug("copilot_snapshot_via_manager_failed", error=str(exc))
357
+
358
+ try:
359
+ credentials = await self.storage.load_credentials()
360
+ if not credentials:
361
+ return None
362
+
363
+ from ..manager import CopilotTokenManager
364
+
365
+ temp_manager = CopilotTokenManager(storage=self.storage)
366
+ return temp_manager._build_token_snapshot(credentials)
367
+ except Exception as exc: # pragma: no cover - defensive logging
368
+ logger.debug("copilot_snapshot_from_credentials_failed", error=str(exc))
369
+ return None
370
+
371
+ async def is_authenticated(self) -> bool:
372
+ """Check if user is authenticated with valid tokens.
373
+
374
+ Returns:
375
+ True if authenticated with valid tokens
376
+ """
377
+ credentials = await self.storage.load_credentials()
378
+ if not credentials:
379
+ return False
380
+
381
+ # Check if OAuth token is expired
382
+ if credentials.oauth_token.is_expired:
383
+ return False
384
+
385
+ # Check if we have a valid (non-expired) Copilot token
386
+ if not credentials.copilot_token:
387
+ return False
388
+
389
+ # Check if Copilot token is expired
390
+ return not credentials.copilot_token.is_expired
391
+
392
+ async def get_copilot_token(self) -> str | None:
393
+ """Get current Copilot service token for API requests.
394
+
395
+ Returns:
396
+ Copilot token if available and valid, None otherwise
397
+ """
398
+ credentials = await self.storage.load_credentials()
399
+ if not credentials or not credentials.copilot_token:
400
+ return None
401
+
402
+ # Check if token is expired
403
+ if credentials.copilot_token.is_expired:
404
+ logger.info(
405
+ "copilot_token_expired_in_get",
406
+ expires_at=credentials.copilot_token.expires_at,
407
+ )
408
+ return None
409
+
410
+ return credentials.copilot_token.token.get_secret_value()
411
+
412
+ async def ensure_oauth_token(self) -> str:
413
+ """Ensure we have a valid OAuth token.
414
+
415
+ Returns:
416
+ Valid OAuth token
417
+
418
+ Raises:
419
+ ValueError: If unable to get valid token
420
+ """
421
+ credentials = await self.storage.load_credentials()
422
+ if not credentials:
423
+ raise ValueError("No credentials found - authorization required")
424
+
425
+ if credentials.oauth_token.is_expired:
426
+ raise ValueError("OAuth token expired - re-authorization required")
427
+
428
+ return credentials.oauth_token.access_token.get_secret_value()
429
+
430
+ async def logout(self) -> None:
431
+ """Clear stored credentials."""
432
+ await self.storage.clear_credentials()
433
+
434
+ def get_storage(self) -> Any:
435
+ """Get storage implementation for this provider.
436
+
437
+ Returns:
438
+ Storage implementation
439
+ """
440
+ return self.storage
441
+
442
+ async def load_credentials(self, custom_path: Any | None = None) -> Any | None:
443
+ """Load credentials from provider's storage.
444
+
445
+ Args:
446
+ custom_path: Optional custom storage path (Path object)
447
+
448
+ Returns:
449
+ Credentials if found, None otherwise
450
+ """
451
+ try:
452
+ if custom_path:
453
+ # Create storage with custom path
454
+ from pathlib import Path
455
+
456
+ from .storage import CopilotOAuthStorage
457
+
458
+ storage = CopilotOAuthStorage(credentials_path=Path(custom_path))
459
+ credentials = await storage.load_credentials()
460
+ else:
461
+ # Load from default storage
462
+ credentials = await self.storage.load_credentials()
463
+
464
+ # Use standardized profile logging
465
+ self._log_credentials_loaded("copilot", credentials)
466
+
467
+ return credentials
468
+ except Exception as e:
469
+ logger.debug(
470
+ "copilot_load_credentials_failed",
471
+ error=str(e),
472
+ exc_info=e,
473
+ )
474
+ return None
475
+
476
+ async def save_credentials(self, credentials: CopilotCredentials | None) -> bool:
477
+ """Save credentials to storage.
478
+
479
+ Args:
480
+ credentials: Copilot credentials to save (None to clear)
481
+
482
+ Returns:
483
+ True if save was successful
484
+ """
485
+ try:
486
+ if credentials is None:
487
+ await self.storage.clear_credentials()
488
+ logger.info("copilot_credentials_cleared")
489
+ return True
490
+ else:
491
+ await self.storage.save_credentials(credentials)
492
+ logger.info(
493
+ "copilot_credentials_saved",
494
+ account_type=credentials.account_type,
495
+ has_oauth=bool(credentials.oauth_token),
496
+ has_copilot_token=bool(credentials.copilot_token),
497
+ )
498
+ return True
499
+ except Exception as e:
500
+ logger.error(
501
+ "copilot_credentials_save_failed",
502
+ error=str(e),
503
+ exc_info=e,
504
+ )
505
+ return False
506
+
507
+ async def create_token_manager(
508
+ self, storage: Any | None = None
509
+ ) -> "CopilotTokenManager":
510
+ """Create a token manager instance wired to this provider's context."""
511
+
512
+ from ..manager import CopilotTokenManager
513
+
514
+ return await CopilotTokenManager.create(
515
+ storage=storage or self.storage,
516
+ config=self.config,
517
+ http_client=self.http_client,
518
+ hook_manager=self.hook_manager,
519
+ detection_service=self.detection_service,
520
+ )
521
+
522
+ def _extract_standard_profile(self, credentials: Any) -> StandardProfileFields:
523
+ """Extract standardized profile fields from Copilot credentials."""
524
+ from .models import CopilotCredentials, CopilotProfileInfo
525
+
526
+ if isinstance(credentials, CopilotProfileInfo):
527
+ return StandardProfileFields(
528
+ account_id=credentials.account_id,
529
+ provider_type="copilot",
530
+ email=credentials.email,
531
+ display_name=credentials.name or credentials.login,
532
+ )
533
+ elif isinstance(credentials, CopilotCredentials):
534
+ # Fallback for when we only have credentials without profile
535
+ return StandardProfileFields(
536
+ account_id="unknown",
537
+ provider_type="copilot",
538
+ email=None,
539
+ display_name="GitHub Copilot User",
540
+ )
541
+ else:
542
+ return StandardProfileFields(
543
+ account_id="unknown",
544
+ provider_type="copilot",
545
+ email=None,
546
+ display_name="Unknown User",
547
+ )
548
+
549
+ async def cleanup(self) -> None:
550
+ """Cleanup resources."""
551
+ try:
552
+ await self.client.close()
553
+ except Exception as e:
554
+ logger.error(
555
+ "provider_cleanup_failed",
556
+ error=str(e),
557
+ exc_info=e,
558
+ )
559
+
560
+ # OAuthProviderInfo protocol implementation
561
+
562
+ @property
563
+ def cli(self) -> CliAuthConfig:
564
+ """Get CLI authentication configuration for this provider."""
565
+ return CliAuthConfig(
566
+ preferred_flow=FlowType.device,
567
+ callback_port=8080,
568
+ callback_path="/callback",
569
+ supports_manual_code=False,
570
+ supports_device_flow=True,
571
+ fixed_redirect_uri=None,
572
+ )
573
+
574
+ def get_provider_info(self) -> OAuthProviderInfo:
575
+ """Get provider information for registry."""
576
+ return OAuthProviderInfo(
577
+ name=self.provider_name,
578
+ display_name=self.provider_display_name,
579
+ description="GitHub Copilot OAuth authentication",
580
+ supports_pkce=self.supports_pkce,
581
+ scopes=["read:user", "copilot"],
582
+ is_available=True,
583
+ plugin_name="copilot",
584
+ )
585
+
586
+ async def exchange_manual_code(self, code: str) -> Any:
587
+ """Exchange manual authorization code for tokens.
588
+
589
+ Note: Copilot primarily uses device code flow, but this method
590
+ is provided for completeness.
591
+
592
+ Args:
593
+ code: Authorization code from manual entry
594
+
595
+ Returns:
596
+ Copilot credentials object
597
+ """
598
+ # Copilot doesn't typically support manual code entry as it uses device flow
599
+ # This is a placeholder implementation
600
+ raise NotImplementedError(
601
+ "Copilot uses device code flow. Manual code entry is not supported."
602
+ )