• Projects
  • Service
  • About
  • branding.bz
  • Podcast
  • Tips
  • FAQ
  • Recruit
  • Download
  • Contact
  • branding.bz(ブランド構築SaaS)
  • DESIGN NOW(デザインメディア)
  • X
  • LinkedIn
  • Spotify
  • Facebook

213-0011 神奈川県川崎市高津区久本3-6-7-303

© 2026 ID INC. All rights reserved

claude-skills/スキル
SKILLOfficialdevelopment

output-dev-cost-hooks

プラグイン
outputai
ソース
GitHub で見る ↗
説明

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 を参照してください。

セットアップ

1. フックファイルを作成する

// 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コストを処理
});

2. ファイルを登録する

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(上記で説明したイベント固有のデータ)。

パターン1: 外部エンドポイントに転送する

支出を外部の観測システムに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);
  }
});

パターン2: 構造化ログ

支出をログプラットフォームに構造化されたフィールドとして記録するだけで良い場合に使用します。別のネットワーク呼び出しは不要です。フレームワークの 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 の欠落をチェックしている
  • [ ] ログ送信または転送前にURL(クエリ文字列を削除)が削除されている
  • [ ] 新規作業に対してレガシーの cost:llm:request ではなく llm:generation:metering が使用されている

関連スキル

  • output-dev-http-client-create — addRequestCost を使用した有料APIクライアント
原文(English)を表示

Cost Observability Hooks

Overview

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.

When to Use This Skill

  • Forwarding per-call spend to an external observability/webhook endpoint
  • Logging cost events as structured fields for a log platform
  • Building alerting or dashboards on top of workflow spend
  • Auditing which workflows/activities are driving cost, in real time rather than after the fact

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.

Setup

1. Create a hook file

// 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
});

2. Register the file

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.

Events You Can Subscribe To

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).

Pattern 1: Forward to an external endpoint

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);
  }
});

Pattern 2: Structured logging

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
  });
});

Handler Safety Rules

  • Handlers are wrapped in try/catch by the framework. A thrown or rejected handler is caught and logged (e.g. <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.
  • Skip incomplete events. Guard on 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.
  • Redact before logging or sending URLs. Some third-party APIs embed API keys or tokens in query strings. Strip the query string (and any path segments you know carry secrets) before it leaves the process.
  • Use eventId as an idempotency key when forwarding to a system that might receive the same event more than once (e.g. retried delivery).
  • Expect one event per attempt. A step or evaluator that retries emits one cost event per attempt — that's correct, since each attempt is a real billed call.
  • Prefer 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).

Verification Checklist

  • [ ] Hook file registered in outputai.hookFiles in package.json, pointing at the built .js path (dist/...), not the .ts source
  • [ ] Hook file imports on from @outputai/core/hooks
  • [ ] Handlers guard on missing workflowDetails / payload
  • [ ] URLs are redacted (query string stripped) before logging or forwarding
  • [ ] llm:generation:metering used instead of legacy cost:llm:request for new work

Related Skills

  • output-dev-http-client-create - Attaching cost to a paid API client with addRequestCost
  • output-dev-workflow-cost - Post-hoc cost calculation for a single completed workflow run via the CLI
  • output-dev-credentials - Storing the observability endpoint URL/token as a credential

原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。