Files
liqiang b119135836
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
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

130 lines
5.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
全局配置:模型与价格。
价格换算成本时使用「每百万 token 单价(美元)」。
默认值取自 OpenAI gpt-4o-mini 的公开定价(2024-2025):
- 输入 : $0.15 / 1M tokens
- 缓存命中输入 : $0.075 / 1M tokens (命中 prompt cache 的输入按 5 折计费)
- 输出 : $0.60 / 1M tokens
注意:
1. 默认模型为 gpt-5.6-luna(当前廉价旗舰)。首选凭据是 OPENAI_API_KEY;若未设置,
自动回退到 OPENROUTER_API_KEY 并把模型名映射成 OpenRouter idgpt-* -> openai/*)。
由于 gpt-5.x 直连 OpenAI 需要组织实名认证,只要 OPENROUTER_API_KEY 存在就优先走
OpenRouter(见 make_client_and_model)。仍可用 COST_DEMO_MODEL / --model 切换任意模型。
2. OpenAI 的 prompt caching 是「自动」的:当请求前缀 >= 1024 token 且与近期请求
命中相同前缀时,usage.prompt_tokens_details.cached_tokens 会大于 0
这部分 token 按缓存价(更便宜)计费。本项目正是用它来真实体现 KV-cache 的节省。
OpenRouter 转发 OpenAI 时同样在 prompt_tokens_details.cached_tokens 回传缓存命中。)
"""
import os
from dataclasses import dataclass
from dotenv import load_dotenv
load_dotenv()
# 使用的模型(默认当前廉价旗舰 gpt-5.6-luna;可用 COST_DEMO_MODEL / --model 覆盖)
MODEL = os.environ.get("COST_DEMO_MODEL", "gpt-5.6-luna")
# OpenRouter 回退:无 OPENAI_API_KEY 时用 OPENROUTER_API_KEY 走 OpenAI 兼容端点。
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
def _to_openrouter_model(model: str) -> str:
"""把模型名映射成 OpenRouter id:含 '/' 视为原生 idgpt-* -> openai/*
claude-* -> anthropic/claude-opus-4.8;其余回退到 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 make_client_and_model(model: str):
"""构造 OpenAI 兼容 client 并返回 (client, 实际调用的模型名)。
回退策略(universal OpenRouter fallback):
- gpt-5.x 且存在 OPENROUTER_API_KEY -> 优先走 OpenRouter(直连需组织实名认证);
- 否则有 OPENAI_API_KEY -> 直连 OpenAI,模型名不变;
- 否则有 OPENROUTER_API_KEY -> 走 OpenRouter,模型名按 _to_openrouter_model 映射;
- 两者皆无 -> 抛出清晰错误。
"""
from openai import OpenAI
primary = os.environ.get("OPENAI_API_KEY", "").strip()
orkey = os.environ.get("OPENROUTER_API_KEY", "").strip()
prefer_openrouter = bool(orkey) and model.startswith("gpt-5")
if not prefer_openrouter and primary:
return OpenAI(timeout=60.0, max_retries=2), model
if orkey:
return (
OpenAI(base_url=OPENROUTER_BASE_URL, api_key=orkey,
timeout=60.0, max_retries=2),
_to_openrouter_model(model),
)
if primary:
return OpenAI(timeout=60.0, max_retries=2), model
raise RuntimeError(
"缺少可用凭据:请设置 OPENAI_API_KEY(直连 OpenAI),或设置 "
"OPENROUTER_API_KEY(自动回退到 OpenRouter);或改用 --offline 离线复算(无需 key)。"
)
# 每百万 token 的美元单价(默认 gpt-4o-mini
PRICE_INPUT_PER_M = 0.15 # 普通输入
PRICE_CACHED_PER_M = 0.075 # 命中缓存的输入(gpt-4o-mini 缓存读取为输入价的 50%)
PRICE_OUTPUT_PER_M = 0.60 # 输出
@dataclass(frozen=True)
class Pricing:
"""一组每百万 token 的美元单价。"""
input_per_m: float
cached_per_m: float
output_per_m: float
def cost_usd(self, prompt_tokens: int, cached_tokens: int,
completion_tokens: int) -> float:
"""按 token 用量换算成本(美元)。
prompt_tokens : usage.prompt_tokens,包含了缓存命中的部分
cached_tokens : usage.prompt_tokens_details.cached_tokens,命中缓存的输入 token
completion_tokens: usage.completion_tokens
未命中缓存的输入 = prompt_tokens - cached_tokens,按普通输入价计费;
命中缓存的输入按缓存价计费。
"""
uncached_input = max(prompt_tokens - cached_tokens, 0)
return (
uncached_input / 1_000_000 * self.input_per_m
+ cached_tokens / 1_000_000 * self.cached_per_m
+ completion_tokens / 1_000_000 * self.output_per_m
)
# 常见 OpenAI 模型的公开单价预设(每百万 token,美元),方便 CLI 用 --model 一键切换。
# 换更强的模型不影响 KV-cache 机制(仍要求稳定前缀 >= 1024 token)。
PRICING_PRESETS = {
"gpt-4o-mini": Pricing(0.15, 0.075, 0.60),
"gpt-4o": Pricing(2.50, 1.25, 10.00),
"gpt-4.1-mini": Pricing(0.40, 0.10, 1.60),
"gpt-4.1": Pricing(2.00, 0.50, 8.00),
}
def default_pricing() -> Pricing:
"""返回默认模型(config 中 MODEL)的单价;未知模型回退到模块级 PRICE_* 默认值。"""
return PRICING_PRESETS.get(
MODEL, Pricing(PRICE_INPUT_PER_M, PRICE_CACHED_PER_M, PRICE_OUTPUT_PER_M)
)
def cost_usd(prompt_tokens: int, cached_tokens: int, completion_tokens: int,
pricing: "Pricing | None" = None) -> float:
"""按 token 用量换算成本(美元)。默认用模块级单价,可传入自定义 Pricing。"""
p = pricing or Pricing(PRICE_INPUT_PER_M, PRICE_CACHED_PER_M, PRICE_OUTPUT_PER_M)
return p.cost_usd(prompt_tokens, cached_tokens, completion_tokens)