LLM(大規模言語モデル)とAPI の利用費に関するイベント(cost:http:request、llm:generation:metering)に登録して、それらの情報を自分たちの監視システム(外部の分析・監視ツール)に送信できます。 次のような場合に使用: - コストや利用額に関するログを追加したい - コスト監視の統合機能を構築したい - リクエストごとのコスト情報を外部システムに転送したい
Subscribe to cost events (cost:http:request, llm:generation:metering) to forward LLM and API spend to your own observability system. Use when adding cost/spend logging, building a cost observability integration, or forwarding per-request cost data to an external system.
このスキルは、Output のコストイベント(発生するLLM呼び出しと、addRequestCost で追加されたコスト付きのHTTP呼び出し)を購読し、あなたの観測システム(ウェブフック、構造化ログパイプライン、メトリクスバックエンドなど)に転送する方法を説明します。ハンドラーのエラーはフレームワークに よってキャッチされログが記録されるため、ワークフローやトリガーしたリクエストに影響することはありません。
このスキルは、プロジェクト全体のフック登録について説明しています。フレームワークがコストを発行するようにHTTPクライアントを設定する方法については、output-dev-http-client-create を参照してください。
// src/cost_hooks.ts
import { on } from '@outputai/core/hooks';
import type { HttpRequestCostEvent } from '@outputai/http';
import type { LLMGenerationMeteringEvent } from '@outputai/llm';
on<HttpRequestCostEvent>('cost:http:request', async event => {
// HTTPコストを処理
});
on<LLMGenerationMeteringEvent>('llm:generation:metering', async event => {
// LLMコストを処理
});
package.json の outputai.hookFiles にファイルを追加してください。既存のフックファイルがあれば一緒に記載します。パスはパッケージルートからの相対パスで、ビルド済み の .js 出力を指します。npm run output:worker:build は src/ を dist/ にコンパイル(tsconfig.json の rootDir/outDir に従う)するため、src/cost_hooks.ts は dist/cost_hooks.js として登録します。ワーカーはこれらのファイルをスタートアップ時に読み込みます。
{
"outputai": {
"hookFiles": [
"node_modules/@outputai/credentials/dist/hooks.js",
"./dist/cost_hooks.js"
]
}
}
ビルドステップをスキップしたい場合は、フックファイルをプレーンな未コンパイルのJavaScriptとして直接 src/ パスに登録することもできます("./src/cost_hooks.js")。詳細は https://docs.output.ai/operations/error-hooks を参照してください。このスキルではTypeScript+ビルド済みパスを使用します。これはフレームワーク自体の例(およびスキャフォルドされたプロジェクト内のすべてのファイル)で使用されている方法だからです。
| イベント | 型のインポート元 | 発火タイミング | 推奨用途 |
|---|---|---|---|
llm:generation:metering |
@outputai/llm の LLMGenerationMeteringEvent |
すべてのLLM生成(テキスト、画像、エージェント、ストリーミング)の後で使用量が報告された場合。失敗した呼び出しでも部分的な使用量が得られれば発火 | 新規のLLMコスト統合 |
cost:llm:request |
@outputai/llm の LLMUsageEvent |
レガシー互換のLLMコストイベント。同じ完了パス | 既存のハンドラーのみ。新規の実装には使用しないでください |
cost:http:request |
@outputai/http の HttpRequestCostEvent |
コード(またはクライアントの afterResponse フック)が addRequestCost(response, total) を呼び出した時のみ |
LLM以外の有料API呼び出し |
すべてのイベントは同じ構造を持ちます:eventId(UUID v4、冪等性キー)、eventDate(ミリ秒単位のエポック時刻)、activityInfo と workflowDetails(ステップ/評価器のコンテキスト内で発火した場合のみ存在)、outputActivityKind、および payload(上記で説明したイベント固有のデータ)。
支出を外部の観測システムにHTTP経由で送る必要がある場合に使用します。生のエンベロープとペイロードを転送してください。ログ送信やURL送信前に、APIキーやトークンなど秘密情報が含まれている可能性のあるものは削除してください。
// src/cost_hooks.ts
import { on } from '@outputai/core/hooks';
import { credentials } from '@outputai/credentials';
import type { HttpRequestCostEvent } from '@outputai/http';
import type { LLMGenerationMeteringEvent } from '@outputai/llm';
// createKyClient/outputFetch ではなくプレーンの fetch を使用してください。
// ハンドラーは発信ステップの非同期コンテキスト内で実行されるため、
// トレース機能を持つクライアントはコストイベント転送のたびに
// ステップのトレースに独自のHTTPトレースイベントを追加してしまいます。
//
// モジュールスコープで `require` ではなく `get` を使って認証情報を
// 遅延読み込みしてください。フックファイルはワーカースタートアップ時に
// 保護されていない `await import()` で読み込まれるため(try/catch がない)、
// ここで `require()` が例外を投げるとワーカー全体が停止します。
// `get`/`require` もアクティビティ内からのみワークフロー スコープの認証情報を参照でき、
// アクティビティ外のスタートアップ時はグローバルな認証情報セットのみを解決するため、
// これはグローバル(ワークフロー単位ではなく)の認証情報ファイルに保管してください。
const getWebhookUrl = (): string | undefined => credentials.get('observability.webhook_url') as string | undefined;
const postEvent = async (json: Record<string, unknown>): Promise<void> => {
const webhookUrl = getWebhookUrl();
if (!webhookUrl) {
return;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
await fetch(`${webhookUrl}/events`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(json),
signal: controller.signal
});
} finally {
clearTimeout(timeout);
}
};
// クエリ文字列を削除 — APIキーやトークンが格納される場合があります。
const redactUrl = (url: string): string => {
try {
const parsed = new URL(url);
return `${parsed.origin}${parsed.pathname}`;
} catch {
return '[unparseable-url]';
}
};
on<HttpRequestCostEvent>('cost:http:request', async event => {
if (!event.workflowDetails || !event.payload) {
return;
}
// ここで発生した失敗はこのハンドラー独自のコンテキストでログされます。
// いずれにせよフレームワークがキャッチしログを記録するため、
// キャッチされていない例外でもワークフロー に影響しません。
try {
await postEvent({
eventId: event.eventId,
eventDate: event.eventDate,
workflowId: event.workflowDetails.workflowId,
kind: 'http',
url: redactUrl(event.payload.url),
totalUsd: event.payload.total
});
} catch (error) {
console.warn('cost_hooks: failed to forward HTTP cost event', error);
}
});
on<LLMGenerationMeteringEvent>('llm:generation:metering', async event => {
if (!event.workflowDetails || !event.payload) {
return;
}
try {
await postEvent({
eventId: event.eventId,
eventDate: event.eventDate,
workflowId: event.workflowDetails.workflowId,
kind: 'llm',
providerId: event.payload.usage.providerId,
modelId: event.payload.usage.modelId,
totalUsd: event.payload.cost?.total ?? null
});
} catch (error) {
console.warn('cost_hooks: failed to forward LLM cost event', error);
}
});
支出をログプラットフォームに構造化されたフィールドとして記録するだけで良い場合に使用します。別のネットワーク呼び出しは不要です。フレームワークの Logger を使用すれば、フィールドはワーカーのログの他の部分と一貫性を持って出力されます。
// src/cost_hooks.ts
import { on } from '@outputai/core/hooks';
import { Logger } from '@outputai/core';
import type { HttpRequestCostEvent } from '@outputai/http';
import type { LLMGenerationMeteringEvent } from '@outputai/llm';
const log = Logger.createLogger('CostObservability');
const redactUrl = (url: string): string => {
try {
const parsed = new URL(url);
return `${parsed.origin}${parsed.pathname}`;
} catch {
return '[unparseable-url]';
}
};
on<HttpRequestCostEvent>('cost:http:request', async event => {
if (!event.workflowDetails || !event.payload) {
return;
}
log.info('http_request_cost', {
eventId: event.eventId,
workflowId: event.workflowDetails.workflowId,
url: redactUrl(event.payload.url),
totalUsd: event.payload.total
});
});
on<LLMGenerationMeteringEvent>('llm:generation:metering', async event => {
if (!event.workflowDetails || !event.payload) {
return;
}
log.info('llm_generation_cost', {
eventId: event.eventId,
workflowId: event.workflowDetails.workflowId,
providerId: event.payload.usage.providerId,
modelId: event.payload.usage.modelId,
totalUsd: event.payload.cost?.total ?? null
});
});
ハンドラーはフレームワークによってtry/catchでラップされます。 スローされたり拒否されたハンドラーはキャッチされてログに記録され(例:<eventName> hook error)、ワークフロー、ワーカー、イベントをトリガーしたリクエストに影響することはありません。onError ハンドラーと同じ保証が適用されます。自分の try/catch を追加することで、転送失敗を自分で制御できるコンテキストでログに記録できるため、追加すると便利ですが、安全性のために必須ではありません。
不完全なイベントをスキップします。 event.workflowDetails と event.payload を使用する前に確認してください。これらはステップ/評価器コンテキスト内で発火した場合にのみ入力されます。一方、eventId と eventDate は常に存在します。
URLをログ送信または外部送信する前に削除します。 サードパーティのAPIはクエリ文字列にAPIキーやトークンを埋め込むことがあります。プロセスから出す前にクエリ文字列(および秘密が含まれることがわかっているパスセグメント)を削除してください。
転送時に eventId をべき等性キー(重複排除キー)として使用します。 同じイベントが複数回受け取られる可能性のあるシステム(例:リトライされた配信)に転送する場合。
1試行ごとに1イベントを想定してください。 リトライするステップまたは評価器は試行ごとに1つのコストイベントを発火させます。これは各試行が実際に請求された呼び出しだからです。
新規統合では cost:llm:request よりも llm:generation:metering を優先します。 レガシーイベントの形状は固定されており、新しいコストタイプ(ツール/グラウンディング料金など)を取りこぼしています。
package.json の outputai.hookFiles に登録されており、.ts ソースではなくビルド済み の .js パス(dist/...)を指している@outputai/core/hooks から on をインポートしているworkflowDetails/payload の欠落をチェックしているcost:llm:request ではなく llm:generation:metering が使用されているoutput-dev-http-client-create — addRequestCost を使用した有料APIクライアントThis skill documents how to subscribe to Output's cost events so every priced LLM call and every HTTP call with attached cost (via addRequestCost, see output-dev-http-client-create) can be forwarded to your own observability system — a webhook, a structured log pipeline, a metrics backend, etc. Handler errors are caught and logged by the framework; they never affect the workflow or the request that triggered them.
This skill is about project-wide hook registration for cost data already emitted by the framework. To make an HTTP client emit cost in the first place, see output-dev-http-client-create.
// src/cost_hooks.ts
import { on } from '@outputai/core/hooks';
import type { HttpRequestCostEvent } from '@outputai/http';
import type { LLMGenerationMeteringEvent } from '@outputai/llm';
on<HttpRequestCostEvent>('cost:http:request', async event => {
// handle HTTP cost
});
on<LLMGenerationMeteringEvent>('llm:generation:metering', async event => {
// handle LLM cost
});
Add the file to outputai.hookFiles in package.json, alongside any existing hook files. Paths are relative to the package root and point at the built .js output — npm run output:worker:build compiles src/ to dist/ (per tsconfig.json's rootDir/outDir), so src/cost_hooks.ts is registered as dist/cost_hooks.js, not the .ts source itself. The worker loads these files at startup.
{
"outputai": {
"hookFiles": [
"node_modules/@outputai/credentials/dist/hooks.js",
"./dist/cost_hooks.js"
]
}
}
If you'd rather skip the build step, a hook file can also be plain, uncompiled JavaScript registered directly at its src/ path ("./src/cost_hooks.js") — see https://docs.output.ai/operations/error-hooks for that variant. The rest of this skill uses TypeScript + the built-output path, since that's what the framework's own examples (and every other file in a scaffolded project) use.
| Event | Type import | When it fires | Prefer for |
|---|---|---|---|
llm:generation:metering |
LLMGenerationMeteringEvent from @outputai/llm |
After every LLM generation (text, image, Agent, streaming) that reports usage — including failed calls that got at least partial usage | New LLM cost integrations |
cost:llm:request |
LLMUsageEvent from @outputai/llm |
Legacy/compatible LLM cost event, same completion path | Existing handlers only — do not use for new work |
cost:http:request |
HttpRequestCostEvent from @outputai/http |
Only when your code (or a client's afterResponse hook) calls addRequestCost(response, total) |
Non-LLM paid API calls |
Every event carries the same envelope: eventId (UUID v4, stable idempotency key), eventDate (ms epoch), activityInfo and workflowDetails (present when emitted from within a step/evaluator), outputActivityKind, and payload (the event-specific data described above).
Use this when spend needs to reach an external observability system over HTTP. Forward the raw envelope plus payload; redact anything that might carry secrets (API keys or tokens embedded in query strings) before logging or sending the URL.
// src/cost_hooks.ts
import { on } from '@outputai/core/hooks';
import { credentials } from '@outputai/credentials';
import type { HttpRequestCostEvent } from '@outputai/http';
import type { LLMGenerationMeteringEvent } from '@outputai/llm';
// Use plain fetch here, not createKyClient/outputFetch. Handlers run inside
// the emitting step's async context, so a traced client would add its own
// HTTP trace event to that step's trace on every forwarded cost event.
//
// Read the credential lazily with `get`, not `require`, at module scope: a
// hook file is imported at worker startup by an unguarded `await import()`
// (there's no try/catch around it), so a `require()` that throws here takes
// the whole worker down. `get`/`require` also only see workflow-scoped
// credentials from inside an activity — at startup, outside any activity,
// they resolve the global credential set only, so keep this in a global
// (not per-workflow) credential file.
const getWebhookUrl = (): string | undefined => credentials.get('observability.webhook_url') as string | undefined;
const postEvent = async (json: Record<string, unknown>): Promise<void> => {
const webhookUrl = getWebhookUrl();
if (!webhookUrl) {
return;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
await fetch(`${webhookUrl}/events`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(json),
signal: controller.signal
});
} finally {
clearTimeout(timeout);
}
};
// Strip query strings — some APIs put API keys or tokens there.
const redactUrl = (url: string): string => {
try {
const parsed = new URL(url);
return `${parsed.origin}${parsed.pathname}`;
} catch {
return '[unparseable-url]';
}
};
on<HttpRequestCostEvent>('cost:http:request', async event => {
if (!event.workflowDetails || !event.payload) {
return;
}
// A caught failure here logs with this handler's own context; an
// uncaught one is still caught and logged by the framework either way.
try {
await postEvent({
eventId: event.eventId,
eventDate: event.eventDate,
workflowId: event.workflowDetails.workflowId,
kind: 'http',
url: redactUrl(event.payload.url),
totalUsd: event.payload.total
});
} catch (error) {
console.warn('cost_hooks: failed to forward HTTP cost event', error);
}
});
on<LLMGenerationMeteringEvent>('llm:generation:metering', async event => {
if (!event.workflowDetails || !event.payload) {
return;
}
try {
await postEvent({
eventId: event.eventId,
eventDate: event.eventDate,
workflowId: event.workflowDetails.workflowId,
kind: 'llm',
providerId: event.payload.usage.providerId,
modelId: event.payload.usage.modelId,
totalUsd: event.payload.cost?.total ?? null
});
} catch (error) {
console.warn('cost_hooks: failed to forward LLM cost event', error);
}
});
Use this when spend just needs to land in your log platform as structured facets, without a separate network call. Use the framework Logger so fields are emitted consistently with the rest of the worker's logs.
// src/cost_hooks.ts
import { on } from '@outputai/core/hooks';
import { Logger } from '@outputai/core';
import type { HttpRequestCostEvent } from '@outputai/http';
import type { LLMGenerationMeteringEvent } from '@outputai/llm';
const log = Logger.createLogger('CostObservability');
const redactUrl = (url: string): string => {
try {
const parsed = new URL(url);
return `${parsed.origin}${parsed.pathname}`;
} catch {
return '[unparseable-url]';
}
};
on<HttpRequestCostEvent>('cost:http:request', async event => {
if (!event.workflowDetails || !event.payload) {
return;
}
log.info('http_request_cost', {
eventId: event.eventId,
workflowId: event.workflowDetails.workflowId,
url: redactUrl(event.payload.url),
totalUsd: event.payload.total
});
});
on<LLMGenerationMeteringEvent>('llm:generation:metering', async event => {
if (!event.workflowDetails || !event.payload) {
return;
}
log.info('llm_generation_cost', {
eventId: event.eventId,
workflowId: event.workflowDetails.workflowId,
providerId: event.payload.usage.providerId,
modelId: event.payload.usage.modelId,
totalUsd: event.payload.cost?.total ?? null
});
});
<eventName> hook error) and never affects the workflow, the worker, or the request that triggered the event — the same guarantee onError handlers get. Adding your own try/catch is still worthwhile so a forwarding failure logs with context you control, but it's not required for safety.event.workflowDetails and event.payload before using them — they're only populated when the event was emitted from within a step/evaluator context; eventId and eventDate, by contrast, are always present.eventId as an idempotency key when forwarding to a system that might receive the same event more than once (e.g. retried delivery).llm:generation:metering over cost:llm:request for new integrations; the legacy event's shape is frozen and misses newer cost types (e.g. tool/grounding charges).outputai.hookFiles in package.json, pointing at the built .js path (dist/...), not the .ts sourceon from @outputai/core/hooksworkflowDetails / payloadllm:generation:metering used instead of legacy cost:llm:request for new workoutput-dev-http-client-create - Attaching cost to a paid API client with addRequestCostoutput-dev-workflow-cost - Post-hoc cost calculation for a single completed workflow run via the CLIoutput-dev-credentials - Storing the observability endpoint URL/token as a credential原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。