echotools 1.0.2__tar.gz → 1.0.4__tar.gz
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.
- {echotools-1.0.2 → echotools-1.0.4}/PKG-INFO +1 -1
- {echotools-1.0.2 → echotools-1.0.4}/pyproject.toml +1 -1
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/cache/list_cache.py +5 -2
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/dispatch/__init__.py +3 -0
- echotools-1.0.4/src/echotools/dispatch/usage.py +60 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/errors/__init__.py +20 -0
- echotools-1.0.4/src/echotools/errors/classify.py +80 -0
- echotools-1.0.4/src/echotools/errors/http.py +126 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools.egg-info/PKG-INFO +1 -1
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools.egg-info/SOURCES.txt +2 -0
- echotools-1.0.2/src/echotools/errors/classify.py +0 -42
- {echotools-1.0.2 → echotools-1.0.4}/README.md +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/setup.cfg +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/cache/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/cache/memory_cache.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/config/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/config/base.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/config/center.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/config/loader.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/config/merge.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/dispatch/candidate.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/dispatch/dispatcher.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/dispatch/selector.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/errors/base.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/errors/common.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/events/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/events/bus.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/events/event.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/files/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/files/file_util.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/parsers/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/parsers/stream.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/parsers/xml_parser.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/prompt/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/prompt/history.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/prompt/inject.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/prompt/templates.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/protocols/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/protocols/antml.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/protocols/bracket.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/protocols/custom.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/protocols/nous.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/protocols/original.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/protocols/xml.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/registry.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/shared/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/shared/coercion.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/shared/loop_detect.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/shared/normalization.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/fncall/shared/xml_helpers.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/ids/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/ids/generator.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/io/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/io/io_utils.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/lifecycle/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/lifecycle/manager.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/lifecycle/updater.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/logger/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/logger/manager.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/network/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/network/http_utils.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/plugin/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/plugin/base.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/plugin/discovery.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/plugin/registry.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/process/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/process/port.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/protocol/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/protocol/base.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/proxy/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/proxy/manager.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/retry/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/retry/retry.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/runtime/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/runtime/collector.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/scheduler/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/scheduler/scheduler.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/sdk/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/sdk/facade.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/tracing/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/tracing/context.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/tracing/span.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/tracing/tracer.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/watcher/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/watcher/file_watcher.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/web/__init__.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/web/application.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools/web/utils.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools.egg-info/dependency_links.txt +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools.egg-info/requires.txt +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/src/echotools.egg-info/top_level.txt +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/tests/test_cache_retry.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/tests/test_config.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/tests/test_dispatch.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/tests/test_events.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/tests/test_plugin.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/tests/test_protocols.py +0 -0
- {echotools-1.0.2 → echotools-1.0.4}/tests/test_tracing.py +0 -0
|
@@ -34,6 +34,7 @@ class ListCache:
|
|
|
34
34
|
fallback: List[str],
|
|
35
35
|
cache_path: str,
|
|
36
36
|
overwrite: bool = True,
|
|
37
|
+
data_key: str = "items",
|
|
37
38
|
) -> None:
|
|
38
39
|
"""初始化列表缓存。
|
|
39
40
|
|
|
@@ -42,6 +43,7 @@ class ListCache:
|
|
|
42
43
|
fallback: 兜底列表。
|
|
43
44
|
cache_path: 持久化文件路径。
|
|
44
45
|
overwrite: True=覆盖,False=只增不减。
|
|
46
|
+
data_key: JSON 中存储列表的键名,默认 "items"。
|
|
45
47
|
"""
|
|
46
48
|
self._name = name
|
|
47
49
|
self._fallback = list(fallback)
|
|
@@ -49,6 +51,7 @@ class ListCache:
|
|
|
49
51
|
self._items: List[str] = list(fallback)
|
|
50
52
|
self._cache_path = Path(cache_path)
|
|
51
53
|
self._refreshing = False
|
|
54
|
+
self._data_key = data_key
|
|
52
55
|
|
|
53
56
|
async def load(self) -> List[str]:
|
|
54
57
|
"""从缓存文件加载列表。
|
|
@@ -60,7 +63,7 @@ class ListCache:
|
|
|
60
63
|
if self._cache_path.is_file():
|
|
61
64
|
text = self._cache_path.read_text(encoding="utf-8")
|
|
62
65
|
data = json.loads(text)
|
|
63
|
-
items = data.get(
|
|
66
|
+
items = data.get(self._data_key, [])
|
|
64
67
|
if items:
|
|
65
68
|
self._items = list(items)
|
|
66
69
|
logger.info(
|
|
@@ -80,7 +83,7 @@ class ListCache:
|
|
|
80
83
|
"""
|
|
81
84
|
try:
|
|
82
85
|
self._cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
-
data = {
|
|
86
|
+
data = {self._data_key: items, "updated_at": int(time.time())}
|
|
84
87
|
self._cache_path.write_text(
|
|
85
88
|
json.dumps(data, ensure_ascii=False, indent=2),
|
|
86
89
|
encoding="utf-8",
|
|
@@ -9,6 +9,7 @@ from echotools.dispatch.selector import (
|
|
|
9
9
|
TASRecord,
|
|
10
10
|
TASWeights,
|
|
11
11
|
)
|
|
12
|
+
from echotools.dispatch.usage import fallback_usage, normalize_usage
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"TaskCandidate",
|
|
@@ -17,4 +18,6 @@ __all__ = [
|
|
|
17
18
|
"TASRecord",
|
|
18
19
|
"TASWeights",
|
|
19
20
|
"TaskDispatcher",
|
|
21
|
+
"normalize_usage",
|
|
22
|
+
"fallback_usage",
|
|
20
23
|
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""LLM token usage 规范化。"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
__all__ = ["normalize_usage", "fallback_usage"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fallback_usage(prompt_len: int, resp_text: str) -> Dict[str, int]:
|
|
11
|
+
"""估算 token 用量(无精确数据时的回退)。
|
|
12
|
+
|
|
13
|
+
按字符数 / 3 粗略估算。
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
prompt_len: 提示文本字符数。
|
|
17
|
+
resp_text: 响应文本。
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
用量字典。
|
|
21
|
+
"""
|
|
22
|
+
pt = max(prompt_len // 3, 1)
|
|
23
|
+
ct = max(len(resp_text) // 3, 0)
|
|
24
|
+
return {"prompt_tokens": pt, "completion_tokens": ct, "total_tokens": pt + ct}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def normalize_usage(
|
|
28
|
+
raw: Any,
|
|
29
|
+
prompt_len: int,
|
|
30
|
+
resp_text: str,
|
|
31
|
+
) -> Dict[str, int]:
|
|
32
|
+
"""规范化 usage 字典。
|
|
33
|
+
|
|
34
|
+
兼容 OpenAI (prompt_tokens/completion_tokens) 和
|
|
35
|
+
Anthropic (input_tokens/output_tokens) 格式。
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
raw: 原始 usage 数据(dict 或 None)。
|
|
39
|
+
prompt_len: 提示文本字符数。
|
|
40
|
+
resp_text: 响应文本。
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
规范化后的用量字典。
|
|
44
|
+
"""
|
|
45
|
+
if not isinstance(raw, dict):
|
|
46
|
+
return fallback_usage(prompt_len, resp_text)
|
|
47
|
+
try:
|
|
48
|
+
pt = int(raw.get("prompt_tokens", raw.get("input_tokens", 0)))
|
|
49
|
+
ct = int(raw.get("completion_tokens", raw.get("output_tokens", 0)))
|
|
50
|
+
if pt <= 0 and ct <= 0:
|
|
51
|
+
return fallback_usage(prompt_len, resp_text)
|
|
52
|
+
if pt <= 0:
|
|
53
|
+
pt = prompt_len // 3
|
|
54
|
+
return {
|
|
55
|
+
"prompt_tokens": pt,
|
|
56
|
+
"completion_tokens": ct,
|
|
57
|
+
"total_tokens": pt + ct,
|
|
58
|
+
}
|
|
59
|
+
except (TypeError, ValueError):
|
|
60
|
+
return fallback_usage(prompt_len, resp_text)
|
|
@@ -14,9 +14,21 @@ from echotools.errors.common import (
|
|
|
14
14
|
TimeoutError,
|
|
15
15
|
ValidationError,
|
|
16
16
|
)
|
|
17
|
+
from echotools.errors.http import (
|
|
18
|
+
AuthError,
|
|
19
|
+
ContextLengthError,
|
|
20
|
+
ForbiddenError,
|
|
21
|
+
HttpError,
|
|
22
|
+
NotFoundError,
|
|
23
|
+
QuotaExceededError,
|
|
24
|
+
RateLimitError,
|
|
25
|
+
ServerError,
|
|
26
|
+
StreamError,
|
|
27
|
+
)
|
|
17
28
|
|
|
18
29
|
__all__ = [
|
|
19
30
|
"EchoError",
|
|
31
|
+
"HttpError",
|
|
20
32
|
"ConfigError",
|
|
21
33
|
"ValidationError",
|
|
22
34
|
"NetworkError",
|
|
@@ -25,5 +37,13 @@ __all__ = [
|
|
|
25
37
|
"NoCandidateError",
|
|
26
38
|
"PluginError",
|
|
27
39
|
"ProtocolError",
|
|
40
|
+
"AuthError",
|
|
41
|
+
"ForbiddenError",
|
|
42
|
+
"NotFoundError",
|
|
43
|
+
"RateLimitError",
|
|
44
|
+
"QuotaExceededError",
|
|
45
|
+
"ContextLengthError",
|
|
46
|
+
"ServerError",
|
|
47
|
+
"StreamError",
|
|
28
48
|
"classify_http_error",
|
|
29
49
|
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""HTTP 状态码错误分类——含上下文长度检测。"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from echotools.errors.base import EchoError
|
|
8
|
+
from echotools.errors.common import (
|
|
9
|
+
NetworkError,
|
|
10
|
+
NotSupportedError,
|
|
11
|
+
TimeoutError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
from echotools.errors.http import (
|
|
15
|
+
AuthError,
|
|
16
|
+
ContextLengthError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
QuotaExceededError,
|
|
19
|
+
RateLimitError,
|
|
20
|
+
ServerError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = ["classify_http_error"]
|
|
24
|
+
|
|
25
|
+
_CONTEXT_KEYWORDS = (
|
|
26
|
+
"context",
|
|
27
|
+
"token",
|
|
28
|
+
"max_tokens",
|
|
29
|
+
"maximum context",
|
|
30
|
+
"prompt is too long",
|
|
31
|
+
"上下文",
|
|
32
|
+
"超长",
|
|
33
|
+
"超出",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def classify_http_error(
|
|
38
|
+
status_code: int,
|
|
39
|
+
message: str,
|
|
40
|
+
original: Optional[Exception] = None,
|
|
41
|
+
) -> EchoError:
|
|
42
|
+
"""根据 HTTP 状态码分类错误。
|
|
43
|
+
|
|
44
|
+
支持的特殊检测:
|
|
45
|
+
- 400 + 上下文关键词 → ContextLengthError
|
|
46
|
+
- 401 → AuthError
|
|
47
|
+
- 402 → QuotaExceededError
|
|
48
|
+
- 404 → NotFoundError
|
|
49
|
+
- 429 → RateLimitError
|
|
50
|
+
- 5xx → ServerError
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
status_code: HTTP 状态码。
|
|
54
|
+
message: 错误信息。
|
|
55
|
+
original: 原始异常。
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
对应的 EchoError 实例。
|
|
59
|
+
"""
|
|
60
|
+
if status_code == 400:
|
|
61
|
+
msg_lower = message.lower()
|
|
62
|
+
if any(kw in msg_lower for kw in _CONTEXT_KEYWORDS):
|
|
63
|
+
return ContextLengthError(message, original=original)
|
|
64
|
+
return ValidationError(message)
|
|
65
|
+
if status_code == 401:
|
|
66
|
+
return AuthError(message, original=original)
|
|
67
|
+
if status_code == 402:
|
|
68
|
+
return QuotaExceededError(message, original=original)
|
|
69
|
+
if status_code == 404:
|
|
70
|
+
return NotFoundError(message, original=original)
|
|
71
|
+
if status_code in (408, 504):
|
|
72
|
+
return TimeoutError(message)
|
|
73
|
+
if status_code == 429:
|
|
74
|
+
return RateLimitError(message, original=original)
|
|
75
|
+
if status_code == 501:
|
|
76
|
+
return NotSupportedError(message)
|
|
77
|
+
if status_code >= 500:
|
|
78
|
+
return ServerError(message, http_status=status_code, original=original)
|
|
79
|
+
err = EchoError(message, original=original, status_code=status_code)
|
|
80
|
+
return err
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""HTTP 状态码相关的通用异常类。
|
|
4
|
+
|
|
5
|
+
适用于 API 网关、代理等 HTTP 服务场景。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from echotools.errors.base import EchoError
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"HttpError",
|
|
14
|
+
"AuthError",
|
|
15
|
+
"ForbiddenError",
|
|
16
|
+
"NotFoundError",
|
|
17
|
+
"RateLimitError",
|
|
18
|
+
"QuotaExceededError",
|
|
19
|
+
"ContextLengthError",
|
|
20
|
+
"ServerError",
|
|
21
|
+
"StreamError",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HttpError(EchoError):
|
|
26
|
+
"""HTTP 错误基类。"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
message: str,
|
|
31
|
+
status_code: int = 500,
|
|
32
|
+
original: Optional[Exception] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
super().__init__(message, original=original, status_code=status_code)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AuthError(HttpError):
|
|
38
|
+
"""认证失败 (401)。"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
message: str = "认证失败",
|
|
43
|
+
original: Optional[Exception] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
super().__init__(message, status_code=401, original=original)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ForbiddenError(HttpError):
|
|
49
|
+
"""权限不足 (403)。"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
message: str = "权限不足",
|
|
54
|
+
original: Optional[Exception] = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
super().__init__(message, status_code=403, original=original)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NotFoundError(HttpError):
|
|
60
|
+
"""资源不存在 (404)。"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
message: str = "资源不存在",
|
|
65
|
+
original: Optional[Exception] = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
super().__init__(message, status_code=404, original=original)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RateLimitError(HttpError):
|
|
71
|
+
"""速率限制 (429)。"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
message: str = "请求频率超限",
|
|
76
|
+
retry_after: Optional[float] = None,
|
|
77
|
+
original: Optional[Exception] = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
super().__init__(message, status_code=429, original=original)
|
|
80
|
+
self.retry_after = retry_after
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class QuotaExceededError(HttpError):
|
|
84
|
+
"""配额耗尽 (402)。"""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
message: str = "配额已耗尽",
|
|
89
|
+
original: Optional[Exception] = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
super().__init__(message, status_code=402, original=original)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ContextLengthError(HttpError):
|
|
95
|
+
"""上下文长度超限 (400)。"""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
message: str = "输入超过最大上下文长度",
|
|
100
|
+
original: Optional[Exception] = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
super().__init__(message, status_code=400, original=original)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ServerError(HttpError):
|
|
106
|
+
"""服务器错误 (5xx)。"""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
message: str = "服务器错误",
|
|
111
|
+
http_status: int = 500,
|
|
112
|
+
original: Optional[Exception] = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
super().__init__(message, status_code=http_status, original=original)
|
|
115
|
+
self.http_status = http_status
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class StreamError(HttpError):
|
|
119
|
+
"""流式响应错误。"""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
message: str = "流式响应中断",
|
|
124
|
+
original: Optional[Exception] = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
super().__init__(message, status_code=502, original=original)
|
|
@@ -18,10 +18,12 @@ src/echotools/dispatch/__init__.py
|
|
|
18
18
|
src/echotools/dispatch/candidate.py
|
|
19
19
|
src/echotools/dispatch/dispatcher.py
|
|
20
20
|
src/echotools/dispatch/selector.py
|
|
21
|
+
src/echotools/dispatch/usage.py
|
|
21
22
|
src/echotools/errors/__init__.py
|
|
22
23
|
src/echotools/errors/base.py
|
|
23
24
|
src/echotools/errors/classify.py
|
|
24
25
|
src/echotools/errors/common.py
|
|
26
|
+
src/echotools/errors/http.py
|
|
25
27
|
src/echotools/events/__init__.py
|
|
26
28
|
src/echotools/events/bus.py
|
|
27
29
|
src/echotools/events/event.py
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
"""HTTP 状态码错误分类。"""
|
|
4
|
-
|
|
5
|
-
from typing import Optional
|
|
6
|
-
|
|
7
|
-
from echotools.errors.base import EchoError
|
|
8
|
-
from echotools.errors.common import (
|
|
9
|
-
NetworkError,
|
|
10
|
-
NotSupportedError,
|
|
11
|
-
TimeoutError,
|
|
12
|
-
ValidationError,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
__all__ = ["classify_http_error"]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def classify_http_error(
|
|
19
|
-
status_code: int,
|
|
20
|
-
message: str,
|
|
21
|
-
original: Optional[Exception] = None,
|
|
22
|
-
) -> EchoError:
|
|
23
|
-
"""根据 HTTP 状态码分类错误。
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
status_code: HTTP 状态码。
|
|
27
|
-
message: 错误信息。
|
|
28
|
-
original: 原始异常。
|
|
29
|
-
|
|
30
|
-
Returns:
|
|
31
|
-
对应的 EchoError 实例。
|
|
32
|
-
"""
|
|
33
|
-
if status_code == 400:
|
|
34
|
-
return ValidationError(message)
|
|
35
|
-
if status_code in (408, 504):
|
|
36
|
-
return TimeoutError(message)
|
|
37
|
-
if status_code == 501:
|
|
38
|
-
return NotSupportedError(message)
|
|
39
|
-
if status_code >= 500:
|
|
40
|
-
return NetworkError(message, original=original)
|
|
41
|
-
err = EchoError(message, original=original, status_code=status_code)
|
|
42
|
-
return err
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|