coding-proxy 0.1.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.
- coding/__init__.py +0 -0
- coding/proxy/__init__.py +3 -0
- coding/proxy/__main__.py +5 -0
- coding/proxy/auth/__init__.py +13 -0
- coding/proxy/auth/providers/__init__.py +6 -0
- coding/proxy/auth/providers/base.py +35 -0
- coding/proxy/auth/providers/github.py +133 -0
- coding/proxy/auth/providers/google.py +237 -0
- coding/proxy/auth/runtime.py +122 -0
- coding/proxy/auth/store.py +74 -0
- coding/proxy/cli/__init__.py +151 -0
- coding/proxy/cli/auth_commands.py +224 -0
- coding/proxy/compat/__init__.py +30 -0
- coding/proxy/compat/canonical.py +193 -0
- coding/proxy/compat/session_store.py +137 -0
- coding/proxy/config/__init__.py +6 -0
- coding/proxy/config/auth_schema.py +24 -0
- coding/proxy/config/loader.py +139 -0
- coding/proxy/config/resiliency.py +46 -0
- coding/proxy/config/routing.py +279 -0
- coding/proxy/config/schema.py +280 -0
- coding/proxy/config/server.py +23 -0
- coding/proxy/config/vendors.py +53 -0
- coding/proxy/convert/__init__.py +14 -0
- coding/proxy/convert/anthropic_to_gemini.py +352 -0
- coding/proxy/convert/anthropic_to_openai.py +352 -0
- coding/proxy/convert/gemini_sse_adapter.py +169 -0
- coding/proxy/convert/gemini_to_anthropic.py +98 -0
- coding/proxy/convert/openai_to_anthropic.py +88 -0
- coding/proxy/logging/__init__.py +49 -0
- coding/proxy/logging/db.py +308 -0
- coding/proxy/logging/stats.py +129 -0
- coding/proxy/model/__init__.py +93 -0
- coding/proxy/model/auth.py +32 -0
- coding/proxy/model/compat.py +153 -0
- coding/proxy/model/constants.py +21 -0
- coding/proxy/model/pricing.py +70 -0
- coding/proxy/model/token.py +64 -0
- coding/proxy/model/vendor.py +218 -0
- coding/proxy/pricing.py +100 -0
- coding/proxy/routing/__init__.py +47 -0
- coding/proxy/routing/circuit_breaker.py +152 -0
- coding/proxy/routing/error_classifier.py +67 -0
- coding/proxy/routing/executor.py +453 -0
- coding/proxy/routing/model_mapper.py +90 -0
- coding/proxy/routing/quota_guard.py +169 -0
- coding/proxy/routing/rate_limit.py +159 -0
- coding/proxy/routing/retry.py +82 -0
- coding/proxy/routing/router.py +84 -0
- coding/proxy/routing/session_manager.py +62 -0
- coding/proxy/routing/tier.py +171 -0
- coding/proxy/routing/usage_parser.py +193 -0
- coding/proxy/routing/usage_recorder.py +131 -0
- coding/proxy/server/__init__.py +1 -0
- coding/proxy/server/app.py +142 -0
- coding/proxy/server/factory.py +175 -0
- coding/proxy/server/request_normalizer.py +139 -0
- coding/proxy/server/responses.py +74 -0
- coding/proxy/server/routes.py +264 -0
- coding/proxy/streaming/__init__.py +1 -0
- coding/proxy/streaming/anthropic_compat.py +484 -0
- coding/proxy/vendors/__init__.py +29 -0
- coding/proxy/vendors/anthropic.py +44 -0
- coding/proxy/vendors/antigravity.py +328 -0
- coding/proxy/vendors/base.py +353 -0
- coding/proxy/vendors/copilot.py +702 -0
- coding/proxy/vendors/copilot_models.py +438 -0
- coding/proxy/vendors/copilot_token_manager.py +167 -0
- coding/proxy/vendors/copilot_urls.py +16 -0
- coding/proxy/vendors/mixins.py +71 -0
- coding/proxy/vendors/token_manager.py +128 -0
- coding/proxy/vendors/zhipu.py +243 -0
- coding_proxy-0.1.0.dist-info/METADATA +184 -0
- coding_proxy-0.1.0.dist-info/RECORD +77 -0
- coding_proxy-0.1.0.dist-info/WHEEL +4 -0
- coding_proxy-0.1.0.dist-info/entry_points.txt +2 -0
- coding_proxy-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""路由模块."""
|
|
2
|
+
|
|
3
|
+
from .circuit_breaker import CircuitBreaker, CircuitState
|
|
4
|
+
from .error_classifier import (
|
|
5
|
+
build_request_capabilities,
|
|
6
|
+
extract_error_payload_from_http_status,
|
|
7
|
+
is_semantic_rejection,
|
|
8
|
+
)
|
|
9
|
+
from .executor import _RouteExecutor
|
|
10
|
+
from .model_mapper import ModelMapper
|
|
11
|
+
from .quota_guard import QuotaGuard, QuotaState
|
|
12
|
+
from .rate_limit import RateLimitInfo
|
|
13
|
+
from .rate_limit import (
|
|
14
|
+
compute_effective_retry_seconds,
|
|
15
|
+
compute_rate_limit_deadline,
|
|
16
|
+
parse_rate_limit_headers,
|
|
17
|
+
)
|
|
18
|
+
from .retry import RetryConfig, calculate_delay, is_retryable_error
|
|
19
|
+
from .router import RequestRouter
|
|
20
|
+
from .session_manager import RouteSessionManager
|
|
21
|
+
from .tier import BackendTier
|
|
22
|
+
from .usage_parser import (
|
|
23
|
+
build_usage_evidence_records,
|
|
24
|
+
has_missing_input_usage_signals,
|
|
25
|
+
parse_usage_from_chunk,
|
|
26
|
+
)
|
|
27
|
+
from .usage_recorder import UsageRecorder
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Core routing
|
|
31
|
+
"CircuitBreaker", "CircuitState",
|
|
32
|
+
"ModelMapper", "RequestRouter", "BackendTier",
|
|
33
|
+
# Decomposed components (internal use)
|
|
34
|
+
"_RouteExecutor", "UsageRecorder", "RouteSessionManager",
|
|
35
|
+
# Resiliency
|
|
36
|
+
"QuotaGuard", "QuotaState", "RateLimitInfo",
|
|
37
|
+
"RetryConfig",
|
|
38
|
+
"parse_rate_limit_headers", "compute_effective_retry_seconds",
|
|
39
|
+
"compute_rate_limit_deadline",
|
|
40
|
+
"is_retryable_error", "calculate_delay",
|
|
41
|
+
# Error classification
|
|
42
|
+
"build_request_capabilities", "is_semantic_rejection",
|
|
43
|
+
"extract_error_payload_from_http_status",
|
|
44
|
+
# Usage parsing
|
|
45
|
+
"build_usage_evidence_records", "has_missing_input_usage_signals",
|
|
46
|
+
"parse_usage_from_chunk",
|
|
47
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""熔断器 (Circuit Breaker) — 状态机实现."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CircuitState(Enum):
|
|
14
|
+
CLOSED = "closed" # 正常:使用主后端
|
|
15
|
+
OPEN = "open" # 故障:使用备选后端
|
|
16
|
+
HALF_OPEN = "half_open" # 试探:测试主后端是否恢复
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CircuitBreaker:
|
|
20
|
+
"""线程安全的熔断器.
|
|
21
|
+
|
|
22
|
+
状态转换:
|
|
23
|
+
- CLOSED → OPEN: 连续 failure_threshold 次失败
|
|
24
|
+
- OPEN → HALF_OPEN: recovery_timeout 后
|
|
25
|
+
- HALF_OPEN → CLOSED: 连续 success_threshold 次成功
|
|
26
|
+
- HALF_OPEN → OPEN: 任意一次失败(指数退避)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
failure_threshold: int = 3,
|
|
32
|
+
recovery_timeout_seconds: int = 300,
|
|
33
|
+
success_threshold: int = 2,
|
|
34
|
+
max_recovery_seconds: int = 3600,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._failure_threshold = failure_threshold
|
|
37
|
+
self._recovery_timeout = recovery_timeout_seconds
|
|
38
|
+
self._success_threshold = success_threshold
|
|
39
|
+
self._max_recovery = max_recovery_seconds
|
|
40
|
+
|
|
41
|
+
self._state = CircuitState.CLOSED
|
|
42
|
+
self._failure_count = 0
|
|
43
|
+
self._success_count = 0
|
|
44
|
+
self._last_failure_time: float | None = None
|
|
45
|
+
self._current_recovery = recovery_timeout_seconds
|
|
46
|
+
self._lock = threading.Lock()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def state(self) -> CircuitState:
|
|
50
|
+
with self._lock:
|
|
51
|
+
self._check_recovery()
|
|
52
|
+
return self._state
|
|
53
|
+
|
|
54
|
+
def can_execute(self) -> bool:
|
|
55
|
+
"""判断是否可以在主后端上执行请求."""
|
|
56
|
+
with self._lock:
|
|
57
|
+
self._check_recovery()
|
|
58
|
+
return self._state in (CircuitState.CLOSED, CircuitState.HALF_OPEN)
|
|
59
|
+
|
|
60
|
+
def record_success(self) -> None:
|
|
61
|
+
"""记录一次成功调用."""
|
|
62
|
+
with self._lock:
|
|
63
|
+
self._failure_count = 0
|
|
64
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
65
|
+
self._success_count += 1
|
|
66
|
+
if self._success_count >= self._success_threshold:
|
|
67
|
+
self._transition_to(CircuitState.CLOSED)
|
|
68
|
+
logger.info("Circuit breaker: HALF_OPEN → CLOSED (recovered)")
|
|
69
|
+
elif self._state == CircuitState.CLOSED:
|
|
70
|
+
# 正常状态下成功,无需操作
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def record_failure(self, retry_after_seconds: float | None = None) -> None:
|
|
74
|
+
"""记录一次失败调用.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
retry_after_seconds: 从响应头解析出的建议恢复时间(秒)。
|
|
78
|
+
若提供且大于当前指数退避值,将覆盖以避免过早探测。
|
|
79
|
+
"""
|
|
80
|
+
with self._lock:
|
|
81
|
+
self._failure_count += 1
|
|
82
|
+
self._success_count = 0
|
|
83
|
+
self._last_failure_time = time.monotonic()
|
|
84
|
+
|
|
85
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
86
|
+
self._transition_to(CircuitState.OPEN)
|
|
87
|
+
self._backoff_recovery(hint_seconds=retry_after_seconds)
|
|
88
|
+
logger.warning(
|
|
89
|
+
"Circuit breaker: HALF_OPEN → OPEN (recovery failed, next retry in %ds)",
|
|
90
|
+
self._current_recovery,
|
|
91
|
+
)
|
|
92
|
+
elif self._state == CircuitState.CLOSED:
|
|
93
|
+
if self._failure_count >= self._failure_threshold:
|
|
94
|
+
self._transition_to(CircuitState.OPEN)
|
|
95
|
+
if retry_after_seconds and retry_after_seconds > self._current_recovery:
|
|
96
|
+
self._current_recovery = min(
|
|
97
|
+
retry_after_seconds, self._max_recovery,
|
|
98
|
+
)
|
|
99
|
+
logger.warning(
|
|
100
|
+
"Circuit breaker: CLOSED → OPEN (%d consecutive failures, next retry in %ds)",
|
|
101
|
+
self._failure_count,
|
|
102
|
+
self._current_recovery,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def reset(self) -> None:
|
|
106
|
+
"""手动重置熔断器为 CLOSED 状态."""
|
|
107
|
+
with self._lock:
|
|
108
|
+
self._transition_to(CircuitState.CLOSED)
|
|
109
|
+
self._current_recovery = self._recovery_timeout
|
|
110
|
+
logger.info("Circuit breaker: manually reset to CLOSED")
|
|
111
|
+
|
|
112
|
+
def get_info(self) -> dict:
|
|
113
|
+
"""获取熔断器状态信息."""
|
|
114
|
+
with self._lock:
|
|
115
|
+
self._check_recovery()
|
|
116
|
+
return {
|
|
117
|
+
"state": self._state.value,
|
|
118
|
+
"failure_count": self._failure_count,
|
|
119
|
+
"success_count": self._success_count,
|
|
120
|
+
"current_recovery_seconds": self._current_recovery,
|
|
121
|
+
"last_failure_time": self._last_failure_time,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def _check_recovery(self) -> None:
|
|
125
|
+
"""检查是否应从 OPEN 转为 HALF_OPEN."""
|
|
126
|
+
if self._state != CircuitState.OPEN:
|
|
127
|
+
return
|
|
128
|
+
if self._last_failure_time is None:
|
|
129
|
+
return
|
|
130
|
+
elapsed = time.monotonic() - self._last_failure_time
|
|
131
|
+
if elapsed >= self._current_recovery:
|
|
132
|
+
self._transition_to(CircuitState.HALF_OPEN)
|
|
133
|
+
logger.info("Circuit breaker: OPEN → HALF_OPEN (recovery timeout)")
|
|
134
|
+
|
|
135
|
+
def _transition_to(self, new_state: CircuitState) -> None:
|
|
136
|
+
old = self._state
|
|
137
|
+
self._state = new_state
|
|
138
|
+
if new_state == CircuitState.CLOSED:
|
|
139
|
+
self._failure_count = 0
|
|
140
|
+
self._success_count = 0
|
|
141
|
+
self._current_recovery = self._recovery_timeout
|
|
142
|
+
elif new_state == CircuitState.HALF_OPEN:
|
|
143
|
+
self._success_count = 0
|
|
144
|
+
|
|
145
|
+
def _backoff_recovery(self, hint_seconds: float | None = None) -> None:
|
|
146
|
+
"""指数退避恢复超时,支持 server-hinted 覆盖."""
|
|
147
|
+
exponential = min(self._current_recovery * 2, self._max_recovery)
|
|
148
|
+
if hint_seconds is not None and hint_seconds > exponential:
|
|
149
|
+
# Server 告知的恢复时间优先于指数退避
|
|
150
|
+
self._current_recovery = min(hint_seconds, self._max_recovery)
|
|
151
|
+
else:
|
|
152
|
+
self._current_recovery = exponential
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""HTTP 错误分类与请求能力画像提取."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ..vendors.base import RequestCapabilities
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract_error_payload_from_http_status(exc: httpx.HTTPStatusError) -> dict[str, Any] | None:
|
|
14
|
+
response = exc.response
|
|
15
|
+
if response is None or not response.content:
|
|
16
|
+
return None
|
|
17
|
+
try:
|
|
18
|
+
payload = response.json()
|
|
19
|
+
except (json.JSONDecodeError, UnicodeDecodeError, TypeError, ValueError):
|
|
20
|
+
return None
|
|
21
|
+
return payload if isinstance(payload, dict) else None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_semantic_rejection(
|
|
25
|
+
*,
|
|
26
|
+
status_code: int,
|
|
27
|
+
error_type: str | None = None,
|
|
28
|
+
error_message: str | None = None,
|
|
29
|
+
) -> bool:
|
|
30
|
+
if status_code != 400:
|
|
31
|
+
return False
|
|
32
|
+
normalized_type = (error_type or "").strip().lower()
|
|
33
|
+
if normalized_type == "invalid_request_error":
|
|
34
|
+
return True
|
|
35
|
+
normalized_message = (error_message or "").lower()
|
|
36
|
+
return any(
|
|
37
|
+
marker in normalized_message
|
|
38
|
+
for marker in (
|
|
39
|
+
"invalid_request_error",
|
|
40
|
+
"should match pattern",
|
|
41
|
+
"validation",
|
|
42
|
+
"tool_use_id",
|
|
43
|
+
"server_tool_use",
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_request_capabilities(body: dict[str, Any]) -> RequestCapabilities:
|
|
49
|
+
"""从请求体提取能力画像."""
|
|
50
|
+
has_images = False
|
|
51
|
+
for msg in body.get("messages", []):
|
|
52
|
+
content = msg.get("content")
|
|
53
|
+
if not isinstance(content, list):
|
|
54
|
+
continue
|
|
55
|
+
if any(
|
|
56
|
+
isinstance(block, dict) and block.get("type") == "image"
|
|
57
|
+
for block in content
|
|
58
|
+
):
|
|
59
|
+
has_images = True
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
return RequestCapabilities(
|
|
63
|
+
has_tools=bool(body.get("tools") or body.get("tool_choice")),
|
|
64
|
+
has_thinking=bool(body.get("thinking") or body.get("extended_thinking")),
|
|
65
|
+
has_images=has_images,
|
|
66
|
+
has_metadata=bool(body.get("metadata")),
|
|
67
|
+
)
|