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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Pricing configuration settings."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
4
5
|
|
|
5
6
|
from pydantic import Field, field_validator
|
|
6
7
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
@@ -8,7 +9,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
8
9
|
from ccproxy.core.system import get_xdg_cache_home
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
class
|
|
12
|
+
class PricingConfig(BaseSettings):
|
|
12
13
|
"""
|
|
13
14
|
Configuration settings for the pricing system.
|
|
14
15
|
|
|
@@ -16,6 +17,11 @@ class PricingSettings(BaseSettings):
|
|
|
16
17
|
Settings can be configured via environment variables with PRICING__ prefix.
|
|
17
18
|
"""
|
|
18
19
|
|
|
20
|
+
enabled: bool = Field(
|
|
21
|
+
default=True,
|
|
22
|
+
description="Whether the pricing plugin is enabled",
|
|
23
|
+
)
|
|
24
|
+
|
|
19
25
|
# Cache settings
|
|
20
26
|
cache_dir: Path = Field(
|
|
21
27
|
default_factory=lambda: get_xdg_cache_home() / "ccproxy",
|
|
@@ -48,11 +54,6 @@ class PricingSettings(BaseSettings):
|
|
|
48
54
|
description="Whether to automatically update stale cache",
|
|
49
55
|
)
|
|
50
56
|
|
|
51
|
-
fallback_to_embedded: bool = Field(
|
|
52
|
-
default=True,
|
|
53
|
-
description="Whether to fallback to embedded pricing on failure",
|
|
54
|
-
)
|
|
55
|
-
|
|
56
57
|
# Memory cache settings
|
|
57
58
|
memory_cache_ttl: int = Field(
|
|
58
59
|
default=300,
|
|
@@ -61,6 +62,31 @@ class PricingSettings(BaseSettings):
|
|
|
61
62
|
description="Time to live for in-memory pricing cache in seconds",
|
|
62
63
|
)
|
|
63
64
|
|
|
65
|
+
# Task scheduling settings
|
|
66
|
+
update_interval_hours: float = Field(
|
|
67
|
+
default=6.0,
|
|
68
|
+
ge=0.1,
|
|
69
|
+
le=168.0, # Max 1 week
|
|
70
|
+
description="Hours between scheduled pricing updates",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
force_refresh_on_startup: bool = Field(
|
|
74
|
+
default=False,
|
|
75
|
+
description="Whether to force pricing refresh on plugin startup",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Backward-compat flag used by older tests; embedded pricing has been removed.
|
|
79
|
+
# Keeping this flag allows type checking and test configuration without effect.
|
|
80
|
+
fallback_to_embedded: bool = Field(
|
|
81
|
+
default=False,
|
|
82
|
+
description="(Deprecated) If true, fall back to embedded pricing when external data is unavailable",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
pricing_provider: Literal["claude", "anthropic", "openai", "all"] = Field(
|
|
86
|
+
default="all",
|
|
87
|
+
description="Which provider pricing to load: 'claude', 'anthropic', 'openai', or 'all'",
|
|
88
|
+
)
|
|
89
|
+
|
|
64
90
|
@field_validator("cache_dir", mode="before")
|
|
65
91
|
@classmethod
|
|
66
92
|
def validate_cache_dir(cls, v: str | Path | None) -> Path:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Pricing service exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PricingError(Exception):
|
|
5
|
+
"""Base exception for pricing-related errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PricingDataNotLoadedError(PricingError):
|
|
11
|
+
"""Raised when pricing data has not been loaded yet."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
message: str = "Pricing data not loaded yet - cost calculation unavailable",
|
|
16
|
+
):
|
|
17
|
+
self.message = message
|
|
18
|
+
super().__init__(self.message)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ModelPricingNotFoundError(PricingError):
|
|
22
|
+
"""Raised when pricing for a specific model is not found."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, model: str, message: str | None = None):
|
|
25
|
+
self.model = model
|
|
26
|
+
self.message = message or f"No pricing data available for model '{model}'"
|
|
27
|
+
super().__init__(self.message)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PricingServiceDisabledError(PricingError):
|
|
31
|
+
"""Raised when pricing service is disabled."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, message: str = "Pricing service is disabled"):
|
|
34
|
+
self.message = message
|
|
35
|
+
super().__init__(self.message)
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""Pricing data loader and format converter for LiteLLM pricing data."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
11
|
+
from ccproxy.plugins.claude_shared.model_defaults import (
|
|
12
|
+
DEFAULT_CLAUDE_MODEL_MAPPINGS,
|
|
13
|
+
)
|
|
14
|
+
from ccproxy.utils.model_mapper import ModelMapper
|
|
15
|
+
|
|
16
|
+
from .models import PricingData
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = get_plugin_logger(__name__)
|
|
20
|
+
|
|
21
|
+
_CLAUDE_MODEL_MAPPER = ModelMapper(DEFAULT_CLAUDE_MODEL_MAPPINGS)
|
|
22
|
+
_CLAUDE_ALIAS_MAP: dict[str, str] = {
|
|
23
|
+
rule.match: rule.target
|
|
24
|
+
for rule in DEFAULT_CLAUDE_MODEL_MAPPINGS
|
|
25
|
+
if rule.match.startswith("claude-")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_openai_model(model_name: str) -> bool:
|
|
30
|
+
lowered = model_name.lower()
|
|
31
|
+
return lowered.startswith(("gpt-", "o1", "o3", "text-davinci"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PricingLoader:
|
|
35
|
+
"""Loads and converts pricing data from LiteLLM format to internal format."""
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def extract_claude_models(
|
|
39
|
+
litellm_data: dict[str, Any], verbose: bool = True
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
"""Extract Claude model entries from LiteLLM data.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
litellm_data: Raw LiteLLM pricing data
|
|
45
|
+
verbose: Whether to log individual model discoveries
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dictionary with only Claude models
|
|
49
|
+
"""
|
|
50
|
+
claude_models = {}
|
|
51
|
+
|
|
52
|
+
for model_name, model_data in litellm_data.items():
|
|
53
|
+
# Check if this is a Claude model
|
|
54
|
+
if (
|
|
55
|
+
isinstance(model_data, dict)
|
|
56
|
+
and model_data.get("litellm_provider") == "anthropic"
|
|
57
|
+
and "claude" in model_name.lower()
|
|
58
|
+
):
|
|
59
|
+
claude_models[model_name] = model_data
|
|
60
|
+
if verbose:
|
|
61
|
+
logger.debug("claude_model_found", model_name=model_name)
|
|
62
|
+
|
|
63
|
+
if verbose:
|
|
64
|
+
logger.info(
|
|
65
|
+
"claude_models_extracted",
|
|
66
|
+
model_count=len(claude_models),
|
|
67
|
+
source="LiteLLM",
|
|
68
|
+
)
|
|
69
|
+
return claude_models
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def extract_openai_models(
|
|
73
|
+
litellm_data: dict[str, Any], verbose: bool = True
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
"""Extract OpenAI model entries from LiteLLM data.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
litellm_data: Raw LiteLLM pricing data
|
|
79
|
+
verbose: Whether to log individual model discoveries
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dictionary with only OpenAI models
|
|
83
|
+
"""
|
|
84
|
+
openai_models = {}
|
|
85
|
+
|
|
86
|
+
for model_name, model_data in litellm_data.items():
|
|
87
|
+
# Check if this is an OpenAI model
|
|
88
|
+
if isinstance(model_data, dict) and (
|
|
89
|
+
model_data.get("litellm_provider") == "openai"
|
|
90
|
+
or _is_openai_model(model_name)
|
|
91
|
+
):
|
|
92
|
+
openai_models[model_name] = model_data
|
|
93
|
+
if verbose:
|
|
94
|
+
logger.debug("openai_model_found", model_name=model_name)
|
|
95
|
+
|
|
96
|
+
if verbose:
|
|
97
|
+
logger.info(
|
|
98
|
+
"openai_models_extracted",
|
|
99
|
+
model_count=len(openai_models),
|
|
100
|
+
source="LiteLLM",
|
|
101
|
+
)
|
|
102
|
+
return openai_models
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def extract_anthropic_models(
|
|
106
|
+
litellm_data: dict[str, Any], verbose: bool = True
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
"""Extract all Anthropic model entries from LiteLLM data.
|
|
109
|
+
|
|
110
|
+
This includes Claude models and any other Anthropic models.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
litellm_data: Raw LiteLLM pricing data
|
|
114
|
+
verbose: Whether to log individual model discoveries
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dictionary with all Anthropic models
|
|
118
|
+
"""
|
|
119
|
+
anthropic_models = {}
|
|
120
|
+
|
|
121
|
+
for model_name, model_data in litellm_data.items():
|
|
122
|
+
# Check if this is an Anthropic model
|
|
123
|
+
if (
|
|
124
|
+
isinstance(model_data, dict)
|
|
125
|
+
and model_data.get("litellm_provider") == "anthropic"
|
|
126
|
+
):
|
|
127
|
+
anthropic_models[model_name] = model_data
|
|
128
|
+
if verbose:
|
|
129
|
+
logger.debug("anthropic_model_found", model_name=model_name)
|
|
130
|
+
|
|
131
|
+
if verbose:
|
|
132
|
+
logger.info(
|
|
133
|
+
"anthropic_models_extracted",
|
|
134
|
+
model_count=len(anthropic_models),
|
|
135
|
+
source="LiteLLM",
|
|
136
|
+
)
|
|
137
|
+
return anthropic_models
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def extract_models_by_provider(
|
|
141
|
+
litellm_data: dict[str, Any],
|
|
142
|
+
provider: Literal["anthropic", "openai", "all", "claude"] = "all",
|
|
143
|
+
verbose: bool = True,
|
|
144
|
+
) -> dict[str, Any]:
|
|
145
|
+
"""Extract models by provider from LiteLLM data.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
litellm_data: Raw LiteLLM pricing data
|
|
149
|
+
provider: Provider to extract models for ("anthropic", "openai", "claude", or "all")
|
|
150
|
+
verbose: Whether to log individual model discoveries
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Dictionary with models from specified provider(s)
|
|
154
|
+
"""
|
|
155
|
+
if provider == "claude":
|
|
156
|
+
return PricingLoader.extract_claude_models(litellm_data, verbose)
|
|
157
|
+
elif provider == "anthropic":
|
|
158
|
+
return PricingLoader.extract_anthropic_models(litellm_data, verbose)
|
|
159
|
+
elif provider == "openai":
|
|
160
|
+
return PricingLoader.extract_openai_models(litellm_data, verbose)
|
|
161
|
+
elif provider == "all":
|
|
162
|
+
# Extract all models that have pricing data
|
|
163
|
+
all_models = {}
|
|
164
|
+
for model_name, model_data in litellm_data.items():
|
|
165
|
+
if isinstance(model_data, dict):
|
|
166
|
+
all_models[model_name] = model_data
|
|
167
|
+
if verbose:
|
|
168
|
+
provider_name = model_data.get("litellm_provider", "unknown")
|
|
169
|
+
logger.debug(
|
|
170
|
+
"model_found",
|
|
171
|
+
model_name=model_name,
|
|
172
|
+
provider=provider_name,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if verbose:
|
|
176
|
+
logger.info(
|
|
177
|
+
"all_models_extracted",
|
|
178
|
+
model_count=len(all_models),
|
|
179
|
+
source="LiteLLM",
|
|
180
|
+
)
|
|
181
|
+
return all_models
|
|
182
|
+
else:
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"Invalid provider: {provider}. Use 'anthropic', 'openai', 'claude', or 'all'"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def convert_to_internal_format(
|
|
189
|
+
models: dict[str, Any], map_to_claude: bool = True, verbose: bool = True
|
|
190
|
+
) -> dict[str, dict[str, Decimal]]:
|
|
191
|
+
"""Convert LiteLLM pricing format to internal format.
|
|
192
|
+
|
|
193
|
+
LiteLLM format uses cost per token, we use cost per 1M tokens as Decimal.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
models: Models in LiteLLM format
|
|
197
|
+
map_to_claude: Whether to map model names to Claude equivalents
|
|
198
|
+
verbose: Whether to log individual model conversions
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Dictionary in internal pricing format
|
|
202
|
+
"""
|
|
203
|
+
internal_format = {}
|
|
204
|
+
|
|
205
|
+
for model_name, model_data in models.items():
|
|
206
|
+
try:
|
|
207
|
+
# Extract pricing fields
|
|
208
|
+
input_cost_per_token = model_data.get("input_cost_per_token")
|
|
209
|
+
output_cost_per_token = model_data.get("output_cost_per_token")
|
|
210
|
+
cache_creation_cost = model_data.get("cache_creation_input_token_cost")
|
|
211
|
+
cache_read_cost = model_data.get("cache_read_input_token_cost")
|
|
212
|
+
|
|
213
|
+
# Skip models without pricing info
|
|
214
|
+
if input_cost_per_token is None or output_cost_per_token is None:
|
|
215
|
+
if verbose:
|
|
216
|
+
logger.warning("model_pricing_missing", model_name=model_name)
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# Convert to per-1M-token pricing (multiply by 1,000,000)
|
|
220
|
+
pricing = {
|
|
221
|
+
"input": Decimal(str(input_cost_per_token * 1_000_000)),
|
|
222
|
+
"output": Decimal(str(output_cost_per_token * 1_000_000)),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Add cache pricing if available
|
|
226
|
+
if cache_creation_cost is not None:
|
|
227
|
+
pricing["cache_write"] = Decimal(
|
|
228
|
+
str(cache_creation_cost * 1_000_000)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if cache_read_cost is not None:
|
|
232
|
+
pricing["cache_read"] = Decimal(str(cache_read_cost * 1_000_000))
|
|
233
|
+
|
|
234
|
+
# Optionally map to canonical model name
|
|
235
|
+
if map_to_claude:
|
|
236
|
+
canonical_name = _CLAUDE_MODEL_MAPPER.map(model_name).mapped
|
|
237
|
+
else:
|
|
238
|
+
canonical_name = model_name
|
|
239
|
+
|
|
240
|
+
internal_format[canonical_name] = pricing
|
|
241
|
+
|
|
242
|
+
if verbose:
|
|
243
|
+
logger.debug(
|
|
244
|
+
"model_pricing_converted",
|
|
245
|
+
original_name=model_name,
|
|
246
|
+
canonical_name=canonical_name,
|
|
247
|
+
input_cost=str(pricing["input"]),
|
|
248
|
+
output_cost=str(pricing["output"]),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
except (ValueError, TypeError) as e:
|
|
252
|
+
if verbose:
|
|
253
|
+
logger.error(
|
|
254
|
+
"pricing_conversion_failed", model_name=model_name, error=str(e)
|
|
255
|
+
)
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
if verbose:
|
|
259
|
+
logger.info("models_converted", model_count=len(internal_format))
|
|
260
|
+
return internal_format
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
def load_pricing_from_data(
|
|
264
|
+
litellm_data: dict[str, Any],
|
|
265
|
+
provider: Literal["anthropic", "openai", "all", "claude"] = "claude",
|
|
266
|
+
map_to_claude: bool = True,
|
|
267
|
+
verbose: bool = True,
|
|
268
|
+
) -> PricingData | None:
|
|
269
|
+
"""Load and convert pricing data from LiteLLM format.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
litellm_data: Raw LiteLLM pricing data
|
|
273
|
+
provider: Provider to load pricing for ("anthropic", "openai", "all", or "claude")
|
|
274
|
+
"claude" is kept for backward compatibility and extracts only Claude models
|
|
275
|
+
map_to_claude: Whether to map model names to Claude equivalents
|
|
276
|
+
verbose: Whether to enable verbose logging
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Validated pricing data as PricingData model, or None if invalid
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
# Extract models based on provider
|
|
283
|
+
if provider == "claude":
|
|
284
|
+
# Backward compatibility - extract only Claude models
|
|
285
|
+
models = PricingLoader.extract_claude_models(
|
|
286
|
+
litellm_data, verbose=verbose
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
models = PricingLoader.extract_models_by_provider(
|
|
290
|
+
litellm_data, provider=provider, verbose=verbose
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if not models:
|
|
294
|
+
if verbose:
|
|
295
|
+
logger.warning(
|
|
296
|
+
"models_not_found", provider=provider, source="LiteLLM"
|
|
297
|
+
)
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
# Convert to internal format
|
|
301
|
+
internal_pricing = PricingLoader.convert_to_internal_format(
|
|
302
|
+
models, map_to_claude=map_to_claude, verbose=verbose
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if not internal_pricing:
|
|
306
|
+
if verbose:
|
|
307
|
+
logger.warning("pricing_data_invalid")
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
# Validate and create PricingData model
|
|
311
|
+
pricing_data = PricingData.model_validate(internal_pricing)
|
|
312
|
+
|
|
313
|
+
if verbose:
|
|
314
|
+
logger.info(
|
|
315
|
+
"pricing_data_loaded",
|
|
316
|
+
model_count=len(pricing_data),
|
|
317
|
+
provider=provider,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return pricing_data
|
|
321
|
+
|
|
322
|
+
except ValidationError as e:
|
|
323
|
+
if verbose:
|
|
324
|
+
logger.error("pricing_validation_failed", error=str(e), exc_info=e)
|
|
325
|
+
return None
|
|
326
|
+
except json.JSONDecodeError as e:
|
|
327
|
+
if verbose:
|
|
328
|
+
logger.error(
|
|
329
|
+
"pricing_json_decode_failed",
|
|
330
|
+
source="LiteLLM",
|
|
331
|
+
error=str(e),
|
|
332
|
+
exc_info=e,
|
|
333
|
+
)
|
|
334
|
+
return None
|
|
335
|
+
except httpx.HTTPError as e:
|
|
336
|
+
if verbose:
|
|
337
|
+
logger.error(
|
|
338
|
+
"pricing_http_error", source="LiteLLM", error=str(e), exc_info=e
|
|
339
|
+
)
|
|
340
|
+
return None
|
|
341
|
+
except OSError as e:
|
|
342
|
+
if verbose:
|
|
343
|
+
logger.error(
|
|
344
|
+
"pricing_io_error", source="LiteLLM", error=str(e), exc_info=e
|
|
345
|
+
)
|
|
346
|
+
return None
|
|
347
|
+
except Exception as e:
|
|
348
|
+
if verbose:
|
|
349
|
+
logger.error(
|
|
350
|
+
"pricing_load_failed", source="LiteLLM", error=str(e), exc_info=e
|
|
351
|
+
)
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def validate_pricing_data(
|
|
356
|
+
pricing_data: Any, verbose: bool = True
|
|
357
|
+
) -> PricingData | None:
|
|
358
|
+
"""Validate pricing data using Pydantic models.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
pricing_data: Pricing data to validate (dict or PricingData)
|
|
362
|
+
verbose: Whether to enable verbose logging
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Valid PricingData model or None if validation fails
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
# If already a PricingData instance, return it
|
|
369
|
+
if isinstance(pricing_data, PricingData):
|
|
370
|
+
if verbose:
|
|
371
|
+
logger.debug(
|
|
372
|
+
"pricing_already_validated", model_count=len(pricing_data)
|
|
373
|
+
)
|
|
374
|
+
return pricing_data
|
|
375
|
+
|
|
376
|
+
# If it's a dict, try to create PricingData from it
|
|
377
|
+
if isinstance(pricing_data, dict):
|
|
378
|
+
if not pricing_data:
|
|
379
|
+
if verbose:
|
|
380
|
+
logger.warning("pricing_data_empty")
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
# Try to create PricingData model
|
|
384
|
+
validated_data = PricingData.model_validate(pricing_data)
|
|
385
|
+
|
|
386
|
+
if verbose:
|
|
387
|
+
logger.debug(
|
|
388
|
+
"pricing_data_validated", model_count=len(validated_data)
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return validated_data
|
|
392
|
+
|
|
393
|
+
# Invalid type
|
|
394
|
+
if verbose:
|
|
395
|
+
logger.error(
|
|
396
|
+
"pricing_data_invalid_type",
|
|
397
|
+
actual_type=type(pricing_data).__name__,
|
|
398
|
+
expected_types=["dict", "PricingData"],
|
|
399
|
+
)
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
except ValidationError as e:
|
|
403
|
+
if verbose:
|
|
404
|
+
logger.error("pricing_validation_failed", error=str(e), exc_info=e)
|
|
405
|
+
return None
|
|
406
|
+
except json.JSONDecodeError as e:
|
|
407
|
+
if verbose:
|
|
408
|
+
logger.error("pricing_validation_json_error", error=str(e), exc_info=e)
|
|
409
|
+
return None
|
|
410
|
+
except OSError as e:
|
|
411
|
+
if verbose:
|
|
412
|
+
logger.error("pricing_validation_io_error", error=str(e), exc_info=e)
|
|
413
|
+
return None
|
|
414
|
+
except Exception as e:
|
|
415
|
+
if verbose:
|
|
416
|
+
logger.error(
|
|
417
|
+
"pricing_validation_unexpected_error", error=str(e), exc_info=e
|
|
418
|
+
)
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
@staticmethod
|
|
422
|
+
def get_model_aliases() -> dict[str, str]:
|
|
423
|
+
"""Get mapping of model aliases to canonical names.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Dictionary mapping aliases to canonical model names
|
|
427
|
+
"""
|
|
428
|
+
return _CLAUDE_ALIAS_MAP.copy()
|
|
429
|
+
|
|
430
|
+
@staticmethod
|
|
431
|
+
def get_canonical_model_name(model_name: str) -> str:
|
|
432
|
+
"""Get canonical model name for a given model name.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
model_name: Model name (possibly an alias)
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Canonical model name
|
|
439
|
+
"""
|
|
440
|
+
return _CLAUDE_MODEL_MAPPER.map(model_name).mapped
|
|
@@ -4,7 +4,14 @@ from collections.abc import Iterator
|
|
|
4
4
|
from decimal import Decimal
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from pydantic import
|
|
7
|
+
from pydantic import (
|
|
8
|
+
BaseModel,
|
|
9
|
+
ConfigDict,
|
|
10
|
+
Field,
|
|
11
|
+
RootModel,
|
|
12
|
+
field_serializer,
|
|
13
|
+
field_validator,
|
|
14
|
+
)
|
|
8
15
|
|
|
9
16
|
|
|
10
17
|
class ModelPricing(BaseModel):
|
|
@@ -32,9 +39,13 @@ class ModelPricing(BaseModel):
|
|
|
32
39
|
return v
|
|
33
40
|
raise TypeError(f"Cannot convert {type(v)} to Decimal")
|
|
34
41
|
|
|
42
|
+
@field_serializer("input", "output", "cache_read", "cache_write")
|
|
43
|
+
def serialize_decimal(self, value: Decimal) -> float:
|
|
44
|
+
"""Serialize Decimal fields as float for JSON compatibility."""
|
|
45
|
+
return float(value)
|
|
46
|
+
|
|
35
47
|
model_config = ConfigDict(
|
|
36
48
|
arbitrary_types_allowed=True,
|
|
37
|
-
json_encoders={Decimal: lambda v: float(v)},
|
|
38
49
|
)
|
|
39
50
|
|
|
40
51
|
|
|
@@ -82,24 +93,3 @@ class PricingData(RootModel[dict[str, ModelPricing]]):
|
|
|
82
93
|
def model_names(self) -> list[str]:
|
|
83
94
|
"""Get list of all model names."""
|
|
84
95
|
return list(self.root.keys())
|
|
85
|
-
|
|
86
|
-
def to_dict(self) -> dict[str, dict[str, Decimal]]:
|
|
87
|
-
"""Convert to legacy dict format for backward compatibility."""
|
|
88
|
-
return {
|
|
89
|
-
model_name: {
|
|
90
|
-
"input": pricing.input,
|
|
91
|
-
"output": pricing.output,
|
|
92
|
-
"cache_read": pricing.cache_read,
|
|
93
|
-
"cache_write": pricing.cache_write,
|
|
94
|
-
}
|
|
95
|
-
for model_name, pricing in self.root.items()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@classmethod
|
|
99
|
-
def from_dict(cls, data: dict[str, dict[str, Any]]) -> "PricingData":
|
|
100
|
-
"""Create PricingData from legacy dict format."""
|
|
101
|
-
models = {
|
|
102
|
-
model_name: ModelPricing(**pricing_dict)
|
|
103
|
-
for model_name, pricing_dict in data.items()
|
|
104
|
-
}
|
|
105
|
-
return cls(root=models)
|