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,465 @@
1
+ import json
2
+ import time
3
+ import uuid
4
+ from typing import Any, cast
5
+
6
+ import httpx
7
+ from starlette.requests import Request
8
+ from starlette.responses import Response
9
+
10
+ from ccproxy.core.logging import get_plugin_logger
11
+ from ccproxy.llms.models.openai import ResponseObject
12
+ from ccproxy.services.adapters.http_adapter import BaseHTTPAdapter
13
+ from ccproxy.utils.headers import (
14
+ extract_request_headers,
15
+ extract_response_headers,
16
+ filter_request_headers,
17
+ filter_response_headers,
18
+ )
19
+
20
+ from .config import CopilotConfig
21
+ from .detection_service import CopilotDetectionService
22
+ from .manager import CopilotTokenManager
23
+ from .oauth.provider import CopilotOAuthProvider
24
+
25
+
26
+ logger = get_plugin_logger()
27
+
28
+
29
+ class CopilotAdapter(BaseHTTPAdapter):
30
+ """Simplified Copilot adapter."""
31
+
32
+ def __init__(
33
+ self,
34
+ config: CopilotConfig,
35
+ auth_manager: CopilotTokenManager | None,
36
+ detection_service: CopilotDetectionService,
37
+ http_pool_manager: Any,
38
+ oauth_provider: CopilotOAuthProvider | None = None,
39
+ **kwargs: Any,
40
+ ) -> None:
41
+ super().__init__(
42
+ config=config,
43
+ auth_manager=auth_manager,
44
+ http_pool_manager=http_pool_manager,
45
+ **kwargs,
46
+ )
47
+ self.oauth_provider = oauth_provider
48
+ self.detection_service = detection_service
49
+ self.token_manager: CopilotTokenManager | None = cast(
50
+ CopilotTokenManager | None, self.auth_manager
51
+ )
52
+
53
+ self.base_url = self.config.base_url.rstrip("/")
54
+
55
+ async def get_target_url(self, endpoint: str) -> str:
56
+ return f"{self.base_url}/{endpoint.lstrip('/')}"
57
+
58
+ async def prepare_provider_request(
59
+ self, body: bytes, headers: dict[str, str], endpoint: str
60
+ ) -> tuple[bytes, dict[str, str]]:
61
+ access_token = await self._resolve_access_token()
62
+
63
+ wants_stream = False
64
+ try:
65
+ parsed_body = json.loads(body.decode()) if body else {}
66
+ except (json.JSONDecodeError, UnicodeDecodeError):
67
+ parsed_body = None
68
+ else:
69
+ if isinstance(parsed_body, dict):
70
+ wants_stream = bool(parsed_body.get("stream"))
71
+
72
+ # Filter headers
73
+ filtered_headers = filter_request_headers(headers, preserve_auth=False)
74
+
75
+ # Add Copilot headers (lowercase keys)
76
+ copilot_headers = {
77
+ key.lower(): str(value)
78
+ for key, value in self.config.api_headers.items()
79
+ if value is not None
80
+ }
81
+
82
+ cli_headers = self._collect_cli_headers()
83
+ for key, value in cli_headers.items():
84
+ copilot_headers.setdefault(key, value)
85
+
86
+ copilot_headers["authorization"] = f"Bearer {access_token}"
87
+ copilot_headers["x-request-id"] = str(uuid.uuid4())
88
+
89
+ if wants_stream and "accept" not in filtered_headers:
90
+ copilot_headers.setdefault("accept", "text/event-stream")
91
+
92
+ # Merge headers
93
+ final_headers = {**filtered_headers, **copilot_headers}
94
+
95
+ logger.debug("copilot_request_prepared", header_count=len(final_headers))
96
+
97
+ return body, final_headers
98
+
99
+ async def _resolve_access_token(self) -> str:
100
+ """Resolve a usable Copilot access token via the configured manager."""
101
+
102
+ auth_manager_name = (
103
+ getattr(self.config, "auth_manager", None) or "oauth_copilot"
104
+ )
105
+
106
+ token_manager = self.token_manager
107
+ if token_manager is None:
108
+ from ccproxy.core.errors import AuthenticationError
109
+
110
+ logger.warning(
111
+ "auth_manager_override_not_resolved",
112
+ plugin="copilot",
113
+ auth_manager_name=auth_manager_name,
114
+ category="auth",
115
+ )
116
+ raise AuthenticationError(
117
+ "Authentication manager not configured for Copilot provider"
118
+ )
119
+
120
+ async def _snapshot_token() -> str | None:
121
+ snapshot = await token_manager.get_token_snapshot()
122
+ if snapshot and snapshot.access_token:
123
+ return str(snapshot.access_token)
124
+ return None
125
+
126
+ credentials = await token_manager.load_credentials()
127
+ if not credentials:
128
+ fallback = await _snapshot_token()
129
+ if fallback:
130
+ return fallback
131
+ raise ValueError("No Copilot credentials available")
132
+
133
+ try:
134
+ if token_manager.should_refresh(credentials):
135
+ logger.debug("copilot_token_refresh_due", category="auth")
136
+ refreshed = await token_manager.get_access_token_with_refresh()
137
+ if refreshed:
138
+ return refreshed
139
+ except Exception as exc: # pragma: no cover - defensive logging
140
+ logger.warning(
141
+ "copilot_token_refresh_failed",
142
+ error=str(exc),
143
+ category="auth",
144
+ )
145
+ fallback = await _snapshot_token()
146
+ if fallback:
147
+ return fallback
148
+
149
+ try:
150
+ token = await token_manager.get_access_token()
151
+ if token:
152
+ return token
153
+ except Exception as exc: # pragma: no cover - defensive logging
154
+ logger.warning(
155
+ "copilot_token_fetch_failed",
156
+ error=str(exc),
157
+ category="auth",
158
+ )
159
+
160
+ fallback = await _snapshot_token()
161
+ if fallback:
162
+ return fallback
163
+
164
+ raise ValueError("No valid Copilot access token available")
165
+
166
+ def _collect_cli_headers(self) -> dict[str, str]:
167
+ """Collect additional headers suggested by CLI detection service."""
168
+
169
+ if not self.detection_service:
170
+ return {}
171
+
172
+ try:
173
+ recommended = self.detection_service.get_recommended_headers()
174
+ except Exception as exc: # pragma: no cover - defensive logging
175
+ logger.debug(
176
+ "copilot_detection_headers_failed",
177
+ error=str(exc),
178
+ category="headers",
179
+ )
180
+ return {}
181
+
182
+ if not isinstance(recommended, dict):
183
+ return {}
184
+
185
+ headers: dict[str, str] = {}
186
+ blocked = {"authorization", "x-request-id"}
187
+ for key, value in recommended.items():
188
+ if not isinstance(key, str) or value is None:
189
+ continue
190
+ lower_key = key.lower()
191
+ if lower_key in blocked:
192
+ continue
193
+ headers[lower_key] = str(value)
194
+
195
+ return headers
196
+
197
+ async def process_provider_response(
198
+ self, response: httpx.Response, endpoint: str
199
+ ) -> Response:
200
+ """Process provider response with format conversion support."""
201
+ # Streaming detection and handling is centralized in BaseHTTPAdapter.
202
+ # Always return a plain Response for non-streaming flows.
203
+ response_headers = extract_response_headers(response)
204
+
205
+ # Normalize Copilot chat completion payloads to include the required
206
+ # OpenAI "created" timestamp field. GitHub's API occasionally omits it,
207
+ # but our OpenAI-compatible schema requires it for validation.
208
+ if (
209
+ response.status_code < 400
210
+ and endpoint.endswith("/chat/completions")
211
+ and "json" in (response.headers.get("content-type", "").lower())
212
+ ):
213
+ try:
214
+ payload = response.json()
215
+ if isinstance(payload, dict) and "choices" in payload:
216
+ if "created" not in payload or not isinstance(
217
+ payload["created"], int
218
+ ):
219
+ payload["created"] = int(time.time())
220
+ body = json.dumps(payload).encode()
221
+ return Response(
222
+ content=body,
223
+ status_code=response.status_code,
224
+ headers=response_headers,
225
+ media_type=response.headers.get("content-type"),
226
+ )
227
+ except (json.JSONDecodeError, UnicodeDecodeError, ValueError):
228
+ # Fall back to the raw payload if normalization fails
229
+ pass
230
+
231
+ if (
232
+ response.status_code < 400
233
+ and endpoint.endswith("/responses")
234
+ and "json" in (response.headers.get("content-type", "").lower())
235
+ ):
236
+ try:
237
+ payload = response.json()
238
+ normalized = self._normalize_response_payload(payload)
239
+ if normalized is not None:
240
+ body = json.dumps(normalized).encode()
241
+ return Response(
242
+ content=body,
243
+ status_code=response.status_code,
244
+ headers=response_headers,
245
+ media_type=response.headers.get("content-type"),
246
+ )
247
+ except (json.JSONDecodeError, UnicodeDecodeError, ValueError):
248
+ # Fall back to raw payload on normalization errors
249
+ pass
250
+
251
+ return Response(
252
+ content=response.content,
253
+ status_code=response.status_code,
254
+ headers=response_headers,
255
+ media_type=response.headers.get("content-type"),
256
+ )
257
+
258
+ async def handle_request_gh_api(self, request: Request) -> Response:
259
+ """Forward request to GitHub API with proper authentication.
260
+
261
+ Args:
262
+ path: API path (e.g., '/copilot_internal/user')
263
+ mode: API mode - 'api' for GitHub API with OAuth token, 'copilot' for Copilot API with Copilot token
264
+ method: HTTP method
265
+ body: Request body
266
+ extra_headers: Additional headers
267
+ """
268
+ auth_manager_name = (
269
+ getattr(self.config, "auth_manager", None) or "oauth_copilot"
270
+ )
271
+
272
+ if self.auth_manager is None:
273
+ from ccproxy.core.errors import AuthenticationError
274
+
275
+ logger.warning(
276
+ "auth_manager_override_not_resolved",
277
+ plugin="copilot",
278
+ auth_manager_name=auth_manager_name,
279
+ category="auth",
280
+ )
281
+ raise AuthenticationError(
282
+ "Authentication manager not configured for Copilot provider"
283
+ )
284
+ oauth_provider = self.oauth_provider
285
+ if oauth_provider is None:
286
+ from ccproxy.core.errors import AuthenticationError
287
+
288
+ logger.warning(
289
+ "oauth_provider_not_available",
290
+ plugin="copilot",
291
+ category="auth",
292
+ )
293
+ raise AuthenticationError(
294
+ "OAuth provider not configured for Copilot provider"
295
+ )
296
+
297
+ access_token = await oauth_provider.ensure_oauth_token()
298
+ base_url = "https://api.github.com"
299
+
300
+ base_headers = {
301
+ "authorization": f"Bearer {access_token}",
302
+ "accept": "application/json",
303
+ }
304
+ # Get context from middleware (already initialized)
305
+ ctx = request.state.context
306
+
307
+ # Step 1: Extract request data
308
+ body = await request.body()
309
+ request_headers = extract_request_headers(request)
310
+ method = request.method
311
+ endpoint = ctx.metadata.get("endpoint", "")
312
+ target_url = f"{base_url}{endpoint}"
313
+
314
+ outgoing_headers = filter_request_headers(request_headers, preserve_auth=False)
315
+ outgoing_headers.update(base_headers)
316
+
317
+ provider_response = await self._execute_http_request(
318
+ method,
319
+ target_url,
320
+ outgoing_headers,
321
+ body,
322
+ )
323
+
324
+ filtered_headers = filter_response_headers(dict(provider_response.headers))
325
+
326
+ return Response(
327
+ content=provider_response.content,
328
+ status_code=provider_response.status_code,
329
+ headers=filtered_headers,
330
+ media_type=provider_response.headers.get(
331
+ "content-type", "application/json"
332
+ ),
333
+ )
334
+
335
+ def _normalize_response_payload(self, payload: Any) -> dict[str, Any] | None:
336
+ """Normalize Response API payloads to align with OpenAI schema expectations."""
337
+ from pydantic import ValidationError
338
+
339
+ if not isinstance(payload, dict):
340
+ return None
341
+
342
+ try:
343
+ # If already valid, return canonical dump
344
+ model = ResponseObject.model_validate(payload)
345
+ return model.model_dump(mode="json", exclude_none=True)
346
+ except ValidationError:
347
+ pass
348
+
349
+ normalized: dict[str, Any] = {}
350
+ response_id = str(payload.get("id") or f"resp-{uuid.uuid4().hex}")
351
+ normalized["id"] = response_id
352
+ normalized["object"] = payload.get("object") or "response"
353
+ normalized["created_at"] = int(payload.get("created_at") or time.time())
354
+
355
+ stop_reason = payload.get("stop_reason")
356
+ status = payload.get("status") or self._map_stop_reason_to_status(stop_reason)
357
+ normalized["status"] = status
358
+ normalized["model"] = payload.get("model") or ""
359
+
360
+ parallel_tool_calls = payload.get("parallel_tool_calls")
361
+ normalized["parallel_tool_calls"] = bool(parallel_tool_calls)
362
+
363
+ # Normalize usage structure
364
+ usage_raw = payload.get("usage") or {}
365
+ if isinstance(usage_raw, dict):
366
+ input_tokens = int(
367
+ usage_raw.get("input_tokens") or usage_raw.get("prompt_tokens") or 0
368
+ )
369
+ output_tokens = int(
370
+ usage_raw.get("output_tokens")
371
+ or usage_raw.get("completion_tokens")
372
+ or 0
373
+ )
374
+ total_tokens = int(
375
+ usage_raw.get("total_tokens") or (input_tokens + output_tokens)
376
+ )
377
+ cached_tokens = int(
378
+ usage_raw.get("input_tokens_details", {}).get("cached_tokens")
379
+ if isinstance(usage_raw.get("input_tokens_details"), dict)
380
+ else usage_raw.get("cached_tokens", 0)
381
+ )
382
+ reasoning_tokens = int(
383
+ usage_raw.get("output_tokens_details", {}).get("reasoning_tokens")
384
+ if isinstance(usage_raw.get("output_tokens_details"), dict)
385
+ else usage_raw.get("reasoning_tokens", 0)
386
+ )
387
+ normalized["usage"] = {
388
+ "input_tokens": input_tokens,
389
+ "input_tokens_details": {"cached_tokens": cached_tokens},
390
+ "output_tokens": output_tokens,
391
+ "output_tokens_details": {"reasoning_tokens": reasoning_tokens},
392
+ "total_tokens": total_tokens,
393
+ }
394
+
395
+ # Normalize output items
396
+ normalized_output: list[dict[str, Any]] = []
397
+ for index, item in enumerate(payload.get("output") or []):
398
+ if not isinstance(item, dict):
399
+ continue
400
+ normalized_item = dict(item)
401
+ normalized_item["id"] = (
402
+ normalized_item.get("id") or f"{response_id}_output_{index}"
403
+ )
404
+ normalized_item["status"] = normalized_item.get("status") or status
405
+ normalized_item["type"] = normalized_item.get("type") or "message"
406
+ normalized_item["role"] = normalized_item.get("role") or "assistant"
407
+
408
+ content_blocks = []
409
+ for part in normalized_item.get("content", []) or []:
410
+ if not isinstance(part, dict):
411
+ continue
412
+ part_type = part.get("type")
413
+ if part_type == "output_text" or part_type == "text":
414
+ text_part = {
415
+ "type": "output_text",
416
+ "text": part.get("text", ""),
417
+ "annotations": part.get("annotations") or [],
418
+ }
419
+ else:
420
+ text_part = part
421
+ content_blocks.append(text_part)
422
+ normalized_item["content"] = content_blocks
423
+ normalized_output.append(normalized_item)
424
+
425
+ normalized["output"] = normalized_output
426
+
427
+ optional_keys = [
428
+ "metadata",
429
+ "instructions",
430
+ "max_output_tokens",
431
+ "previous_response_id",
432
+ "reasoning",
433
+ "store",
434
+ "temperature",
435
+ "text",
436
+ "tool_choice",
437
+ "tools",
438
+ "top_p",
439
+ "truncation",
440
+ "user",
441
+ ]
442
+
443
+ for key in optional_keys:
444
+ if key in payload and payload[key] is not None:
445
+ normalized[key] = payload[key]
446
+
447
+ try:
448
+ model = ResponseObject.model_validate(normalized)
449
+ return model.model_dump(mode="json", exclude_none=True)
450
+ except ValidationError:
451
+ logger.debug(
452
+ "response_payload_normalization_failed",
453
+ payload_keys=list(payload.keys()),
454
+ )
455
+ return None
456
+
457
+ @staticmethod
458
+ def _map_stop_reason_to_status(stop_reason: Any) -> str:
459
+ mapping = {
460
+ "end_turn": "completed",
461
+ "max_output_tokens": "incomplete",
462
+ "stop_sequence": "completed",
463
+ "cancelled": "cancelled",
464
+ }
465
+ return mapping.get(stop_reason, "completed")
@@ -0,0 +1,155 @@
1
+ """Configuration models for GitHub Copilot plugin."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ccproxy.models.provider import (
6
+ ModelCard,
7
+ ModelMappingRule,
8
+ ProviderConfig,
9
+ )
10
+
11
+ from .model_defaults import (
12
+ DEFAULT_COPILOT_MODEL_CARDS,
13
+ DEFAULT_COPILOT_MODEL_MAPPINGS,
14
+ )
15
+
16
+
17
+ class CopilotOAuthConfig(BaseModel):
18
+ """OAuth-specific configuration for GitHub Copilot."""
19
+
20
+ "https://api.individual.githubcopilot.com/chat/completions"
21
+ client_id: str = Field(
22
+ default="Iv1.b507a08c87ecfe98",
23
+ description="GitHub Copilot OAuth client ID",
24
+ )
25
+ authorize_url: str = Field(
26
+ default="https://github.com/login/device/code",
27
+ description="GitHub OAuth device code authorization endpoint",
28
+ )
29
+ token_url: str = Field(
30
+ default="https://github.com/login/oauth/access_token",
31
+ description="GitHub OAuth token endpoint",
32
+ )
33
+ copilot_token_url: str = Field(
34
+ default="https://api.github.com/copilot_internal/v2/token",
35
+ description="GitHub Copilot token exchange endpoint",
36
+ )
37
+ scopes: list[str] = Field(
38
+ default_factory=lambda: ["read:user"],
39
+ description="OAuth scopes to request from GitHub",
40
+ )
41
+ use_pkce: bool = Field(
42
+ default=True,
43
+ description="Whether to use PKCE flow for security",
44
+ )
45
+ request_timeout: int = Field(
46
+ default=30,
47
+ description="Timeout in seconds for OAuth requests",
48
+ ge=1,
49
+ le=300,
50
+ )
51
+ callback_timeout: int = Field(
52
+ default=300,
53
+ description="Timeout in seconds for OAuth callback",
54
+ ge=60,
55
+ le=600,
56
+ )
57
+ callback_port: int = Field(
58
+ default=8080,
59
+ description="Port for OAuth callback server",
60
+ ge=1024,
61
+ le=65535,
62
+ )
63
+ redirect_uri: str | None = Field(
64
+ default=None,
65
+ description="OAuth redirect URI (auto-generated from callback_port if not set)",
66
+ )
67
+
68
+ def get_redirect_uri(self) -> str:
69
+ """Return redirect URI, auto-generated from callback_port when unset."""
70
+ if self.redirect_uri:
71
+ return self.redirect_uri
72
+ return f"http://localhost:{self.callback_port}/callback"
73
+
74
+
75
+ class CopilotProviderConfig(ProviderConfig):
76
+ """Provider-specific configuration for GitHub Copilot API."""
77
+
78
+ name: str = "copilot"
79
+ base_url: str = "https://api.githubcopilot.com"
80
+ supports_streaming: bool = True
81
+ requires_auth: bool = True
82
+ auth_type: str | None = "oauth"
83
+
84
+ # Claude API specific settings
85
+ enabled: bool = True
86
+ priority: int = 5 # Higher priority than SDK-based approach
87
+ default_max_tokens: int = 4096
88
+
89
+ account_type: str = Field(
90
+ default="individual",
91
+ description="Account type: individual, business, or enterprise",
92
+ )
93
+ request_timeout: int = Field(
94
+ default=30,
95
+ description="Timeout for API requests in seconds",
96
+ ge=1,
97
+ le=300,
98
+ )
99
+ max_retries: int = Field(
100
+ default=3,
101
+ description="Maximum number of retries for failed requests",
102
+ ge=0,
103
+ le=10,
104
+ )
105
+ retry_delay: float = Field(
106
+ default=1.0,
107
+ description="Base delay between retries in seconds",
108
+ ge=0.1,
109
+ le=60.0,
110
+ )
111
+
112
+ auth_manager: str | None = Field(
113
+ default=None,
114
+ description="Override auth manager name (e.g., 'oauth_copilot_lb' for load balancing)",
115
+ )
116
+
117
+ api_headers: dict[str, str] = Field(
118
+ default_factory=lambda: {
119
+ "Content-Type": "application/json",
120
+ "Copilot-Integration-Id": "vscode-chat",
121
+ "Editor-Version": "vscode/1.85.0",
122
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
123
+ "User-Agent": "GitHubCopilotChat/0.26.7",
124
+ "X-GitHub-Api-Version": "2025-04-01",
125
+ },
126
+ description="Default headers for Copilot API requests",
127
+ )
128
+
129
+ model_mappings: list[ModelMappingRule] = Field(
130
+ default_factory=lambda: [
131
+ rule.model_copy(deep=True) for rule in DEFAULT_COPILOT_MODEL_MAPPINGS
132
+ ],
133
+ description=(
134
+ "Ordered model translation rules mapping client model identifiers to "
135
+ "Copilot upstream equivalents."
136
+ ),
137
+ )
138
+ models_endpoint: list[ModelCard] = Field(
139
+ default_factory=lambda: [
140
+ card.model_copy(deep=True) for card in DEFAULT_COPILOT_MODEL_CARDS
141
+ ],
142
+ description=(
143
+ "Fallback metadata served from /models when the Copilot API listing is "
144
+ "unavailable."
145
+ ),
146
+ )
147
+
148
+
149
+ class CopilotConfig(CopilotProviderConfig):
150
+ """Complete configuration for GitHub Copilot plugin."""
151
+
152
+ oauth: CopilotOAuthConfig = Field(
153
+ default_factory=CopilotOAuthConfig,
154
+ description="OAuth authentication configuration",
155
+ )
@@ -0,0 +1,41 @@
1
+ {
2
+ "models": [
3
+ {
4
+ "id": "gpt-4",
5
+ "object": "model",
6
+ "created": 1687882411,
7
+ "owned_by": "github"
8
+ },
9
+ {
10
+ "id": "gpt-4-turbo",
11
+ "object": "model",
12
+ "created": 1687882411,
13
+ "owned_by": "github"
14
+ },
15
+ {
16
+ "id": "gpt-3.5-turbo",
17
+ "object": "model",
18
+ "created": 1687882411,
19
+ "owned_by": "github"
20
+ },
21
+ {
22
+ "id": "text-embedding-ada-002",
23
+ "object": "model",
24
+ "created": 1687882411,
25
+ "owned_by": "github"
26
+ }
27
+ ],
28
+ "base_urls": {
29
+ "individual": "https://api.githubcopilot.com",
30
+ "business": "https://api.business.githubcopilot.com",
31
+ "enterprise": "https://api.enterprise.githubcopilot.com"
32
+ },
33
+ "headers": {
34
+ "Content-Type": "application/json",
35
+ "Copilot-Integration-Id": "vscode-chat",
36
+ "Editor-Version": "vscode/1.85.0",
37
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
38
+ "User-Agent": "GitHubCopilotChat/0.26.7",
39
+ "X-GitHub-Api-Version": "2025-04-01"
40
+ }
41
+ }