ccproxy-api 0.1.6__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 +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- 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 +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- 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 +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- 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 +95 -342
- ccproxy/utils/version_checker.py +279 -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.6.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 -1231
- 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 -269
- ccproxy/services/codex_detection_service.py +0 -263
- 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.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.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.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
"""Simple request logging utility for content logging across all service layers."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
from datetime import UTC, datetime
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
import structlog
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
logger = structlog.get_logger(__name__)
|
|
14
|
-
|
|
15
|
-
# Global batching settings for streaming logs
|
|
16
|
-
_STREAMING_BATCH_SIZE = 8192 # Batch chunks until we have 8KB
|
|
17
|
-
_STREAMING_BATCH_TIMEOUT = 0.1 # Or flush after 100ms
|
|
18
|
-
_streaming_batches: dict[str, dict[str, Any]] = {} # request_id -> batch info
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def should_log_requests() -> bool:
|
|
22
|
-
"""Check if request logging is enabled via environment variable.
|
|
23
|
-
|
|
24
|
-
Returns:
|
|
25
|
-
True if CCPROXY_LOG_REQUESTS is set to 'true' (case-insensitive)
|
|
26
|
-
"""
|
|
27
|
-
return os.environ.get("CCPROXY_LOG_REQUESTS", "false").lower() == "true"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def get_request_log_dir() -> Path | None:
|
|
31
|
-
"""Get the request log directory from environment variable.
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
Path object if CCPROXY_REQUEST_LOG_DIR is set and valid, None otherwise
|
|
35
|
-
"""
|
|
36
|
-
log_dir = os.environ.get("CCPROXY_REQUEST_LOG_DIR")
|
|
37
|
-
if not log_dir:
|
|
38
|
-
return None
|
|
39
|
-
|
|
40
|
-
path = Path(log_dir)
|
|
41
|
-
try:
|
|
42
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
43
|
-
return path
|
|
44
|
-
except Exception as e:
|
|
45
|
-
logger.error(
|
|
46
|
-
"failed_to_create_request_log_dir",
|
|
47
|
-
log_dir=log_dir,
|
|
48
|
-
error=str(e),
|
|
49
|
-
)
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def get_timestamp_prefix() -> str:
|
|
54
|
-
"""Generate timestamp prefix in YYYYMMDDhhmmss format.
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
Timestamp string in YYYYMMDDhhmmss format (UTC)
|
|
58
|
-
"""
|
|
59
|
-
return datetime.now(UTC).strftime("%Y%m%d%H%M%S")
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
async def write_request_log(
|
|
63
|
-
request_id: str,
|
|
64
|
-
log_type: str,
|
|
65
|
-
data: dict[str, Any],
|
|
66
|
-
timestamp: str | None = None,
|
|
67
|
-
) -> None:
|
|
68
|
-
"""Write request/response data to JSON file.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
request_id: Unique request identifier
|
|
72
|
-
log_type: Type of log (e.g., 'middleware_request', 'upstream_response')
|
|
73
|
-
data: Data to log as JSON
|
|
74
|
-
timestamp: Optional timestamp prefix (defaults to current time)
|
|
75
|
-
"""
|
|
76
|
-
if not should_log_requests():
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
log_dir = get_request_log_dir()
|
|
80
|
-
if not log_dir:
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
timestamp = timestamp or get_timestamp_prefix()
|
|
84
|
-
filename = f"{timestamp}_{request_id}_{log_type}.json"
|
|
85
|
-
file_path = log_dir / filename
|
|
86
|
-
|
|
87
|
-
try:
|
|
88
|
-
# Write JSON data to file asynchronously
|
|
89
|
-
def write_file() -> None:
|
|
90
|
-
with file_path.open("w", encoding="utf-8") as f:
|
|
91
|
-
json.dump(data, f, indent=2, default=str, ensure_ascii=False)
|
|
92
|
-
|
|
93
|
-
# Run in thread pool to avoid blocking
|
|
94
|
-
await asyncio.get_event_loop().run_in_executor(None, write_file)
|
|
95
|
-
|
|
96
|
-
logger.debug(
|
|
97
|
-
"request_log_written",
|
|
98
|
-
request_id=request_id,
|
|
99
|
-
log_type=log_type,
|
|
100
|
-
file_path=str(file_path),
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
except Exception as e:
|
|
104
|
-
logger.error(
|
|
105
|
-
"failed_to_write_request_log",
|
|
106
|
-
request_id=request_id,
|
|
107
|
-
log_type=log_type,
|
|
108
|
-
file_path=str(file_path),
|
|
109
|
-
error=str(e),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
async def write_streaming_log(
|
|
114
|
-
request_id: str,
|
|
115
|
-
log_type: str,
|
|
116
|
-
data: bytes,
|
|
117
|
-
timestamp: str | None = None,
|
|
118
|
-
) -> None:
|
|
119
|
-
"""Write streaming data to raw file.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
request_id: Unique request identifier
|
|
123
|
-
log_type: Type of log (e.g., 'middleware_streaming', 'upstream_streaming')
|
|
124
|
-
data: Raw bytes to log
|
|
125
|
-
timestamp: Optional timestamp prefix (defaults to current time)
|
|
126
|
-
"""
|
|
127
|
-
if not should_log_requests():
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
log_dir = get_request_log_dir()
|
|
131
|
-
if not log_dir:
|
|
132
|
-
return
|
|
133
|
-
|
|
134
|
-
timestamp = timestamp or get_timestamp_prefix()
|
|
135
|
-
filename = f"{timestamp}_{request_id}_{log_type}.raw"
|
|
136
|
-
file_path = log_dir / filename
|
|
137
|
-
|
|
138
|
-
try:
|
|
139
|
-
# Write raw data to file asynchronously
|
|
140
|
-
def write_file() -> None:
|
|
141
|
-
with file_path.open("wb") as f:
|
|
142
|
-
f.write(data)
|
|
143
|
-
|
|
144
|
-
# Run in thread pool to avoid blocking
|
|
145
|
-
await asyncio.get_event_loop().run_in_executor(None, write_file)
|
|
146
|
-
|
|
147
|
-
logger.debug(
|
|
148
|
-
"streaming_log_written",
|
|
149
|
-
request_id=request_id,
|
|
150
|
-
log_type=log_type,
|
|
151
|
-
file_path=str(file_path),
|
|
152
|
-
data_size=len(data),
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
except Exception as e:
|
|
156
|
-
logger.error(
|
|
157
|
-
"failed_to_write_streaming_log",
|
|
158
|
-
request_id=request_id,
|
|
159
|
-
log_type=log_type,
|
|
160
|
-
file_path=str(file_path),
|
|
161
|
-
error=str(e),
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
async def append_streaming_log(
|
|
166
|
-
request_id: str,
|
|
167
|
-
log_type: str,
|
|
168
|
-
data: bytes,
|
|
169
|
-
timestamp: str | None = None,
|
|
170
|
-
) -> None:
|
|
171
|
-
"""Append streaming data using batching for performance.
|
|
172
|
-
|
|
173
|
-
Args:
|
|
174
|
-
request_id: Unique request identifier
|
|
175
|
-
log_type: Type of log (e.g., 'middleware_streaming', 'upstream_streaming')
|
|
176
|
-
data: Raw bytes to append
|
|
177
|
-
timestamp: Optional timestamp prefix (defaults to current time)
|
|
178
|
-
"""
|
|
179
|
-
if not should_log_requests():
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
log_dir = get_request_log_dir()
|
|
183
|
-
if not log_dir:
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
timestamp = timestamp or get_timestamp_prefix()
|
|
187
|
-
batch_key = f"{request_id}_{log_type}"
|
|
188
|
-
|
|
189
|
-
# Get or create batch for this request/log_type combination
|
|
190
|
-
if batch_key not in _streaming_batches:
|
|
191
|
-
_streaming_batches[batch_key] = {
|
|
192
|
-
"request_id": request_id,
|
|
193
|
-
"log_type": log_type,
|
|
194
|
-
"timestamp": timestamp,
|
|
195
|
-
"data": bytearray(),
|
|
196
|
-
"chunk_count": 0,
|
|
197
|
-
"first_chunk_time": asyncio.get_event_loop().time(),
|
|
198
|
-
"last_flush_task": None,
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
batch = _streaming_batches[batch_key]
|
|
202
|
-
batch["data"].extend(data)
|
|
203
|
-
batch["chunk_count"] += 1
|
|
204
|
-
|
|
205
|
-
# Cancel previous flush task if it exists
|
|
206
|
-
if batch["last_flush_task"] and not batch["last_flush_task"].done():
|
|
207
|
-
batch["last_flush_task"].cancel()
|
|
208
|
-
|
|
209
|
-
# Check if we should flush now
|
|
210
|
-
should_flush = (
|
|
211
|
-
len(batch["data"]) >= _STREAMING_BATCH_SIZE
|
|
212
|
-
or batch["chunk_count"] >= 50 # Max 50 chunks per batch
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
if should_flush:
|
|
216
|
-
await _flush_streaming_batch(batch_key)
|
|
217
|
-
else:
|
|
218
|
-
# Schedule a delayed flush
|
|
219
|
-
batch["last_flush_task"] = asyncio.create_task(
|
|
220
|
-
_delayed_flush_streaming_batch(batch_key, _STREAMING_BATCH_TIMEOUT)
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
async def _delayed_flush_streaming_batch(batch_key: str, delay: float) -> None:
|
|
225
|
-
"""Flush a streaming batch after a delay."""
|
|
226
|
-
try:
|
|
227
|
-
await asyncio.sleep(delay)
|
|
228
|
-
if batch_key in _streaming_batches:
|
|
229
|
-
await _flush_streaming_batch(batch_key)
|
|
230
|
-
except asyncio.CancelledError:
|
|
231
|
-
# Task was cancelled, don't flush
|
|
232
|
-
pass
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
async def _flush_streaming_batch(batch_key: str) -> None:
|
|
236
|
-
"""Flush a streaming batch to disk."""
|
|
237
|
-
if batch_key not in _streaming_batches:
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
batch = _streaming_batches.pop(batch_key)
|
|
241
|
-
|
|
242
|
-
if not batch["data"]:
|
|
243
|
-
return # Nothing to flush
|
|
244
|
-
|
|
245
|
-
log_dir = get_request_log_dir()
|
|
246
|
-
if not log_dir:
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
filename = f"{batch['timestamp']}_{batch['request_id']}_{batch['log_type']}.raw"
|
|
250
|
-
file_path = log_dir / filename
|
|
251
|
-
|
|
252
|
-
try:
|
|
253
|
-
# Append batched data to file asynchronously
|
|
254
|
-
def append_file() -> None:
|
|
255
|
-
with file_path.open("ab") as f:
|
|
256
|
-
f.write(batch["data"])
|
|
257
|
-
|
|
258
|
-
# Run in thread pool to avoid blocking
|
|
259
|
-
await asyncio.get_event_loop().run_in_executor(None, append_file)
|
|
260
|
-
|
|
261
|
-
logger.debug(
|
|
262
|
-
"streaming_batch_flushed",
|
|
263
|
-
request_id=batch["request_id"],
|
|
264
|
-
log_type=batch["log_type"],
|
|
265
|
-
file_path=str(file_path),
|
|
266
|
-
batch_size=len(batch["data"]),
|
|
267
|
-
chunk_count=batch["chunk_count"],
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
except Exception as e:
|
|
271
|
-
logger.error(
|
|
272
|
-
"failed_to_flush_streaming_batch",
|
|
273
|
-
request_id=batch["request_id"],
|
|
274
|
-
log_type=batch["log_type"],
|
|
275
|
-
file_path=str(file_path),
|
|
276
|
-
error=str(e),
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
async def flush_all_streaming_batches() -> None:
|
|
281
|
-
"""Flush all pending streaming batches. Call this on shutdown."""
|
|
282
|
-
batch_keys = list(_streaming_batches.keys())
|
|
283
|
-
for batch_key in batch_keys:
|
|
284
|
-
await _flush_streaming_batch(batch_key)
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
"""Streaming metrics extraction utilities.
|
|
2
|
-
|
|
3
|
-
This module provides utilities for extracting token usage and calculating costs
|
|
4
|
-
from Anthropic streaming responses in a testable, modular way.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
import structlog
|
|
11
|
-
|
|
12
|
-
from ccproxy.models.types import StreamingTokenMetrics, UsageData
|
|
13
|
-
from ccproxy.utils.cost_calculator import calculate_token_cost
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
logger = structlog.get_logger(__name__)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def extract_usage_from_streaming_chunk(chunk_data: Any) -> UsageData | None:
|
|
20
|
-
"""Extract usage information from Anthropic streaming response chunk.
|
|
21
|
-
|
|
22
|
-
This function looks for usage information in both message_start and message_delta events
|
|
23
|
-
from Anthropic's streaming API responses. message_start contains initial input tokens,
|
|
24
|
-
message_delta contains final output tokens.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
chunk_data: Streaming response chunk dictionary
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
UsageData with token counts or None if no usage found
|
|
31
|
-
"""
|
|
32
|
-
if not isinstance(chunk_data, dict):
|
|
33
|
-
return None
|
|
34
|
-
|
|
35
|
-
chunk_type = chunk_data.get("type")
|
|
36
|
-
|
|
37
|
-
# Look for message_start events with initial usage (input tokens)
|
|
38
|
-
if chunk_type == "message_start" and "message" in chunk_data:
|
|
39
|
-
message = chunk_data["message"]
|
|
40
|
-
if "usage" in message:
|
|
41
|
-
usage = message["usage"]
|
|
42
|
-
return UsageData(
|
|
43
|
-
input_tokens=usage.get("input_tokens"),
|
|
44
|
-
output_tokens=usage.get(
|
|
45
|
-
"output_tokens"
|
|
46
|
-
), # Initial output tokens (usually small)
|
|
47
|
-
cache_read_input_tokens=usage.get("cache_read_input_tokens"),
|
|
48
|
-
cache_creation_input_tokens=usage.get("cache_creation_input_tokens"),
|
|
49
|
-
event_type="message_start",
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
# Look for message_delta events with final usage (output tokens)
|
|
53
|
-
elif chunk_type == "message_delta" and "usage" in chunk_data:
|
|
54
|
-
usage = chunk_data["usage"]
|
|
55
|
-
return UsageData(
|
|
56
|
-
input_tokens=usage.get("input_tokens"), # Usually None in delta
|
|
57
|
-
output_tokens=usage.get("output_tokens"), # Final output token count
|
|
58
|
-
cache_read_input_tokens=usage.get("cache_read_input_tokens"),
|
|
59
|
-
cache_creation_input_tokens=usage.get("cache_creation_input_tokens"),
|
|
60
|
-
event_type="message_delta",
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
return None
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class StreamingMetricsCollector:
|
|
67
|
-
"""Collects and manages token metrics during streaming responses."""
|
|
68
|
-
|
|
69
|
-
def __init__(self, request_id: str | None = None) -> None:
|
|
70
|
-
"""Initialize the metrics collector.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
request_id: Optional request ID for logging context
|
|
74
|
-
"""
|
|
75
|
-
self.request_id = request_id
|
|
76
|
-
self.metrics = StreamingTokenMetrics(
|
|
77
|
-
tokens_input=None,
|
|
78
|
-
tokens_output=None,
|
|
79
|
-
cache_read_tokens=None,
|
|
80
|
-
cache_write_tokens=None,
|
|
81
|
-
cost_usd=None,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
def process_chunk(self, chunk_str: str) -> bool:
|
|
85
|
-
"""Process a streaming chunk to extract token metrics.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
chunk_str: Raw chunk string from streaming response
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
True if this was the final chunk with complete metrics, False otherwise
|
|
92
|
-
"""
|
|
93
|
-
# Check if this chunk contains usage information
|
|
94
|
-
# Look for usage data in any chunk - the event type will be determined from the JSON
|
|
95
|
-
if "usage" not in chunk_str:
|
|
96
|
-
return False
|
|
97
|
-
|
|
98
|
-
logger.debug(
|
|
99
|
-
"Processing chunk with usage",
|
|
100
|
-
chunk_preview=chunk_str[:300],
|
|
101
|
-
request_id=self.request_id,
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
# Parse SSE data lines to find usage information
|
|
106
|
-
for line in chunk_str.split("\n"):
|
|
107
|
-
if line.startswith("data: "):
|
|
108
|
-
data_str = line[6:].strip()
|
|
109
|
-
if data_str and data_str != "[DONE]":
|
|
110
|
-
event_data = json.loads(data_str)
|
|
111
|
-
usage_data = extract_usage_from_streaming_chunk(event_data)
|
|
112
|
-
|
|
113
|
-
if usage_data:
|
|
114
|
-
event_type = usage_data.get("event_type")
|
|
115
|
-
|
|
116
|
-
# Handle message_start: get input tokens and initial cache tokens
|
|
117
|
-
if event_type == "message_start":
|
|
118
|
-
self.metrics["tokens_input"] = usage_data.get(
|
|
119
|
-
"input_tokens"
|
|
120
|
-
)
|
|
121
|
-
self.metrics["cache_read_tokens"] = (
|
|
122
|
-
usage_data.get("cache_read_input_tokens")
|
|
123
|
-
or self.metrics["cache_read_tokens"]
|
|
124
|
-
)
|
|
125
|
-
self.metrics["cache_write_tokens"] = (
|
|
126
|
-
usage_data.get("cache_creation_input_tokens")
|
|
127
|
-
or self.metrics["cache_write_tokens"]
|
|
128
|
-
)
|
|
129
|
-
logger.debug(
|
|
130
|
-
"Extracted input tokens from message_start",
|
|
131
|
-
tokens_input=self.metrics["tokens_input"],
|
|
132
|
-
cache_read_tokens=self.metrics["cache_read_tokens"],
|
|
133
|
-
cache_write_tokens=self.metrics[
|
|
134
|
-
"cache_write_tokens"
|
|
135
|
-
],
|
|
136
|
-
request_id=self.request_id,
|
|
137
|
-
)
|
|
138
|
-
return False # Not final yet
|
|
139
|
-
|
|
140
|
-
# Handle message_delta: get final output tokens
|
|
141
|
-
elif event_type == "message_delta":
|
|
142
|
-
self.metrics["tokens_output"] = usage_data.get(
|
|
143
|
-
"output_tokens"
|
|
144
|
-
)
|
|
145
|
-
logger.debug(
|
|
146
|
-
"Extracted output tokens from message_delta",
|
|
147
|
-
tokens_output=self.metrics["tokens_output"],
|
|
148
|
-
request_id=self.request_id,
|
|
149
|
-
)
|
|
150
|
-
return True # This is the final event
|
|
151
|
-
|
|
152
|
-
break # Only process first valid data line
|
|
153
|
-
|
|
154
|
-
except (json.JSONDecodeError, KeyError) as e:
|
|
155
|
-
logger.debug(
|
|
156
|
-
"Failed to parse streaming token metrics",
|
|
157
|
-
error=str(e),
|
|
158
|
-
request_id=self.request_id,
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
return False
|
|
162
|
-
|
|
163
|
-
def calculate_final_cost(self, model: str | None) -> float | None:
|
|
164
|
-
"""Calculate the final cost based on collected metrics.
|
|
165
|
-
|
|
166
|
-
Args:
|
|
167
|
-
model: Model name for pricing lookup
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
Final cost in USD or None if calculation fails
|
|
171
|
-
"""
|
|
172
|
-
cost_usd = calculate_token_cost(
|
|
173
|
-
self.metrics["tokens_input"],
|
|
174
|
-
self.metrics["tokens_output"],
|
|
175
|
-
model,
|
|
176
|
-
self.metrics["cache_read_tokens"],
|
|
177
|
-
self.metrics["cache_write_tokens"],
|
|
178
|
-
)
|
|
179
|
-
self.metrics["cost_usd"] = cost_usd
|
|
180
|
-
|
|
181
|
-
logger.debug(
|
|
182
|
-
"Final streaming token metrics",
|
|
183
|
-
tokens_input=self.metrics["tokens_input"],
|
|
184
|
-
tokens_output=self.metrics["tokens_output"],
|
|
185
|
-
cache_read_tokens=self.metrics["cache_read_tokens"],
|
|
186
|
-
cache_write_tokens=self.metrics["cache_write_tokens"],
|
|
187
|
-
cost_usd=cost_usd,
|
|
188
|
-
request_id=self.request_id,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
return cost_usd
|
|
192
|
-
|
|
193
|
-
def get_metrics(self) -> StreamingTokenMetrics:
|
|
194
|
-
"""Get the current collected metrics.
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
Current token metrics
|
|
198
|
-
"""
|
|
199
|
-
return self.metrics.copy()
|