mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 07:11:57 +00:00
257 lines
9.7 KiB
Python
257 lines
9.7 KiB
Python
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 模型目录
|
||
#
|
||
# pricing 单位是「美元 / 百万 token」,用于 run 的成本估算。
|
||
# 新模型的价格会变,这份表只是开箱即用的默认值,用户可以在设置页改。
|
||
# --------------------------------------------------------------------------
|
||
|
||
ANTHROPIC_MODELS: list[dict[str, Any]] = [
|
||
{"id": "claude-opus-5", "label": "Claude Opus 5", "context": 1_000_000,
|
||
"pricing": {"input": 5.0, "output": 25.0}, "thinking": "adaptive"},
|
||
{"id": "claude-fable-5-1", "label": "Claude Fable 5.1", "context": 1_000_000,
|
||
"pricing": {"input": 10.0, "output": 50.0}, "thinking": "always"},
|
||
{"id": "claude-opus-4-8", "label": "Claude Opus 4.8", "context": 1_000_000,
|
||
"pricing": {"input": 5.0, "output": 25.0}, "thinking": "adaptive"},
|
||
{"id": "claude-sonnet-5", "label": "Claude Sonnet 5", "context": 1_000_000,
|
||
"pricing": {"input": 2.0, "output": 10.0}, "thinking": "adaptive"},
|
||
{"id": "claude-haiku-4-5", "label": "Claude Haiku 4.5", "context": 200_000,
|
||
"pricing": {"input": 1.0, "output": 5.0}, "thinking": "budget"},
|
||
]
|
||
|
||
OPENAI_MODELS: list[dict[str, Any]] = [
|
||
{"id": "gpt-5.2", "label": "GPT-5.2", "context": 400_000,
|
||
"pricing": {"input": 1.25, "output": 10.0}},
|
||
{"id": "gpt-5.2-mini", "label": "GPT-5.2 mini", "context": 400_000,
|
||
"pricing": {"input": 0.25, "output": 2.0}},
|
||
{"id": "o4", "label": "o4", "context": 200_000,
|
||
"pricing": {"input": 3.0, "output": 12.0}},
|
||
]
|
||
|
||
# 常见的 OpenAI 兼容服务,一键添加用
|
||
COMPATIBLE_PRESETS: list[dict[str, Any]] = [
|
||
{
|
||
"key": "deepseek",
|
||
"label": "DeepSeek",
|
||
"base_url": "https://api.deepseek.com/v1",
|
||
"models": [
|
||
{"id": "deepseek-chat", "label": "DeepSeek Chat", "context": 128_000,
|
||
"pricing": {"input": 0.27, "output": 1.1}},
|
||
{"id": "deepseek-reasoner", "label": "DeepSeek Reasoner", "context": 128_000,
|
||
"pricing": {"input": 0.55, "output": 2.19}},
|
||
],
|
||
},
|
||
{
|
||
"key": "moonshot",
|
||
"label": "Moonshot / Kimi",
|
||
"base_url": "https://api.moonshot.cn/v1",
|
||
"models": [
|
||
{"id": "kimi-k2-turbo-preview", "label": "Kimi K2 Turbo", "context": 256_000,
|
||
"pricing": {"input": 0.6, "output": 2.5}},
|
||
{"id": "moonshot-v1-128k", "label": "Moonshot v1 128k", "context": 128_000,
|
||
"pricing": {"input": 0.8, "output": 3.0}},
|
||
],
|
||
},
|
||
{
|
||
"key": "dashscope",
|
||
"label": "阿里云百炼 / 通义千问",
|
||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||
"models": [
|
||
{"id": "qwen-max", "label": "Qwen Max", "context": 32_000,
|
||
"pricing": {"input": 0.34, "output": 1.37}},
|
||
{"id": "qwen-plus", "label": "Qwen Plus", "context": 131_000,
|
||
"pricing": {"input": 0.11, "output": 0.28}},
|
||
],
|
||
},
|
||
{
|
||
"key": "zhipu",
|
||
"label": "智谱 GLM",
|
||
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
||
"models": [
|
||
{"id": "glm-4.6", "label": "GLM-4.6", "context": 200_000,
|
||
"pricing": {"input": 0.6, "output": 2.2}},
|
||
],
|
||
},
|
||
{
|
||
"key": "siliconflow",
|
||
"label": "硅基流动",
|
||
"base_url": "https://api.siliconflow.cn/v1",
|
||
"models": [
|
||
{"id": "deepseek-ai/DeepSeek-V3", "label": "DeepSeek V3", "context": 128_000,
|
||
"pricing": {"input": 0.27, "output": 1.1}},
|
||
],
|
||
},
|
||
{
|
||
"key": "openrouter",
|
||
"label": "OpenRouter",
|
||
"base_url": "https://openrouter.ai/api/v1",
|
||
"models": [
|
||
{"id": "anthropic/claude-opus-5", "label": "Claude Opus 5", "context": 1_000_000,
|
||
"pricing": {"input": 5.0, "output": 25.0}},
|
||
],
|
||
},
|
||
{
|
||
"key": "ollama",
|
||
"label": "Ollama(本地)",
|
||
"base_url": "http://localhost:11434/v1",
|
||
"api_key_optional": True,
|
||
"models": [
|
||
{"id": "qwen3:8b", "label": "Qwen3 8B(本地)", "context": 32_000,
|
||
"pricing": {"input": 0.0, "output": 0.0}},
|
||
{"id": "llama3.2", "label": "Llama 3.2(本地)", "context": 128_000,
|
||
"pricing": {"input": 0.0, "output": 0.0}},
|
||
],
|
||
},
|
||
]
|
||
|
||
MOCK_MODELS: list[dict[str, Any]] = [
|
||
{"id": "mock-fast", "label": "演示模型(无需 API Key)", "context": 128_000,
|
||
"pricing": {"input": 0.0, "output": 0.0}},
|
||
]
|
||
|
||
PROVIDER_KINDS = [
|
||
{
|
||
"kind": "anthropic",
|
||
"label": "Anthropic",
|
||
"base_url_hint": "留空使用官方地址;填写后使用自定义网关(默认读取 ANTHROPIC_BASE_URL)",
|
||
"models": ANTHROPIC_MODELS,
|
||
"default_model": "claude-opus-5",
|
||
},
|
||
{
|
||
"kind": "openai",
|
||
"label": "OpenAI",
|
||
"base_url_hint": "留空使用官方地址",
|
||
"models": OPENAI_MODELS,
|
||
"default_model": "gpt-5.2",
|
||
},
|
||
{
|
||
"kind": "openai_compatible",
|
||
"label": "OpenAI 兼容(DeepSeek / Kimi / 通义 / Ollama…)",
|
||
"base_url_hint": "必填,例如 https://api.deepseek.com/v1",
|
||
"models": [],
|
||
"presets": COMPATIBLE_PRESETS,
|
||
"default_model": "",
|
||
},
|
||
{
|
||
"kind": "mock",
|
||
"label": "演示模型(无需 API Key)",
|
||
"base_url_hint": "",
|
||
"models": MOCK_MODELS,
|
||
"default_model": "mock-fast",
|
||
},
|
||
]
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 模型能力
|
||
# --------------------------------------------------------------------------
|
||
|
||
# Claude 4.6 及以后的模型移除了采样参数,传 temperature/top_p 会直接 400。
|
||
_NO_SAMPLING_PREFIXES = (
|
||
"claude-fable-5",
|
||
"claude-mythos-5",
|
||
"claude-opus-5",
|
||
"claude-opus-4-8",
|
||
"claude-opus-4-7",
|
||
"claude-opus-4-6",
|
||
"claude-sonnet-5",
|
||
"claude-sonnet-4-6",
|
||
)
|
||
|
||
# 支持 adaptive thinking(而不是老的 budget_tokens)的模型
|
||
_ADAPTIVE_THINKING_PREFIXES = _NO_SAMPLING_PREFIXES
|
||
|
||
# 支持 output_config.effort
|
||
_EFFORT_PREFIXES = _NO_SAMPLING_PREFIXES + ("claude-opus-4-5",)
|
||
|
||
# 交错思考(工具结果回来之后再想一段,而不是一轮开头想完就只剩执行)。
|
||
#
|
||
# 4.6 及以后走 adaptive thinking,交错是自带的,不需要也不该再挂 beta header
|
||
# ——在 Opus 4.6 上手动模式压根没有交错思考,只有 adaptive 有。上面那张
|
||
# _ADAPTIVE_THINKING_PREFIXES 覆盖的都属于这一档。
|
||
#
|
||
# 需要显式开的是 Claude 4 / 4.5 这一代。目录里没有列它们,但模型名是放行的
|
||
# (用户可以手填),所以这条路径要留着。Haiku 4.5 不支持,前缀也就不包含它。
|
||
_INTERLEAVED_BETA_PREFIXES = ("claude-opus-4", "claude-sonnet-4")
|
||
INTERLEAVED_BETA = "interleaved-thinking-2025-05-14"
|
||
|
||
|
||
def needs_interleaved_beta(model: str) -> bool:
|
||
"""这个模型要不要靠 beta header 才会在工具之间思考。"""
|
||
name = _norm(model)
|
||
if supports_adaptive_thinking(name):
|
||
return False # adaptive 自带,多挂一个 header 只会是噪音
|
||
return name.startswith(_INTERLEAVED_BETA_PREFIXES)
|
||
|
||
|
||
def _norm(model: str) -> str:
|
||
"""去掉 openrouter 之类的前缀,只看模型名本身。"""
|
||
return (model or "").split("/")[-1].strip()
|
||
|
||
|
||
def supports_sampling(model: str) -> bool:
|
||
"""这个模型还能不能传 temperature / top_p。"""
|
||
return not _norm(model).startswith(_NO_SAMPLING_PREFIXES)
|
||
|
||
|
||
def supports_adaptive_thinking(model: str) -> bool:
|
||
return _norm(model).startswith(_ADAPTIVE_THINKING_PREFIXES)
|
||
|
||
|
||
def supports_effort(model: str) -> bool:
|
||
return _norm(model).startswith(_EFFORT_PREFIXES)
|
||
|
||
|
||
def find_pricing(model: str) -> dict[str, float] | None:
|
||
target = _norm(model)
|
||
pools = [ANTHROPIC_MODELS, OPENAI_MODELS, MOCK_MODELS]
|
||
pools += [p["models"] for p in COMPATIBLE_PRESETS]
|
||
for pool in pools:
|
||
for entry in pool:
|
||
if _norm(entry["id"]) == target:
|
||
return entry.get("pricing")
|
||
return None
|
||
|
||
|
||
def find_context(model: str) -> int | None:
|
||
"""目录里登记的上下文窗口(token)。没登记的返回 None,由调用方取保守值。"""
|
||
target = _norm(model)
|
||
pools = [ANTHROPIC_MODELS, OPENAI_MODELS, MOCK_MODELS]
|
||
pools += [p["models"] for p in COMPATIBLE_PRESETS]
|
||
for pool in pools:
|
||
for entry in pool:
|
||
if _norm(entry["id"]) == target:
|
||
value = entry.get("context")
|
||
return int(value) if isinstance(value, (int, float)) and value > 0 else None
|
||
return None
|
||
|
||
|
||
def unpriced_call(data: Any) -> bool:
|
||
"""一次模型调用(llm.end 的数据)的金额估不出来:用了 token,模型却不在价格目录里,estimate_cost 只能给 0。
|
||
|
||
这时写 $0 像是没花钱(真实运行里上百次调用、上百万 token,运行页写着 $0)。本地模型在目录里标的是 0 价,
|
||
不算估不出来。事件自己标了 unpriced 的以它为准。"""
|
||
if not isinstance(data, dict):
|
||
return False
|
||
if isinstance(data.get("unpriced"), bool):
|
||
return data["unpriced"]
|
||
tokens = sum(v for v in (data.get("input_tokens"), data.get("output_tokens"))
|
||
if isinstance(v, (int, float)) and not isinstance(v, bool))
|
||
model = data.get("model")
|
||
return tokens > 0 and isinstance(model, str) and bool(model) and not data.get("cost_usd") \
|
||
and find_pricing(model) is None
|
||
|
||
|
||
def estimate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
|
||
"""按目录价估算一次调用的花费(美元)。"""
|
||
pricing = find_pricing(model)
|
||
if not pricing:
|
||
return 0.0
|
||
return (
|
||
input_tokens / 1_000_000 * pricing.get("input", 0.0)
|
||
+ output_tokens / 1_000_000 * pricing.get("output", 0.0)
|
||
)
|