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
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Claude API plugin implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
6
|
+
from ccproxy.core.plugins import (
|
|
7
|
+
BaseProviderPluginFactory,
|
|
8
|
+
FormatAdapterSpec,
|
|
9
|
+
FormatPair,
|
|
10
|
+
PluginContext,
|
|
11
|
+
PluginManifest,
|
|
12
|
+
ProviderPluginRuntime,
|
|
13
|
+
TaskSpec,
|
|
14
|
+
)
|
|
15
|
+
from ccproxy.core.plugins.declaration import RouterSpec
|
|
16
|
+
from ccproxy.llms.streaming.accumulators import ClaudeAccumulator
|
|
17
|
+
from ccproxy.plugins.oauth_claude.manager import ClaudeApiTokenManager
|
|
18
|
+
|
|
19
|
+
from .adapter import ClaudeAPIAdapter
|
|
20
|
+
from .config import ClaudeAPISettings
|
|
21
|
+
from .detection_service import ClaudeAPIDetectionService
|
|
22
|
+
from .health import claude_api_health_check
|
|
23
|
+
from .routes import router as claude_api_router
|
|
24
|
+
from .tasks import ClaudeAPIDetectionRefreshTask
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = get_plugin_logger()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ClaudeAPIRuntime(ProviderPluginRuntime):
|
|
31
|
+
"""Runtime for Claude API plugin."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, manifest: PluginManifest):
|
|
34
|
+
"""Initialize runtime."""
|
|
35
|
+
self.credential_manager: ClaudeApiTokenManager | None = None
|
|
36
|
+
super().__init__(manifest)
|
|
37
|
+
self.config: ClaudeAPISettings | None = None
|
|
38
|
+
|
|
39
|
+
async def _on_initialize(self) -> None:
|
|
40
|
+
"""Initialize the Claude API plugin."""
|
|
41
|
+
if not self.context:
|
|
42
|
+
raise RuntimeError("Context not set")
|
|
43
|
+
|
|
44
|
+
# Call parent initialization first
|
|
45
|
+
await super()._on_initialize()
|
|
46
|
+
|
|
47
|
+
# Get configuration
|
|
48
|
+
try:
|
|
49
|
+
config = self.context.get(ClaudeAPISettings)
|
|
50
|
+
except ValueError:
|
|
51
|
+
logger.warning("plugin_no_config")
|
|
52
|
+
# Use default config if none provided
|
|
53
|
+
config = ClaudeAPISettings()
|
|
54
|
+
logger.debug("plugin_using_default_config", category="plugin")
|
|
55
|
+
self.config = config
|
|
56
|
+
|
|
57
|
+
# Register streaming metrics hook
|
|
58
|
+
await self._register_streaming_metrics_hook()
|
|
59
|
+
|
|
60
|
+
# Initialize detection service to populate cached data
|
|
61
|
+
if self.detection_service:
|
|
62
|
+
try:
|
|
63
|
+
# This will detect headers and system prompt
|
|
64
|
+
await self.detection_service.initialize_detection()
|
|
65
|
+
version = self.detection_service.get_version()
|
|
66
|
+
cli_path = self.detection_service.get_cli_path()
|
|
67
|
+
|
|
68
|
+
if not cli_path:
|
|
69
|
+
logger.warning(
|
|
70
|
+
"cli_detection_completed",
|
|
71
|
+
cli_available=False,
|
|
72
|
+
version=None,
|
|
73
|
+
cli_path=None,
|
|
74
|
+
source="unknown",
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(
|
|
78
|
+
"claude_detection_initialization_failed",
|
|
79
|
+
error=str(e),
|
|
80
|
+
exc_info=e,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Get CLI info for consolidated logging (only for successful detection)
|
|
84
|
+
cli_info = {}
|
|
85
|
+
if self.detection_service and self.detection_service.get_cli_path():
|
|
86
|
+
cli_info.update(
|
|
87
|
+
{
|
|
88
|
+
"cli_available": True,
|
|
89
|
+
"cli_version": self.detection_service.get_version(),
|
|
90
|
+
"cli_path": self.detection_service.get_cli_path(),
|
|
91
|
+
"cli_source": "package_manager",
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
logger.debug(
|
|
96
|
+
"plugin_initialized",
|
|
97
|
+
plugin="claude_api",
|
|
98
|
+
version=self.manifest.version,
|
|
99
|
+
status="initialized",
|
|
100
|
+
has_credentials=self.credentials_manager is not None,
|
|
101
|
+
base_url=self.config.base_url,
|
|
102
|
+
models_count=len(self.config.models_endpoint)
|
|
103
|
+
if self.config.models_endpoint
|
|
104
|
+
else 0,
|
|
105
|
+
has_adapter=self.adapter is not None,
|
|
106
|
+
**cli_info,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
async def _get_health_details(self) -> dict[str, Any]:
|
|
110
|
+
"""Get health check details."""
|
|
111
|
+
details = await super()._get_health_details()
|
|
112
|
+
|
|
113
|
+
# Add claude-api specific health check
|
|
114
|
+
if self.config and self.detection_service and self.credentials_manager:
|
|
115
|
+
try:
|
|
116
|
+
health_result = await claude_api_health_check(
|
|
117
|
+
self.config,
|
|
118
|
+
self.detection_service,
|
|
119
|
+
self.credentials_manager,
|
|
120
|
+
version=self.manifest.version,
|
|
121
|
+
)
|
|
122
|
+
details.update(
|
|
123
|
+
{
|
|
124
|
+
"health_check_status": health_result.status,
|
|
125
|
+
"health_check_detail": health_result.details,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
details["health_check_error"] = str(e)
|
|
130
|
+
|
|
131
|
+
return details
|
|
132
|
+
|
|
133
|
+
async def _register_streaming_metrics_hook(self) -> None:
|
|
134
|
+
"""Register the streaming metrics extraction hook."""
|
|
135
|
+
try:
|
|
136
|
+
if not self.context:
|
|
137
|
+
logger.warning(
|
|
138
|
+
"streaming_metrics_hook_not_registered",
|
|
139
|
+
reason="no_context",
|
|
140
|
+
plugin="claude_api",
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
# Debug: Log context details
|
|
144
|
+
logger.debug(
|
|
145
|
+
"streaming_metrics_hook_context_check",
|
|
146
|
+
plugin="claude_api",
|
|
147
|
+
has_context=self.context is not None,
|
|
148
|
+
context_type=type(self.context).__name__ if self.context else None,
|
|
149
|
+
context_keys=list(self.context.keys()) if self.context else [],
|
|
150
|
+
has_hook_registry="hook_registry" in (self.context or {}),
|
|
151
|
+
has_plugin_registry="plugin_registry" in (self.context or {}),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Get hook registry from context
|
|
155
|
+
from ccproxy.core.plugins.hooks.registry import HookRegistry
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
hook_registry = self.context.get(HookRegistry)
|
|
159
|
+
except ValueError:
|
|
160
|
+
logger.warning(
|
|
161
|
+
"streaming_metrics_hook_not_registered",
|
|
162
|
+
reason="no_hook_registry",
|
|
163
|
+
plugin="claude_api",
|
|
164
|
+
context_keys=list(self.context.keys()) if self.context else [],
|
|
165
|
+
)
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
# Get pricing service from plugin registry if available
|
|
169
|
+
pricing_service = None
|
|
170
|
+
if "plugin_registry" in self.context:
|
|
171
|
+
try:
|
|
172
|
+
from ccproxy.plugins.pricing.service import PricingService
|
|
173
|
+
|
|
174
|
+
plugin_registry = self.context["plugin_registry"]
|
|
175
|
+
logger.debug(
|
|
176
|
+
"getting_pricing_service",
|
|
177
|
+
plugin="claude_api",
|
|
178
|
+
registry_type=type(plugin_registry).__name__,
|
|
179
|
+
)
|
|
180
|
+
pricing_service = plugin_registry.get_service(
|
|
181
|
+
"pricing", PricingService
|
|
182
|
+
)
|
|
183
|
+
logger.debug(
|
|
184
|
+
"pricing_service_obtained",
|
|
185
|
+
plugin="claude_api",
|
|
186
|
+
service_type=type(pricing_service).__name__
|
|
187
|
+
if pricing_service
|
|
188
|
+
else None,
|
|
189
|
+
is_none=pricing_service is None,
|
|
190
|
+
)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.debug(
|
|
193
|
+
"pricing_service_not_available_for_hook",
|
|
194
|
+
plugin="claude_api",
|
|
195
|
+
error=str(e),
|
|
196
|
+
error_type=type(e).__name__,
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
logger.debug(
|
|
200
|
+
"plugin_registry_not_in_context",
|
|
201
|
+
plugin="claude_api",
|
|
202
|
+
context_keys=list(self.context.keys()) if self.context else [],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Create and register the hook
|
|
206
|
+
from .hooks import ClaudeAPIStreamingMetricsHook
|
|
207
|
+
|
|
208
|
+
# Pass both pricing_service (if available now) and plugin_registry (for lazy loading)
|
|
209
|
+
metrics_hook = ClaudeAPIStreamingMetricsHook(
|
|
210
|
+
pricing_service=pricing_service,
|
|
211
|
+
plugin_registry=self.context.get("plugin_registry"),
|
|
212
|
+
)
|
|
213
|
+
hook_registry.register(metrics_hook)
|
|
214
|
+
|
|
215
|
+
logger.debug(
|
|
216
|
+
"streaming_metrics_hook_registered",
|
|
217
|
+
plugin="claude_api",
|
|
218
|
+
hook_name=metrics_hook.name,
|
|
219
|
+
priority=metrics_hook.priority,
|
|
220
|
+
has_pricing=pricing_service is not None,
|
|
221
|
+
pricing_service_type=type(pricing_service).__name__
|
|
222
|
+
if pricing_service
|
|
223
|
+
else "None",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(
|
|
228
|
+
"streaming_metrics_hook_registration_failed",
|
|
229
|
+
plugin="claude_api",
|
|
230
|
+
error=str(e),
|
|
231
|
+
exc_info=e,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class ClaudeAPIFactory(BaseProviderPluginFactory):
|
|
236
|
+
"""Factory for Claude API plugin."""
|
|
237
|
+
|
|
238
|
+
cli_safe = False # Heavy provider plugin - not safe for CLI
|
|
239
|
+
|
|
240
|
+
# Plugin configuration via class attributes
|
|
241
|
+
plugin_name = "claude_api"
|
|
242
|
+
plugin_description = "Claude API provider plugin with support for both native Anthropic format and OpenAI-compatible format"
|
|
243
|
+
runtime_class = ClaudeAPIRuntime
|
|
244
|
+
adapter_class = ClaudeAPIAdapter
|
|
245
|
+
detection_service_class = ClaudeAPIDetectionService
|
|
246
|
+
config_class = ClaudeAPISettings
|
|
247
|
+
# String-based auth manager reference
|
|
248
|
+
auth_manager_name = "oauth_claude"
|
|
249
|
+
credentials_manager_class = ClaudeApiTokenManager
|
|
250
|
+
routers = [
|
|
251
|
+
RouterSpec(router=claude_api_router, prefix="/claude", tags=["claude-api"]),
|
|
252
|
+
]
|
|
253
|
+
# OAuth provider is optional because the token manager can operate
|
|
254
|
+
# without a globally-registered auth provider. When present, it enables
|
|
255
|
+
# first-class OAuth flows in the UI.
|
|
256
|
+
dependencies = ["oauth_claude"]
|
|
257
|
+
optional_requires = ["pricing"]
|
|
258
|
+
|
|
259
|
+
# No format adapters needed - core provides all required conversions
|
|
260
|
+
format_adapters: list[FormatAdapterSpec] = []
|
|
261
|
+
|
|
262
|
+
# Define requirements for adapters this plugin needs
|
|
263
|
+
requires_format_adapters: list[FormatPair] = [
|
|
264
|
+
# Core-provided adapters handle remaining dependencies
|
|
265
|
+
]
|
|
266
|
+
tasks = [
|
|
267
|
+
TaskSpec(
|
|
268
|
+
task_name="claude_api_detection_refresh",
|
|
269
|
+
task_type="claude_api_detection_refresh",
|
|
270
|
+
task_class=ClaudeAPIDetectionRefreshTask,
|
|
271
|
+
interval_seconds=3600,
|
|
272
|
+
enabled=True,
|
|
273
|
+
kwargs={"skip_initial_run": True},
|
|
274
|
+
)
|
|
275
|
+
]
|
|
276
|
+
tool_accumulator_class = ClaudeAccumulator
|
|
277
|
+
|
|
278
|
+
def create_detection_service(self, context: PluginContext) -> Any:
|
|
279
|
+
"""Create detection service and inject it into task kwargs.
|
|
280
|
+
|
|
281
|
+
Ensures the scheduled detection-refresh task uses the same instance
|
|
282
|
+
that the runtime receives via context.
|
|
283
|
+
"""
|
|
284
|
+
detection_service = super().create_detection_service(context)
|
|
285
|
+
|
|
286
|
+
if self.manifest.tasks and detection_service is not None:
|
|
287
|
+
for task_spec in self.manifest.tasks:
|
|
288
|
+
if task_spec.task_name == "claude_api_detection_refresh":
|
|
289
|
+
task_spec.kwargs["detection_service"] = detection_service
|
|
290
|
+
|
|
291
|
+
return detection_service
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# Create factory instance for plugin discovery
|
|
295
|
+
# Note: This follows the existing pattern but creates a singleton
|
|
296
|
+
factory = ClaudeAPIFactory()
|
|
297
|
+
|
|
298
|
+
__all__ = ["ClaudeAPIFactory", "ClaudeAPIRuntime", "factory"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""API routes for Claude API plugin."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import TYPE_CHECKING, Annotated, Any, cast
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, Request
|
|
7
|
+
from fastapi.responses import Response, StreamingResponse
|
|
8
|
+
|
|
9
|
+
from ccproxy.api.decorators import with_format_chain
|
|
10
|
+
from ccproxy.api.dependencies import (
|
|
11
|
+
get_plugin_adapter,
|
|
12
|
+
get_provider_config_dependency,
|
|
13
|
+
)
|
|
14
|
+
from ccproxy.auth.dependencies import ConditionalAuthDep
|
|
15
|
+
from ccproxy.core.constants import (
|
|
16
|
+
FORMAT_ANTHROPIC_MESSAGES,
|
|
17
|
+
FORMAT_OPENAI_CHAT,
|
|
18
|
+
FORMAT_OPENAI_RESPONSES,
|
|
19
|
+
UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES,
|
|
20
|
+
)
|
|
21
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
22
|
+
from ccproxy.llms.models import anthropic as anthropic_models
|
|
23
|
+
from ccproxy.llms.models import openai as openai_models
|
|
24
|
+
from ccproxy.streaming import DeferredStreaming
|
|
25
|
+
|
|
26
|
+
from .config import ClaudeAPISettings
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
logger = get_plugin_logger()
|
|
33
|
+
|
|
34
|
+
ClaudeAPIAdapterDep = Annotated[Any, Depends(get_plugin_adapter("claude_api"))]
|
|
35
|
+
ClaudeAPIConfigDep = Annotated[
|
|
36
|
+
ClaudeAPISettings,
|
|
37
|
+
Depends(get_provider_config_dependency("claude_api", ClaudeAPISettings)),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
APIResponse = Response | StreamingResponse | DeferredStreaming
|
|
41
|
+
|
|
42
|
+
# Main API Router - Core Claude API endpoints
|
|
43
|
+
router = APIRouter()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _cast_result(result: object) -> APIResponse:
|
|
47
|
+
return cast(APIResponse, result)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _handle_adapter_request(
|
|
51
|
+
request: Request,
|
|
52
|
+
adapter: Any,
|
|
53
|
+
) -> APIResponse:
|
|
54
|
+
result = await adapter.handle_request(request)
|
|
55
|
+
return _cast_result(result)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@router.post(
|
|
59
|
+
"/v1/messages",
|
|
60
|
+
response_model=anthropic_models.MessageResponse | anthropic_models.APIError,
|
|
61
|
+
)
|
|
62
|
+
@with_format_chain(
|
|
63
|
+
[FORMAT_ANTHROPIC_MESSAGES], endpoint=UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES
|
|
64
|
+
)
|
|
65
|
+
async def create_anthropic_message(
|
|
66
|
+
request: Request,
|
|
67
|
+
_: anthropic_models.CreateMessageRequest,
|
|
68
|
+
auth: ConditionalAuthDep,
|
|
69
|
+
adapter: ClaudeAPIAdapterDep,
|
|
70
|
+
) -> APIResponse:
|
|
71
|
+
"""Create a message using Claude AI with native Anthropic format."""
|
|
72
|
+
return await _handle_adapter_request(request, adapter)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@router.post(
|
|
76
|
+
"/v1/chat/completions",
|
|
77
|
+
response_model=openai_models.ChatCompletionResponse | openai_models.ErrorResponse,
|
|
78
|
+
)
|
|
79
|
+
@with_format_chain(
|
|
80
|
+
[FORMAT_OPENAI_CHAT, FORMAT_ANTHROPIC_MESSAGES],
|
|
81
|
+
endpoint=UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES,
|
|
82
|
+
)
|
|
83
|
+
async def create_openai_chat_completion(
|
|
84
|
+
request: Request,
|
|
85
|
+
_: openai_models.ChatCompletionRequest,
|
|
86
|
+
auth: ConditionalAuthDep,
|
|
87
|
+
adapter: ClaudeAPIAdapterDep,
|
|
88
|
+
) -> APIResponse:
|
|
89
|
+
"""Create a chat completion using Claude AI with OpenAI-compatible format."""
|
|
90
|
+
return await _handle_adapter_request(request, adapter)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router.post("/v1/responses", response_model=None)
|
|
94
|
+
@with_format_chain(
|
|
95
|
+
[FORMAT_OPENAI_RESPONSES, FORMAT_ANTHROPIC_MESSAGES],
|
|
96
|
+
endpoint=UPSTREAM_ENDPOINT_ANTHROPIC_MESSAGES,
|
|
97
|
+
)
|
|
98
|
+
async def claude_v1_responses(
|
|
99
|
+
request: Request,
|
|
100
|
+
auth: ConditionalAuthDep,
|
|
101
|
+
adapter: ClaudeAPIAdapterDep,
|
|
102
|
+
) -> APIResponse:
|
|
103
|
+
"""Response API compatible endpoint using Claude backend."""
|
|
104
|
+
# Ensure format chain is present for request/response conversion
|
|
105
|
+
# format chain and endpoint set by decorator
|
|
106
|
+
session_id = request.headers.get("session_id") or str(uuid.uuid4())
|
|
107
|
+
return await _handle_adapter_request(request, adapter)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@router.get("/v1/models", response_model=openai_models.ModelList)
|
|
111
|
+
async def list_models(
|
|
112
|
+
request: Request,
|
|
113
|
+
auth: ConditionalAuthDep,
|
|
114
|
+
config: ClaudeAPIConfigDep,
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
"""List available Claude models from configuration."""
|
|
117
|
+
models = [card.model_dump(mode="json") for card in config.models_endpoint]
|
|
118
|
+
return {"object": "list", "data": models}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Claude API streaming metrics extraction utilities.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for extracting token usage from
|
|
4
|
+
Anthropic streaming responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, TypedDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UsageData(TypedDict, total=False):
|
|
11
|
+
"""Token usage data extracted from streaming or non-streaming responses."""
|
|
12
|
+
|
|
13
|
+
input_tokens: int | None
|
|
14
|
+
output_tokens: int | None
|
|
15
|
+
cache_read_input_tokens: int | None
|
|
16
|
+
cache_creation_input_tokens: int | None
|
|
17
|
+
event_type: str | None # Extra field for tracking event source
|
|
18
|
+
model: str | None # Extra field for model information
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def extract_usage_from_streaming_chunk(chunk_data: Any) -> UsageData | None:
|
|
22
|
+
"""Extract usage information from Anthropic streaming response chunk.
|
|
23
|
+
|
|
24
|
+
This function looks for usage information in both message_start and message_delta events
|
|
25
|
+
from Anthropic's streaming API responses. message_start contains initial input tokens,
|
|
26
|
+
message_delta contains final output tokens.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
chunk_data: Streaming response chunk dictionary
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
UsageData with token counts or None if no usage found
|
|
33
|
+
"""
|
|
34
|
+
if not isinstance(chunk_data, dict):
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
chunk_type = chunk_data.get("type")
|
|
38
|
+
|
|
39
|
+
# Look for message_start events with initial usage (input tokens)
|
|
40
|
+
if chunk_type == "message_start" and "message" in chunk_data:
|
|
41
|
+
message = chunk_data["message"]
|
|
42
|
+
# Extract model name if present
|
|
43
|
+
model = message.get("model")
|
|
44
|
+
if "usage" in message:
|
|
45
|
+
usage = message["usage"]
|
|
46
|
+
return UsageData(
|
|
47
|
+
input_tokens=usage.get("input_tokens"),
|
|
48
|
+
output_tokens=usage.get(
|
|
49
|
+
"output_tokens"
|
|
50
|
+
), # Initial output tokens (usually small)
|
|
51
|
+
cache_read_input_tokens=usage.get("cache_read_input_tokens"),
|
|
52
|
+
cache_creation_input_tokens=usage.get("cache_creation_input_tokens"),
|
|
53
|
+
event_type="message_start",
|
|
54
|
+
model=model, # Include model in usage data
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Look for message_delta events with final usage (output tokens)
|
|
58
|
+
elif chunk_type == "message_delta" and "usage" in chunk_data:
|
|
59
|
+
usage = chunk_data["usage"]
|
|
60
|
+
return UsageData(
|
|
61
|
+
input_tokens=usage.get("input_tokens"), # Usually None in delta
|
|
62
|
+
output_tokens=usage.get("output_tokens"), # Final output token count
|
|
63
|
+
cache_read_input_tokens=usage.get("cache_read_input_tokens"),
|
|
64
|
+
cache_creation_input_tokens=usage.get("cache_creation_input_tokens"),
|
|
65
|
+
event_type="message_delta",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return None
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Scheduled tasks for Claude API plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
6
|
+
from ccproxy.scheduler.tasks import BaseScheduledTask
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .detection_service import ClaudeAPIDetectionService
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = get_plugin_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClaudeAPIDetectionRefreshTask(BaseScheduledTask):
|
|
17
|
+
"""Task to periodically refresh Claude CLI detection headers."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
name: str,
|
|
22
|
+
interval_seconds: float,
|
|
23
|
+
detection_service: "ClaudeAPIDetectionService",
|
|
24
|
+
enabled: bool = True,
|
|
25
|
+
skip_initial_run: bool = True,
|
|
26
|
+
**kwargs: Any,
|
|
27
|
+
):
|
|
28
|
+
super().__init__(
|
|
29
|
+
name=name,
|
|
30
|
+
interval_seconds=interval_seconds,
|
|
31
|
+
enabled=enabled,
|
|
32
|
+
**kwargs,
|
|
33
|
+
)
|
|
34
|
+
self.detection_service = detection_service
|
|
35
|
+
self.skip_initial_run = skip_initial_run
|
|
36
|
+
self._first_run = True
|
|
37
|
+
|
|
38
|
+
async def run(self) -> bool:
|
|
39
|
+
"""Execute the detection refresh."""
|
|
40
|
+
if self._first_run and self.skip_initial_run:
|
|
41
|
+
self._first_run = False
|
|
42
|
+
logger.debug(
|
|
43
|
+
"claude_api_detection_refresh_skipped_initial",
|
|
44
|
+
task_name=self.name,
|
|
45
|
+
)
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
self._first_run = False
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
logger.info(
|
|
52
|
+
"claude_api_detection_refresh_starting",
|
|
53
|
+
task_name=self.name,
|
|
54
|
+
)
|
|
55
|
+
detection_data = await self.detection_service.initialize_detection()
|
|
56
|
+
|
|
57
|
+
logger.info(
|
|
58
|
+
"claude_api_detection_refresh_completed",
|
|
59
|
+
task_name=self.name,
|
|
60
|
+
version=detection_data.claude_version if detection_data else "unknown",
|
|
61
|
+
)
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(
|
|
66
|
+
"claude_api_detection_refresh_failed",
|
|
67
|
+
task_name=self.name,
|
|
68
|
+
error=str(e),
|
|
69
|
+
)
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
async def setup(self) -> None:
|
|
73
|
+
"""Setup before task execution starts."""
|
|
74
|
+
logger.debug(
|
|
75
|
+
"claude_api_detection_refresh_setup",
|
|
76
|
+
task_name=self.name,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
async def cleanup(self) -> None:
|
|
80
|
+
"""Cleanup after task execution stops."""
|
|
81
|
+
logger.info(
|
|
82
|
+
"claude_api_detection_refresh_cleanup",
|
|
83
|
+
task_name=self.name,
|
|
84
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Claude SDK Plugin
|
|
2
|
+
|
|
3
|
+
Runs Claude through the local Claude Code SDK and CLI with session management.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
- Wraps the SDK adapter with reusable session and streaming support
|
|
7
|
+
- Validates CLI availability via the detection service and refresh task
|
|
8
|
+
- Shares the Claude accumulator to emit streaming metrics comparable to the API
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
- `ClaudeSDKSettings` covers CLI discovery, auth, and session pooling options
|
|
12
|
+
- Requires the Claude CLI to be installed and reachable on `PATH`
|
|
13
|
+
- Generate defaults with `python3 scripts/generate_config_from_model.py \
|
|
14
|
+
--format toml --plugin claude_sdk --config-class ClaudeSDKSettings`
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[plugins.claude_sdk]
|
|
18
|
+
# enabled = true
|
|
19
|
+
# base_url = "claude-sdk://local"
|
|
20
|
+
# session_pool_enabled = false
|
|
21
|
+
# session_pool_size = 5
|
|
22
|
+
# include_system_messages_in_stream = true
|
|
23
|
+
# sdk_message_mode = "formatted"
|
|
24
|
+
|
|
25
|
+
[plugins.claude_sdk.sdk_session_pool]
|
|
26
|
+
# enabled = true
|
|
27
|
+
# session_ttl = 3600
|
|
28
|
+
# max_sessions = 1000
|
|
29
|
+
# cleanup_interval = 300
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Related Components
|
|
33
|
+
- `adapter.py`: bridge between CCProxy requests and the SDK client
|
|
34
|
+
- `tasks.py`: periodic detection refresh for CLI state
|
|
35
|
+
- `routes.py`: FastAPI router served under `/claude/sdk`
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Claude SDK integration module."""
|
|
2
|
+
|
|
3
|
+
from .client import ClaudeSDKClient
|
|
4
|
+
from .exceptions import ClaudeSDKError, StreamTimeoutError
|
|
5
|
+
from .models import (
|
|
6
|
+
AssistantMessage,
|
|
7
|
+
ContentBlock,
|
|
8
|
+
ExtendedContentBlock,
|
|
9
|
+
ResultMessage,
|
|
10
|
+
ResultMessageBlock,
|
|
11
|
+
SDKContentBlock,
|
|
12
|
+
SDKMessage,
|
|
13
|
+
SDKMessageContent,
|
|
14
|
+
SDKMessageMode,
|
|
15
|
+
SystemMessage,
|
|
16
|
+
TextBlock,
|
|
17
|
+
ThinkingBlock,
|
|
18
|
+
ToolResultBlock,
|
|
19
|
+
ToolResultSDKBlock,
|
|
20
|
+
ToolUseBlock,
|
|
21
|
+
ToolUseSDKBlock,
|
|
22
|
+
UserMessage,
|
|
23
|
+
convert_sdk_result_message,
|
|
24
|
+
convert_sdk_system_message,
|
|
25
|
+
convert_sdk_text_block,
|
|
26
|
+
convert_sdk_tool_result_block,
|
|
27
|
+
convert_sdk_tool_use_block,
|
|
28
|
+
create_sdk_message,
|
|
29
|
+
to_sdk_variant,
|
|
30
|
+
)
|
|
31
|
+
from .options import OptionsHandler
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Lazy import to avoid circular dependency
|
|
35
|
+
def __getattr__(name: str) -> object:
|
|
36
|
+
if name == "MessageConverter":
|
|
37
|
+
from .converter import MessageConverter
|
|
38
|
+
|
|
39
|
+
return MessageConverter
|
|
40
|
+
if name == "parse_formatted_sdk_content":
|
|
41
|
+
from .parser import parse_formatted_sdk_content
|
|
42
|
+
|
|
43
|
+
return parse_formatted_sdk_content
|
|
44
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Session Context will be imported here once created
|
|
49
|
+
"ClaudeSDKClient",
|
|
50
|
+
"ClaudeSDKError",
|
|
51
|
+
"StreamTimeoutError",
|
|
52
|
+
"MessageConverter", # Lazy loaded
|
|
53
|
+
"OptionsHandler",
|
|
54
|
+
"parse_formatted_sdk_content", # Lazy loaded
|
|
55
|
+
# Re-export SDK models from core adapter
|
|
56
|
+
"AssistantMessage",
|
|
57
|
+
"ContentBlock",
|
|
58
|
+
"ExtendedContentBlock",
|
|
59
|
+
"ResultMessage",
|
|
60
|
+
"ResultMessageBlock",
|
|
61
|
+
"SDKContentBlock",
|
|
62
|
+
"SDKMessage",
|
|
63
|
+
"SDKMessageContent",
|
|
64
|
+
"SDKMessageMode",
|
|
65
|
+
"SystemMessage",
|
|
66
|
+
"TextBlock",
|
|
67
|
+
"ThinkingBlock",
|
|
68
|
+
"ToolResultBlock",
|
|
69
|
+
"ToolResultSDKBlock",
|
|
70
|
+
"ToolUseBlock",
|
|
71
|
+
"ToolUseSDKBlock",
|
|
72
|
+
"UserMessage",
|
|
73
|
+
"convert_sdk_result_message",
|
|
74
|
+
"convert_sdk_system_message",
|
|
75
|
+
"convert_sdk_text_block",
|
|
76
|
+
"convert_sdk_tool_result_block",
|
|
77
|
+
"convert_sdk_tool_use_block",
|
|
78
|
+
"create_sdk_message",
|
|
79
|
+
"to_sdk_variant",
|
|
80
|
+
]
|