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.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.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
|
+
}
|