ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Max tokens adapter implementation using hook system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
9
|
+
from ccproxy.core.plugins.hooks import Hook, HookContext, HookEvent
|
|
10
|
+
from ccproxy.core.plugins.hooks.layers import HookLayer
|
|
11
|
+
|
|
12
|
+
from .config import MaxTokensConfig
|
|
13
|
+
from .service import TokenLimitsService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = get_plugin_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MaxTokensHook(Hook):
|
|
20
|
+
"""Hook that enforces `max_tokens` limits before provider dispatch."""
|
|
21
|
+
|
|
22
|
+
name = "max_tokens"
|
|
23
|
+
events = [HookEvent.PROVIDER_REQUEST_PREPARED]
|
|
24
|
+
priority = HookLayer.PROCESSING # Run before observation hooks
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: MaxTokensConfig, service: TokenLimitsService):
|
|
27
|
+
"""Initialize max tokens hook."""
|
|
28
|
+
self.config = config
|
|
29
|
+
self.service = service
|
|
30
|
+
|
|
31
|
+
async def __call__(self, context: HookContext) -> None:
|
|
32
|
+
"""Adjust token limits in provider request payload when required."""
|
|
33
|
+
if not self.config.enabled:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
if context.event is not HookEvent.PROVIDER_REQUEST_PREPARED:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
provider = context.provider
|
|
40
|
+
if provider and not self.config.should_process_provider(provider):
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
payload = context.data.get("body")
|
|
44
|
+
if not isinstance(payload, dict):
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
model = payload.get("model")
|
|
48
|
+
if not isinstance(model, str):
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
provider_model = context.metadata.get("provider_model")
|
|
52
|
+
if not isinstance(provider_model, str):
|
|
53
|
+
provider_model = model
|
|
54
|
+
|
|
55
|
+
alias_map = context.metadata.get("_model_alias_map")
|
|
56
|
+
client_model = context.metadata.get("client_model")
|
|
57
|
+
if not client_model and isinstance(alias_map, dict):
|
|
58
|
+
client_model = alias_map.get(provider_model) or alias_map.get(model)
|
|
59
|
+
|
|
60
|
+
modifiers = context.data.setdefault("modifiers", {})
|
|
61
|
+
modified = False
|
|
62
|
+
|
|
63
|
+
# Ensure we work with the latest payload returned from the service
|
|
64
|
+
modified_payload, modification = self.service.modify_max_tokens(
|
|
65
|
+
payload, provider_model, provider
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if modification and modification.was_modified():
|
|
69
|
+
payload = modified_payload
|
|
70
|
+
context.data["body"] = payload
|
|
71
|
+
modifiers["max_tokens"] = {
|
|
72
|
+
"original": modification.original_max_tokens,
|
|
73
|
+
"new": modification.new_max_tokens,
|
|
74
|
+
"reason": modification.reason,
|
|
75
|
+
}
|
|
76
|
+
modified = True
|
|
77
|
+
else:
|
|
78
|
+
payload = modified_payload
|
|
79
|
+
context.data["body"] = payload
|
|
80
|
+
|
|
81
|
+
current_max_output = payload.get("max_output_tokens")
|
|
82
|
+
provider_limit = self.service.get_max_output_tokens(provider_model)
|
|
83
|
+
if provider_limit is None and self.config.fallback_max_tokens:
|
|
84
|
+
provider_limit = self.config.fallback_max_tokens
|
|
85
|
+
|
|
86
|
+
new_max_output: int | None = None
|
|
87
|
+
output_reason: str | None = None
|
|
88
|
+
|
|
89
|
+
if isinstance(current_max_output, int) and current_max_output > 0:
|
|
90
|
+
original_limit = (
|
|
91
|
+
self.service.get_max_output_tokens(client_model)
|
|
92
|
+
if client_model
|
|
93
|
+
else None
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
provider_limit
|
|
98
|
+
and original_limit
|
|
99
|
+
and client_model
|
|
100
|
+
and client_model != provider_model
|
|
101
|
+
and current_max_output == original_limit
|
|
102
|
+
and provider_limit != original_limit
|
|
103
|
+
):
|
|
104
|
+
new_max_output = provider_limit
|
|
105
|
+
output_reason = "max_output_tokens_aligned_with_mapped_model"
|
|
106
|
+
elif provider_limit and current_max_output > provider_limit:
|
|
107
|
+
new_max_output = provider_limit
|
|
108
|
+
output_reason = "max_output_tokens_capped_to_provider_limit"
|
|
109
|
+
|
|
110
|
+
if new_max_output is not None and new_max_output != current_max_output:
|
|
111
|
+
payload["max_output_tokens"] = new_max_output
|
|
112
|
+
modifiers["max_output_tokens"] = {
|
|
113
|
+
"original": current_max_output,
|
|
114
|
+
"new": new_max_output,
|
|
115
|
+
"reason": output_reason,
|
|
116
|
+
}
|
|
117
|
+
modified = True
|
|
118
|
+
|
|
119
|
+
if self.config.log_modifications:
|
|
120
|
+
logger.info(
|
|
121
|
+
"max_output_tokens_adjusted",
|
|
122
|
+
provider=provider,
|
|
123
|
+
provider_model=provider_model,
|
|
124
|
+
client_model=client_model,
|
|
125
|
+
original=current_max_output,
|
|
126
|
+
new=new_max_output,
|
|
127
|
+
reason=output_reason,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if modified:
|
|
131
|
+
context.data["body"] = payload
|
|
132
|
+
context.data["body_kind"] = "json"
|
|
133
|
+
context.data["body_raw"] = json.dumps(payload).encode("utf-8")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class MaxTokensAdapter:
|
|
137
|
+
"""Max tokens adapter using hook-based request interception."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, config: MaxTokensConfig):
|
|
140
|
+
"""Initialize max tokens adapter."""
|
|
141
|
+
self.config = config
|
|
142
|
+
self.service = TokenLimitsService(config)
|
|
143
|
+
self.hook = MaxTokensHook(config, self.service)
|
|
144
|
+
self._initialized = False
|
|
145
|
+
|
|
146
|
+
async def initialize(self) -> None:
|
|
147
|
+
"""Initialize the adapter and register hooks."""
|
|
148
|
+
if self._initialized:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
if not self.config.enabled:
|
|
152
|
+
logger.debug("max_tokens_adapter_disabled")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
# Initialize the service
|
|
156
|
+
self.service.initialize()
|
|
157
|
+
|
|
158
|
+
# Register hook with hook manager
|
|
159
|
+
try:
|
|
160
|
+
from ccproxy.services.container import ServiceContainer
|
|
161
|
+
|
|
162
|
+
container = ServiceContainer.get_current(strict=False)
|
|
163
|
+
if container:
|
|
164
|
+
try:
|
|
165
|
+
hook_registry = container.get_hook_registry()
|
|
166
|
+
if hook_registry:
|
|
167
|
+
hook_registry.register(self.hook)
|
|
168
|
+
logger.debug(
|
|
169
|
+
"max_tokens_hook_registered",
|
|
170
|
+
hook_name=self.hook.name,
|
|
171
|
+
events=[event.value for event in self.hook.events],
|
|
172
|
+
priority=self.hook.priority,
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
logger.warning("no_hook_registry_available")
|
|
176
|
+
except Exception as service_error:
|
|
177
|
+
logger.warning(
|
|
178
|
+
"hook_registry_service_not_available",
|
|
179
|
+
error=str(service_error),
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
logger.warning("no_service_container_available")
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(
|
|
186
|
+
"failed_to_register_max_tokens_hook",
|
|
187
|
+
error=str(e),
|
|
188
|
+
exc_info=e,
|
|
189
|
+
)
|
|
190
|
+
# Continue without hook registration - plugin will be effectively disabled
|
|
191
|
+
|
|
192
|
+
self._initialized = True
|
|
193
|
+
logger.info("max_tokens_adapter_initialized")
|
|
194
|
+
|
|
195
|
+
async def cleanup(self) -> None:
|
|
196
|
+
"""Cleanup resources."""
|
|
197
|
+
if not self._initialized:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
# Unregister hook
|
|
202
|
+
from ccproxy.services.container import ServiceContainer
|
|
203
|
+
|
|
204
|
+
container = ServiceContainer.get_current(strict=False)
|
|
205
|
+
if container:
|
|
206
|
+
try:
|
|
207
|
+
hook_registry = container.get_hook_registry()
|
|
208
|
+
if hook_registry:
|
|
209
|
+
hook_registry.unregister(self.hook)
|
|
210
|
+
logger.debug("max_tokens_hook_unregistered")
|
|
211
|
+
except Exception as service_error:
|
|
212
|
+
logger.debug(
|
|
213
|
+
"hook_registry_service_not_available_during_cleanup",
|
|
214
|
+
error=str(service_error),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(
|
|
219
|
+
"failed_to_unregister_max_tokens_hook",
|
|
220
|
+
error=str(e),
|
|
221
|
+
exc_info=e,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self._initialized = False
|
|
225
|
+
logger.debug("max_tokens_adapter_cleanup_completed")
|
|
226
|
+
|
|
227
|
+
def get_modification_stats(self) -> dict[str, Any]:
|
|
228
|
+
"""Get statistics about max_tokens modifications."""
|
|
229
|
+
# This could be enhanced to track modification statistics
|
|
230
|
+
return {
|
|
231
|
+
"adapter_initialized": self._initialized,
|
|
232
|
+
"config_enabled": self.config.enabled,
|
|
233
|
+
"target_providers": self.config.target_providers,
|
|
234
|
+
"fallback_max_tokens": self.config.fallback_max_tokens,
|
|
235
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Configuration for max_tokens plugin."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, model_validator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MaxTokensConfig(BaseModel):
|
|
10
|
+
"""Configuration for the max_tokens plugin."""
|
|
11
|
+
|
|
12
|
+
enabled: bool = Field(
|
|
13
|
+
default=True, description="Whether the max_tokens plugin is enabled"
|
|
14
|
+
)
|
|
15
|
+
default_token_limits_file: str = Field(
|
|
16
|
+
default=str(Path(__file__).parent / "token_limits.json"),
|
|
17
|
+
description="Path to JSON file containing default token limits",
|
|
18
|
+
)
|
|
19
|
+
fallback_max_tokens: int = Field(
|
|
20
|
+
default=4096,
|
|
21
|
+
ge=1,
|
|
22
|
+
description="Fallback max_tokens when model limits are unknown",
|
|
23
|
+
)
|
|
24
|
+
apply_to_all_providers: bool = Field(
|
|
25
|
+
default=True,
|
|
26
|
+
description="Whether to apply to all providers or only specific ones",
|
|
27
|
+
)
|
|
28
|
+
target_providers: list[str] = Field(
|
|
29
|
+
default_factory=lambda: ["claude_api", "claude_sdk", "codex", "copilot"],
|
|
30
|
+
description="List of providers to apply max_tokens modifications to",
|
|
31
|
+
)
|
|
32
|
+
require_pricing_data: bool = Field(
|
|
33
|
+
default=False,
|
|
34
|
+
description=(
|
|
35
|
+
"If True, only modify requests when pricing data is available. "
|
|
36
|
+
"If False, use fallback limits when pricing data is not available."
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
log_modifications: bool = Field(
|
|
40
|
+
default=True, description="Whether to log max_tokens modifications"
|
|
41
|
+
)
|
|
42
|
+
enforce_mode: bool = Field(
|
|
43
|
+
default=False,
|
|
44
|
+
description=(
|
|
45
|
+
"When enabled, always set max_tokens to the model's maximum limit, "
|
|
46
|
+
"ignoring the request's current max_tokens value"
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
prioritize_local_file: bool = Field(
|
|
50
|
+
default=False,
|
|
51
|
+
description=(
|
|
52
|
+
"When enabled, local token_limits.json values take precedence over "
|
|
53
|
+
"pricing cache values. When disabled, local file is only used as fallback "
|
|
54
|
+
"when pricing cache is unavailable or model is not found in cache."
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
modification_reasons: dict[str, str] = Field(
|
|
58
|
+
default_factory=lambda: {
|
|
59
|
+
"missing": "max_tokens was missing from request",
|
|
60
|
+
"invalid": "max_tokens was invalid or too high",
|
|
61
|
+
"exceeded": "max_tokens exceeded model limit",
|
|
62
|
+
"enforced": "max_tokens enforced to model limit (enforce mode)",
|
|
63
|
+
},
|
|
64
|
+
description="Reason templates for modifications",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@model_validator(mode="before")
|
|
68
|
+
@classmethod
|
|
69
|
+
def validate_config(cls, data: dict[str, Any]) -> dict[str, Any]:
|
|
70
|
+
"""Validate configuration values."""
|
|
71
|
+
# Ensure target_providers is a list
|
|
72
|
+
if isinstance(data.get("target_providers"), str):
|
|
73
|
+
data["target_providers"] = [data["target_providers"]]
|
|
74
|
+
return data
|
|
75
|
+
|
|
76
|
+
def should_process_provider(self, provider: str) -> bool:
|
|
77
|
+
"""Check if plugin should process requests for given provider."""
|
|
78
|
+
if self.apply_to_all_providers:
|
|
79
|
+
return True
|
|
80
|
+
return provider in self.target_providers
|
|
81
|
+
|
|
82
|
+
def get_modification_reason(self, reason_type: str) -> str:
|
|
83
|
+
"""Get modification reason text for given reason type."""
|
|
84
|
+
return self.modification_reasons.get(
|
|
85
|
+
reason_type, f"Unknown reason: {reason_type}"
|
|
86
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Pydantic models for max_tokens plugin."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ModelTokenLimits(BaseModel):
|
|
7
|
+
"""Token limits for a specific model."""
|
|
8
|
+
|
|
9
|
+
max_output_tokens: int = Field(
|
|
10
|
+
..., ge=1, description="Maximum output tokens for the model"
|
|
11
|
+
)
|
|
12
|
+
max_input_tokens: int | None = Field(
|
|
13
|
+
default=None, ge=1, description="Maximum input tokens for the model"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TokenLimitsData(BaseModel):
|
|
18
|
+
"""Complete token limits data for all models."""
|
|
19
|
+
|
|
20
|
+
models: dict[str, ModelTokenLimits] = Field(
|
|
21
|
+
default_factory=dict, description="Model name to token limits mapping"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def get_max_output_tokens(self, model_name: str) -> int | None:
|
|
25
|
+
"""Get maximum output tokens for a model."""
|
|
26
|
+
model_limits = self.models.get(model_name)
|
|
27
|
+
return model_limits.max_output_tokens if model_limits else None
|
|
28
|
+
|
|
29
|
+
def get_max_input_tokens(self, model_name: str) -> int | None:
|
|
30
|
+
"""Get maximum input tokens for a model."""
|
|
31
|
+
model_limits = self.models.get(model_name)
|
|
32
|
+
return model_limits.max_input_tokens if model_limits else None
|
|
33
|
+
|
|
34
|
+
def has_model(self, model_name: str) -> bool:
|
|
35
|
+
"""Check if model limits exist for the given model."""
|
|
36
|
+
return model_name in self.models
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MaxTokensModification(BaseModel):
|
|
40
|
+
"""Information about max_tokens modification made by the plugin."""
|
|
41
|
+
|
|
42
|
+
original_max_tokens: int | None = Field(
|
|
43
|
+
description="Original max_tokens value from request"
|
|
44
|
+
)
|
|
45
|
+
new_max_tokens: int | None = Field(
|
|
46
|
+
description="New max_tokens value after modification"
|
|
47
|
+
)
|
|
48
|
+
model: str = Field(description="Model name")
|
|
49
|
+
reason: str = Field(description="Reason for modification")
|
|
50
|
+
|
|
51
|
+
def was_modified(self) -> bool:
|
|
52
|
+
"""Check if max_tokens was modified."""
|
|
53
|
+
return self.original_max_tokens != self.new_max_tokens
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Max tokens 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
|
+
PluginManifest,
|
|
8
|
+
SystemPluginFactory,
|
|
9
|
+
SystemPluginRuntime,
|
|
10
|
+
)
|
|
11
|
+
from ccproxy.core.plugins.hooks import HookRegistry
|
|
12
|
+
|
|
13
|
+
from .adapter import MaxTokensHook
|
|
14
|
+
from .config import MaxTokensConfig
|
|
15
|
+
from .service import TokenLimitsService
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = get_plugin_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MaxTokensRuntime(SystemPluginRuntime):
|
|
22
|
+
"""Runtime for max_tokens plugin."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, manifest: PluginManifest):
|
|
25
|
+
"""Initialize runtime."""
|
|
26
|
+
super().__init__(manifest)
|
|
27
|
+
self.config: MaxTokensConfig | None = None
|
|
28
|
+
self.service: TokenLimitsService | None = None
|
|
29
|
+
self.hook: MaxTokensHook | None = None
|
|
30
|
+
self.hook_registered = False
|
|
31
|
+
|
|
32
|
+
async def _on_initialize(self) -> None:
|
|
33
|
+
"""Initialize the max tokens plugin."""
|
|
34
|
+
if not self.context:
|
|
35
|
+
raise RuntimeError("Context not set")
|
|
36
|
+
|
|
37
|
+
# Get configuration
|
|
38
|
+
config = self.context.get("config")
|
|
39
|
+
if not isinstance(config, MaxTokensConfig):
|
|
40
|
+
logger.debug("plugin_no_config_using_defaults", category="plugin")
|
|
41
|
+
# Use default config if none provided
|
|
42
|
+
self.config = MaxTokensConfig()
|
|
43
|
+
else:
|
|
44
|
+
self.config = config
|
|
45
|
+
|
|
46
|
+
logger.debug("initializing_max_tokens_plugin", enabled=self.config.enabled)
|
|
47
|
+
|
|
48
|
+
if not self.config.enabled:
|
|
49
|
+
logger.debug("max_tokens_plugin_disabled")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Create and initialize service
|
|
53
|
+
self.service = TokenLimitsService(self.config)
|
|
54
|
+
self.service.initialize()
|
|
55
|
+
|
|
56
|
+
# Create hook instance
|
|
57
|
+
self.hook = MaxTokensHook(self.config, self.service)
|
|
58
|
+
|
|
59
|
+
# Attempt to register hook with available registry sources
|
|
60
|
+
hook_registry: HookRegistry | None = None
|
|
61
|
+
|
|
62
|
+
# 1. Directly from context (preferred when ServiceContainer wires it)
|
|
63
|
+
hook_registry = self.context.get("hook_registry")
|
|
64
|
+
|
|
65
|
+
# 2. Fallback to service container if available
|
|
66
|
+
if not hook_registry:
|
|
67
|
+
container = self.context.get("service_container")
|
|
68
|
+
if container:
|
|
69
|
+
try:
|
|
70
|
+
hook_registry = container.get_hook_registry()
|
|
71
|
+
except Exception as container_error: # pragma: no cover - defensive
|
|
72
|
+
logger.debug(
|
|
73
|
+
"max_tokens_hook_registry_from_container_failed",
|
|
74
|
+
error=str(container_error),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# 3. Fallback to app.state when running inside FastAPI app
|
|
78
|
+
if not hook_registry:
|
|
79
|
+
app = self.context.get("app")
|
|
80
|
+
if app and hasattr(app.state, "hook_registry"):
|
|
81
|
+
hook_registry = app.state.hook_registry
|
|
82
|
+
|
|
83
|
+
if hook_registry and isinstance(hook_registry, HookRegistry):
|
|
84
|
+
hook_registry.register(self.hook)
|
|
85
|
+
self.hook_registered = True
|
|
86
|
+
logger.debug(
|
|
87
|
+
"max_tokens_hook_registered",
|
|
88
|
+
providers="*"
|
|
89
|
+
if self.config.apply_to_all_providers
|
|
90
|
+
else self.config.target_providers,
|
|
91
|
+
fallback=self.config.fallback_max_tokens,
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
logger.warning(
|
|
95
|
+
"max_tokens_hook_registry_unavailable",
|
|
96
|
+
message="max_tokens adjustments disabled",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
logger.info(
|
|
100
|
+
"max_tokens_plugin_initialized",
|
|
101
|
+
target_providers=self.config.target_providers,
|
|
102
|
+
fallback_max_tokens=self.config.fallback_max_tokens,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def _on_shutdown(self) -> None:
|
|
106
|
+
"""Shutdown the plugin and cleanup resources."""
|
|
107
|
+
logger.debug("shutting_down_max_tokens_plugin")
|
|
108
|
+
|
|
109
|
+
# Unregister hook if we registered one
|
|
110
|
+
if self.hook:
|
|
111
|
+
hook_registry: HookRegistry | None = None
|
|
112
|
+
if self.context:
|
|
113
|
+
hook_registry = self.context.get("hook_registry")
|
|
114
|
+
if not hook_registry:
|
|
115
|
+
container = self.context.get("service_container")
|
|
116
|
+
if container:
|
|
117
|
+
try:
|
|
118
|
+
hook_registry = container.get_hook_registry()
|
|
119
|
+
except Exception as container_error: # pragma: no cover
|
|
120
|
+
logger.debug(
|
|
121
|
+
"max_tokens_hook_registry_from_container_failed_shutdown",
|
|
122
|
+
error=str(container_error),
|
|
123
|
+
)
|
|
124
|
+
if not hook_registry:
|
|
125
|
+
app = self.context.get("app")
|
|
126
|
+
if app and hasattr(app.state, "hook_registry"):
|
|
127
|
+
hook_registry = app.state.hook_registry
|
|
128
|
+
|
|
129
|
+
if hook_registry and isinstance(hook_registry, HookRegistry):
|
|
130
|
+
hook_registry.unregister(self.hook)
|
|
131
|
+
self.hook_registered = False
|
|
132
|
+
logger.debug("max_tokens_hook_unregistered")
|
|
133
|
+
|
|
134
|
+
self.hook = None
|
|
135
|
+
|
|
136
|
+
if self.service:
|
|
137
|
+
self.service = None
|
|
138
|
+
|
|
139
|
+
logger.debug("max_tokens_plugin_shutdown_complete")
|
|
140
|
+
|
|
141
|
+
async def _get_health_details(self) -> dict[str, Any]:
|
|
142
|
+
"""Get health check details."""
|
|
143
|
+
try:
|
|
144
|
+
base_health = {
|
|
145
|
+
"type": "system",
|
|
146
|
+
"initialized": self.initialized,
|
|
147
|
+
"enabled": self.config.enabled if self.config else False,
|
|
148
|
+
"hook_registered": self.hook_registered,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if not self.config or not self.config.enabled:
|
|
152
|
+
return base_health
|
|
153
|
+
|
|
154
|
+
# Add service health info
|
|
155
|
+
health_details = base_health.copy()
|
|
156
|
+
|
|
157
|
+
if self.service:
|
|
158
|
+
health_details["models_count"] = len(
|
|
159
|
+
self.service.token_limits_data.models
|
|
160
|
+
)
|
|
161
|
+
health_details["fallback_max_tokens"] = self.config.fallback_max_tokens
|
|
162
|
+
|
|
163
|
+
return health_details
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error("health_check_failed", error=str(e))
|
|
167
|
+
return {
|
|
168
|
+
"type": "system",
|
|
169
|
+
"initialized": self.initialized,
|
|
170
|
+
"enabled": self.config.enabled if self.config else False,
|
|
171
|
+
"error": str(e),
|
|
172
|
+
"hook_registered": self.hook_registered,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class MaxTokensFactory(SystemPluginFactory):
|
|
177
|
+
"""Factory for max_tokens plugin."""
|
|
178
|
+
|
|
179
|
+
def __init__(self) -> None:
|
|
180
|
+
"""Initialize factory with manifest."""
|
|
181
|
+
# Create manifest - max_tokens logic is now integrated into HTTP adapter
|
|
182
|
+
manifest = PluginManifest(
|
|
183
|
+
name="max_tokens",
|
|
184
|
+
version="0.1.0",
|
|
185
|
+
description="Automatically sets max_tokens based on model limits when missing or invalid",
|
|
186
|
+
is_provider=False,
|
|
187
|
+
config_class=MaxTokensConfig,
|
|
188
|
+
provides=["max_tokens"], # This plugin provides the max_tokens service
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Initialize with manifest
|
|
192
|
+
super().__init__(manifest)
|
|
193
|
+
|
|
194
|
+
def create_runtime(self) -> MaxTokensRuntime:
|
|
195
|
+
"""Create runtime instance."""
|
|
196
|
+
return MaxTokensRuntime(self.manifest)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Export the factory instance
|
|
200
|
+
factory = MaxTokensFactory()
|