模型用量与成本追踪

Langfuse 追踪你 LLM generations 的用量和成本,并按用量类型提供细分。用量和成本可以在类型为 generation 和 embedding 的 observations 上追踪。
- 用量详情(Usage details):每种用量类型消耗的单位数
- 成本详情(Cost details):每种用量类型的美元成本
用量类型可以是任意字符串,并因 LLM 提供商而异。在最高层级,它们可以只是 input 和 output。随着 LLM 变得更加复杂,需要额外的用量类型,如 cached_tokens、audio_tokens、image_tokens。
在 UI 中,Langfuse 将所有包含字符串 input 的用量类型汇总为输入用量类型,类似地将 output 汇总为输出用量类型。如果没有接入 total 用量类型,Langfuse 会将所有用量类型单位求和为 total。为此,用量类型必须是互斥的桶。
用量详情和成本详情都可以是
- 通过 API、SDK 或集成接入
- 或基于 generation 的
model参数推断。Langfuse 自带一份预定义的流行模型及其分词器列表,包括 OpenAI、Anthropic 和 Google 模型。你也可以添加自己的自定义模型定义,或通过 GitHub 请求对新模型的官方支持。推断的成本在接入时使用该时间点可用的模型和价格信息计算。
接入的用量和成本优先于推断的用量和成本:
flowchart LR
A[Ingested Observation]
B["Usage (tokens or other unit)"]
C["Cost (in USD)"]
A --> D{Includes usage?}
D -->|Yes| B
D -->|No| E(Use tokenizer) --> B
A --> F{Includes cost?}
F -->|Yes| C
F -->|No| G(Use model price/unit) --> C
B -->|use usage| G
通过 Metrics API,你可以从 Langfuse 检索聚合的用量和成本指标,用于分析、计费和速率限制的下游使用。该 API 允许你按应用类型、用户或标签过滤。关于归因和控制支出的更广泛 playbook,参见我们的 LLM 成本管理 指南。
接入用量和/或成本
如果 LLM 响应中可用,接入用量和/或成本是在 Langfuse 中追踪用量最准确、最稳健的方式。
许多 Langfuse 集成会自动从 LLM 响应捕获用量详情和成本详情数据。如果这未按预期工作,请在 GitHub 上创建一个 issue。
使用 @observe() 装饰器时:
from langfuse import observe, get_client
import anthropic
langfuse = get_client()
anthropic_client = anthropic.Anthropic()
@observe(as_type="generation")
def anthropic_completion(**kwargs):
# optional, extract some fields from kwargs
kwargs_clone = kwargs.copy()
input = kwargs_clone.pop('messages', None)
model = kwargs_clone.pop('model', None)
langfuse.update_current_generation(
input=input,
model=model,
metadata=kwargs_clone
)
response = anthropic_client.messages.create(**kwargs)
langfuse.update_current_generation(
usage_details={
"input": response.usage.input_tokens,
"output": response.usage.output_tokens,
"cache_read_input_tokens": response.usage.cache_read_input_tokens
# "total": int, # if not set, it is derived as the sum of all usage types
},
# Optionally, also ingest usd cost. Alternatively, you can infer it via a model definition in Langfuse.
cost_details={
# Here we assume the input and output cost are 1 USD each and half the price for cached tokens.
"input": 1,
"cache_read_input_tokens": 0.5,
"output": 1,
# "total": float, # if not set, it is derived as the sum of all usage types
}
)
# return result
return response.content[0].text
@observe()
def main():
return anthropic_completion(
model="claude-3-opus-20240229",
max_tokens=1024,
messages=[
{"role": "user", "content": "Hello, Claude"}
]
)
main()
创建手动 generations 时:
from langfuse import get_client
import anthropic
langfuse = get_client()
anthropic_client = anthropic.Anthropic()
with langfuse.start_as_current_observation(
as_type="generation",
name="anthropic-completion",
model="claude-3-opus-20240229",
input=[{"role": "user", "content": "Hello, Claude"}]
) as generation:
response = anthropic_client.messages.create(
model="claude-3-opus-20240229",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}]
)
generation.update(
output=response.content[0].text,
usage_details={
"input": response.usage.input_tokens,
"output": response.usage.output_tokens,
"cache_read_input_tokens": response.usage.cache_read_input_tokens
# "total": int, # if not set, it is derived as the sum of all usage types
},
# Optionally, also ingest usd cost. Alternatively, you can infer it via a model definition in Langfuse.
cost_details={
# Here we assume the input and output cost are 1 USD each and half the price for cached tokens.
"input": 1,
"cache_read_input_tokens": 0.5,
"output": 1,
# "total": float, # if not set, it is derived as the sum of all usage types
}
)
使用上下文管理器时:
import {
startActiveObservation,
startObservation,
updateActiveObservation,
} from "@langfuse/tracing";
await startActiveObservation("context-manager", async (span) => {
span.update({
input: { query: "What is the capital of France?" },
});
// This generation will automatically be a child of "user-request"
const generation = startObservation(
"llm-call",
{
model: "gpt-4",
input: [{ role: "user", content: "What is the capital of France?" }],
},
{ asType: "generation" }
);
// ... LLM call logic ...
generation.update({
usageDetails: {
input: 10,
output: 5,
cache_read_input_tokens: 2,
some_other_token_count: 10,
total: 27, // optional, it is derived as the sum of all usage types
},
costDetails: {
// If you don't want the costs to be calculated based on model definitions, you can pass the costDetails manually.
input: 1,
output: 1,
cache_read_input_tokens: 0.5,
some_other_token_count: 1,
total: 3.5,
},
output: { content: "The capital of France is Paris." },
});
generation.end();
});
使用 observe 包装器时:
import { observe, updateActiveObservation } from "@langfuse/tracing";
// An existing function
async function fetchData(source: string) {
updateActiveObservation(
{
usageDetails: {
input: 10,
output: 5,
cache_read_input_tokens: 2,
some_other_token_count: 10,
total: 27, // optional, it is derived as the sum of all usage types
},
costDetails: {
// If you don't want the costs to be calculated based on model definitions, you can pass the costDetails manually.
input: 1,
output: 1,
cache_read_input_tokens: 0.5,
some_other_token_count: 1,
total: 3.5,
},
},
{ asType: "generation" }
);
// ... logic to fetch data
return { data: `some data from ${source}` };
}
// Wrap the function to trace it
const tracedFetchData = observe(fetchData, {
name: "observe-wrapper",
asType: "generation",
});
const result = await tracedFetchData("API");
手动创建 observations 时:
const span = startObservation("manual-observation", {
input: { query: "What is the capital of France?" },
});
const generation = span.startObservation(
"llm-call",
{
model: "gpt-4",
input: [{ role: "user", content: "What is the capital of France?" }],
output: { content: "The capital of France is Paris." },
},
{ asType: "generation" }
);
generation.update({
usageDetails: {
input: 10,
output: 5,
cache_read_input_tokens: 2,
some_other_token_count: 10,
total: 27, // optional, it is derived as the sum of all usage types
},
costDetails: {
// If you don't want the costs to be calculated based on model definitions, you can pass the costDetails manually.
input: 1,
output: 1,
cache_read_input_tokens: 0.5,
some_other_token_count: 1,
total: 3.5,
},
});
generation
.update({
output: { content: "The capital of France is Paris." },
})
.end();
span.update({ output: "Successfully answered user request." }).end();
你也可以通过 generation.update() 更新用量和成本。
用量类型是互斥的桶
Langfuse 将 usage_details 中的每个键视为一个独立的、不重叠的桶:每个 token 必须恰好在一个键中计数。特别是,input 必须排除已在另一个 input_* 键(如 input_cached_tokens 或 input_cache_creation)中计数的 token,output 必须排除在另一个 output_* 键(如 output_reasoning_tokens)中计数的 token。唯一的例外是 total:它本身不是一个桶,而是跨越所有桶并等于它们的和。
Langfuse 在三个地方依赖此契约:
- 显示:UI 将所有包含
input的用量类型求和以显示总输入用量,将所有包含output的用量类型求和以显示总输出用量。 - 成本推断:每种用量类型与模型定义的每用量类型价格精确匹配,产生的成本被求和。
- Total:如果没有接入
total,Langfuse 将其推导为所有桶的和。
如果桶重叠——例如,如果 input 仍包含在 input_cached_tokens 中报告的 token——那些 token 会显示两次,推断的成本会重复计算它们:Langfuse 中显示的成本会高估你的提供商实际收取的费用。直接接入的 cost_details 按原样使用,不受影响。
许多 LLM 提供商报告包含性计数。例如,OpenAI 的 prompt_tokens(Chat Completions API)和 input_tokens(Responses API)包含缓存的 token。相比之下,Anthropic 的 input_tokens 已经排除了缓存读取和缓存写入。包含性计数在存储之前必须转换为排他性桶,通过从顶层计数中减去明细计数。
例如,一个 OpenAI 风格的响应,有 17,903 个提示词 token(其中 17,817 个是缓存命中)和 188 个补全 token:
| 提供商报告(包含性) | 存储在 Langfuse(排他性) |
|---|---|
prompt_tokens: 17903 |
input: 86 |
prompt_tokens_details: |
input_cached_tokens: 17817 |
completion_tokens: 188 |
output: 188 |
total_tokens: 18091 |
total: 18091 |
Langfuse 何时为你转换
你是否需要自己做此转换,取决于用量数据如何到达 Langfuse:
- Langfuse 集成和 SDK 包装器(例如 OpenAI 包装器)从提供商响应捕获用量并为你转换。
- OpenTelemetry 用量属性(
gen_ai.usage.*,如 OpenTelemetry GenAI 语义约定所定义,加上某些埋点库使用的llm.token_count.*命名空间):被视为包含性。Langfuse 在接入期间通过从input中减去缓存读取和缓存创建 token 来规范化它们。 - OpenAI 用量 schema 作为用量详情传递(嵌套的
prompt_tokens_details/completion_tokens_details,参见与 OpenAI 的兼容性):在接入期间被识别和规范化。匹配是严格的:如果对象包含 OpenAI 用量字段之外的任何键,它会被当作扁平键处理。 - Langfuse 风格的扁平键 通过 SDK 或 API 作为用量详情传递(
usage_details/usageDetails,OTel 属性langfuse.observation.usage_details):按原样存储,不应用规范化。值必须已经是排他性的。
如果你编写自己的埋点,用扁平键设置用量详情,请检查你的提供商如何报告用量,并在将它们传递给 Langfuse 之前,从顶层 input/output 计数中减去缓存或明细 token 计数——恰好一次。
与 OpenAI 的兼容性
为提高与 OpenAI 的兼容性,你也可以使用 OpenAI Usage schema。prompt_tokens 将映射到 input,completion_tokens 将映射到 output,total_tokens 将映射到 total。嵌套在 prompt_tokens_details 中的键将以 input_ 前缀展平,completion_tokens_details 将以 output_ 前缀展平。由于 OpenAI 包含性地报告这些明细计数,Langfuse 会分别从 input 和 output 中减去它们,以便存储的桶是互斥的。
Schema 识别是严格的:用量对象必须仅包含下面显示的 OpenAI 用量字段。如果它包含任何额外的键(例如,某些网关附加一个 cost 字段),它不会被识别为 OpenAI 风格的用量,而是作为扁平键按原样存储——没有映射也没有减法。此失败模式是静默的,因此在接入前剥离额外的键。
from langfuse import get_client
langfuse = get_client()
with langfuse.start_as_current_observation(
as_type="generation",
name="openai-style-generation",
model="gpt-4o"
) as generation:
# Simulate LLM call
# response = openai_client.chat.completions.create(...)
generation.update(
usage_details={
# usage (OpenAI-style schema)
"prompt_tokens": 10,
"completion_tokens": 25,
"total_tokens": 35,
"prompt_tokens_details": {
"cached_tokens": 5,
"audio_tokens": 2,
},
"completion_tokens_details": {
"reasoning_tokens": 15,
},
}
)
import { startObservation } from "@langfuse/tracing";
const generation = startObservation(
"openai-style-generation",
{
model: "gpt-4o",
usageDetails: {
// usage (OpenAI-style schema)
prompt_tokens: 10,
completion_tokens: 25,
total_tokens: 35,
prompt_tokens_details: {
cached_tokens: 5,
audio_tokens: 2,
},
completion_tokens_details: {
reasoning_tokens: 15,
},
},
},
{ asType: "generation" },
);
generation.end();
你也可以通过 generation.update() 和 generation.end() 接入 OpenAI 风格的用量。
推断用量和/或成本
如果用量或成本未被接入,Langfuse 将尝试在接入时基于 generation 的 model 参数推断缺失的值。这对于某些模型提供商或不在响应中包含用量或成本的自托管模型尤其有用。
Langfuse 自带一份预定义的流行模型及其分词器列表,包括 OpenAI、Anthropic、Google。查看完整列表(你需要登录)。
你也可以添加自己的自定义模型定义(见下文),或通过 GitHub 请求对新模型的官方支持。
用量
如果为模型指定了分词器,Langfuse 会自动计算接入的 generations 的 token 量。
目前支持以下分词器:
| 模型 | 分词器 | 使用的包 | 备注 |
|---|---|---|---|
gpt-4o |
o200k_base |
tiktoken |
|
gpt* |
cl100k_base |
tiktoken |
|
claude* |
claude |
@anthropic-ai/tokenizer |
据 Anthropic 称,他们的分词器对 Claude 3 模型不准确。如果可能,请发送我们来自其 API 响应的 token。 |
成本
模型定义包括每用量类型的价格。用量类型必须与 generation 的 usage_details 对象中的键精确匹配。
如果 (1) 用量被接入或推断,且 (2) 匹配的模型定义包含价格,Langfuse 会在接入时自动计算接入的 generations 的成本。
定价层级(Pricing Tiers)
某些模型提供商根据使用的输入 token 数量收取不同的费率。例如,Anthropic 的 Claude Sonnet 4.5 和 Google 的 Gemini 2.5 Pro 在使用超过 200K 输入 token 时应用更高的定价。
Langfuse 支持模型的定价层级,以便为这些依赖于上下文的定价结构实现准确的成本计算。
层级匹配如何工作
每个模型可以有多个定价层级,每个层级具有:
- 名称:描述性名称(例如 "Standard"、"Large Context")
- 优先级:评估顺序(0 保留给默认层级)
- 条件:决定层级何时应用的规则
- 价格:此层级每用量类型的成本
计算成本时,Langfuse 按优先级顺序评估层级(排除默认层级)。使用第一个条件满足的层级。如果没有条件层级匹配,则应用默认层级。
条件格式:
usageDetailPattern:匹配用量明细键的正则模式(例如input匹配input_tokens、input_cached_tokens等)operator:比较运算符(gt、gte、lt、lte、eq、neq)value:要比较的阈值caseSensitive:模式匹配是否区分大小写(默认:false)
例如,Claude Sonnet 4.5 的 "Large Context" 层级有一个条件:input > 200000,意味着当匹配模式 "input" 的所有用量明细之和超过 200,000 个 token 时应用。
自定义模型定义
你可以灵活地将自己的模型定义(包括定价层级)添加到 Langfuse。这对于未包含在 Langfuse 维护模型列表中的自托管或微调模型尤其有用。
要在 Langfuse UI 中添加自定义模型定义,你可以点击模型名称旁边的 "+" 号,或导航到 Project Settings > Models 添加新的模型定义。
然后你可以添加每 token 类型的价格并保存模型定义。现在所有带此模型的新 traces 都将推断出正确的 token 用量和成本。
模型定义也可以通过 Models API 以编程方式管理:
GET /api/public/models
POST /api/public/models
GET /api/public/models/{id}
DELETE /api/public/models/{id}
模型基于以下内容与 generations 匹配:
| Generation 属性 | 模型属性 | 备注 |
|---|---|---|
model |
match_pattern |
使用正则表达式,例如 (?i)^(gpt-4-0125-preview)$ 匹配 gpt-4-0125-preview。 |
用户定义的模型优先于 Langfuse 维护的模型。
更多细节
使用 openai 分词器时,你需要指定以下分词配置。你也可以从预定义 OpenAI 模型列表中复制配置。更多细节参见 OpenAI 文档。tokensPerName 和 tokensPerMessage 对聊天模型是必需的。
{
"tokenizerModel": "gpt-3.5-turbo", // tiktoken model name
"tokensPerName": -1, // OpenAI Chatmessage tokenization config
"tokensPerMessage": 4 // OpenAI Chatmessage tokenization config
}
推理模型的成本推断
通过对 LLM 输入和输出分词来进行成本推断,不支持 OpenAI o1 模型系列等推理模型。也就是说,如果没有接入 token 计数,Langfuse 无法为推理模型推断成本。
推理模型需要多个步骤才能得出响应。每个步骤的结果产生作为输出 token 计费的推理 token。因此具有成本效益的输出 token 计数是所有推理 token 与最终补全的 token 计数之和。由于 Langfuse 无法看到推理 token,它无法为没有提供 token 用量的 generations 推断正确的成本。
要受益于 Langfuse 成本追踪,请在接入 o1 模型 generations 时提供 token 用量。当使用 Langfuse OpenAI 包装器 或 Langchain、LlamaIndex 或 LiteLLM 等集成时,token 用量会自动为你收集和提供。
更多细节参见关于推理模型如何工作的 OpenAI 指南。
故障排查
- 如果你更改模型定义,更新后的成本仅应用于记录到 Langfuse 的新 generations。
- 只有类型为
generation和embedding的 observations 才能追踪成本和用量。 - 如果你使用 OpenRouter,Langfuse 可以直接捕获 OpenRouter 成本信息。在这里了解更多。
- 如果你使用 LiteLLM,Langfuse 直接捕获每个 LiteLLM 响应中返回的成本信息。