zall 0.4.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.
- zall/__init__.py +7 -0
- zall/__main__.py +3 -0
- zall/_util/__init__.py +15 -0
- zall/_util/file.py +115 -0
- zall/_util/file_state.py +123 -0
- zall/_util/logging.py +130 -0
- zall/_util/model_registry.py +225 -0
- zall/_util/path.py +74 -0
- zall/_util/string.py +22 -0
- zall/_util/toml.py +223 -0
- zall/_util/win32.py +91 -0
- zall/adapters/__init__.py +26 -0
- zall/adapters/anthropic.py +399 -0
- zall/adapters/base.py +208 -0
- zall/adapters/gemini.py +343 -0
- zall/adapters/ollama.py +305 -0
- zall/adapters/openai_compat.py +446 -0
- zall/cli/__init__.py +8 -0
- zall/cli/app.py +131 -0
- zall/cli/commands/__init__.py +159 -0
- zall/cli/commands/_common.py +1029 -0
- zall/cli/commands/_legacy.py +101 -0
- zall/cli/commands/files.py +318 -0
- zall/cli/commands/git.py +210 -0
- zall/cli/commands/model.py +697 -0
- zall/cli/commands/reload.py +92 -0
- zall/cli/commands/session.py +474 -0
- zall/cli/commands/system.py +460 -0
- zall/cli/commands/v040.py +428 -0
- zall/cli/config.py +419 -0
- zall/cli/config_layers.py +246 -0
- zall/cli/environment.py +738 -0
- zall/cli/init_wizard.py +284 -0
- zall/cli/judge.py +206 -0
- zall/cli/orchestrator.py +527 -0
- zall/cli/prompt.py +268 -0
- zall/cli/render.py +966 -0
- zall/cli/repl_ui.py +439 -0
- zall/cli/replay.py +310 -0
- zall/cli/responder.py +247 -0
- zall/cli/session.py +650 -0
- zall/cli/update.py +263 -0
- zall/codegraph/__init__.py +919 -0
- zall/core/__init__.py +40 -0
- zall/core/accountability.py +318 -0
- zall/core/action.py +66 -0
- zall/core/agent.py +530 -0
- zall/core/builder.py +407 -0
- zall/core/chat_state.py +596 -0
- zall/core/checkpoint.py +416 -0
- zall/core/compactor.py +460 -0
- zall/core/context.py +117 -0
- zall/core/events.py +148 -0
- zall/core/executor.py +361 -0
- zall/core/extension.py +238 -0
- zall/core/gate.py +337 -0
- zall/core/goal.py +420 -0
- zall/core/lifecycle.py +262 -0
- zall/core/loop.py +1597 -0
- zall/core/memory.py +216 -0
- zall/core/model.py +241 -0
- zall/core/refiner.py +320 -0
- zall/core/safety.py +316 -0
- zall/core/tool.py +159 -0
- zall/core/toolset.py +223 -0
- zall/core/verifiability.py +571 -0
- zall/eval/__init__.py +15 -0
- zall/eval/metrics.py +349 -0
- zall/extensions/__init__.py +1 -0
- zall/extensions/auto_learn.py +507 -0
- zall/extensions/usage_tracker.py +91 -0
- zall/lsp/__init__.py +849 -0
- zall/mcp/__init__.py +22 -0
- zall/mcp/client.py +270 -0
- zall/mcp/config.py +191 -0
- zall/mcp/tool.py +124 -0
- zall/plugin/__init__.py +585 -0
- zall/safety/__init__.py +10 -0
- zall/safety/config.py +162 -0
- zall/safety/rules_file.py +579 -0
- zall/sandbox/__init__.py +599 -0
- zall/skills/__init__.py +11 -0
- zall/skills/loader.py +202 -0
- zall/tools/__init__.py +11 -0
- zall/tools/_bash_semantics.py +257 -0
- zall/tools/_diff.py +24 -0
- zall/tools/bash.py +633 -0
- zall/tools/batch_edit.py +315 -0
- zall/tools/code_understanding.py +249 -0
- zall/tools/codegraph.py +330 -0
- zall/tools/edit_file.py +188 -0
- zall/tools/git_protect.py +189 -0
- zall/tools/glob.py +133 -0
- zall/tools/grep.py +271 -0
- zall/tools/list_dir.py +151 -0
- zall/tools/lsp_diagnostics.py +315 -0
- zall/tools/project_analysis.py +217 -0
- zall/tools/pty_executor.py +253 -0
- zall/tools/read_file.py +257 -0
- zall/tools/read_image.py +312 -0
- zall/tools/search.py +397 -0
- zall/tools/spawn_subagent.py +687 -0
- zall/tools/todo.py +137 -0
- zall/tools/web_fetch.py +488 -0
- zall/tools/write_file.py +141 -0
- zall-0.4.0.dist-info/METADATA +316 -0
- zall-0.4.0.dist-info/RECORD +111 -0
- zall-0.4.0.dist-info/WHEEL +5 -0
- zall-0.4.0.dist-info/entry_points.txt +2 -0
- zall-0.4.0.dist-info/licenses/LICENSE +21 -0
- zall-0.4.0.dist-info/top_level.txt +1 -0
zall/__init__.py
ADDED
zall/__main__.py
ADDED
zall/_util/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""zall._util — 内部共享toolfunction库。
|
|
2
|
+
|
|
3
|
+
本包为 zall 内部使用的工具函数, 不构成 public API。
|
|
4
|
+
各模块消除重复代码用 (B22/B23/B24 等)。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from zall._util.file import is_binary, read_text_file
|
|
8
|
+
from zall._util.path import NOISE_DIRS, is_noise, skip_noise_dirs
|
|
9
|
+
from zall._util.string import unquote
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"is_binary", "read_text_file",
|
|
13
|
+
"NOISE_DIRS", "is_noise", "skip_noise_dirs",
|
|
14
|
+
"unquote",
|
|
15
|
+
]
|
zall/_util/file.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""zall._util.file — File operations共享toolfunction。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import locale
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _preferred_encoding() -> str:
|
|
10
|
+
"""Get the system's preferred encoding (Windows Chinese is GBK/CP936, do not hardcode UTF-8)."""
|
|
11
|
+
try:
|
|
12
|
+
enc = locale.getpreferredencoding(False)
|
|
13
|
+
if enc:
|
|
14
|
+
return enc
|
|
15
|
+
except (ValueError, LookupError):
|
|
16
|
+
pass
|
|
17
|
+
return "utf-8" # fallback
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def detect_text_encoding(path: Path) -> str:
|
|
21
|
+
"""Detect text file encoding: try UTF-8 first, fallback to system preferred encoding.
|
|
22
|
+
|
|
23
|
+
Most modern code/text files are UTF-8. On Chinese Windows, the system default is GBK,
|
|
24
|
+
but opening a UTF-8 file with GBK produces mojibake. This function tries UTF-8 first
|
|
25
|
+
(strict), and verifies via round-trip to avoid GBK text accidentally passing UTF-8
|
|
26
|
+
validation (e.g., GBK bytes that happen to form valid UTF-8 sequences).
|
|
27
|
+
|
|
28
|
+
Note: the returned encoding should be used WITHOUT errors='replace', because
|
|
29
|
+
replacement characters (U+FFFD) cannot be encoded in the target encoding
|
|
30
|
+
(e.g., GBK) when the content is sent to the API pipeline.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
with open(path, "rb") as f:
|
|
34
|
+
raw = f.read(8192)
|
|
35
|
+
# Strict UTF-8 decode
|
|
36
|
+
decoded = raw.decode("utf-8")
|
|
37
|
+
# Round-trip verification: re-encode must match original bytes exactly
|
|
38
|
+
# This catches GBK text that happens to be valid UTF-8 (different characters)
|
|
39
|
+
if decoded.encode("utf-8") == raw:
|
|
40
|
+
return "utf-8"
|
|
41
|
+
except (UnicodeDecodeError, OSError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# Try system encoding (GBK/CP936 on Chinese Windows)
|
|
45
|
+
sys_enc = _preferred_encoding()
|
|
46
|
+
if sys_enc.lower() != "utf-8":
|
|
47
|
+
try:
|
|
48
|
+
raw.decode(sys_enc)
|
|
49
|
+
return sys_enc
|
|
50
|
+
except (UnicodeDecodeError, LookupError):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
return "utf-8" # best-effort fallback
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_binary(path: Path) -> bool:
|
|
57
|
+
"""检测二进制file: 读前 8KB, check空字节。
|
|
58
|
+
|
|
59
|
+
失败时返回 True (fail-safe: 二进制文件不读).
|
|
60
|
+
B23: 统一 grep.py 的 _is_binary 和 read_file.py 的内联检测。
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
with open(path, "rb") as f:
|
|
64
|
+
chunk = f.read(8192)
|
|
65
|
+
return b"\0" in chunk
|
|
66
|
+
except OSError:
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def read_text_file(path: Path, encoding: str | None = None) -> str:
|
|
71
|
+
"""read文本file, 统一exceptionhandle。
|
|
72
|
+
|
|
73
|
+
B22: 统一 edit_file.py 和 batch_edit.py 的文件读取逻辑。
|
|
74
|
+
自动解析相对路径, 检查存在性/类型, 返回文件内容。
|
|
75
|
+
抛出 OSError 时调用方自行处理。
|
|
76
|
+
编码默认自动检测: 先尝试 UTF-8, 失败回退系统编码。
|
|
77
|
+
"""
|
|
78
|
+
if encoding is None:
|
|
79
|
+
encoding = detect_text_encoding(path)
|
|
80
|
+
path = Path.cwd() / path
|
|
81
|
+
if not path.exists():
|
|
82
|
+
raise FileNotFoundError(f"file not found: {path}")
|
|
83
|
+
if not path.is_file():
|
|
84
|
+
raise IsADirectoryError(f"not a file: {path}")
|
|
85
|
+
return path.read_text(encoding=encoding)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def atomic_write(path: Path, content: str, encoding: str | None = None) -> Path:
|
|
89
|
+
"""原子writefile — 使用唯一临时file名, 避免 concurrent write竞态。
|
|
90
|
+
|
|
91
|
+
v2 fix: 旧实现 path.with_suffix(suffix + ".zall_tmp") 在两个 agent
|
|
92
|
+
实例同时编辑同一文件时会产生相同临时文件路径, 导致竞态。
|
|
93
|
+
新实现使用 uuid 生成唯一文件名, 保证并发安全。
|
|
94
|
+
编码默认 UTF-8 (现代标准, 与 detect_text_encoding 的优先尝试一致)。
|
|
95
|
+
|
|
96
|
+
Returns: 临时文件路径 (已被 os.replace 移走, 不再存在)
|
|
97
|
+
Raises: OSError on write/replace failure (临时文件已清理)
|
|
98
|
+
"""
|
|
99
|
+
import os
|
|
100
|
+
import uuid
|
|
101
|
+
if encoding is None:
|
|
102
|
+
encoding = "utf-8"
|
|
103
|
+
tmp_name = f".zall_tmp_{uuid.uuid4().hex[:8]}"
|
|
104
|
+
tmp = path.parent / tmp_name
|
|
105
|
+
try:
|
|
106
|
+
tmp.write_text(content, encoding=encoding)
|
|
107
|
+
os.replace(str(tmp), str(path))
|
|
108
|
+
except OSError:
|
|
109
|
+
try:
|
|
110
|
+
if tmp.exists():
|
|
111
|
+
tmp.unlink()
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
raise
|
|
115
|
+
return tmp
|
zall/_util/file_state.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""File state cache for tracking read file contents and modification state.
|
|
2
|
+
|
|
3
|
+
缓存每个文件的最后读取时间和 mtime, 支持:
|
|
4
|
+
- is_file_unchanged(path) — 文件是否在上次读取后未被修改
|
|
5
|
+
- get_cached_mtime(path) — 获取缓存的 mtime
|
|
6
|
+
- mark_file_read(path) — 标记文件已读取
|
|
7
|
+
- invalidate(path) — 使缓存失效
|
|
8
|
+
|
|
9
|
+
用途: 避免 agent 重复读取未修改的文件, 节省 API token。
|
|
10
|
+
当 agent 调用 read_file 后, 文件状态被缓存; 下次读取前先检查 mtime,
|
|
11
|
+
如果未变化则提示 agent "file unchanged, no need to re-read"。
|
|
12
|
+
|
|
13
|
+
IPR constraints:
|
|
14
|
+
IPR-3: stdlib only, no model SDK
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FileStateCache:
|
|
26
|
+
"""filestatecache — 跟踪file mtime + read时间。
|
|
27
|
+
|
|
28
|
+
线程安全: 使用 RLock 保护内部 dict。
|
|
29
|
+
缓存上限: 1000 条 (LRU 淘汰), 防长会话无界增长。
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__test__ = False
|
|
33
|
+
|
|
34
|
+
_MAX_ENTRIES = 1000
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self._lock = threading.RLock()
|
|
38
|
+
# key: str(path), value: {"mtime": float, "read_at": float, "size": int}
|
|
39
|
+
self._cache: dict[str, dict[str, Any]] = {}
|
|
40
|
+
|
|
41
|
+
def mark_file_read(self, path: Path | str) -> None:
|
|
42
|
+
"""标记file已read, cache其 mtime 和 size。"""
|
|
43
|
+
key = str(path)
|
|
44
|
+
try:
|
|
45
|
+
stat = Path(path).stat()
|
|
46
|
+
mtime = stat.st_mtime
|
|
47
|
+
size = stat.st_size
|
|
48
|
+
except OSError:
|
|
49
|
+
return
|
|
50
|
+
with self._lock:
|
|
51
|
+
# LRU 淘汰
|
|
52
|
+
if len(self._cache) >= self._MAX_ENTRIES:
|
|
53
|
+
# 淘汰最旧的条目
|
|
54
|
+
oldest_key = min(self._cache, key=lambda k: self._cache[k].get("read_at", 0))
|
|
55
|
+
self._cache.pop(oldest_key, None)
|
|
56
|
+
self._cache[key] = {"mtime": mtime, "read_at": time.time(), "size": size}
|
|
57
|
+
|
|
58
|
+
def is_file_unchanged(self, path: Path | str) -> bool:
|
|
59
|
+
"""checkfile是否在上次read后未被修改。
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
True — 文件未修改 (可以跳过重复读取)
|
|
63
|
+
False — 文件已修改或未被缓存过 (需要重新读取)
|
|
64
|
+
"""
|
|
65
|
+
key = str(path)
|
|
66
|
+
with self._lock:
|
|
67
|
+
cached = self._cache.get(key)
|
|
68
|
+
if cached is None:
|
|
69
|
+
return False
|
|
70
|
+
try:
|
|
71
|
+
current_mtime = Path(path).stat().st_mtime
|
|
72
|
+
except OSError:
|
|
73
|
+
return False
|
|
74
|
+
return bool(cached["mtime"] == current_mtime)
|
|
75
|
+
|
|
76
|
+
def get_cached_mtime(self, path: Path | str) -> float | None:
|
|
77
|
+
"""获取cache的 mtime (不读盘)。"""
|
|
78
|
+
key = str(path)
|
|
79
|
+
with self._lock:
|
|
80
|
+
entry = self._cache.get(key)
|
|
81
|
+
return entry["mtime"] if entry else None
|
|
82
|
+
|
|
83
|
+
def invalidate(self, path: Path | str | None = None) -> None:
|
|
84
|
+
"""使cache失效。path=None 时清除全部cache。"""
|
|
85
|
+
with self._lock:
|
|
86
|
+
if path is None:
|
|
87
|
+
self._cache.clear()
|
|
88
|
+
else:
|
|
89
|
+
self._cache.pop(str(path), None)
|
|
90
|
+
|
|
91
|
+
def get_changed_files(self) -> list[str]:
|
|
92
|
+
"""return自上次read以来被修改的filelist (供 /doctor 用)。"""
|
|
93
|
+
changed = []
|
|
94
|
+
with self._lock:
|
|
95
|
+
for key, entry in self._cache.items():
|
|
96
|
+
try:
|
|
97
|
+
current_mtime = Path(key).stat().st_mtime
|
|
98
|
+
if current_mtime != entry["mtime"]:
|
|
99
|
+
changed.append(key)
|
|
100
|
+
except OSError:
|
|
101
|
+
changed.append(key) # 文件被删除也算变更
|
|
102
|
+
return changed
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def size(self) -> int:
|
|
106
|
+
"""当前cache条目数。"""
|
|
107
|
+
with self._lock:
|
|
108
|
+
return len(self._cache)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# 全局singleton (process级)
|
|
112
|
+
_global_cache: FileStateCache | None = None
|
|
113
|
+
_global_lock = threading.Lock()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_file_state_cache() -> FileStateCache:
|
|
117
|
+
"""获取全局 FileStateCache singleton。"""
|
|
118
|
+
global _global_cache
|
|
119
|
+
if _global_cache is None:
|
|
120
|
+
with _global_lock:
|
|
121
|
+
if _global_cache is None:
|
|
122
|
+
_global_cache = FileStateCache()
|
|
123
|
+
return _global_cache
|
zall/_util/logging.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""zall._util.logging — Unified logging configuration (IPR-0: self-falsifiable errors).
|
|
2
|
+
|
|
3
|
+
Inspired by Grok Build's xai-tracing crate: structured, contextual logging that
|
|
4
|
+
makes non-fatal errors observable without changing control flow.
|
|
5
|
+
|
|
6
|
+
Design:
|
|
7
|
+
- Single entry point: get_zall_logger(__name__) → standard library logger
|
|
8
|
+
- ZALL_LOG_LEVEL env var controls verbosity (default: WARNING)
|
|
9
|
+
- IPR-0 invariant: observer/renderer errors are swallowed but LOGGED
|
|
10
|
+
(silent pass → violates falsifiability; logged pass → observable)
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from zall._util.logging import get_zall_logger
|
|
14
|
+
logger = get_zall_logger(__name__)
|
|
15
|
+
...
|
|
16
|
+
except Exception as e:
|
|
17
|
+
logger.warning("operation failed: %s", e) # observable, non-fatal
|
|
18
|
+
|
|
19
|
+
Corresponds to:
|
|
20
|
+
§0 PR-0: self-falsifiability — silent errors are unfalsifiable
|
|
21
|
+
IPR-0 Self-falsifiability code form — errors must be observable
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
from typing import TextIO
|
|
30
|
+
|
|
31
|
+
# ── Defaults ──
|
|
32
|
+
|
|
33
|
+
_DEFAULT_LEVEL = logging.WARNING
|
|
34
|
+
_ENV_VAR = "ZALL_LOG_LEVEL"
|
|
35
|
+
|
|
36
|
+
# Level name → int mapping for env var parsing
|
|
37
|
+
_LEVEL_NAMES: dict[str, int] = {
|
|
38
|
+
"DEBUG": logging.DEBUG,
|
|
39
|
+
"INFO": logging.INFO,
|
|
40
|
+
"WARNING": logging.WARNING,
|
|
41
|
+
"ERROR": logging.ERROR,
|
|
42
|
+
"CRITICAL": logging.CRITICAL,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _resolve_level() -> int:
|
|
47
|
+
"""Resolve log level from environment or default."""
|
|
48
|
+
raw = os.environ.get(_ENV_VAR, "").strip().upper()
|
|
49
|
+
if raw in _LEVEL_NAMES:
|
|
50
|
+
return _LEVEL_NAMES[raw]
|
|
51
|
+
if raw and raw.isdigit():
|
|
52
|
+
return int(raw)
|
|
53
|
+
return _DEFAULT_LEVEL
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── Formatter ──
|
|
57
|
+
|
|
58
|
+
_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
59
|
+
_FORMAT_DATE = "%H:%M:%S"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _make_handler(stream: TextIO = sys.stderr) -> logging.Handler:
|
|
63
|
+
"""Create a stderr handler with the standard format."""
|
|
64
|
+
handler = logging.StreamHandler(stream)
|
|
65
|
+
handler.setFormatter(logging.Formatter(_FORMAT, _FORMAT_DATE))
|
|
66
|
+
return handler
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── Global state (lazy-initialized) ──
|
|
70
|
+
|
|
71
|
+
_initialized = False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def setup_logging(
|
|
75
|
+
*,
|
|
76
|
+
level: int | None = None,
|
|
77
|
+
stream: TextIO | None = None,
|
|
78
|
+
force: bool = False,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Configure the root zall logger.
|
|
81
|
+
|
|
82
|
+
Called once at process start (from main/REPL). Safe to call multiple
|
|
83
|
+
times — subsequent calls are no-ops unless *force=True*.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
level: Log level (default: from ZALL_LOG_LEVEL env, or WARNING).
|
|
87
|
+
stream: Output stream (default: sys.stderr).
|
|
88
|
+
force: Reconfigure even if already initialized.
|
|
89
|
+
"""
|
|
90
|
+
global _initialized
|
|
91
|
+
if _initialized and not force:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
root = logging.getLogger("zall")
|
|
95
|
+
root.setLevel(level if level is not None else _resolve_level())
|
|
96
|
+
|
|
97
|
+
# Remove existing handlers to avoid duplicates on force re-init
|
|
98
|
+
if force:
|
|
99
|
+
root.handlers.clear()
|
|
100
|
+
|
|
101
|
+
if not root.handlers:
|
|
102
|
+
root.addHandler(_make_handler(stream or sys.stderr))
|
|
103
|
+
|
|
104
|
+
_initialized = True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_zall_logger(name: str) -> logging.Logger:
|
|
108
|
+
"""Get a zall-namespaced logger.
|
|
109
|
+
|
|
110
|
+
Usage:
|
|
111
|
+
logger = get_zall_logger(__name__)
|
|
112
|
+
logger.warning("non-fatal issue: %s", detail)
|
|
113
|
+
|
|
114
|
+
The logger name is prefixed with 'zall.' to keep it under the
|
|
115
|
+
zall root logger's level configuration.
|
|
116
|
+
"""
|
|
117
|
+
# Strip leading 'zall.' if already present to avoid 'zall.zall...'
|
|
118
|
+
if name.startswith("zall."):
|
|
119
|
+
logger_name = name
|
|
120
|
+
elif name.startswith("zall"):
|
|
121
|
+
logger_name = f"zall.{name}"
|
|
122
|
+
else:
|
|
123
|
+
logger_name = f"zall.{name}"
|
|
124
|
+
|
|
125
|
+
return logging.getLogger(logger_name)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_log_level() -> int:
|
|
129
|
+
"""Return the current effective log level for the zall root logger."""
|
|
130
|
+
return logging.getLogger("zall").level
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""zall._util.model_registry — 统一model元数据 (window大小 + 价格 + 别名 + provider register表).
|
|
2
|
+
|
|
3
|
+
C1: 消除模型元数据分散在两处的问题:
|
|
4
|
+
- compactor.py 的 _KNOWN_WINDOWS (窗口大小)
|
|
5
|
+
- app.py _cmd_cost 的 _PRICES (价格表)
|
|
6
|
+
- cli/config.py 的 _MODEL_PRESETS (别名/预设)
|
|
7
|
+
|
|
8
|
+
新增模型时只需更新本文件一处。
|
|
9
|
+
|
|
10
|
+
重命名自 model_meta.py (v0.2.2): 更准确的命名反映其包含 provider 注册表。"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
16
|
+
# modelwindow大小 (token)
|
|
17
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
_KNOWN_WINDOWS: dict[str, int] = {
|
|
20
|
+
# OpenAI
|
|
21
|
+
"gpt-4o": 128000,
|
|
22
|
+
"gpt-4o-mini": 128000,
|
|
23
|
+
"gpt-4-turbo": 128000,
|
|
24
|
+
"gpt-4.1": 128000,
|
|
25
|
+
"gpt-4.1-mini": 128000,
|
|
26
|
+
"gpt-4.1-nano": 128000,
|
|
27
|
+
"o1": 200000,
|
|
28
|
+
"o3-mini": 200000,
|
|
29
|
+
"o4-mini": 200000,
|
|
30
|
+
# Anthropic
|
|
31
|
+
"claude-3-5-sonnet": 200000,
|
|
32
|
+
"claude-3-5-haiku": 200000,
|
|
33
|
+
"claude-3-opus": 200000,
|
|
34
|
+
"claude-3-haiku": 200000,
|
|
35
|
+
"claude-sonnet-4": 200000,
|
|
36
|
+
"claude-sonnet-4-20250514": 200000,
|
|
37
|
+
# Google
|
|
38
|
+
"gemini-2.5-pro": 1_000_000,
|
|
39
|
+
"gemini-2.5-flash": 1_000_000,
|
|
40
|
+
"gemini-2.0-flash": 1_000_000,
|
|
41
|
+
# DeepSeek
|
|
42
|
+
"deepseek-chat": 128000,
|
|
43
|
+
"deepseek-reasoner": 128000,
|
|
44
|
+
"deepseek-v3": 128000,
|
|
45
|
+
"deepseek-r1": 128000,
|
|
46
|
+
# Meta
|
|
47
|
+
"llama3.1": 128000,
|
|
48
|
+
"llama3": 8192,
|
|
49
|
+
"llama-3": 8192,
|
|
50
|
+
# Qwen (all modern variants 128K)
|
|
51
|
+
"qwen2.5": 128000,
|
|
52
|
+
"qwen2.5-coder": 128000,
|
|
53
|
+
"qwen-plus": 131072,
|
|
54
|
+
"qwen3": 131072,
|
|
55
|
+
# 其他
|
|
56
|
+
"agnes-1.5-flash": 128000,
|
|
57
|
+
"agnes-2.0-flash": 128000,
|
|
58
|
+
"agnes-2.5-flash": 128000,
|
|
59
|
+
"glm-4-flash": 128000,
|
|
60
|
+
"glm-4": 128000,
|
|
61
|
+
# llama.cpp (window大小由启动parameter决定, 写常见值)
|
|
62
|
+
"llama.cpp-local": 8192,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# defaultwindow大小 (未知model)
|
|
66
|
+
_DEFAULT_WINDOW: int = 32000
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
70
|
+
# model价格表 ($/1M tokens)
|
|
71
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
_KNOWN_PRICES: dict[str, tuple[float, float]] = {
|
|
74
|
+
# OpenAI
|
|
75
|
+
"gpt-4o": (2.50, 10.00),
|
|
76
|
+
"gpt-4o-mini": (0.15, 0.60),
|
|
77
|
+
"gpt-4-turbo": (10.00, 30.00),
|
|
78
|
+
"gpt-4.1": (2.00, 8.00),
|
|
79
|
+
"gpt-4.1-mini": (0.40, 1.60),
|
|
80
|
+
"gpt-4.1-nano": (0.10, 0.40),
|
|
81
|
+
"o1": (15.00, 60.00),
|
|
82
|
+
"o3-mini": (1.10, 4.40),
|
|
83
|
+
"o4-mini": (0.15, 0.60),
|
|
84
|
+
# Anthropic
|
|
85
|
+
"claude-3-5-sonnet": (3.00, 15.00),
|
|
86
|
+
"claude-3-5-haiku": (0.80, 4.00),
|
|
87
|
+
"claude-3-opus": (15.00, 75.00),
|
|
88
|
+
"claude-3-haiku": (0.25, 1.25),
|
|
89
|
+
"claude-sonnet-4": (3.00, 15.00),
|
|
90
|
+
"claude-sonnet-4-20250514": (3.00, 15.00),
|
|
91
|
+
# Google
|
|
92
|
+
"gemini-2.5-pro": (1.25, 5.00),
|
|
93
|
+
"gemini-2.5-flash": (0.15, 0.60),
|
|
94
|
+
"gemini-2.0-flash": (0.10, 0.40),
|
|
95
|
+
# DeepSeek
|
|
96
|
+
"deepseek-chat": (0.14, 0.28),
|
|
97
|
+
"deepseek-reasoner": (0.55, 2.19),
|
|
98
|
+
"deepseek-v3": (0.27, 1.10),
|
|
99
|
+
"deepseek-r1": (0.55, 2.19),
|
|
100
|
+
# Qwen
|
|
101
|
+
"qwen2.5": (0.35, 1.20),
|
|
102
|
+
"qwen2.5-coder": (0.35, 1.20),
|
|
103
|
+
"qwen-plus": (0.80, 2.00),
|
|
104
|
+
"qwen3": (0.80, 2.00),
|
|
105
|
+
# Zhipu GLM
|
|
106
|
+
"glm-4-flash": (0.10, 0.10),
|
|
107
|
+
"glm-4": (0.50, 1.50),
|
|
108
|
+
# 本地model / 其他
|
|
109
|
+
"agnes-1.5-flash": (0.15, 0.60),
|
|
110
|
+
"agnes-2.0-flash": (0.15, 0.60),
|
|
111
|
+
"agnes-2.5-flash": (0.15, 0.60),
|
|
112
|
+
"llama3.1": (0.0, 0.0), # local
|
|
113
|
+
"llama.cpp-local": (0.0, 0.0), # local (llama.cpp server)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# default价格 (未知model用保守值)
|
|
117
|
+
_DEFAULT_PRICE_IN: float = 3.0 # $3/M input
|
|
118
|
+
_DEFAULT_PRICE_OUT: float = 15.0 # $15/M output
|
|
119
|
+
|
|
120
|
+
# O8: 模块级预sort (按名称长度降序),避免每次调 get_window_size/get_price 都 sorted()
|
|
121
|
+
_SORTED_WINDOWS: list[tuple[str, int]] = sorted(
|
|
122
|
+
_KNOWN_WINDOWS.items(), key=lambda x: -len(x[0])
|
|
123
|
+
)
|
|
124
|
+
_SORTED_PRICES: list[tuple[str, tuple[float, float]]] = sorted(
|
|
125
|
+
_KNOWN_PRICES.items(), key=lambda x: -len(x[0])
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_window_size(model_name: str) -> int:
|
|
130
|
+
"""查modelwindow大小。已知modelreturn精确值, 未知return保守default值。"""
|
|
131
|
+
if not model_name:
|
|
132
|
+
return _DEFAULT_WINDOW
|
|
133
|
+
# B1 fix: 先精确匹配完整名称
|
|
134
|
+
if model_name in _KNOWN_WINDOWS:
|
|
135
|
+
return _KNOWN_WINDOWS[model_name]
|
|
136
|
+
# 再按前缀匹配, 按名称长度降序 (长前缀优先, 防 gpt-4o-mini 误配 gpt-4o)
|
|
137
|
+
for known, size in _SORTED_WINDOWS:
|
|
138
|
+
if model_name.startswith(known):
|
|
139
|
+
return size
|
|
140
|
+
return _DEFAULT_WINDOW
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_price(model_name: str) -> tuple[float, float]:
|
|
144
|
+
"""获取model价格 (input_price, output_price) $/1M tokens。
|
|
145
|
+
|
|
146
|
+
未知模型返回保守默认值 ($3/$15 per 1M)。
|
|
147
|
+
B1 fix: 先精确匹配完整名称, 再按长前缀降序匹配。
|
|
148
|
+
"""
|
|
149
|
+
if not model_name:
|
|
150
|
+
return (_DEFAULT_PRICE_IN, _DEFAULT_PRICE_OUT)
|
|
151
|
+
# 先精确匹配
|
|
152
|
+
if model_name in _KNOWN_PRICES:
|
|
153
|
+
return _KNOWN_PRICES[model_name]
|
|
154
|
+
# 再按前缀长度降序匹配
|
|
155
|
+
for prefix, prices in _SORTED_PRICES:
|
|
156
|
+
if model_name.startswith(prefix):
|
|
157
|
+
return prices
|
|
158
|
+
return (_DEFAULT_PRICE_IN, _DEFAULT_PRICE_OUT)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
162
|
+
# Item B: Provider register表 (统一元数据, 消除 if/elif 链)
|
|
163
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
# Item D: 6-tuple: (display, env_var, default_api_base, get_key_url, model_prefixes, adapter_import_path)
|
|
166
|
+
# adapter_import_path 格式: "module.path:ClassName" — 供 _build_adapter 用 importlib dynamicload
|
|
167
|
+
_ProviderMeta = dict[str, tuple[str, str, str, str, tuple[str, ...], str]]
|
|
168
|
+
|
|
169
|
+
_PROVIDER_REGISTRY: _ProviderMeta = {
|
|
170
|
+
"openai": ("OpenAI-compatible", "ZALL_API_KEY", "https://api.openai.com/v1", "https://platform.openai.com/api-keys", ("gpt-", "o1", "o3", "o4", "glm-", "qwen"), "zall.adapters.openai_compat:OpenAICompatAdapter"),
|
|
171
|
+
"anthropic": ("Anthropic Claude", "ANTHROPIC_API_KEY", "https://api.anthropic.com", "https://console.anthropic.com/", ("claude-", "claude"), "zall.adapters.anthropic:AnthropicAdapter"),
|
|
172
|
+
"gemini": ("Google Gemini", "GOOGLE_API_KEY", "https://generativelanguage.googleapis.com", "https://aistudio.google.com/app/apikey", ("gemini-", "gemini"), "zall.adapters.gemini:GeminiAdapter"),
|
|
173
|
+
"deepseek": ("DeepSeek", "DEEPSEEK_API_KEY", "https://api.deepseek.com/v1", "https://platform.deepseek.com/api_keys", ("deepseek-", "deepseek"), "zall.adapters.openai_compat:OpenAICompatAdapter"),
|
|
174
|
+
"ollama": ("Ollama (local)", "", "http://localhost:11434", "https://ollama.ai", ("ollama-", "llama"), "zall.adapters.ollama:OllamaAdapter"),
|
|
175
|
+
"agnes": ("Agnes AI", "ZALL_API_KEY", "https://apihub.agnes-ai.com/v1", "https://apihub.agnes-ai.com", ("agnes-",), "zall.adapters.openai_compat:OpenAICompatAdapter"),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# compatible性: preserve旧 dict reference (代码中仍reference _PROVIDER_DISPLAY 等)
|
|
179
|
+
_provider_display = {k: v[0] for k, v in _PROVIDER_REGISTRY.items()}
|
|
180
|
+
_provider_env_vars = {k: v[1] for k, v in _PROVIDER_REGISTRY.items()}
|
|
181
|
+
_provider_api_bases = {k: v[2] for k, v in _PROVIDER_REGISTRY.items()}
|
|
182
|
+
_provider_key_urls = {k: v[3] for k, v in _PROVIDER_REGISTRY.items()}
|
|
183
|
+
|
|
184
|
+
# Item D: 已知 adapter type → import path mapping (供 TOML 自定义 provider 用)
|
|
185
|
+
_ADAPTER_TYPE_MAP: dict[str, str] = {
|
|
186
|
+
"openai-compat": "zall.adapters.openai_compat:OpenAICompatAdapter",
|
|
187
|
+
"anthropic": "zall.adapters.anthropic:AnthropicAdapter",
|
|
188
|
+
"gemini": "zall.adapters.gemini:GeminiAdapter",
|
|
189
|
+
"ollama": "zall.adapters.ollama:OllamaAdapter",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_model_provider(model_name: str) -> str:
|
|
194
|
+
"""根据model名推断 provider type (Item B: 基于register表)。"""
|
|
195
|
+
if not model_name:
|
|
196
|
+
return "openai"
|
|
197
|
+
model_lower = model_name.lower()
|
|
198
|
+
for provider, (_display, _env, _base, _url, prefixes, _adapter) in _PROVIDER_REGISTRY.items():
|
|
199
|
+
for prefix in prefixes:
|
|
200
|
+
if model_lower.startswith(prefix):
|
|
201
|
+
return provider
|
|
202
|
+
return "openai" # default
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
206
|
+
# model预设list (供交互式 picker 使用, 从 config.py 迁移至此)
|
|
207
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
_MODEL_PRESETS: list[tuple[str, str, str, str]] = [
|
|
210
|
+
("agnes-2.0-flash", "agnes-2.0-flash", "fast / cheap (default + latest)", "agnes"),
|
|
211
|
+
("agnes-2.5-flash", "agnes-2.5-flash", "fast / cheap (newer)", "agnes"),
|
|
212
|
+
("agnes-1.5-flash", "agnes-1.5-flash", "fast / cheap (legacy)", "agnes"),
|
|
213
|
+
("gpt-4o-mini", "gpt-4o-mini", "OpenAI, cheap", "openai"),
|
|
214
|
+
("gpt-4o", "gpt-4o", "OpenAI, capable", "openai"),
|
|
215
|
+
("claude-3-5-sonnet", "claude-3-5-sonnet", "Anthropic", "anthropic"),
|
|
216
|
+
("claude-sonnet-4", "claude-sonnet-4-20250514", "Anthropic Claude Sonnet 4", "anthropic"),
|
|
217
|
+
("deepseek-chat", "deepseek-chat", "DeepSeek", "deepseek"),
|
|
218
|
+
("glm-4-flash", "glm-4-flash", "Zhipu GLM", "openai"),
|
|
219
|
+
("qwen-plus", "qwen-plus", "Alibaba Qwen", "openai"),
|
|
220
|
+
("gemini-2.5-pro", "gemini-2.5-pro-exp-03-25", "Google Gemini 2.5 Pro", "gemini"),
|
|
221
|
+
("gemini-2.5-flash", "gemini-2.5-flash-001", "Google Gemini 2.5 Flash", "gemini"),
|
|
222
|
+
("ollama-llama3", "llama3.1", "Ollama local (llama3.1)", "ollama"),
|
|
223
|
+
("ollama-qwen25", "qwen2.5", "Ollama local (qwen2.5)", "ollama"),
|
|
224
|
+
("llama.cpp", "llama.cpp-local", "llama.cpp local server (OpenAI-compat)", "openai"),
|
|
225
|
+
]
|