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
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Codex-specific streaming metrics extraction utilities.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for extracting token usage from
|
|
4
|
+
OpenAI/Codex streaming responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
11
|
+
from ccproxy.streaming import StreamingMetrics
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = get_plugin_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_usage_from_codex_chunk(chunk_data: Any) -> dict[str, Any] | None:
|
|
18
|
+
"""Extract usage information from OpenAI/Codex streaming response chunk.
|
|
19
|
+
|
|
20
|
+
OpenAI/Codex sends usage information in the final streaming chunk where
|
|
21
|
+
usage is not null. Earlier chunks have usage=null.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
chunk_data: Streaming response chunk dictionary
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Dictionary with token counts or None if no usage found
|
|
28
|
+
"""
|
|
29
|
+
if not isinstance(chunk_data, dict):
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
# Extract model if present
|
|
33
|
+
model = chunk_data.get("model")
|
|
34
|
+
|
|
35
|
+
# Check for different Codex response formats
|
|
36
|
+
# 1. Standard OpenAI format (chat.completion.chunk)
|
|
37
|
+
object_type = chunk_data.get("object", "")
|
|
38
|
+
usage = chunk_data.get("usage")
|
|
39
|
+
|
|
40
|
+
if usage and object_type.startswith(("chat.completion", "codex.response")):
|
|
41
|
+
# Extract basic tokens
|
|
42
|
+
result = {
|
|
43
|
+
"input_tokens": usage.get("prompt_tokens") or usage.get("input_tokens", 0),
|
|
44
|
+
"output_tokens": usage.get("completion_tokens")
|
|
45
|
+
or usage.get("output_tokens", 0),
|
|
46
|
+
"total_tokens": usage.get("total_tokens"),
|
|
47
|
+
"event_type": "openai_completion",
|
|
48
|
+
"model": model,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Extract detailed token information if available
|
|
52
|
+
if "input_tokens_details" in usage:
|
|
53
|
+
result["cache_read_tokens"] = usage["input_tokens_details"].get(
|
|
54
|
+
"cached_tokens", 0
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if "output_tokens_details" in usage:
|
|
58
|
+
result["reasoning_tokens"] = usage["output_tokens_details"].get(
|
|
59
|
+
"reasoning_tokens", 0
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
# 2. Codex CLI response format (response.completed event)
|
|
65
|
+
event_type = chunk_data.get("type", "")
|
|
66
|
+
if event_type == "response.completed" and "response" in chunk_data:
|
|
67
|
+
response_data = chunk_data["response"]
|
|
68
|
+
if isinstance(response_data, dict) and "usage" in response_data:
|
|
69
|
+
usage = response_data["usage"]
|
|
70
|
+
if usage:
|
|
71
|
+
# Codex CLI uses various formats
|
|
72
|
+
result = {
|
|
73
|
+
"input_tokens": usage.get("input_tokens")
|
|
74
|
+
or usage.get("prompt_tokens", 0),
|
|
75
|
+
"output_tokens": usage.get("output_tokens")
|
|
76
|
+
or usage.get("completion_tokens", 0),
|
|
77
|
+
"total_tokens": usage.get("total_tokens"),
|
|
78
|
+
"event_type": "codex_cli_response",
|
|
79
|
+
"model": response_data.get("model") or model,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Check for detailed tokens
|
|
83
|
+
if "input_tokens_details" in usage:
|
|
84
|
+
result["cache_read_tokens"] = usage["input_tokens_details"].get(
|
|
85
|
+
"cached_tokens", 0
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if "output_tokens_details" in usage:
|
|
89
|
+
result["reasoning_tokens"] = usage["output_tokens_details"].get(
|
|
90
|
+
"reasoning_tokens", 0
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CodexStreamingMetricsCollector:
|
|
99
|
+
"""Collects and manages token metrics during Codex streaming responses.
|
|
100
|
+
|
|
101
|
+
Implements IStreamingMetricsCollector interface for Codex/OpenAI.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
request_id: str | None = None,
|
|
107
|
+
pricing_service: Any = None,
|
|
108
|
+
model: str | None = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Initialize the metrics collector.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
request_id: Optional request ID for logging context
|
|
114
|
+
pricing_service: Optional pricing service for cost calculation
|
|
115
|
+
model: Optional model name for cost calculation (can also be extracted from chunks)
|
|
116
|
+
"""
|
|
117
|
+
self.request_id = request_id
|
|
118
|
+
self.pricing_service = pricing_service
|
|
119
|
+
self.model = model
|
|
120
|
+
self.reasoning_tokens: int | None = None # Store reasoning tokens separately
|
|
121
|
+
self.metrics: StreamingMetrics = {
|
|
122
|
+
"tokens_input": None,
|
|
123
|
+
"tokens_output": None,
|
|
124
|
+
"cache_read_tokens": None, # OpenAI might support in the future
|
|
125
|
+
"cache_write_tokens": None,
|
|
126
|
+
"cost_usd": None,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def process_raw_chunk(self, chunk_str: str) -> bool:
|
|
130
|
+
"""Process raw Codex format chunk before any conversion.
|
|
131
|
+
|
|
132
|
+
This handles Codex's native response.completed event format.
|
|
133
|
+
"""
|
|
134
|
+
return self.process_chunk(chunk_str)
|
|
135
|
+
|
|
136
|
+
def process_converted_chunk(self, chunk_str: str) -> bool:
|
|
137
|
+
"""Process chunk after conversion to OpenAI format.
|
|
138
|
+
|
|
139
|
+
When Codex responses are converted to OpenAI chat completion format,
|
|
140
|
+
this method extracts metrics from the converted OpenAI format.
|
|
141
|
+
"""
|
|
142
|
+
# After conversion, we'd see standard OpenAI format
|
|
143
|
+
# For now, delegate to main process_chunk which handles both
|
|
144
|
+
return self.process_chunk(chunk_str)
|
|
145
|
+
|
|
146
|
+
def process_chunk(self, chunk_str: str) -> bool:
|
|
147
|
+
"""Process a streaming chunk to extract OpenAI/Codex token metrics.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
chunk_str: Raw chunk string from streaming response
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
True if this was the final chunk with complete metrics, False otherwise
|
|
154
|
+
"""
|
|
155
|
+
# Check if this chunk contains usage information
|
|
156
|
+
if "usage" not in chunk_str:
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
logger.debug(
|
|
160
|
+
"processing_chunk",
|
|
161
|
+
chunk_preview=chunk_str[:300],
|
|
162
|
+
request_id=self.request_id,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
# Parse SSE data lines to find usage information
|
|
167
|
+
# Codex sends complete JSON on a single line after "data: "
|
|
168
|
+
for line in chunk_str.split("\n"):
|
|
169
|
+
if line.startswith("data: "):
|
|
170
|
+
data_str = line[6:].strip()
|
|
171
|
+
if data_str and data_str != "[DONE]":
|
|
172
|
+
event_data = json.loads(data_str)
|
|
173
|
+
|
|
174
|
+
# Log event type for debugging
|
|
175
|
+
event_type = event_data.get("type", "")
|
|
176
|
+
if event_type == "response.completed":
|
|
177
|
+
logger.debug(
|
|
178
|
+
"completed_event_found",
|
|
179
|
+
has_response=("response" in event_data),
|
|
180
|
+
has_usage=("usage" in event_data.get("response", {}))
|
|
181
|
+
if "response" in event_data
|
|
182
|
+
else False,
|
|
183
|
+
request_id=self.request_id,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
usage_data = extract_usage_from_codex_chunk(event_data)
|
|
187
|
+
|
|
188
|
+
if usage_data:
|
|
189
|
+
# Store token counts from the event
|
|
190
|
+
self.metrics["tokens_input"] = usage_data.get(
|
|
191
|
+
"input_tokens"
|
|
192
|
+
)
|
|
193
|
+
self.metrics["tokens_output"] = usage_data.get(
|
|
194
|
+
"output_tokens"
|
|
195
|
+
)
|
|
196
|
+
self.metrics["cache_read_tokens"] = usage_data.get(
|
|
197
|
+
"cache_read_tokens", 0
|
|
198
|
+
)
|
|
199
|
+
self.reasoning_tokens = usage_data.get(
|
|
200
|
+
"reasoning_tokens", 0
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Extract model from the chunk if we don't have it yet
|
|
204
|
+
if not self.model and usage_data.get("model"):
|
|
205
|
+
self.model = usage_data.get("model")
|
|
206
|
+
logger.debug(
|
|
207
|
+
"model_extracted_from_stream",
|
|
208
|
+
plugin="codex",
|
|
209
|
+
model=self.model,
|
|
210
|
+
request_id=self.request_id,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Calculate cost synchronously when we have complete metrics
|
|
214
|
+
if self.pricing_service:
|
|
215
|
+
if self.model:
|
|
216
|
+
try:
|
|
217
|
+
# Import pricing exceptions
|
|
218
|
+
from ccproxy.plugins.pricing.exceptions import (
|
|
219
|
+
ModelPricingNotFoundError,
|
|
220
|
+
PricingDataNotLoadedError,
|
|
221
|
+
PricingServiceDisabledError,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
cost_decimal = self.pricing_service.calculate_cost_sync(
|
|
225
|
+
model_name=self.model,
|
|
226
|
+
input_tokens=self.metrics["tokens_input"]
|
|
227
|
+
or 0,
|
|
228
|
+
output_tokens=self.metrics["tokens_output"]
|
|
229
|
+
or 0,
|
|
230
|
+
cache_read_tokens=self.metrics[
|
|
231
|
+
"cache_read_tokens"
|
|
232
|
+
]
|
|
233
|
+
or 0,
|
|
234
|
+
cache_write_tokens=0, # OpenAI doesn't have cache write
|
|
235
|
+
)
|
|
236
|
+
self.metrics["cost_usd"] = float(cost_decimal)
|
|
237
|
+
logger.debug(
|
|
238
|
+
"streaming_cost_calculated",
|
|
239
|
+
model=self.model,
|
|
240
|
+
cost_usd=self.metrics["cost_usd"],
|
|
241
|
+
tokens_input=self.metrics["tokens_input"],
|
|
242
|
+
tokens_output=self.metrics["tokens_output"],
|
|
243
|
+
request_id=self.request_id,
|
|
244
|
+
)
|
|
245
|
+
except ModelPricingNotFoundError as e:
|
|
246
|
+
logger.warning(
|
|
247
|
+
"model_pricing_not_found",
|
|
248
|
+
model=self.model,
|
|
249
|
+
message=str(e),
|
|
250
|
+
tokens_input=self.metrics["tokens_input"],
|
|
251
|
+
tokens_output=self.metrics["tokens_output"],
|
|
252
|
+
request_id=self.request_id,
|
|
253
|
+
)
|
|
254
|
+
except PricingDataNotLoadedError as e:
|
|
255
|
+
logger.warning(
|
|
256
|
+
"pricing_data_not_loaded",
|
|
257
|
+
model=self.model,
|
|
258
|
+
message=str(e),
|
|
259
|
+
request_id=self.request_id,
|
|
260
|
+
)
|
|
261
|
+
except PricingServiceDisabledError as e:
|
|
262
|
+
logger.debug(
|
|
263
|
+
"pricing_service_disabled",
|
|
264
|
+
message=str(e),
|
|
265
|
+
request_id=self.request_id,
|
|
266
|
+
)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.debug(
|
|
269
|
+
"streaming_cost_calculation_failed",
|
|
270
|
+
error=str(e),
|
|
271
|
+
model=self.model,
|
|
272
|
+
request_id=self.request_id,
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
logger.warning(
|
|
276
|
+
"streaming_cost_calculation_skipped_no_model",
|
|
277
|
+
plugin="codex",
|
|
278
|
+
request_id=self.request_id,
|
|
279
|
+
tokens_input=self.metrics["tokens_input"],
|
|
280
|
+
tokens_output=self.metrics["tokens_output"],
|
|
281
|
+
message="Model not found in streaming response, cannot calculate cost",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
logger.debug(
|
|
285
|
+
"token_metrics_extracted",
|
|
286
|
+
plugin="codex",
|
|
287
|
+
tokens_input=self.metrics["tokens_input"],
|
|
288
|
+
tokens_output=self.metrics["tokens_output"],
|
|
289
|
+
cache_read_tokens=self.metrics["cache_read_tokens"],
|
|
290
|
+
reasoning_tokens=self.reasoning_tokens,
|
|
291
|
+
total_tokens=usage_data.get("total_tokens"),
|
|
292
|
+
event_type=usage_data.get("event_type"),
|
|
293
|
+
cost_usd=self.metrics.get("cost_usd"),
|
|
294
|
+
request_id=self.request_id,
|
|
295
|
+
)
|
|
296
|
+
return True # This is the final event with complete metrics
|
|
297
|
+
|
|
298
|
+
break # Only process first valid data line
|
|
299
|
+
|
|
300
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
301
|
+
logger.debug(
|
|
302
|
+
"metrics_parse_failed",
|
|
303
|
+
plugin="codex",
|
|
304
|
+
error=str(e),
|
|
305
|
+
request_id=self.request_id,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
def get_metrics(self) -> StreamingMetrics:
|
|
311
|
+
"""Get the current collected metrics.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Current token metrics
|
|
315
|
+
"""
|
|
316
|
+
return self.metrics.copy()
|
|
317
|
+
|
|
318
|
+
def get_reasoning_tokens(self) -> int | None:
|
|
319
|
+
"""Get reasoning tokens if available (for o1 models).
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Reasoning tokens count or None
|
|
323
|
+
"""
|
|
324
|
+
return self.reasoning_tokens
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Scheduled tasks for Codex plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from ccproxy.core.logging import get_plugin_logger
|
|
6
|
+
from ccproxy.scheduler.tasks import BaseScheduledTask
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .detection_service import CodexDetectionService
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = get_plugin_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CodexDetectionRefreshTask(BaseScheduledTask):
|
|
17
|
+
"""Task to periodically refresh Codex CLI detection headers."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
name: str,
|
|
22
|
+
interval_seconds: float,
|
|
23
|
+
detection_service: "CodexDetectionService",
|
|
24
|
+
enabled: bool = True,
|
|
25
|
+
skip_initial_run: bool = True,
|
|
26
|
+
**kwargs: Any,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Initialize the Codex detection refresh task.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name: Task name
|
|
32
|
+
interval_seconds: Interval between refreshes
|
|
33
|
+
detection_service: The Codex detection service to refresh
|
|
34
|
+
enabled: Whether the task is enabled
|
|
35
|
+
skip_initial_run: Whether to skip the initial run at startup
|
|
36
|
+
**kwargs: Additional arguments for BaseScheduledTask
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(
|
|
39
|
+
name=name,
|
|
40
|
+
interval_seconds=interval_seconds,
|
|
41
|
+
enabled=enabled,
|
|
42
|
+
**kwargs,
|
|
43
|
+
)
|
|
44
|
+
self.detection_service = detection_service
|
|
45
|
+
self.skip_initial_run = skip_initial_run
|
|
46
|
+
self._first_run = True
|
|
47
|
+
|
|
48
|
+
async def run(self) -> bool:
|
|
49
|
+
"""Execute the detection refresh.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
True if refresh was successful, False otherwise
|
|
53
|
+
"""
|
|
54
|
+
# Skip the first run if configured to do so
|
|
55
|
+
if self._first_run and self.skip_initial_run:
|
|
56
|
+
self._first_run = False
|
|
57
|
+
logger.debug(
|
|
58
|
+
"codex_detection_refresh_skipped_initial",
|
|
59
|
+
task_name=self.name,
|
|
60
|
+
reason="Initial run skipped to avoid duplicate detection at startup",
|
|
61
|
+
)
|
|
62
|
+
return True # Return success to avoid triggering backoff
|
|
63
|
+
|
|
64
|
+
self._first_run = False
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
logger.info(
|
|
68
|
+
"codex_detection_refresh_starting",
|
|
69
|
+
task_name=self.name,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Refresh the detection data
|
|
73
|
+
detection_data = await self.detection_service.initialize_detection()
|
|
74
|
+
|
|
75
|
+
logger.info(
|
|
76
|
+
"codex_detection_refresh_completed",
|
|
77
|
+
task_name=self.name,
|
|
78
|
+
version=detection_data.codex_version if detection_data else "unknown",
|
|
79
|
+
has_cached_data=detection_data is not None,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(
|
|
86
|
+
"codex_detection_refresh_failed",
|
|
87
|
+
task_name=self.name,
|
|
88
|
+
error=str(e),
|
|
89
|
+
error_type=type(e).__name__,
|
|
90
|
+
)
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
async def setup(self) -> None:
|
|
94
|
+
"""Perform any setup required before task execution starts."""
|
|
95
|
+
logger.debug(
|
|
96
|
+
"codex_detection_refresh_setup",
|
|
97
|
+
task_name=self.name,
|
|
98
|
+
interval_seconds=self.interval_seconds,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def cleanup(self) -> None:
|
|
102
|
+
"""Perform any cleanup required after task execution stops."""
|
|
103
|
+
logger.info(
|
|
104
|
+
"codex_detection_refresh_cleanup",
|
|
105
|
+
task_name=self.name,
|
|
106
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility modules for Codex plugin."""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""SSE (Server-Sent Events) parser for Codex responses."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_sse_line(line: str) -> tuple[str | None, Any | None]:
|
|
8
|
+
"""Parse a single SSE line.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
line: SSE line to parse
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Tuple of (event_type, data) or (None, None) if not parseable
|
|
15
|
+
"""
|
|
16
|
+
line = line.strip()
|
|
17
|
+
|
|
18
|
+
if not line:
|
|
19
|
+
return None, None
|
|
20
|
+
|
|
21
|
+
if line.startswith("event:"):
|
|
22
|
+
return line[6:].strip(), None
|
|
23
|
+
|
|
24
|
+
if line.startswith("data:"):
|
|
25
|
+
data_str = line[5:].strip()
|
|
26
|
+
|
|
27
|
+
if data_str == "[DONE]":
|
|
28
|
+
return "done", None
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
return "data", json.loads(data_str)
|
|
32
|
+
except json.JSONDecodeError:
|
|
33
|
+
return None, None
|
|
34
|
+
|
|
35
|
+
return None, None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def extract_final_response(sse_content: str) -> dict[str, Any] | None:
|
|
39
|
+
"""Extract the final response from SSE content.
|
|
40
|
+
|
|
41
|
+
Looks for the response.completed event in SSE stream.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
sse_content: Complete SSE response content
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Final response data or None if not found
|
|
48
|
+
"""
|
|
49
|
+
lines = sse_content.strip().split("\n")
|
|
50
|
+
final_response = None
|
|
51
|
+
|
|
52
|
+
for line in lines:
|
|
53
|
+
event_type, data = parse_sse_line(line)
|
|
54
|
+
|
|
55
|
+
if event_type == "data" and data and isinstance(data, dict):
|
|
56
|
+
# Check for response.completed event
|
|
57
|
+
if data.get("type") == "response.completed":
|
|
58
|
+
# Found the completed response
|
|
59
|
+
if "response" in data:
|
|
60
|
+
final_response = data["response"]
|
|
61
|
+
else:
|
|
62
|
+
final_response = data
|
|
63
|
+
elif data.get("type") == "response.in_progress" and "response" in data:
|
|
64
|
+
# Update with in-progress data, but keep looking
|
|
65
|
+
final_response = data["response"]
|
|
66
|
+
|
|
67
|
+
return final_response
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_sse_stream(chunks: list[bytes]) -> dict[str, Any] | None:
|
|
71
|
+
"""Parse SSE stream chunks to extract final response.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
chunks: List of byte chunks from SSE stream
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Final response data or None if not found
|
|
78
|
+
"""
|
|
79
|
+
# Combine all chunks
|
|
80
|
+
full_content = b"".join(chunks).decode("utf-8", errors="replace")
|
|
81
|
+
return extract_final_response(full_content)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_sse_response(content: bytes | str) -> bool:
|
|
85
|
+
"""Check if content appears to be SSE format.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
content: Response content to check
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if content appears to be SSE format
|
|
92
|
+
"""
|
|
93
|
+
if isinstance(content, bytes):
|
|
94
|
+
try:
|
|
95
|
+
content = content.decode("utf-8", errors="replace")
|
|
96
|
+
except Exception:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
# Check for SSE markers
|
|
100
|
+
content_start = content[:100].strip()
|
|
101
|
+
return (
|
|
102
|
+
content_start.startswith("event:")
|
|
103
|
+
or content_start.startswith("data:")
|
|
104
|
+
or "\nevent:" in content_start
|
|
105
|
+
or "\ndata:" in content_start
|
|
106
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Command Replay Plugin
|
|
2
|
+
|
|
3
|
+
Generates reproducible `curl` and `xh` commands for captured provider requests.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
- Subscribes to provider hook events to snapshot raw HTTP payloads
|
|
7
|
+
- Emits commands to stdout or disk with configurable file layout
|
|
8
|
+
- Supports URL include/exclude filters and provider-only logging
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
- `CommandReplayConfig` toggles command types, log directory, and filters
|
|
12
|
+
- Enable via `plugins.command_replay` settings or matching environment vars
|
|
13
|
+
- Generate defaults with `python3 scripts/generate_config_from_model.py \
|
|
14
|
+
--format toml --plugin command_replay --config-class CommandReplayConfig`
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[plugins.command_replay]
|
|
18
|
+
# enabled = true
|
|
19
|
+
# generate_curl = true
|
|
20
|
+
# generate_xh = true
|
|
21
|
+
# log_dir = "/tmp/ccproxy/command_replay"
|
|
22
|
+
# write_to_files = true
|
|
23
|
+
# separate_files_per_command = true
|
|
24
|
+
# include_url_patterns = ["api.anthropic.com", "api.openai.com", "claude.ai", "chatgpt.com"]
|
|
25
|
+
# exclude_url_patterns = []
|
|
26
|
+
# log_to_console = false
|
|
27
|
+
# log_level = "TRACE"
|
|
28
|
+
# only_provider_requests = false
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Related Components
|
|
32
|
+
- `hook.py`: assembles commands from hook payloads
|
|
33
|
+
- `formatter.py`: file naming and formatting helpers
|
|
34
|
+
- `plugin.py`: runtime wiring and hook registration
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Command Replay Plugin - Generate curl and xh commands for provider requests."""
|
|
2
|
+
|
|
3
|
+
from .config import CommandReplayConfig
|
|
4
|
+
from .hook import CommandReplayHook
|
|
5
|
+
from .plugin import CommandReplayFactory, CommandReplayRuntime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Export the factory for auto-discovery
|
|
9
|
+
factory = CommandReplayFactory()
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CommandReplayConfig",
|
|
13
|
+
"CommandReplayHook",
|
|
14
|
+
"CommandReplayRuntime",
|
|
15
|
+
"CommandReplayFactory",
|
|
16
|
+
"factory",
|
|
17
|
+
]
|