@usagetap/observer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UsageTap
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @usagetap/observer
2
+
3
+ A tiny, fail-open wrapper for UsageTap's 30-day usage and cost analysis trial.
4
+ It sends only provider/model metadata, token counts, timing, status, and optional
5
+ feature tags. It never sends prompts, model responses, or provider credentials.
6
+
7
+ ```ts
8
+ import OpenAI from "openai";
9
+ import { observeOpenAI } from "@usagetap/observer";
10
+
11
+ const openai = observeOpenAI(new OpenAI(), {
12
+ apiKey: process.env.USAGETAP_API_KEY,
13
+ customerId: process.env.USAGETAP_CUSTOMER_ID!,
14
+ feature: "support-assistant",
15
+ });
16
+
17
+ const response = await openai.chat.completions.create({
18
+ model: "gpt-4.1-mini",
19
+ messages,
20
+ });
21
+ ```
22
+
23
+ Anthropic uses the same options:
24
+
25
+ ```ts
26
+ import Anthropic from "@anthropic-ai/sdk";
27
+ import { observeAnthropic } from "@usagetap/observer";
28
+
29
+ const anthropic = observeAnthropic(new Anthropic(), {
30
+ apiKey: process.env.USAGETAP_API_KEY,
31
+ customerId: process.env.USAGETAP_CUSTOMER_ID!,
32
+ });
33
+ ```
34
+
35
+ The first lightweight release observes non-streaming OpenAI-compatible chat
36
+ completions, Responses API calls, and Anthropic Messages calls. Streaming calls pass through unchanged.
37
+ Move to `@usagetap/sdk` when you need streaming telemetry, Anthropic adapters,
38
+ limits, gateway routing, sampling, or compression.
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var v=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var A=(n,e)=>{for(var r in e)v(n,r,{get:e[r],enumerable:!0})},D=(n,e,r,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of x(e))!C.call(n,t)&&t!==r&&v(n,t,{get:()=>e[t],enumerable:!(o=P(e,t))||o.enumerable});return n};var K=n=>D(v({},"__esModule",{value:!0}),n);var M={};A(M,{UsageTapObserver:()=>w,extractAnthropicUsage:()=>R,extractOpenAIUsage:()=>_,observeAnthropic:()=>B,observeOpenAI:()=>q});module.exports=K(M);var S="https://api.usagetap.com",j="application/vnd.usagetap.v1+json";function l(n){return!!n&&typeof n=="object"&&!Array.isArray(n)}function c(n){return typeof n=="number"&&Number.isFinite(n)&&n>=0?Math.round(n):void 0}function g(n,e){let r=n[e];return l(r)?r:void 0}function T(n,e){let r=n[e];return typeof r=="string"&&r.trim()?r:void 0}function _(n,e="openai"){if(!l(n))return;let r=g(n,"usage");if(!r)return;let o=g(r,"prompt_tokens_details")??g(r,"input_tokens_details"),t=g(r,"completion_tokens_details")??g(r,"output_tokens_details");return{providerUsed:e,modelUsed:T(n,"model"),inputTokens:c(r.prompt_tokens)??c(r.input_tokens),responseTokens:c(r.completion_tokens)??c(r.output_tokens),cachedInputTokens:c(o?.cached_tokens)??c(r.cached_tokens),cacheWriteInputTokens:c(o?.cache_write_tokens),reasoningTokens:c(t?.reasoning_tokens)}}function R(n,e="anthropic"){if(!l(n))return;let r=g(n,"usage");if(r)return{providerUsed:e,modelUsed:T(n,"model"),inputTokens:c(r.input_tokens),responseTokens:c(r.output_tokens),cachedInputTokens:c(r.cache_read_input_tokens),cacheWriteInputTokens:c(r.cache_creation_input_tokens)}}function $(n){return n instanceof Error?{code:n.name.slice(0,80)||"PROVIDER_ERROR",message:n.message.slice(0,500)||"Provider request failed"}:{code:"PROVIDER_ERROR",message:"Provider request failed"}}function I(){return typeof globalThis.crypto?.randomUUID=="function"?globalThis.crypto.randomUUID():`${Date.now()}-${Math.random().toString(36).slice(2)}`}var w=class{apiKey;customerId;baseUrl;defaultFeature;defaultTags;fetchImpl;onTelemetryError;constructor(e){let r=e.apiKey??(typeof process<"u"?process.env?.USAGETAP_API_KEY:void 0);if(!r?.trim())throw new Error("UsageTap observer requires an apiKey");if(!e.customerId?.trim())throw new Error("UsageTap observer requires a customerId");this.apiKey=r.trim(),this.customerId=e.customerId.trim(),this.baseUrl=(e.baseUrl??S).replace(/\/$/,""),this.defaultFeature=e.feature,this.defaultTags=e.tags,this.fetchImpl=e.fetch??globalThis.fetch,this.onTelemetryError=e.onTelemetryError}telemetryError(e){let r=e instanceof Error?e:new Error(String(e));this.onTelemetryError?.(r)}async post(e,r){let o=await this.fetchImpl(`${this.baseUrl}/${e}`,{method:"POST",headers:{Accept:j,"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`,"Idempotency-Key":I(),"X-Usage-SDK":"observer-js/0.1.0"},body:JSON.stringify(r)}),t=await o.json();if(!o.ok||!t.data)throw new Error(t.error?.message??t.result?.message??`UsageTap returned ${o.status}`);return t.data}async track(e,r,o){let t;try{t=await this.post("call_begin",{customerId:this.customerId,feature:e.feature??this.defaultFeature,tags:e.tags??this.defaultTags,idempotencyKey:I()})}catch(s){this.telemetryError(s)}try{let s=await r();if(t?.callId)try{await this.post("call_end",{callId:t.callId,providerUsed:e.provider,modelUsed:e.model,...o(s)})}catch(a){this.telemetryError(a)}return s}catch(s){if(t?.callId)try{await this.post("call_end",{callId:t.callId,providerUsed:e.provider,modelUsed:e.model,error:$(s)})}catch(a){this.telemetryError(a)}throw s}}};function q(n,e){let r=new w(e),o=e.provider??"openai",t=n,s=d=>{if(!d)return;let i=d.create;return typeof i!="function"?d:new Proxy(d,{get(p,h,y){return h!=="create"?Reflect.get(p,h,y):async(k,...E)=>{let b=l(k)?k:{},U=()=>Reflect.apply(i,p,[k,...E]);return b.stream===!0?U():r.track({provider:o,model:T(b,"model")},()=>Promise.resolve(U()),O=>_(O,o))}}})},a=l(t.chat)?t.chat:void 0,m=s(a&&l(a.completions)?a.completions:void 0),f=a&&m?new Proxy(a,{get(d,i,p){return i==="completions"?m:Reflect.get(d,i,p)}}):a,u=s(l(t.responses)?t.responses:void 0);return new Proxy(n,{get(d,i,p){return i==="chat"&&f?f:i==="responses"&&u?u:i==="unwrap"?()=>n:Reflect.get(d,i,p)}})}function B(n,e){let r=new w(e),o=e.provider??"anthropic",t=n,s=l(t.messages)?t.messages:void 0,a=s?.create,m=s&&typeof a=="function"?new Proxy(s,{get(f,u,d){return u!=="create"?Reflect.get(f,u,d):async(i,...p)=>{let h=l(i)?i:{},y=()=>Reflect.apply(a,f,[i,...p]);return h.stream===!0?y():r.track({provider:o,model:T(h,"model")},()=>Promise.resolve(y()),k=>R(k,o))}}}):s;return new Proxy(n,{get(f,u,d){return u==="messages"&&m?m:u==="unwrap"?()=>n:Reflect.get(f,u,d)}})}0&&(module.exports={UsageTapObserver,extractAnthropicUsage,extractOpenAIUsage,observeAnthropic,observeOpenAI});
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["const DEFAULT_BASE_URL = \"https://api.usagetap.com\";\nconst MEDIA_TYPE = \"application/vnd.usagetap.v1+json\";\n\ntype UnknownRecord = Record<string, unknown>;\n\nexport type ObserverMetadata = {\n provider: string;\n model?: string;\n feature?: string;\n tags?: string[];\n};\n\nexport type ObservedUsage = {\n providerUsed?: string;\n modelUsed?: string;\n inputTokens?: number;\n responseTokens?: number;\n cachedInputTokens?: number;\n cacheWriteInputTokens?: number;\n reasoningTokens?: number;\n};\n\nexport type UsageTapObserverOptions = {\n apiKey?: string;\n customerId: string;\n baseUrl?: string;\n feature?: string;\n tags?: string[];\n fetch?: typeof globalThis.fetch;\n onTelemetryError?: (error: Error) => void;\n};\n\nexport type ObserveOpenAIOptions = UsageTapObserverOptions & {\n provider?: string;\n};\n\nexport type ObserveAnthropicOptions = UsageTapObserverOptions & {\n provider?: string;\n};\n\ntype CallBeginData = {\n callId: string;\n allowed?: { standard?: boolean; premium?: boolean };\n};\n\ntype Envelope<T> = {\n data?: T;\n error?: { message?: string };\n result?: { message?: string };\n};\n\nfunction isRecord(value: unknown): value is UnknownRecord {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction finiteNonNegative(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0\n ? Math.round(value)\n : undefined;\n}\n\nfunction readRecord(parent: UnknownRecord, key: string): UnknownRecord | undefined {\n const value = parent[key];\n return isRecord(value) ? value : undefined;\n}\n\nfunction readString(parent: UnknownRecord, key: string): string | undefined {\n const value = parent[key];\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\nexport function extractOpenAIUsage(\n response: unknown,\n provider = \"openai\",\n): ObservedUsage | undefined {\n if (!isRecord(response)) return undefined;\n const usage = readRecord(response, \"usage\");\n if (!usage) return undefined;\n const promptDetails =\n readRecord(usage, \"prompt_tokens_details\") ??\n readRecord(usage, \"input_tokens_details\");\n const completionDetails =\n readRecord(usage, \"completion_tokens_details\") ??\n readRecord(usage, \"output_tokens_details\");\n\n return {\n providerUsed: provider,\n modelUsed: readString(response, \"model\"),\n inputTokens:\n finiteNonNegative(usage.prompt_tokens) ??\n finiteNonNegative(usage.input_tokens),\n responseTokens:\n finiteNonNegative(usage.completion_tokens) ??\n finiteNonNegative(usage.output_tokens),\n cachedInputTokens:\n finiteNonNegative(promptDetails?.cached_tokens) ??\n finiteNonNegative(usage.cached_tokens),\n cacheWriteInputTokens: finiteNonNegative(promptDetails?.cache_write_tokens),\n reasoningTokens: finiteNonNegative(completionDetails?.reasoning_tokens),\n };\n}\n\nexport function extractAnthropicUsage(\n response: unknown,\n provider = \"anthropic\",\n): ObservedUsage | undefined {\n if (!isRecord(response)) return undefined;\n const usage = readRecord(response, \"usage\");\n if (!usage) return undefined;\n return {\n providerUsed: provider,\n modelUsed: readString(response, \"model\"),\n inputTokens: finiteNonNegative(usage.input_tokens),\n responseTokens: finiteNonNegative(usage.output_tokens),\n cachedInputTokens: finiteNonNegative(usage.cache_read_input_tokens),\n cacheWriteInputTokens: finiteNonNegative(usage.cache_creation_input_tokens),\n };\n}\n\nfunction cleanError(error: unknown): { code: string; message: string } {\n if (error instanceof Error) {\n return {\n code: error.name.slice(0, 80) || \"PROVIDER_ERROR\",\n message: error.message.slice(0, 500) || \"Provider request failed\",\n };\n }\n return { code: \"PROVIDER_ERROR\", message: \"Provider request failed\" };\n}\n\nfunction randomId(): string {\n return typeof globalThis.crypto?.randomUUID === \"function\"\n ? globalThis.crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n\nexport class UsageTapObserver {\n private readonly apiKey: string;\n private readonly customerId: string;\n private readonly baseUrl: string;\n private readonly defaultFeature?: string;\n private readonly defaultTags?: string[];\n private readonly fetchImpl: typeof globalThis.fetch;\n private readonly onTelemetryError?: (error: Error) => void;\n\n constructor(options: UsageTapObserverOptions) {\n const apiKey = options.apiKey ??\n (typeof process !== \"undefined\" ? process.env?.USAGETAP_API_KEY : undefined);\n if (!apiKey?.trim()) throw new Error(\"UsageTap observer requires an apiKey\");\n if (!options.customerId?.trim()) {\n throw new Error(\"UsageTap observer requires a customerId\");\n }\n this.apiKey = apiKey.trim();\n this.customerId = options.customerId.trim();\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n this.defaultFeature = options.feature;\n this.defaultTags = options.tags;\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n this.onTelemetryError = options.onTelemetryError;\n }\n\n private telemetryError(error: unknown): void {\n const normalized = error instanceof Error ? error : new Error(String(error));\n this.onTelemetryError?.(normalized);\n }\n\n private async post<T>(path: string, body: UnknownRecord): Promise<T> {\n const result = await this.fetchImpl(`${this.baseUrl}/${path}`, {\n method: \"POST\",\n headers: {\n Accept: MEDIA_TYPE,\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n \"Idempotency-Key\": randomId(),\n \"X-Usage-SDK\": \"observer-js/0.1.0\",\n },\n body: JSON.stringify(body),\n });\n const envelope = (await result.json()) as Envelope<T>;\n if (!result.ok || !envelope.data) {\n throw new Error(\n envelope.error?.message ?? envelope.result?.message ?? `UsageTap returned ${result.status}`,\n );\n }\n return envelope.data;\n }\n\n /**\n * Observe one provider call. The callback receives no UsageTap data and the\n * observer never serializes the callback arguments, prompt, or response.\n * Telemetry fails open so provider behavior is unchanged.\n */\n async track<T>(\n metadata: ObserverMetadata,\n call: () => Promise<T>,\n extractUsage: (response: T) => ObservedUsage | undefined,\n ): Promise<T> {\n let begin: CallBeginData | undefined;\n try {\n begin = await this.post<CallBeginData>(\"call_begin\", {\n customerId: this.customerId,\n feature: metadata.feature ?? this.defaultFeature,\n tags: metadata.tags ?? this.defaultTags,\n idempotencyKey: randomId(),\n });\n } catch (error) {\n this.telemetryError(error);\n }\n\n try {\n const result = await call();\n if (begin?.callId) {\n try {\n await this.post(\"call_end\", {\n callId: begin.callId,\n providerUsed: metadata.provider,\n modelUsed: metadata.model,\n ...extractUsage(result),\n });\n } catch (error) {\n this.telemetryError(error);\n }\n }\n return result;\n } catch (error) {\n if (begin?.callId) {\n try {\n await this.post(\"call_end\", {\n callId: begin.callId,\n providerUsed: metadata.provider,\n modelUsed: metadata.model,\n error: cleanError(error),\n });\n } catch (telemetryError) {\n this.telemetryError(telemetryError);\n }\n }\n throw error;\n }\n }\n}\n\n/**\n * Wrap an OpenAI-compatible client without importing OpenAI or retaining\n * prompts. Non-streaming chat completions and Responses API calls are observed.\n * Streaming requests pass through unchanged in this first lightweight build.\n */\nexport function observeOpenAI<TClient extends object>(\n client: TClient,\n options: ObserveOpenAIOptions,\n): TClient & { unwrap(): TClient } {\n const observer = new UsageTapObserver(options);\n const provider = options.provider ?? \"openai\";\n const root = client as UnknownRecord;\n\n const wrapCreate = (\n resource: UnknownRecord | undefined,\n ): UnknownRecord | undefined => {\n if (!resource) return undefined;\n const original = resource.create;\n if (typeof original !== \"function\") return resource;\n return new Proxy(resource, {\n get(target, property, receiver): unknown {\n if (property !== \"create\") {\n return Reflect.get(target, property, receiver) as unknown;\n }\n return async (params: unknown, ...rest: unknown[]): Promise<unknown> => {\n const request = isRecord(params) ? params : {};\n const invoke = (): unknown =>\n Reflect.apply(original, target, [params, ...rest]) as unknown;\n if (request.stream === true) {\n return invoke();\n }\n return observer.track(\n {\n provider,\n model: readString(request, \"model\"),\n },\n () => Promise.resolve(invoke()),\n (response) => extractOpenAIUsage(response, provider),\n );\n };\n },\n });\n };\n\n const chat = isRecord(root.chat) ? root.chat : undefined;\n const wrappedCompletions = wrapCreate(\n chat && isRecord(chat.completions) ? chat.completions : undefined,\n );\n const wrappedChat = chat && wrappedCompletions\n ? new Proxy(chat, {\n get(target, property, receiver): unknown {\n return property === \"completions\"\n ? wrappedCompletions\n : Reflect.get(target, property, receiver) as unknown;\n },\n })\n : chat;\n const wrappedResponses = wrapCreate(\n isRecord(root.responses) ? root.responses : undefined,\n );\n\n return new Proxy(client, {\n get(target, property, receiver) {\n if (property === \"chat\" && wrappedChat) return wrappedChat;\n if (property === \"responses\" && wrappedResponses) return wrappedResponses;\n if (property === \"unwrap\") return () => client;\n return Reflect.get(target, property, receiver);\n },\n }) as TClient & { unwrap(): TClient };\n}\n\n/** Wrap a non-streaming Anthropic Messages client with metadata-only usage observation. */\nexport function observeAnthropic<TClient extends object>(\n client: TClient,\n options: ObserveAnthropicOptions,\n): TClient & { unwrap(): TClient } {\n const observer = new UsageTapObserver(options);\n const provider = options.provider ?? \"anthropic\";\n const root = client as UnknownRecord;\n const messages = isRecord(root.messages) ? root.messages : undefined;\n const original = messages?.create;\n const wrappedMessages =\n messages && typeof original === \"function\"\n ? new Proxy(messages, {\n get(target, property, receiver): unknown {\n if (property !== \"create\") {\n return Reflect.get(target, property, receiver) as unknown;\n }\n return async (\n params: unknown,\n ...rest: unknown[]\n ): Promise<unknown> => {\n const request = isRecord(params) ? params : {};\n const invoke = (): unknown =>\n Reflect.apply(original, target, [params, ...rest]) as unknown;\n if (request.stream === true) {\n return invoke();\n }\n return observer.track(\n {\n provider,\n model: readString(request, \"model\"),\n },\n () => Promise.resolve(invoke()),\n (response) => extractAnthropicUsage(response, provider),\n );\n };\n },\n })\n : messages;\n\n return new Proxy(client, {\n get(target, property, receiver) {\n if (property === \"messages\" && wrappedMessages) return wrappedMessages;\n if (property === \"unwrap\") return () => client;\n return Reflect.get(target, property, receiver);\n },\n }) as TClient & { unwrap(): TClient };\n}\n"],"mappings":"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","names":["src_exports","__export","UsageTapObserver","extractAnthropicUsage","extractOpenAIUsage","observeAnthropic","observeOpenAI","__toCommonJS","DEFAULT_BASE_URL","MEDIA_TYPE","isRecord","value","finiteNonNegative","readRecord","parent","key","readString","response","provider","usage","promptDetails","completionDetails","cleanError","error","randomId","options","apiKey","normalized","path","body","result","envelope","metadata","call","extractUsage","begin","telemetryError","client","observer","root","wrapCreate","resource","original","target","property","receiver","params","rest","request","invoke","chat","wrappedCompletions","wrappedChat","wrappedResponses","messages","wrappedMessages"]}
@@ -0,0 +1,64 @@
1
+ type ObserverMetadata = {
2
+ provider: string;
3
+ model?: string;
4
+ feature?: string;
5
+ tags?: string[];
6
+ };
7
+ type ObservedUsage = {
8
+ providerUsed?: string;
9
+ modelUsed?: string;
10
+ inputTokens?: number;
11
+ responseTokens?: number;
12
+ cachedInputTokens?: number;
13
+ cacheWriteInputTokens?: number;
14
+ reasoningTokens?: number;
15
+ };
16
+ type UsageTapObserverOptions = {
17
+ apiKey?: string;
18
+ customerId: string;
19
+ baseUrl?: string;
20
+ feature?: string;
21
+ tags?: string[];
22
+ fetch?: typeof globalThis.fetch;
23
+ onTelemetryError?: (error: Error) => void;
24
+ };
25
+ type ObserveOpenAIOptions = UsageTapObserverOptions & {
26
+ provider?: string;
27
+ };
28
+ type ObserveAnthropicOptions = UsageTapObserverOptions & {
29
+ provider?: string;
30
+ };
31
+ declare function extractOpenAIUsage(response: unknown, provider?: string): ObservedUsage | undefined;
32
+ declare function extractAnthropicUsage(response: unknown, provider?: string): ObservedUsage | undefined;
33
+ declare class UsageTapObserver {
34
+ private readonly apiKey;
35
+ private readonly customerId;
36
+ private readonly baseUrl;
37
+ private readonly defaultFeature?;
38
+ private readonly defaultTags?;
39
+ private readonly fetchImpl;
40
+ private readonly onTelemetryError?;
41
+ constructor(options: UsageTapObserverOptions);
42
+ private telemetryError;
43
+ private post;
44
+ /**
45
+ * Observe one provider call. The callback receives no UsageTap data and the
46
+ * observer never serializes the callback arguments, prompt, or response.
47
+ * Telemetry fails open so provider behavior is unchanged.
48
+ */
49
+ track<T>(metadata: ObserverMetadata, call: () => Promise<T>, extractUsage: (response: T) => ObservedUsage | undefined): Promise<T>;
50
+ }
51
+ /**
52
+ * Wrap an OpenAI-compatible client without importing OpenAI or retaining
53
+ * prompts. Non-streaming chat completions and Responses API calls are observed.
54
+ * Streaming requests pass through unchanged in this first lightweight build.
55
+ */
56
+ declare function observeOpenAI<TClient extends object>(client: TClient, options: ObserveOpenAIOptions): TClient & {
57
+ unwrap(): TClient;
58
+ };
59
+ /** Wrap a non-streaming Anthropic Messages client with metadata-only usage observation. */
60
+ declare function observeAnthropic<TClient extends object>(client: TClient, options: ObserveAnthropicOptions): TClient & {
61
+ unwrap(): TClient;
62
+ };
63
+
64
+ export { type ObserveAnthropicOptions, type ObserveOpenAIOptions, type ObservedUsage, type ObserverMetadata, UsageTapObserver, type UsageTapObserverOptions, extractAnthropicUsage, extractOpenAIUsage, observeAnthropic, observeOpenAI };
@@ -0,0 +1,64 @@
1
+ type ObserverMetadata = {
2
+ provider: string;
3
+ model?: string;
4
+ feature?: string;
5
+ tags?: string[];
6
+ };
7
+ type ObservedUsage = {
8
+ providerUsed?: string;
9
+ modelUsed?: string;
10
+ inputTokens?: number;
11
+ responseTokens?: number;
12
+ cachedInputTokens?: number;
13
+ cacheWriteInputTokens?: number;
14
+ reasoningTokens?: number;
15
+ };
16
+ type UsageTapObserverOptions = {
17
+ apiKey?: string;
18
+ customerId: string;
19
+ baseUrl?: string;
20
+ feature?: string;
21
+ tags?: string[];
22
+ fetch?: typeof globalThis.fetch;
23
+ onTelemetryError?: (error: Error) => void;
24
+ };
25
+ type ObserveOpenAIOptions = UsageTapObserverOptions & {
26
+ provider?: string;
27
+ };
28
+ type ObserveAnthropicOptions = UsageTapObserverOptions & {
29
+ provider?: string;
30
+ };
31
+ declare function extractOpenAIUsage(response: unknown, provider?: string): ObservedUsage | undefined;
32
+ declare function extractAnthropicUsage(response: unknown, provider?: string): ObservedUsage | undefined;
33
+ declare class UsageTapObserver {
34
+ private readonly apiKey;
35
+ private readonly customerId;
36
+ private readonly baseUrl;
37
+ private readonly defaultFeature?;
38
+ private readonly defaultTags?;
39
+ private readonly fetchImpl;
40
+ private readonly onTelemetryError?;
41
+ constructor(options: UsageTapObserverOptions);
42
+ private telemetryError;
43
+ private post;
44
+ /**
45
+ * Observe one provider call. The callback receives no UsageTap data and the
46
+ * observer never serializes the callback arguments, prompt, or response.
47
+ * Telemetry fails open so provider behavior is unchanged.
48
+ */
49
+ track<T>(metadata: ObserverMetadata, call: () => Promise<T>, extractUsage: (response: T) => ObservedUsage | undefined): Promise<T>;
50
+ }
51
+ /**
52
+ * Wrap an OpenAI-compatible client without importing OpenAI or retaining
53
+ * prompts. Non-streaming chat completions and Responses API calls are observed.
54
+ * Streaming requests pass through unchanged in this first lightweight build.
55
+ */
56
+ declare function observeOpenAI<TClient extends object>(client: TClient, options: ObserveOpenAIOptions): TClient & {
57
+ unwrap(): TClient;
58
+ };
59
+ /** Wrap a non-streaming Anthropic Messages client with metadata-only usage observation. */
60
+ declare function observeAnthropic<TClient extends object>(client: TClient, options: ObserveAnthropicOptions): TClient & {
61
+ unwrap(): TClient;
62
+ };
63
+
64
+ export { type ObserveAnthropicOptions, type ObserveOpenAIOptions, type ObservedUsage, type ObserverMetadata, UsageTapObserver, type UsageTapObserverOptions, extractAnthropicUsage, extractOpenAIUsage, observeAnthropic, observeOpenAI };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var R="https://api.usagetap.com",E="application/vnd.usagetap.v1+json";function l(r){return!!r&&typeof r=="object"&&!Array.isArray(r)}function c(r){return typeof r=="number"&&Number.isFinite(r)&&r>=0?Math.round(r):void 0}function g(r,n){let e=r[n];return l(e)?e:void 0}function T(r,n){let e=r[n];return typeof e=="string"&&e.trim()?e:void 0}function O(r,n="openai"){if(!l(r))return;let e=g(r,"usage");if(!e)return;let s=g(e,"prompt_tokens_details")??g(e,"input_tokens_details"),t=g(e,"completion_tokens_details")??g(e,"output_tokens_details");return{providerUsed:n,modelUsed:T(r,"model"),inputTokens:c(e.prompt_tokens)??c(e.input_tokens),responseTokens:c(e.completion_tokens)??c(e.output_tokens),cachedInputTokens:c(s?.cached_tokens)??c(e.cached_tokens),cacheWriteInputTokens:c(s?.cache_write_tokens),reasoningTokens:c(t?.reasoning_tokens)}}function P(r,n="anthropic"){if(!l(r))return;let e=g(r,"usage");if(e)return{providerUsed:n,modelUsed:T(r,"model"),inputTokens:c(e.input_tokens),responseTokens:c(e.output_tokens),cachedInputTokens:c(e.cache_read_input_tokens),cacheWriteInputTokens:c(e.cache_creation_input_tokens)}}function x(r){return r instanceof Error?{code:r.name.slice(0,80)||"PROVIDER_ERROR",message:r.message.slice(0,500)||"Provider request failed"}:{code:"PROVIDER_ERROR",message:"Provider request failed"}}function U(){return typeof globalThis.crypto?.randomUUID=="function"?globalThis.crypto.randomUUID():`${Date.now()}-${Math.random().toString(36).slice(2)}`}var y=class{apiKey;customerId;baseUrl;defaultFeature;defaultTags;fetchImpl;onTelemetryError;constructor(n){let e=n.apiKey??(typeof process<"u"?process.env?.USAGETAP_API_KEY:void 0);if(!e?.trim())throw new Error("UsageTap observer requires an apiKey");if(!n.customerId?.trim())throw new Error("UsageTap observer requires a customerId");this.apiKey=e.trim(),this.customerId=n.customerId.trim(),this.baseUrl=(n.baseUrl??R).replace(/\/$/,""),this.defaultFeature=n.feature,this.defaultTags=n.tags,this.fetchImpl=n.fetch??globalThis.fetch,this.onTelemetryError=n.onTelemetryError}telemetryError(n){let e=n instanceof Error?n:new Error(String(n));this.onTelemetryError?.(e)}async post(n,e){let s=await this.fetchImpl(`${this.baseUrl}/${n}`,{method:"POST",headers:{Accept:E,"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`,"Idempotency-Key":U(),"X-Usage-SDK":"observer-js/0.1.0"},body:JSON.stringify(e)}),t=await s.json();if(!s.ok||!t.data)throw new Error(t.error?.message??t.result?.message??`UsageTap returned ${s.status}`);return t.data}async track(n,e,s){let t;try{t=await this.post("call_begin",{customerId:this.customerId,feature:n.feature??this.defaultFeature,tags:n.tags??this.defaultTags,idempotencyKey:U()})}catch(o){this.telemetryError(o)}try{let o=await e();if(t?.callId)try{await this.post("call_end",{callId:t.callId,providerUsed:n.provider,modelUsed:n.model,...s(o)})}catch(a){this.telemetryError(a)}return o}catch(o){if(t?.callId)try{await this.post("call_end",{callId:t.callId,providerUsed:n.provider,modelUsed:n.model,error:x(o)})}catch(a){this.telemetryError(a)}throw o}}};function C(r,n){let e=new y(n),s=n.provider??"openai",t=r,o=d=>{if(!d)return;let i=d.create;return typeof i!="function"?d:new Proxy(d,{get(p,h,w){return h!=="create"?Reflect.get(p,h,w):async(k,...I)=>{let v=l(k)?k:{},b=()=>Reflect.apply(i,p,[k,...I]);return v.stream===!0?b():e.track({provider:s,model:T(v,"model")},()=>Promise.resolve(b()),_=>O(_,s))}}})},a=l(t.chat)?t.chat:void 0,m=o(a&&l(a.completions)?a.completions:void 0),f=a&&m?new Proxy(a,{get(d,i,p){return i==="completions"?m:Reflect.get(d,i,p)}}):a,u=o(l(t.responses)?t.responses:void 0);return new Proxy(r,{get(d,i,p){return i==="chat"&&f?f:i==="responses"&&u?u:i==="unwrap"?()=>r:Reflect.get(d,i,p)}})}function A(r,n){let e=new y(n),s=n.provider??"anthropic",t=r,o=l(t.messages)?t.messages:void 0,a=o?.create,m=o&&typeof a=="function"?new Proxy(o,{get(f,u,d){return u!=="create"?Reflect.get(f,u,d):async(i,...p)=>{let h=l(i)?i:{},w=()=>Reflect.apply(a,f,[i,...p]);return h.stream===!0?w():e.track({provider:s,model:T(h,"model")},()=>Promise.resolve(w()),k=>P(k,s))}}}):o;return new Proxy(r,{get(f,u,d){return u==="messages"&&m?m:u==="unwrap"?()=>r:Reflect.get(f,u,d)}})}export{y as UsageTapObserver,P as extractAnthropicUsage,O as extractOpenAIUsage,A as observeAnthropic,C as observeOpenAI};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["const DEFAULT_BASE_URL = \"https://api.usagetap.com\";\nconst MEDIA_TYPE = \"application/vnd.usagetap.v1+json\";\n\ntype UnknownRecord = Record<string, unknown>;\n\nexport type ObserverMetadata = {\n provider: string;\n model?: string;\n feature?: string;\n tags?: string[];\n};\n\nexport type ObservedUsage = {\n providerUsed?: string;\n modelUsed?: string;\n inputTokens?: number;\n responseTokens?: number;\n cachedInputTokens?: number;\n cacheWriteInputTokens?: number;\n reasoningTokens?: number;\n};\n\nexport type UsageTapObserverOptions = {\n apiKey?: string;\n customerId: string;\n baseUrl?: string;\n feature?: string;\n tags?: string[];\n fetch?: typeof globalThis.fetch;\n onTelemetryError?: (error: Error) => void;\n};\n\nexport type ObserveOpenAIOptions = UsageTapObserverOptions & {\n provider?: string;\n};\n\nexport type ObserveAnthropicOptions = UsageTapObserverOptions & {\n provider?: string;\n};\n\ntype CallBeginData = {\n callId: string;\n allowed?: { standard?: boolean; premium?: boolean };\n};\n\ntype Envelope<T> = {\n data?: T;\n error?: { message?: string };\n result?: { message?: string };\n};\n\nfunction isRecord(value: unknown): value is UnknownRecord {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction finiteNonNegative(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0\n ? Math.round(value)\n : undefined;\n}\n\nfunction readRecord(parent: UnknownRecord, key: string): UnknownRecord | undefined {\n const value = parent[key];\n return isRecord(value) ? value : undefined;\n}\n\nfunction readString(parent: UnknownRecord, key: string): string | undefined {\n const value = parent[key];\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\nexport function extractOpenAIUsage(\n response: unknown,\n provider = \"openai\",\n): ObservedUsage | undefined {\n if (!isRecord(response)) return undefined;\n const usage = readRecord(response, \"usage\");\n if (!usage) return undefined;\n const promptDetails =\n readRecord(usage, \"prompt_tokens_details\") ??\n readRecord(usage, \"input_tokens_details\");\n const completionDetails =\n readRecord(usage, \"completion_tokens_details\") ??\n readRecord(usage, \"output_tokens_details\");\n\n return {\n providerUsed: provider,\n modelUsed: readString(response, \"model\"),\n inputTokens:\n finiteNonNegative(usage.prompt_tokens) ??\n finiteNonNegative(usage.input_tokens),\n responseTokens:\n finiteNonNegative(usage.completion_tokens) ??\n finiteNonNegative(usage.output_tokens),\n cachedInputTokens:\n finiteNonNegative(promptDetails?.cached_tokens) ??\n finiteNonNegative(usage.cached_tokens),\n cacheWriteInputTokens: finiteNonNegative(promptDetails?.cache_write_tokens),\n reasoningTokens: finiteNonNegative(completionDetails?.reasoning_tokens),\n };\n}\n\nexport function extractAnthropicUsage(\n response: unknown,\n provider = \"anthropic\",\n): ObservedUsage | undefined {\n if (!isRecord(response)) return undefined;\n const usage = readRecord(response, \"usage\");\n if (!usage) return undefined;\n return {\n providerUsed: provider,\n modelUsed: readString(response, \"model\"),\n inputTokens: finiteNonNegative(usage.input_tokens),\n responseTokens: finiteNonNegative(usage.output_tokens),\n cachedInputTokens: finiteNonNegative(usage.cache_read_input_tokens),\n cacheWriteInputTokens: finiteNonNegative(usage.cache_creation_input_tokens),\n };\n}\n\nfunction cleanError(error: unknown): { code: string; message: string } {\n if (error instanceof Error) {\n return {\n code: error.name.slice(0, 80) || \"PROVIDER_ERROR\",\n message: error.message.slice(0, 500) || \"Provider request failed\",\n };\n }\n return { code: \"PROVIDER_ERROR\", message: \"Provider request failed\" };\n}\n\nfunction randomId(): string {\n return typeof globalThis.crypto?.randomUUID === \"function\"\n ? globalThis.crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n\nexport class UsageTapObserver {\n private readonly apiKey: string;\n private readonly customerId: string;\n private readonly baseUrl: string;\n private readonly defaultFeature?: string;\n private readonly defaultTags?: string[];\n private readonly fetchImpl: typeof globalThis.fetch;\n private readonly onTelemetryError?: (error: Error) => void;\n\n constructor(options: UsageTapObserverOptions) {\n const apiKey = options.apiKey ??\n (typeof process !== \"undefined\" ? process.env?.USAGETAP_API_KEY : undefined);\n if (!apiKey?.trim()) throw new Error(\"UsageTap observer requires an apiKey\");\n if (!options.customerId?.trim()) {\n throw new Error(\"UsageTap observer requires a customerId\");\n }\n this.apiKey = apiKey.trim();\n this.customerId = options.customerId.trim();\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n this.defaultFeature = options.feature;\n this.defaultTags = options.tags;\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n this.onTelemetryError = options.onTelemetryError;\n }\n\n private telemetryError(error: unknown): void {\n const normalized = error instanceof Error ? error : new Error(String(error));\n this.onTelemetryError?.(normalized);\n }\n\n private async post<T>(path: string, body: UnknownRecord): Promise<T> {\n const result = await this.fetchImpl(`${this.baseUrl}/${path}`, {\n method: \"POST\",\n headers: {\n Accept: MEDIA_TYPE,\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n \"Idempotency-Key\": randomId(),\n \"X-Usage-SDK\": \"observer-js/0.1.0\",\n },\n body: JSON.stringify(body),\n });\n const envelope = (await result.json()) as Envelope<T>;\n if (!result.ok || !envelope.data) {\n throw new Error(\n envelope.error?.message ?? envelope.result?.message ?? `UsageTap returned ${result.status}`,\n );\n }\n return envelope.data;\n }\n\n /**\n * Observe one provider call. The callback receives no UsageTap data and the\n * observer never serializes the callback arguments, prompt, or response.\n * Telemetry fails open so provider behavior is unchanged.\n */\n async track<T>(\n metadata: ObserverMetadata,\n call: () => Promise<T>,\n extractUsage: (response: T) => ObservedUsage | undefined,\n ): Promise<T> {\n let begin: CallBeginData | undefined;\n try {\n begin = await this.post<CallBeginData>(\"call_begin\", {\n customerId: this.customerId,\n feature: metadata.feature ?? this.defaultFeature,\n tags: metadata.tags ?? this.defaultTags,\n idempotencyKey: randomId(),\n });\n } catch (error) {\n this.telemetryError(error);\n }\n\n try {\n const result = await call();\n if (begin?.callId) {\n try {\n await this.post(\"call_end\", {\n callId: begin.callId,\n providerUsed: metadata.provider,\n modelUsed: metadata.model,\n ...extractUsage(result),\n });\n } catch (error) {\n this.telemetryError(error);\n }\n }\n return result;\n } catch (error) {\n if (begin?.callId) {\n try {\n await this.post(\"call_end\", {\n callId: begin.callId,\n providerUsed: metadata.provider,\n modelUsed: metadata.model,\n error: cleanError(error),\n });\n } catch (telemetryError) {\n this.telemetryError(telemetryError);\n }\n }\n throw error;\n }\n }\n}\n\n/**\n * Wrap an OpenAI-compatible client without importing OpenAI or retaining\n * prompts. Non-streaming chat completions and Responses API calls are observed.\n * Streaming requests pass through unchanged in this first lightweight build.\n */\nexport function observeOpenAI<TClient extends object>(\n client: TClient,\n options: ObserveOpenAIOptions,\n): TClient & { unwrap(): TClient } {\n const observer = new UsageTapObserver(options);\n const provider = options.provider ?? \"openai\";\n const root = client as UnknownRecord;\n\n const wrapCreate = (\n resource: UnknownRecord | undefined,\n ): UnknownRecord | undefined => {\n if (!resource) return undefined;\n const original = resource.create;\n if (typeof original !== \"function\") return resource;\n return new Proxy(resource, {\n get(target, property, receiver): unknown {\n if (property !== \"create\") {\n return Reflect.get(target, property, receiver) as unknown;\n }\n return async (params: unknown, ...rest: unknown[]): Promise<unknown> => {\n const request = isRecord(params) ? params : {};\n const invoke = (): unknown =>\n Reflect.apply(original, target, [params, ...rest]) as unknown;\n if (request.stream === true) {\n return invoke();\n }\n return observer.track(\n {\n provider,\n model: readString(request, \"model\"),\n },\n () => Promise.resolve(invoke()),\n (response) => extractOpenAIUsage(response, provider),\n );\n };\n },\n });\n };\n\n const chat = isRecord(root.chat) ? root.chat : undefined;\n const wrappedCompletions = wrapCreate(\n chat && isRecord(chat.completions) ? chat.completions : undefined,\n );\n const wrappedChat = chat && wrappedCompletions\n ? new Proxy(chat, {\n get(target, property, receiver): unknown {\n return property === \"completions\"\n ? wrappedCompletions\n : Reflect.get(target, property, receiver) as unknown;\n },\n })\n : chat;\n const wrappedResponses = wrapCreate(\n isRecord(root.responses) ? root.responses : undefined,\n );\n\n return new Proxy(client, {\n get(target, property, receiver) {\n if (property === \"chat\" && wrappedChat) return wrappedChat;\n if (property === \"responses\" && wrappedResponses) return wrappedResponses;\n if (property === \"unwrap\") return () => client;\n return Reflect.get(target, property, receiver);\n },\n }) as TClient & { unwrap(): TClient };\n}\n\n/** Wrap a non-streaming Anthropic Messages client with metadata-only usage observation. */\nexport function observeAnthropic<TClient extends object>(\n client: TClient,\n options: ObserveAnthropicOptions,\n): TClient & { unwrap(): TClient } {\n const observer = new UsageTapObserver(options);\n const provider = options.provider ?? \"anthropic\";\n const root = client as UnknownRecord;\n const messages = isRecord(root.messages) ? root.messages : undefined;\n const original = messages?.create;\n const wrappedMessages =\n messages && typeof original === \"function\"\n ? new Proxy(messages, {\n get(target, property, receiver): unknown {\n if (property !== \"create\") {\n return Reflect.get(target, property, receiver) as unknown;\n }\n return async (\n params: unknown,\n ...rest: unknown[]\n ): Promise<unknown> => {\n const request = isRecord(params) ? params : {};\n const invoke = (): unknown =>\n Reflect.apply(original, target, [params, ...rest]) as unknown;\n if (request.stream === true) {\n return invoke();\n }\n return observer.track(\n {\n provider,\n model: readString(request, \"model\"),\n },\n () => Promise.resolve(invoke()),\n (response) => extractAnthropicUsage(response, provider),\n );\n };\n },\n })\n : messages;\n\n return new Proxy(client, {\n get(target, property, receiver) {\n if (property === \"messages\" && wrappedMessages) return wrappedMessages;\n if (property === \"unwrap\") return () => client;\n return Reflect.get(target, property, receiver);\n },\n }) as TClient & { unwrap(): TClient };\n}\n"],"mappings":"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","names":["DEFAULT_BASE_URL","MEDIA_TYPE","isRecord","value","finiteNonNegative","readRecord","parent","key","readString","extractOpenAIUsage","response","provider","usage","promptDetails","completionDetails","extractAnthropicUsage","cleanError","error","randomId","UsageTapObserver","options","apiKey","normalized","path","body","result","envelope","metadata","call","extractUsage","begin","telemetryError","observeOpenAI","client","observer","root","wrapCreate","resource","original","target","property","receiver","params","rest","request","invoke","chat","wrappedCompletions","wrappedChat","wrappedResponses","observeAnthropic","messages","wrappedMessages"]}
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@usagetap/observer",
3
+ "version": "0.1.0",
4
+ "description": "A tiny metadata-only UsageTap observer for AI usage and cost trials.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/PredictabilityAtScale/usagetap.git",
9
+ "directory": "packages/usagetap-observer"
10
+ },
11
+ "homepage": "https://usagetap.com/trial/usage",
12
+ "bugs": {
13
+ "url": "https://github.com/PredictabilityAtScale/usagetap/issues"
14
+ },
15
+ "keywords": [
16
+ "usagetap",
17
+ "llm",
18
+ "ai",
19
+ "observability",
20
+ "usage",
21
+ "cost",
22
+ "openai",
23
+ "anthropic"
24
+ ],
25
+ "type": "module",
26
+ "main": "./dist/index.cjs",
27
+ "module": "./dist/index.mjs",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "import": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.mjs"
34
+ },
35
+ "require": {
36
+ "types": "./dist/index.d.cts",
37
+ "default": "./dist/index.cjs"
38
+ }
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "sideEffects": false,
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md"
49
+ ],
50
+ "scripts": {
51
+ "clean": "rimraf dist",
52
+ "build": "tsup",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "typecheck": "tsc --noEmit",
56
+ "lint": "eslint --config ../../libraries.config.mjs --max-warnings=0 --ignore-pattern \"src/**/*.test.ts\" \"src/**/*.ts\"",
57
+ "lint:fix": "npm run lint -- --fix",
58
+ "prepublishOnly": "npm run lint && npm run typecheck && npm test",
59
+ "prepare": "npm run build"
60
+ },
61
+ "engines": {
62
+ "node": ">=18.17"
63
+ },
64
+ "peerDependencies": {
65
+ "@anthropic-ai/sdk": ">=0.20.0",
66
+ "openai": ">=4.0.0"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "@anthropic-ai/sdk": {
70
+ "optional": true
71
+ },
72
+ "openai": {
73
+ "optional": true
74
+ }
75
+ },
76
+ "devDependencies": {
77
+ "tsup": "^8.3.5",
78
+ "typescript": "^5.9.2",
79
+ "vitest": "^2.1.5"
80
+ }
81
+ }