ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+223
View File
@@ -0,0 +1,223 @@
"""
统一的 LLM 客户端配置。
默认使用 OpenAI(读取 OPENAI_API_KEY,模型 gpt-5.6-luna)。
也支持通过环境变量 LLM_PROVIDER 切换到 Moonshot / 火山方舟(ARK)
它们都兼容 OpenAI 的 Chat Completions + 工具调用接口。
export LLM_PROVIDER=openai # 默认
export LLM_PROVIDER=moonshot # 用 MOONSHOT_API_KEY
export LLM_PROVIDER=ark # 用 ARK_API_KEY,并需设置 ARK_MODEL
统一的 OpenRouter 兜底(fallback):
若所选 provider 自己的 Key 缺失,但设置了 OPENROUTER_API_KEY,则自动改走
OpenRouterhttps://openrouter.ai/api/v1),并把模型名映射到 OpenRouter 命名:
gpt-* -> openai/gpt-*
claude-* -> anthropic/claude-opus-4.8
"/" -> 原样透传
其它 -> openai/gpt-5.6-luna
"""
import os
import time
from typing import Any
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
# 各提供商的默认配置:base_url / 环境变量名 / 默认模型
_PROVIDERS = {
"openai": {
"base_url": None, # 使用 SDK 默认
"key_env": "OPENAI_API_KEY",
"default_model": "gpt-5.6-luna",
},
"moonshot": {
"base_url": "https://api.moonshot.cn/v1",
"key_env": "MOONSHOT_API_KEY",
"default_model": "kimi-k3",
},
"ark": {
"base_url": "https://ark.cn-beijing.volces.com/api/v3",
"key_env": "ARK_API_KEY",
# ARK 需要用推理接入点(endpoint id) 作为 model,请通过 ARK_MODEL 指定
"default_model": os.getenv("ARK_MODEL", "doubao-seed-1-6-250615"),
},
"openrouter": {
"base_url": OPENROUTER_BASE_URL,
"key_env": "OPENROUTER_API_KEY",
"default_model": "openai/gpt-4o-mini",
},
}
API_TURNS = []
def _jsonable(value: Any) -> Any:
if hasattr(value, "model_dump"):
return _jsonable(value.model_dump(mode="json", exclude_none=True))
if isinstance(value, dict):
return {str(key): _jsonable(item) for key, item in value.items()}
if isinstance(value, (list, tuple)):
return [_jsonable(item) for item in value]
return value
def get_provider() -> str:
return os.getenv("LLM_PROVIDER", "openai").lower().strip()
def _to_openrouter_model(model: str) -> str:
"""把常见模型名映射到 OpenRouter 命名空间。"""
if not model:
return "openai/gpt-5.6-luna"
if "/" in model:
return model
if model.startswith("gpt-"):
return "openai/" + model
if model.startswith("claude-"):
return "anthropic/claude-opus-4.8"
return "openai/gpt-5.6-luna"
def _is_reasoning_model(model: str) -> bool:
"""gpt-5.x / o1·o3·o4 / kimi-k3 / *reasoner 等推理模型:不接受 temperature=0
直连 gpt-5.x 还需组织实名且工具调用受限,故优先走 OpenRouter。"""
m = (model or "").lower()
return (m.startswith(("gpt-5", "o1", "o3", "o4"))
or m.startswith("kimi-k3")
or "reasoner" in m or "thinking" in m)
def _use_openrouter(cfg: dict) -> bool:
"""走 OpenRouter 的两种情形:
1) provider 自己的 Key 缺失、但有 OPENROUTER_API_KEY(统一兜底);
2) 目标是 gpt-5.x 且有 OPENROUTER_API_KEY —— 直连 gpt-5.x 需组织实名、
且 /chat/completions 工具调用受限,故即便有 OPENAI_API_KEY 也优先 OpenRouter。"""
if not os.getenv("OPENROUTER_API_KEY"):
return False
if not os.getenv(cfg["key_env"]):
return True
model = os.getenv("LLM_MODEL") or cfg["default_model"]
return (model or "").lower().startswith("gpt-5")
def get_model() -> str:
"""允许用 LLM_MODEL 覆盖默认模型;OpenRouter 兜底路径下映射模型名。"""
provider = get_provider()
if provider not in _PROVIDERS:
raise ValueError(f"未知的 LLM_PROVIDER: {provider}")
cfg = _PROVIDERS[provider]
model = os.getenv("LLM_MODEL") or cfg["default_model"]
if _use_openrouter(cfg):
return _to_openrouter_model(model)
return model
def get_client() -> OpenAI:
provider = get_provider()
if provider not in _PROVIDERS:
raise ValueError(f"未知的 LLM_PROVIDER: {provider}")
cfg = _PROVIDERS[provider]
if _use_openrouter(cfg):
return OpenAI(api_key=os.getenv("OPENROUTER_API_KEY"), base_url=OPENROUTER_BASE_URL)
api_key = os.getenv(cfg["key_env"])
if not api_key:
raise RuntimeError(
f"环境变量 {cfg['key_env']} 未设置,也未设置 OPENROUTER_API_KEY。"
f"请参考 env.example 配置其一(OpenRouter 可作为统一兜底)后重试。"
)
kwargs = {"api_key": api_key}
if cfg["base_url"]:
kwargs["base_url"] = cfg["base_url"]
return OpenAI(**kwargs)
def record_completion(client: OpenAI, *, kind: str, **request: Any):
"""Execute and retain a credential-free raw request/response receipt."""
started = time.time()
response = client.chat.completions.create(**request)
API_TURNS.append({
"kind": kind,
"provider": get_provider(),
"endpoint": get_backend_metadata()["endpoint"],
"request": _jsonable(request),
"response": response.model_dump(mode="json", exclude_none=True),
"elapsed_seconds": round(time.time() - started, 6),
})
return response
def reset_api_turns() -> None:
API_TURNS.clear()
def get_api_turns() -> list[dict]:
return list(API_TURNS)
def get_backend_metadata() -> dict[str, Any]:
provider = get_provider()
cfg = _PROVIDERS[provider]
if _use_openrouter(cfg):
base_url = OPENROUTER_BASE_URL
key_env = "OPENROUTER_API_KEY"
routed_provider = "openrouter"
else:
base_url = cfg["base_url"] or "https://api.openai.com/v1"
key_env = cfg["key_env"]
routed_provider = provider
return {
"configured_provider": provider,
"routed_provider": routed_provider,
"model": get_model(),
"endpoint": f"{base_url}/chat/completions",
"credential_source_env": key_env,
"credential_value_recorded": False,
}
def usage_summary() -> dict[str, Any]:
prompt = completion = total = 0
native_cost = 0.0
native_cost_count = 0
for turn in API_TURNS:
usage = turn.get("response", {}).get("usage") or {}
prompt += int(usage.get("prompt_tokens") or 0)
completion += int(usage.get("completion_tokens") or 0)
total += int(usage.get("total_tokens") or 0)
if usage.get("cost") is not None:
native_cost += float(usage["cost"])
native_cost_count += 1
return {
"prompt_tokens": prompt,
"completion_tokens": completion,
"total_tokens": total or prompt + completion,
"provider_reported_cost_usd": round(native_cost, 9) if native_cost_count else None,
"provider_reported_cost_observations": native_cost_count,
"cost_qualification": (
"provider-native usage.cost summed across calls"
if native_cost_count else "provider did not expose monetary cost; no price was guessed"
),
}
# 全部 LLM 调用统一使用低温度,保证结果可复现;
# 但推理模型(gpt-5.x / o 系列 / kimi-k3 等)只接受默认 temperature=1
# 故按当前解析出的模型自动选择默认温度(可用 LLM_TEMPERATURE 显式覆盖)。
def _default_temperature() -> str:
provider = get_provider()
cfg = _PROVIDERS.get(provider, _PROVIDERS["openai"])
model = os.getenv("LLM_MODEL") or cfg["default_model"]
return "1" if _is_reasoning_model(model) else "0"
def get_temperature() -> float:
"""在调用时按当前解析出的模型选择温度,使 CLI/env 的 --model/--provider
覆盖生效。原来的模块级 TEMPERATURE 常量在 import 时就被固定,而 demo.py 在
import 之后才设置 LLM_MODEL/LLM_PROVIDER,导致温度停留在默认模型的值
(例如把非推理模型误用 temperature=1,破坏了本文件追求的可复现性)。"""
return float(os.getenv("LLM_TEMPERATURE", _default_temperature()))