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.
- 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.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.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.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
SQLModel schema definitions for observability storage.
|
|
3
|
-
|
|
4
|
-
This module provides the centralized schema definitions for access logs and metrics
|
|
5
|
-
using SQLModel to ensure type safety and eliminate column name repetition.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
|
|
10
|
-
from sqlmodel import Field, SQLModel
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class AccessLog(SQLModel, table=True):
|
|
14
|
-
"""Access log model for storing request/response data."""
|
|
15
|
-
|
|
16
|
-
__tablename__ = "access_logs"
|
|
17
|
-
|
|
18
|
-
# Core request identification
|
|
19
|
-
request_id: str = Field(primary_key=True)
|
|
20
|
-
timestamp: datetime = Field(default_factory=datetime.now, index=True)
|
|
21
|
-
|
|
22
|
-
# Request details
|
|
23
|
-
method: str
|
|
24
|
-
endpoint: str
|
|
25
|
-
path: str
|
|
26
|
-
query: str = Field(default="")
|
|
27
|
-
client_ip: str
|
|
28
|
-
user_agent: str
|
|
29
|
-
|
|
30
|
-
# Service and model info
|
|
31
|
-
service_type: str
|
|
32
|
-
model: str
|
|
33
|
-
streaming: bool = Field(default=False)
|
|
34
|
-
|
|
35
|
-
# Response details
|
|
36
|
-
status_code: int
|
|
37
|
-
duration_ms: float
|
|
38
|
-
duration_seconds: float
|
|
39
|
-
|
|
40
|
-
# Token and cost tracking
|
|
41
|
-
tokens_input: int = Field(default=0)
|
|
42
|
-
tokens_output: int = Field(default=0)
|
|
43
|
-
cache_read_tokens: int = Field(default=0)
|
|
44
|
-
cache_write_tokens: int = Field(default=0)
|
|
45
|
-
cost_usd: float = Field(default=0.0)
|
|
46
|
-
cost_sdk_usd: float = Field(default=0.0)
|
|
47
|
-
num_turns: int = Field(default=0) # number of conversation turns
|
|
48
|
-
|
|
49
|
-
# Session context metadata
|
|
50
|
-
session_type: str = Field(default="") # "session_pool" or "direct"
|
|
51
|
-
session_status: str = Field(default="") # active, idle, connecting, etc.
|
|
52
|
-
session_age_seconds: float = Field(default=0.0) # how long session has been alive
|
|
53
|
-
session_message_count: int = Field(default=0) # number of messages in session
|
|
54
|
-
session_client_id: str = Field(default="") # unique session client identifier
|
|
55
|
-
session_pool_enabled: bool = Field(
|
|
56
|
-
default=False
|
|
57
|
-
) # whether session pooling is enabled
|
|
58
|
-
session_idle_seconds: float = Field(default=0.0) # how long since last activity
|
|
59
|
-
session_error_count: int = Field(default=0) # number of errors in this session
|
|
60
|
-
session_is_new: bool = Field(
|
|
61
|
-
default=True
|
|
62
|
-
) # whether this is a newly created session
|
|
63
|
-
|
|
64
|
-
class Config:
|
|
65
|
-
"""SQLModel configuration."""
|
|
66
|
-
|
|
67
|
-
# Enable automatic conversion from dict
|
|
68
|
-
from_attributes = True
|
|
69
|
-
# Use enum values
|
|
70
|
-
use_enum_values = True
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
"""FastAPI StreamingResponse with automatic access logging on completion.
|
|
2
|
-
|
|
3
|
-
This module provides a reusable StreamingResponseWithLogging class that wraps
|
|
4
|
-
any async generator and handles access logging when the stream completes,
|
|
5
|
-
eliminating code duplication between different streaming endpoints.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from collections.abc import AsyncGenerator, AsyncIterator
|
|
11
|
-
from typing import TYPE_CHECKING, Any
|
|
12
|
-
|
|
13
|
-
import structlog
|
|
14
|
-
from fastapi.responses import StreamingResponse
|
|
15
|
-
|
|
16
|
-
from ccproxy.observability.access_logger import log_request_access
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if TYPE_CHECKING:
|
|
20
|
-
from ccproxy.observability.context import RequestContext
|
|
21
|
-
from ccproxy.observability.metrics import PrometheusMetrics
|
|
22
|
-
|
|
23
|
-
logger = structlog.get_logger(__name__)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class StreamingResponseWithLogging(StreamingResponse):
|
|
27
|
-
"""FastAPI StreamingResponse that triggers access logging on completion.
|
|
28
|
-
|
|
29
|
-
This class wraps a streaming response generator to automatically trigger
|
|
30
|
-
access logging when the stream completes (either successfully or with an error).
|
|
31
|
-
This eliminates the need for manual access logging in individual stream processors.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
def __init__(
|
|
35
|
-
self,
|
|
36
|
-
content: AsyncGenerator[bytes, None] | AsyncIterator[bytes],
|
|
37
|
-
request_context: RequestContext,
|
|
38
|
-
metrics: PrometheusMetrics | None = None,
|
|
39
|
-
status_code: int = 200,
|
|
40
|
-
**kwargs: Any,
|
|
41
|
-
) -> None:
|
|
42
|
-
"""Initialize streaming response with logging capability.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
content: The async generator producing streaming content
|
|
46
|
-
request_context: The request context for access logging
|
|
47
|
-
metrics: Optional PrometheusMetrics instance for recording metrics
|
|
48
|
-
status_code: HTTP status code for the response
|
|
49
|
-
**kwargs: Additional arguments passed to StreamingResponse
|
|
50
|
-
"""
|
|
51
|
-
# Wrap the content generator to add logging
|
|
52
|
-
logged_content = self._wrap_with_logging(
|
|
53
|
-
content, request_context, metrics, status_code
|
|
54
|
-
)
|
|
55
|
-
super().__init__(logged_content, status_code=status_code, **kwargs)
|
|
56
|
-
|
|
57
|
-
async def _wrap_with_logging(
|
|
58
|
-
self,
|
|
59
|
-
content: AsyncGenerator[bytes, None] | AsyncIterator[bytes],
|
|
60
|
-
context: RequestContext,
|
|
61
|
-
metrics: PrometheusMetrics | None,
|
|
62
|
-
status_code: int,
|
|
63
|
-
) -> AsyncGenerator[bytes, None]:
|
|
64
|
-
"""Wrap content generator with access logging on completion.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
content: The original content generator
|
|
68
|
-
context: Request context for logging
|
|
69
|
-
metrics: Optional metrics instance
|
|
70
|
-
status_code: HTTP status code
|
|
71
|
-
|
|
72
|
-
Yields:
|
|
73
|
-
bytes: Content chunks from the original generator
|
|
74
|
-
"""
|
|
75
|
-
try:
|
|
76
|
-
# Stream all content from the original generator
|
|
77
|
-
async for chunk in content:
|
|
78
|
-
yield chunk
|
|
79
|
-
except GeneratorExit:
|
|
80
|
-
# Client disconnected - log this and re-raise to propagate to underlying generators
|
|
81
|
-
logger.info(
|
|
82
|
-
"streaming_response_client_disconnected",
|
|
83
|
-
request_id=context.request_id,
|
|
84
|
-
message="Client disconnected from streaming response, propagating GeneratorExit",
|
|
85
|
-
)
|
|
86
|
-
# CRITICAL: Re-raise GeneratorExit to propagate disconnect to create_listener()
|
|
87
|
-
raise
|
|
88
|
-
finally:
|
|
89
|
-
# Log access when stream completes (success or error)
|
|
90
|
-
try:
|
|
91
|
-
# Add streaming completion event type to context
|
|
92
|
-
context.add_metadata(event_type="streaming_complete")
|
|
93
|
-
|
|
94
|
-
# Check if status_code was updated in context metadata (e.g., due to error)
|
|
95
|
-
final_status_code = context.metadata.get("status_code", status_code)
|
|
96
|
-
|
|
97
|
-
await log_request_access(
|
|
98
|
-
context=context,
|
|
99
|
-
status_code=final_status_code,
|
|
100
|
-
metrics=metrics,
|
|
101
|
-
)
|
|
102
|
-
except Exception as e:
|
|
103
|
-
logger.warning(
|
|
104
|
-
"streaming_access_log_failed",
|
|
105
|
-
error=str(e),
|
|
106
|
-
request_id=context.request_id,
|
|
107
|
-
)
|
ccproxy/pricing/__init__.py
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"""Dynamic pricing system for Claude models.
|
|
2
|
-
|
|
3
|
-
This module provides dynamic pricing capabilities by downloading and caching
|
|
4
|
-
pricing information from external sources like LiteLLM.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from .cache import PricingCache
|
|
8
|
-
from .loader import PricingLoader
|
|
9
|
-
from .models import ModelPricing, PricingData
|
|
10
|
-
from .updater import PricingUpdater
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
__all__ = [
|
|
14
|
-
"PricingCache",
|
|
15
|
-
"PricingLoader",
|
|
16
|
-
"PricingUpdater",
|
|
17
|
-
"ModelPricing",
|
|
18
|
-
"PricingData",
|
|
19
|
-
]
|
ccproxy/pricing/loader.py
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
"""Pricing data loader and format converter for LiteLLM pricing data."""
|
|
2
|
-
|
|
3
|
-
from decimal import Decimal
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from pydantic import ValidationError
|
|
7
|
-
from structlog import get_logger
|
|
8
|
-
|
|
9
|
-
from ccproxy.utils.model_mapping import get_claude_aliases_mapping, map_model_to_claude
|
|
10
|
-
|
|
11
|
-
from .models import PricingData
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
logger = get_logger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class PricingLoader:
|
|
18
|
-
"""Loads and converts pricing data from LiteLLM format to internal format."""
|
|
19
|
-
|
|
20
|
-
@staticmethod
|
|
21
|
-
def extract_claude_models(
|
|
22
|
-
litellm_data: dict[str, Any], verbose: bool = True
|
|
23
|
-
) -> dict[str, Any]:
|
|
24
|
-
"""Extract Claude model entries from LiteLLM data.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
litellm_data: Raw LiteLLM pricing data
|
|
28
|
-
verbose: Whether to log individual model discoveries
|
|
29
|
-
|
|
30
|
-
Returns:
|
|
31
|
-
Dictionary with only Claude models
|
|
32
|
-
"""
|
|
33
|
-
claude_models = {}
|
|
34
|
-
|
|
35
|
-
for model_name, model_data in litellm_data.items():
|
|
36
|
-
# Check if this is a Claude model
|
|
37
|
-
if (
|
|
38
|
-
isinstance(model_data, dict)
|
|
39
|
-
and model_data.get("litellm_provider") == "anthropic"
|
|
40
|
-
and "claude" in model_name.lower()
|
|
41
|
-
):
|
|
42
|
-
claude_models[model_name] = model_data
|
|
43
|
-
if verbose:
|
|
44
|
-
logger.debug("claude_model_found", model_name=model_name)
|
|
45
|
-
|
|
46
|
-
if verbose:
|
|
47
|
-
logger.info(
|
|
48
|
-
"claude_models_extracted",
|
|
49
|
-
model_count=len(claude_models),
|
|
50
|
-
source="LiteLLM",
|
|
51
|
-
)
|
|
52
|
-
return claude_models
|
|
53
|
-
|
|
54
|
-
@staticmethod
|
|
55
|
-
def convert_to_internal_format(
|
|
56
|
-
claude_models: dict[str, Any], verbose: bool = True
|
|
57
|
-
) -> dict[str, dict[str, Decimal]]:
|
|
58
|
-
"""Convert LiteLLM pricing format to internal format.
|
|
59
|
-
|
|
60
|
-
LiteLLM format uses cost per token, we use cost per 1M tokens as Decimal.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
claude_models: Claude models in LiteLLM format
|
|
64
|
-
verbose: Whether to log individual model conversions
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
Dictionary in internal pricing format
|
|
68
|
-
"""
|
|
69
|
-
internal_format = {}
|
|
70
|
-
|
|
71
|
-
for model_name, model_data in claude_models.items():
|
|
72
|
-
try:
|
|
73
|
-
# Extract pricing fields
|
|
74
|
-
input_cost_per_token = model_data.get("input_cost_per_token")
|
|
75
|
-
output_cost_per_token = model_data.get("output_cost_per_token")
|
|
76
|
-
cache_creation_cost = model_data.get("cache_creation_input_token_cost")
|
|
77
|
-
cache_read_cost = model_data.get("cache_read_input_token_cost")
|
|
78
|
-
|
|
79
|
-
# Skip models without pricing info
|
|
80
|
-
if input_cost_per_token is None or output_cost_per_token is None:
|
|
81
|
-
if verbose:
|
|
82
|
-
logger.warning("model_pricing_missing", model_name=model_name)
|
|
83
|
-
continue
|
|
84
|
-
|
|
85
|
-
# Convert to per-1M-token pricing (multiply by 1,000,000)
|
|
86
|
-
pricing = {
|
|
87
|
-
"input": Decimal(str(input_cost_per_token * 1_000_000)),
|
|
88
|
-
"output": Decimal(str(output_cost_per_token * 1_000_000)),
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
# Add cache pricing if available
|
|
92
|
-
if cache_creation_cost is not None:
|
|
93
|
-
pricing["cache_write"] = Decimal(
|
|
94
|
-
str(cache_creation_cost * 1_000_000)
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
if cache_read_cost is not None:
|
|
98
|
-
pricing["cache_read"] = Decimal(str(cache_read_cost * 1_000_000))
|
|
99
|
-
|
|
100
|
-
# Map to canonical model name if needed
|
|
101
|
-
canonical_name = map_model_to_claude(model_name)
|
|
102
|
-
internal_format[canonical_name] = pricing
|
|
103
|
-
|
|
104
|
-
if verbose:
|
|
105
|
-
logger.debug(
|
|
106
|
-
"model_pricing_converted",
|
|
107
|
-
original_name=model_name,
|
|
108
|
-
canonical_name=canonical_name,
|
|
109
|
-
input_cost=str(pricing["input"]),
|
|
110
|
-
output_cost=str(pricing["output"]),
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
except (ValueError, TypeError) as e:
|
|
114
|
-
if verbose:
|
|
115
|
-
logger.error(
|
|
116
|
-
"pricing_conversion_failed", model_name=model_name, error=str(e)
|
|
117
|
-
)
|
|
118
|
-
continue
|
|
119
|
-
|
|
120
|
-
if verbose:
|
|
121
|
-
logger.info("models_converted", model_count=len(internal_format))
|
|
122
|
-
return internal_format
|
|
123
|
-
|
|
124
|
-
@staticmethod
|
|
125
|
-
def load_pricing_from_data(
|
|
126
|
-
litellm_data: dict[str, Any], verbose: bool = True
|
|
127
|
-
) -> PricingData | None:
|
|
128
|
-
"""Load and convert pricing data from LiteLLM format.
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
litellm_data: Raw LiteLLM pricing data
|
|
132
|
-
verbose: Whether to enable verbose logging
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
Validated pricing data as PricingData model, or None if invalid
|
|
136
|
-
"""
|
|
137
|
-
try:
|
|
138
|
-
# Extract Claude models
|
|
139
|
-
claude_models = PricingLoader.extract_claude_models(
|
|
140
|
-
litellm_data, verbose=verbose
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
if not claude_models:
|
|
144
|
-
if verbose:
|
|
145
|
-
logger.warning("claude_models_not_found", source="LiteLLM")
|
|
146
|
-
return None
|
|
147
|
-
|
|
148
|
-
# Convert to internal format
|
|
149
|
-
internal_pricing = PricingLoader.convert_to_internal_format(
|
|
150
|
-
claude_models, verbose=verbose
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
if not internal_pricing:
|
|
154
|
-
if verbose:
|
|
155
|
-
logger.warning("pricing_data_invalid")
|
|
156
|
-
return None
|
|
157
|
-
|
|
158
|
-
# Validate and create PricingData model
|
|
159
|
-
pricing_data = PricingData.from_dict(internal_pricing)
|
|
160
|
-
|
|
161
|
-
if verbose:
|
|
162
|
-
logger.info("pricing_data_loaded", model_count=len(pricing_data))
|
|
163
|
-
|
|
164
|
-
return pricing_data
|
|
165
|
-
|
|
166
|
-
except ValidationError as e:
|
|
167
|
-
if verbose:
|
|
168
|
-
logger.error("pricing_validation_failed", error=str(e))
|
|
169
|
-
return None
|
|
170
|
-
except Exception as e:
|
|
171
|
-
if verbose:
|
|
172
|
-
logger.error("pricing_load_failed", source="LiteLLM", error=str(e))
|
|
173
|
-
return None
|
|
174
|
-
|
|
175
|
-
@staticmethod
|
|
176
|
-
def validate_pricing_data(
|
|
177
|
-
pricing_data: Any, verbose: bool = True
|
|
178
|
-
) -> PricingData | None:
|
|
179
|
-
"""Validate pricing data using Pydantic models.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
pricing_data: Pricing data to validate (dict or PricingData)
|
|
183
|
-
verbose: Whether to enable verbose logging
|
|
184
|
-
|
|
185
|
-
Returns:
|
|
186
|
-
Valid PricingData model or None if validation fails
|
|
187
|
-
"""
|
|
188
|
-
try:
|
|
189
|
-
# If already a PricingData instance, return it
|
|
190
|
-
if isinstance(pricing_data, PricingData):
|
|
191
|
-
if verbose:
|
|
192
|
-
logger.debug(
|
|
193
|
-
"pricing_already_validated", model_count=len(pricing_data)
|
|
194
|
-
)
|
|
195
|
-
return pricing_data
|
|
196
|
-
|
|
197
|
-
# If it's a dict, try to create PricingData from it
|
|
198
|
-
if isinstance(pricing_data, dict):
|
|
199
|
-
if not pricing_data:
|
|
200
|
-
if verbose:
|
|
201
|
-
logger.warning("pricing_data_empty")
|
|
202
|
-
return None
|
|
203
|
-
|
|
204
|
-
# Try to create PricingData model
|
|
205
|
-
validated_data = PricingData.from_dict(pricing_data)
|
|
206
|
-
|
|
207
|
-
if verbose:
|
|
208
|
-
logger.debug(
|
|
209
|
-
"pricing_data_validated", model_count=len(validated_data)
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
return validated_data
|
|
213
|
-
|
|
214
|
-
# Invalid type
|
|
215
|
-
if verbose:
|
|
216
|
-
logger.error(
|
|
217
|
-
"pricing_data_invalid_type",
|
|
218
|
-
actual_type=type(pricing_data).__name__,
|
|
219
|
-
expected_types=["dict", "PricingData"],
|
|
220
|
-
)
|
|
221
|
-
return None
|
|
222
|
-
|
|
223
|
-
except ValidationError as e:
|
|
224
|
-
if verbose:
|
|
225
|
-
logger.error("pricing_validation_failed", error=str(e))
|
|
226
|
-
return None
|
|
227
|
-
except Exception as e:
|
|
228
|
-
if verbose:
|
|
229
|
-
logger.error("pricing_validation_unexpected_error", error=str(e))
|
|
230
|
-
return None
|
|
231
|
-
|
|
232
|
-
@staticmethod
|
|
233
|
-
def get_model_aliases() -> dict[str, str]:
|
|
234
|
-
"""Get mapping of model aliases to canonical names.
|
|
235
|
-
|
|
236
|
-
Returns:
|
|
237
|
-
Dictionary mapping aliases to canonical model names
|
|
238
|
-
"""
|
|
239
|
-
return get_claude_aliases_mapping()
|
|
240
|
-
|
|
241
|
-
@staticmethod
|
|
242
|
-
def get_canonical_model_name(model_name: str) -> str:
|
|
243
|
-
"""Get canonical model name for a given model name.
|
|
244
|
-
|
|
245
|
-
Args:
|
|
246
|
-
model_name: Model name (possibly an alias)
|
|
247
|
-
|
|
248
|
-
Returns:
|
|
249
|
-
Canonical model name
|
|
250
|
-
"""
|
|
251
|
-
return map_model_to_claude(model_name)
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
"""Service for automatically detecting Claude CLI headers at startup."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import socket
|
|
9
|
-
import subprocess
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
import structlog
|
|
14
|
-
from fastapi import FastAPI, Request, Response
|
|
15
|
-
|
|
16
|
-
from ccproxy.config.discovery import get_ccproxy_cache_dir
|
|
17
|
-
from ccproxy.config.settings import Settings
|
|
18
|
-
from ccproxy.models.detection import (
|
|
19
|
-
ClaudeCacheData,
|
|
20
|
-
ClaudeCodeHeaders,
|
|
21
|
-
SystemPromptData,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
logger = structlog.get_logger(__name__)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class ClaudeDetectionService:
|
|
29
|
-
"""Service for automatically detecting Claude CLI headers at startup."""
|
|
30
|
-
|
|
31
|
-
def __init__(self, settings: Settings) -> None:
|
|
32
|
-
"""Initialize Claude detection service."""
|
|
33
|
-
self.settings = settings
|
|
34
|
-
self.cache_dir = get_ccproxy_cache_dir()
|
|
35
|
-
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
-
self._cached_data: ClaudeCacheData | None = None
|
|
37
|
-
|
|
38
|
-
async def initialize_detection(self) -> ClaudeCacheData:
|
|
39
|
-
"""Initialize Claude detection at startup."""
|
|
40
|
-
try:
|
|
41
|
-
# Get current Claude version
|
|
42
|
-
current_version = await self._get_claude_version()
|
|
43
|
-
|
|
44
|
-
# Try to load from cache first
|
|
45
|
-
detected_data = self._load_from_cache(current_version)
|
|
46
|
-
cached = detected_data is not None
|
|
47
|
-
if cached:
|
|
48
|
-
logger.debug("detection_claude_headers_debug", version=current_version)
|
|
49
|
-
else:
|
|
50
|
-
# No cache or version changed - detect fresh
|
|
51
|
-
detected_data = await self._detect_claude_headers(current_version)
|
|
52
|
-
# Cache the results
|
|
53
|
-
self._save_to_cache(detected_data)
|
|
54
|
-
|
|
55
|
-
self._cached_data = detected_data
|
|
56
|
-
|
|
57
|
-
logger.info(
|
|
58
|
-
"detection_claude_headers_completed",
|
|
59
|
-
version=current_version,
|
|
60
|
-
cached=cached,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
# TODO: add proper testing without claude cli installed
|
|
64
|
-
if detected_data is None:
|
|
65
|
-
raise ValueError("Claude detection failed")
|
|
66
|
-
return detected_data
|
|
67
|
-
|
|
68
|
-
except Exception as e:
|
|
69
|
-
logger.warning("detection_claude_headers_failed", fallback=True, error=e)
|
|
70
|
-
# Return fallback data
|
|
71
|
-
fallback_data = self._get_fallback_data()
|
|
72
|
-
self._cached_data = fallback_data
|
|
73
|
-
return fallback_data
|
|
74
|
-
|
|
75
|
-
def get_cached_data(self) -> ClaudeCacheData | None:
|
|
76
|
-
"""Get currently cached detection data."""
|
|
77
|
-
return self._cached_data
|
|
78
|
-
|
|
79
|
-
async def _get_claude_version(self) -> str:
|
|
80
|
-
"""Get Claude CLI version."""
|
|
81
|
-
try:
|
|
82
|
-
result = subprocess.run(
|
|
83
|
-
["claude", "--version"],
|
|
84
|
-
capture_output=True,
|
|
85
|
-
text=True,
|
|
86
|
-
timeout=10,
|
|
87
|
-
)
|
|
88
|
-
if result.returncode == 0:
|
|
89
|
-
# Extract version from output like "1.0.60 (Claude Code)"
|
|
90
|
-
version_line = result.stdout.strip()
|
|
91
|
-
if "/" in version_line:
|
|
92
|
-
# Handle "claude-cli/1.0.60" format
|
|
93
|
-
version_line = version_line.split("/")[-1]
|
|
94
|
-
if "(" in version_line:
|
|
95
|
-
# Handle "1.0.60 (Claude Code)" format - extract just the version number
|
|
96
|
-
return version_line.split("(")[0].strip()
|
|
97
|
-
return version_line
|
|
98
|
-
else:
|
|
99
|
-
raise RuntimeError(f"Claude version command failed: {result.stderr}")
|
|
100
|
-
|
|
101
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
|
|
102
|
-
logger.warning("claude_version_detection_failed", error=str(e))
|
|
103
|
-
return "unknown"
|
|
104
|
-
|
|
105
|
-
async def _detect_claude_headers(self, version: str) -> ClaudeCacheData:
|
|
106
|
-
"""Execute Claude CLI with proxy to capture headers and system prompt."""
|
|
107
|
-
# Data captured from the request
|
|
108
|
-
captured_data: dict[str, Any] = {}
|
|
109
|
-
|
|
110
|
-
async def capture_handler(request: Request) -> Response:
|
|
111
|
-
"""Capture the Claude CLI request."""
|
|
112
|
-
captured_data["headers"] = dict(request.headers)
|
|
113
|
-
captured_data["body"] = await request.body()
|
|
114
|
-
# Return a mock response to satisfy Claude CLI
|
|
115
|
-
return Response(
|
|
116
|
-
content='{"type": "message", "content": [{"type": "text", "text": "Test response"}]}',
|
|
117
|
-
media_type="application/json",
|
|
118
|
-
status_code=200,
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
# Create temporary FastAPI app
|
|
122
|
-
temp_app = FastAPI()
|
|
123
|
-
temp_app.post("/v1/messages")(capture_handler)
|
|
124
|
-
|
|
125
|
-
# Find available port
|
|
126
|
-
sock = socket.socket()
|
|
127
|
-
sock.bind(("", 0))
|
|
128
|
-
port = sock.getsockname()[1]
|
|
129
|
-
sock.close()
|
|
130
|
-
|
|
131
|
-
# Start server in background
|
|
132
|
-
from uvicorn import Config, Server
|
|
133
|
-
|
|
134
|
-
config = Config(temp_app, host="127.0.0.1", port=port, log_level="error")
|
|
135
|
-
server = Server(config)
|
|
136
|
-
|
|
137
|
-
server_task = asyncio.create_task(server.serve())
|
|
138
|
-
|
|
139
|
-
try:
|
|
140
|
-
# Wait for server to start
|
|
141
|
-
await asyncio.sleep(0.5)
|
|
142
|
-
|
|
143
|
-
# Execute Claude CLI with proxy
|
|
144
|
-
env = {**dict(os.environ), "ANTHROPIC_BASE_URL": f"http://127.0.0.1:{port}"}
|
|
145
|
-
|
|
146
|
-
process = await asyncio.create_subprocess_exec(
|
|
147
|
-
"claude",
|
|
148
|
-
"test",
|
|
149
|
-
env=env,
|
|
150
|
-
stdout=asyncio.subprocess.PIPE,
|
|
151
|
-
stderr=asyncio.subprocess.PIPE,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
# Wait for process with timeout
|
|
155
|
-
try:
|
|
156
|
-
await asyncio.wait_for(process.wait(), timeout=30)
|
|
157
|
-
except TimeoutError:
|
|
158
|
-
process.kill()
|
|
159
|
-
await process.wait()
|
|
160
|
-
|
|
161
|
-
# Stop server
|
|
162
|
-
server.should_exit = True
|
|
163
|
-
await server_task
|
|
164
|
-
|
|
165
|
-
if not captured_data:
|
|
166
|
-
raise RuntimeError("Failed to capture Claude CLI request")
|
|
167
|
-
|
|
168
|
-
# Extract headers and system prompt
|
|
169
|
-
headers = self._extract_headers(captured_data["headers"])
|
|
170
|
-
system_prompt = self._extract_system_prompt(captured_data["body"])
|
|
171
|
-
|
|
172
|
-
return ClaudeCacheData(
|
|
173
|
-
claude_version=version, headers=headers, system_prompt=system_prompt
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
except Exception as e:
|
|
177
|
-
# Ensure server is stopped
|
|
178
|
-
server.should_exit = True
|
|
179
|
-
if not server_task.done():
|
|
180
|
-
await server_task
|
|
181
|
-
raise
|
|
182
|
-
|
|
183
|
-
def _load_from_cache(self, version: str) -> ClaudeCacheData | None:
|
|
184
|
-
"""Load cached data for specific Claude version."""
|
|
185
|
-
cache_file = self.cache_dir / f"claude_headers_{version}.json"
|
|
186
|
-
|
|
187
|
-
if not cache_file.exists():
|
|
188
|
-
return None
|
|
189
|
-
|
|
190
|
-
try:
|
|
191
|
-
with cache_file.open("r") as f:
|
|
192
|
-
data = json.load(f)
|
|
193
|
-
return ClaudeCacheData.model_validate(data)
|
|
194
|
-
except Exception:
|
|
195
|
-
return None
|
|
196
|
-
|
|
197
|
-
def _save_to_cache(self, data: ClaudeCacheData) -> None:
|
|
198
|
-
"""Save detection data to cache."""
|
|
199
|
-
cache_file = self.cache_dir / f"claude_headers_{data.claude_version}.json"
|
|
200
|
-
|
|
201
|
-
try:
|
|
202
|
-
with cache_file.open("w") as f:
|
|
203
|
-
json.dump(data.model_dump(), f, indent=2, default=str)
|
|
204
|
-
logger.debug(
|
|
205
|
-
"cache_saved", file=str(cache_file), version=data.claude_version
|
|
206
|
-
)
|
|
207
|
-
except Exception as e:
|
|
208
|
-
logger.warning("cache_save_failed", file=str(cache_file), error=str(e))
|
|
209
|
-
|
|
210
|
-
def _extract_headers(self, headers: dict[str, str]) -> ClaudeCodeHeaders:
|
|
211
|
-
"""Extract Claude CLI headers from captured request."""
|
|
212
|
-
try:
|
|
213
|
-
return ClaudeCodeHeaders.model_validate(headers)
|
|
214
|
-
except Exception as e:
|
|
215
|
-
logger.error("header_extraction_failed", error=str(e))
|
|
216
|
-
raise ValueError(f"Failed to extract required headers: {e}") from e
|
|
217
|
-
|
|
218
|
-
def _extract_system_prompt(self, body: bytes) -> SystemPromptData:
|
|
219
|
-
"""Extract system prompt from captured request body."""
|
|
220
|
-
try:
|
|
221
|
-
data = json.loads(body.decode("utf-8"))
|
|
222
|
-
system_content = data.get("system")
|
|
223
|
-
|
|
224
|
-
if system_content is None:
|
|
225
|
-
raise ValueError("No system field found in request body")
|
|
226
|
-
|
|
227
|
-
return SystemPromptData(system_field=system_content)
|
|
228
|
-
|
|
229
|
-
except Exception as e:
|
|
230
|
-
logger.error("system_prompt_extraction_failed", error=str(e))
|
|
231
|
-
raise ValueError(f"Failed to extract system prompt: {e}") from e
|
|
232
|
-
|
|
233
|
-
def _get_fallback_data(self) -> ClaudeCacheData:
|
|
234
|
-
"""Get fallback data when detection fails."""
|
|
235
|
-
logger.warning("using_fallback_claude_data")
|
|
236
|
-
|
|
237
|
-
# Load fallback data from package data file
|
|
238
|
-
package_data_file = (
|
|
239
|
-
Path(__file__).parent.parent / "data" / "claude_headers_fallback.json"
|
|
240
|
-
)
|
|
241
|
-
with package_data_file.open("r") as f:
|
|
242
|
-
fallback_data_dict = json.load(f)
|
|
243
|
-
return ClaudeCacheData.model_validate(fallback_data_dict)
|