@usagetap/sdk 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.
@@ -0,0 +1,398 @@
1
+ import OpenAI from 'openai';
2
+
3
+ type ReasoningLevel = "NONE" | "LOW" | "MEDIUM" | "HIGH";
4
+ type LimitType = "NONE" | "BLOCK" | "DOWNGRADE";
5
+ interface RequestedEntitlements {
6
+ standard?: boolean;
7
+ premium?: boolean;
8
+ audio?: boolean;
9
+ image?: boolean;
10
+ search?: boolean;
11
+ reasoningLevel?: ReasoningLevel;
12
+ }
13
+ type AllowedEntitlements = Required<RequestedEntitlements>;
14
+ type ModelTier = "premium" | "standard" | "none";
15
+ interface EntitlementDowngradeHint {
16
+ reason: string;
17
+ fallbackTier?: ModelTier;
18
+ }
19
+ interface EntitlementHints {
20
+ suggestedModelTier: ModelTier;
21
+ reasoningLevel: ReasoningLevel;
22
+ policy: LimitType;
23
+ downgrade?: EntitlementDowngradeHint;
24
+ }
25
+ interface MeterSummary {
26
+ remaining: number | null;
27
+ limit: number | null;
28
+ used: number | null;
29
+ unlimited: boolean;
30
+ ratio: number | null;
31
+ }
32
+ type MeterSnapshot = Record<string, MeterSummary>;
33
+ type RemainingRatios = Record<string, number | null | undefined>;
34
+ interface SubscriptionSnapshot {
35
+ id: string | null;
36
+ usagePlanVersionId: string | null;
37
+ planName: string | null;
38
+ planVersion: string | null;
39
+ limitType: LimitType;
40
+ reasoningLevel: ReasoningLevel;
41
+ lastReplenishedAt: string | null;
42
+ nextReplenishAt: string | null;
43
+ subscriptionVersion: number | null;
44
+ customerFriendlyName?: string | null;
45
+ customerEmail?: string | null;
46
+ pending?: {
47
+ usagePlanVersionId: string | null;
48
+ strategy: string | null;
49
+ effectiveAt: string | null;
50
+ };
51
+ stripeCustomerId?: string | null;
52
+ }
53
+ type ModelHints = Record<string, string[]>;
54
+ interface IdempotencyMetadata {
55
+ key: string;
56
+ source: "explicit" | "derived";
57
+ }
58
+ interface VendorHints {
59
+ preferredModel?: string;
60
+ reasoning?: ReasoningLevel;
61
+ maxInputTokens?: number;
62
+ maxResponseTokens?: number;
63
+ }
64
+ interface BeginCallRequest {
65
+ customerId: string;
66
+ requested?: RequestedEntitlements;
67
+ feature?: string;
68
+ tags?: string[];
69
+ idempotency?: string;
70
+ customerName?: string;
71
+ customerEmail?: string;
72
+ stripeCustomerId?: string;
73
+ }
74
+ interface BalanceSummary {
75
+ standardCallsRemaining?: number;
76
+ premiumCallsRemaining?: number;
77
+ tokensRemaining?: number;
78
+ searchesRemaining?: number;
79
+ audioSecondsRemaining?: number;
80
+ }
81
+ interface PlanSummary {
82
+ id: string | null;
83
+ name: string | null;
84
+ version: string | null;
85
+ }
86
+ interface BeginCallResponseBody {
87
+ callId: string;
88
+ startTime: string;
89
+ feature?: string;
90
+ tags?: string[];
91
+ newCustomer: boolean;
92
+ canceled: boolean;
93
+ policy: LimitType;
94
+ allowed: AllowedEntitlements;
95
+ entitlementHints: EntitlementHints;
96
+ meters: MeterSnapshot;
97
+ remainingRatios: RemainingRatios;
98
+ subscription: SubscriptionSnapshot;
99
+ models?: ModelHints;
100
+ idempotency?: IdempotencyMetadata;
101
+ vendorHints?: VendorHints;
102
+ plan?: PlanSummary;
103
+ balances?: BalanceSummary;
104
+ stripeCustomerId?: string | null;
105
+ }
106
+ interface EndCallRequest {
107
+ callId: string;
108
+ modelUsed?: string;
109
+ inputTokens?: number;
110
+ responseTokens?: number;
111
+ cachedTokens?: number;
112
+ reasoningTokens?: number;
113
+ searches?: number;
114
+ audio?: number;
115
+ audioSeconds?: number;
116
+ error?: {
117
+ code: string;
118
+ message: string;
119
+ };
120
+ stripeCustomerId?: string;
121
+ }
122
+ interface MeteredUsage {
123
+ calls?: number;
124
+ tokens?: number;
125
+ reasoningTokens?: number;
126
+ searches?: number;
127
+ audio?: number;
128
+ audioSeconds?: number;
129
+ }
130
+ interface EndCallResponseBody {
131
+ callId: string;
132
+ costUSD: number;
133
+ metered?: MeteredUsage;
134
+ balances?: BalanceSummary;
135
+ stripeCustomerId?: string | null;
136
+ }
137
+ type UsageTapResultStatus = "ACCEPTED" | "ERROR";
138
+ interface UsageTapResultEnvelope {
139
+ status: UsageTapResultStatus;
140
+ code?: string;
141
+ message?: string;
142
+ timestamp?: string;
143
+ }
144
+ interface UsageTapErrorPayload {
145
+ code: string;
146
+ message: string;
147
+ details?: Record<string, unknown>;
148
+ }
149
+ interface UsageTapSuccessResponse<TData> {
150
+ result: UsageTapResultEnvelope;
151
+ data: TData;
152
+ correlationId: string;
153
+ }
154
+ interface UsageTapErrorResponse {
155
+ result: UsageTapResultEnvelope;
156
+ error: UsageTapErrorPayload;
157
+ correlationId: string;
158
+ }
159
+ interface RetryOptions {
160
+ /**
161
+ * Maximum number of attempts including the initial try.
162
+ * @default 3
163
+ */
164
+ maxAttempts?: number;
165
+ /**
166
+ * Base backoff delay in milliseconds.
167
+ * @default 250
168
+ */
169
+ baseDelayMs?: number;
170
+ /**
171
+ * Maximum backoff delay in milliseconds.
172
+ * @default 5000
173
+ */
174
+ maxDelayMs?: number;
175
+ /**
176
+ * Jitter ratio between 0 and 1 applied to the computed delay.
177
+ * @default 0.2
178
+ */
179
+ jitterRatio?: number;
180
+ }
181
+ interface UsageTapLogEntry {
182
+ event: "request:start" | "request:success" | "request:error" | "retry:scheduled" | "retry:exhausted";
183
+ path: string;
184
+ attempt: number;
185
+ elapsedMs?: number;
186
+ idempotencyKey?: string;
187
+ correlationId?: string;
188
+ error?: unknown;
189
+ }
190
+ interface UsageTapClientOptions {
191
+ apiKey: string;
192
+ baseUrl: string;
193
+ defaultFeature?: string;
194
+ defaultTags?: string[];
195
+ fetchImpl?: typeof fetch;
196
+ headers?: Record<string, string>;
197
+ retries?: RetryOptions;
198
+ idempotencyGenerator?: () => string;
199
+ /**
200
+ * When true (default), the client auto-generates an idempotency key when one is not provided.
201
+ * Set to false to rely on the backend's deterministic fallback.
202
+ */
203
+ autoIdempotency?: boolean;
204
+ onLog?: (entry: UsageTapLogEntry) => void;
205
+ /**
206
+ * Override the default Authorization header with x-api-key when true.
207
+ */
208
+ useApiKeyHeader?: boolean;
209
+ /**
210
+ * Allow constructing the client in browser-like environments for testing.
211
+ */
212
+ allowBrowser?: boolean;
213
+ }
214
+ interface RequestOptions {
215
+ signal?: AbortSignal;
216
+ headers?: Record<string, string>;
217
+ retries?: RetryOptions;
218
+ }
219
+ interface BeginCallOptions extends RequestOptions {
220
+ correlationId?: string;
221
+ }
222
+ interface EndCallOptions extends RequestOptions {
223
+ correlationId?: string;
224
+ }
225
+ interface WithUsageContext {
226
+ begin: UsageTapSuccessResponse<BeginCallResponseBody>;
227
+ setUsage: (usage: Partial<Omit<EndCallRequest, "callId" | "error">>) => void;
228
+ setError: (error: EndCallRequest["error"]) => void;
229
+ }
230
+ interface WithUsageOptions {
231
+ signal?: AbortSignal;
232
+ headers?: Record<string, string>;
233
+ retries?: RetryOptions;
234
+ correlationId?: string;
235
+ /**
236
+ * Default error payload applied when the handler throws and setError was never invoked.
237
+ */
238
+ defaultErrorCode?: string;
239
+ }
240
+
241
+ declare class UsageTapClient {
242
+ private readonly apiKey;
243
+ private readonly baseUrl;
244
+ private readonly fetchImpl;
245
+ private readonly defaultFeature?;
246
+ private readonly defaultTags?;
247
+ private readonly defaultHeaders;
248
+ private readonly retryDefaults;
249
+ private readonly idempotencyGenerator;
250
+ private readonly logFn?;
251
+ private readonly authHeader;
252
+ private readonly autoIdempotency;
253
+ constructor(options: UsageTapClientOptions);
254
+ beginCall(request: BeginCallRequest, options?: BeginCallOptions): Promise<UsageTapSuccessResponse<BeginCallResponseBody>>;
255
+ endCall(request: EndCallRequest, options?: EndCallOptions): Promise<UsageTapSuccessResponse<EndCallResponseBody>>;
256
+ withUsage<T>(beginRequest: BeginCallRequest, handler: (context: WithUsageContext) => Promise<T>, options?: WithUsageOptions): Promise<T>;
257
+ private request;
258
+ private performFetch;
259
+ private composeHeaders;
260
+ private log;
261
+ private mergeTags;
262
+ private shouldRetry;
263
+ private toHttpError;
264
+ private toApiError;
265
+ }
266
+
267
+ interface OpenAIAdapterInit {
268
+ client: OpenAI;
269
+ usageTap: UsageTapClient;
270
+ }
271
+ interface OpenAIRequestContext {
272
+ hints?: VendorHints;
273
+ begin: UsageTapSuccessResponse<BeginCallResponseBody>;
274
+ }
275
+ interface OpenAIInvokeParams<TResponse> {
276
+ begin: BeginCallRequest;
277
+ call: (client: OpenAI, ctx: OpenAIRequestContext) => Promise<TResponse>;
278
+ extractUsage?: (response: TResponse) => Partial<Omit<EndCallRequest, "callId" | "error">> | void;
279
+ withUsageOptions?: WithUsageOptions;
280
+ }
281
+ interface OpenAIInvokeResult<TResponse> {
282
+ data: TResponse;
283
+ begin: UsageTapSuccessResponse<BeginCallResponseBody>;
284
+ }
285
+ interface OpenAIStreamCallResult<TStream> {
286
+ stream: AsyncIterable<TStream>;
287
+ onComplete?: () => Promise<Partial<Omit<EndCallRequest, "callId" | "error">> | void> | Partial<Omit<EndCallRequest, "callId" | "error">> | void;
288
+ }
289
+ interface OpenAIStreamParams<TStream> {
290
+ begin: BeginCallRequest;
291
+ call: (client: OpenAI, ctx: OpenAIRequestContext) => Promise<OpenAIStreamCallResult<TStream>>;
292
+ withUsageOptions?: WithUsageOptions;
293
+ }
294
+ interface OpenAIStreamResult<TStream> {
295
+ stream: AsyncIterable<TStream> & {
296
+ __usageTapFinalize?: () => Promise<void>;
297
+ };
298
+ begin: UsageTapSuccessResponse<BeginCallResponseBody>;
299
+ finalize: () => Promise<void>;
300
+ }
301
+ interface OpenAIAdapter {
302
+ invoke<TResponse>(params: OpenAIInvokeParams<TResponse>): Promise<OpenAIInvokeResult<TResponse>>;
303
+ invokeStream<TStream>(params: OpenAIStreamParams<TStream>): Promise<OpenAIStreamResult<TStream>>;
304
+ }
305
+ type ReplaceProperty<T, K extends keyof T, V> = Omit<T, K> & Record<K, V>;
306
+ type WrapOpenAIContext = BeginCallRequest;
307
+ interface WrapOpenAIOptions {
308
+ defaultContext?: Partial<WrapOpenAIContext>;
309
+ applyVendorHints?: boolean;
310
+ }
311
+ type ChatCompletionsResource = OpenAI["chat"]["completions"];
312
+ type ChatCompletionCreate = ChatCompletionsResource["create"];
313
+ type ChatCompletionCreateParams = Parameters<ChatCompletionCreate>[0];
314
+ type ChatCompletionCreateOptions = Parameters<ChatCompletionCreate>[1];
315
+ type ChatCompletionCreateReturn = ReturnType<ChatCompletionCreate>;
316
+ type WrapOpenAICallOptions = (ChatCompletionCreateOptions extends undefined ? {
317
+ usageTap?: Partial<WrapOpenAIContext>;
318
+ withUsage?: WithUsageOptions;
319
+ } : ChatCompletionCreateOptions & {
320
+ usageTap?: Partial<WrapOpenAIContext>;
321
+ withUsage?: WithUsageOptions;
322
+ });
323
+ interface WrappedChatCompletions extends Omit<ChatCompletionsResource, "create"> {
324
+ create: (params: ChatCompletionCreateParams, options?: WrapOpenAICallOptions) => ChatCompletionCreateReturn;
325
+ }
326
+ type ResponsesResource = OpenAI extends {
327
+ responses: infer R;
328
+ } ? R : never;
329
+ type ResponsesCreate = ResponsesResource extends {
330
+ create: infer T;
331
+ } ? T : never;
332
+ type ResponsesCreateParams = ResponsesCreate extends (...args: infer P) => unknown ? P[0] : never;
333
+ type ResponsesCreateOptions = ResponsesCreate extends (...args: infer P) => unknown ? P[1] : never;
334
+ type ResponsesCreateReturn = ResponsesCreate extends (...args: unknown[]) => infer R ? R : never;
335
+ type WrapOpenAIResponseCallOptions = (ResponsesCreateOptions extends undefined ? {
336
+ usageTap?: Partial<WrapOpenAIContext>;
337
+ withUsage?: WithUsageOptions;
338
+ } : ResponsesCreateOptions & {
339
+ usageTap?: Partial<WrapOpenAIContext>;
340
+ withUsage?: WithUsageOptions;
341
+ });
342
+ type WrappedResponses = ResponsesResource extends undefined ? undefined : Omit<NonNullable<ResponsesResource>, "create"> & {
343
+ create: (params: ResponsesCreateParams, options?: WrapOpenAIResponseCallOptions) => ResponsesCreateReturn;
344
+ };
345
+ type WrappedOpenAI = OpenAI & {
346
+ chat: ReplaceProperty<OpenAI["chat"], "completions", WrappedChatCompletions>;
347
+ } & (ResponsesResource extends undefined ? {
348
+ responses?: undefined;
349
+ } : {
350
+ responses: WrappedResponses;
351
+ }) & {
352
+ toNextResponse: typeof toNextResponse;
353
+ pipeToResponse: typeof pipeToResponse;
354
+ unwrap: () => OpenAI;
355
+ };
356
+ interface StreamOpenAIRouteOptions {
357
+ getRequest: (req: Request) => Promise<{
358
+ params: ChatCompletionCreateParams;
359
+ usageTap?: Partial<WrapOpenAIContext>;
360
+ withUsage?: WithUsageOptions;
361
+ }>;
362
+ wrapOptions?: WrapOpenAIOptions;
363
+ defaultContext?: Partial<WrapOpenAIContext>;
364
+ stream?: {
365
+ mode?: StreamMode;
366
+ headers?: Record<string, string>;
367
+ responseInit?: ResponseInit;
368
+ };
369
+ }
370
+ type StreamMode = "text" | "sse";
371
+ interface StreamToResponseOptions {
372
+ mode?: StreamMode;
373
+ headers?: Record<string, string>;
374
+ contentType?: string;
375
+ sse?: {
376
+ event?: string;
377
+ retry?: number;
378
+ };
379
+ }
380
+ declare function createOpenAIAdapter(init: OpenAIAdapterInit): OpenAIAdapter;
381
+ type UsageTapStream<T> = AsyncIterable<T> & {
382
+ __usageTapFinalize?: () => Promise<void>;
383
+ };
384
+ declare function toNextResponse<T>(stream: UsageTapStream<T>, options?: StreamToResponseOptions): Response;
385
+ declare function pipeToResponse<T>(stream: UsageTapStream<T>, res: NodeResponseLike, options?: StreamToResponseOptions): Promise<void>;
386
+ declare function wrapOpenAI(client: OpenAI, usageTap: UsageTapClient, options?: WrapOpenAIOptions): WrappedOpenAI;
387
+ declare function streamOpenAIRoute(usageTap: UsageTapClient, openai: OpenAI, options: StreamOpenAIRouteOptions): (req: Request) => Promise<Response>;
388
+ interface NodeResponseLike {
389
+ write(chunk: string | Uint8Array | Buffer): unknown;
390
+ end(chunk?: string | Uint8Array | Buffer): unknown;
391
+ setHeader?(name: string, value: string): void;
392
+ headersSent?: boolean;
393
+ statusCode?: number;
394
+ status?(code: number): void;
395
+ flush?(): void;
396
+ }
397
+
398
+ export { type AllowedEntitlements as A, type BeginCallRequest as B, type UsageTapClientOptions as C, type UsageTapSuccessResponse as D, type EndCallOptions as E, type UsageTapErrorResponse as F, type UsageTapResultEnvelope as G, type UsageTapResultStatus as H, type UsageTapLogEntry as I, type WithUsageContext as J, type WithUsageOptions as K, type LimitType as L, type MeterSummary as M, type NodeResponseLike as N, type OpenAIAdapter as O, type PlanSummary as P, type SubscriptionSnapshot as Q, type RemainingRatios as R, type StreamMode as S, type ModelHints as T, UsageTapClient as U, type VendorHints as V, type WrapOpenAIContext as W, type IdempotencyMetadata as X, type OpenAIRequestContext as Y, type OpenAIStreamCallResult as Z, type UsageTapStream as _, type WrapOpenAIOptions as a, type OpenAIAdapterInit as b, createOpenAIAdapter as c, type OpenAIInvokeParams as d, type OpenAIInvokeResult as e, type OpenAIStreamParams as f, type OpenAIStreamResult as g, type StreamToResponseOptions as h, type WrapOpenAICallOptions as i, type WrapOpenAIResponseCallOptions as j, type WrappedOpenAI as k, type StreamOpenAIRouteOptions as l, type BeginCallOptions as m, type BeginCallResponseBody as n, type EndCallRequest as o, pipeToResponse as p, type EndCallResponseBody as q, type BalanceSummary as r, streamOpenAIRoute as s, toNextResponse as t, type EntitlementHints as u, type MeterSnapshot as v, wrapOpenAI as w, type MeteredUsage as x, type RequestedEntitlements as y, type RetryOptions as z };
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/react/useChatWithUsage.ts
6
+ function isJsonRecord(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ function extractAssistantContent(payload) {
10
+ if (!isJsonRecord(payload)) {
11
+ return "";
12
+ }
13
+ const choices = payload.choices;
14
+ if (Array.isArray(choices)) {
15
+ for (const choice of choices) {
16
+ if (!isJsonRecord(choice)) continue;
17
+ const message = choice.message;
18
+ if (!isJsonRecord(message)) continue;
19
+ const content = message.content;
20
+ if (typeof content === "string") {
21
+ return content;
22
+ }
23
+ }
24
+ }
25
+ const directContent = payload.content;
26
+ return typeof directContent === "string" ? directContent : "";
27
+ }
28
+ function extractStreamingContent(payload) {
29
+ if (!isJsonRecord(payload)) {
30
+ return "";
31
+ }
32
+ const choices = payload.choices;
33
+ if (Array.isArray(choices)) {
34
+ for (const choice of choices) {
35
+ if (!isJsonRecord(choice)) continue;
36
+ const delta = choice.delta;
37
+ if (!isJsonRecord(delta)) continue;
38
+ const content = delta.content;
39
+ if (typeof content === "string") {
40
+ return content;
41
+ }
42
+ if (Array.isArray(content)) {
43
+ return content.map((entry) => {
44
+ if (!entry) return "";
45
+ if (typeof entry === "string") return entry;
46
+ if (isJsonRecord(entry) && typeof entry.text === "string") return entry.text;
47
+ return "";
48
+ }).join("");
49
+ }
50
+ }
51
+ }
52
+ const directContent = payload.content;
53
+ return typeof directContent === "string" ? directContent : "";
54
+ }
55
+ function tryParseJson(text) {
56
+ try {
57
+ return JSON.parse(text);
58
+ } catch {
59
+ return void 0;
60
+ }
61
+ }
62
+ function isAbortError(error) {
63
+ if (typeof DOMException !== "undefined" && error instanceof DOMException) {
64
+ return error.name === "AbortError";
65
+ }
66
+ if (isJsonRecord(error) && typeof error.name === "string") {
67
+ return error.name === "AbortError";
68
+ }
69
+ return false;
70
+ }
71
+ function useChatWithUsage(options) {
72
+ const {
73
+ api,
74
+ customerId,
75
+ feature,
76
+ tags,
77
+ initialMessages = [],
78
+ onError,
79
+ onFinish,
80
+ headers: customHeaders = {}
81
+ } = options;
82
+ const [messages, setMessages] = react.useState(initialMessages);
83
+ const [input, setInput] = react.useState("");
84
+ const [isLoading, setIsLoading] = react.useState(false);
85
+ const [error, setError] = react.useState(null);
86
+ const abortControllerRef = react.useRef(null);
87
+ const generateId = () => `msg_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
88
+ const append = react.useCallback(
89
+ async (message) => {
90
+ setIsLoading(true);
91
+ setError(null);
92
+ const messageWithId = { ...message, id: message.id || generateId() };
93
+ const newMessages = [...messages, messageWithId];
94
+ setMessages(newMessages);
95
+ abortControllerRef.current = new AbortController();
96
+ try {
97
+ const response = await fetch(api, {
98
+ method: "POST",
99
+ headers: {
100
+ "Content-Type": "application/json",
101
+ ...customHeaders
102
+ },
103
+ body: JSON.stringify({
104
+ messages: newMessages,
105
+ customerId,
106
+ feature,
107
+ tags
108
+ }),
109
+ signal: abortControllerRef.current.signal
110
+ });
111
+ if (!response.ok) {
112
+ throw new Error(`HTTP error! status: ${response.status}`);
113
+ }
114
+ const contentType = response.headers.get("content-type");
115
+ const isStream = contentType?.includes("text/event-stream") || contentType?.includes("text/plain");
116
+ if (isStream && response.body) {
117
+ const reader = response.body.getReader();
118
+ const decoder = new TextDecoder();
119
+ let assistantMessage = "";
120
+ const assistantId = generateId();
121
+ while (true) {
122
+ const readResult = await reader.read();
123
+ if (readResult.done) break;
124
+ const chunkValue = readResult.value instanceof Uint8Array ? readResult.value : new Uint8Array();
125
+ const chunk = decoder.decode(chunkValue, { stream: true });
126
+ const lines = chunk.split("\n");
127
+ for (const line of lines) {
128
+ if (line.startsWith("data: ")) {
129
+ const data = line.slice(6);
130
+ if (data === "[DONE]") continue;
131
+ const parsed = tryParseJson(data);
132
+ if (parsed) {
133
+ const content = extractStreamingContent(parsed);
134
+ if (content) {
135
+ assistantMessage += content;
136
+ setMessages([
137
+ ...newMessages,
138
+ { role: "assistant", content: assistantMessage, id: assistantId }
139
+ ]);
140
+ }
141
+ } else {
142
+ assistantMessage += data;
143
+ setMessages([
144
+ ...newMessages,
145
+ { role: "assistant", content: assistantMessage, id: assistantId }
146
+ ]);
147
+ }
148
+ }
149
+ }
150
+ }
151
+ const finalMessage = {
152
+ role: "assistant",
153
+ content: assistantMessage,
154
+ id: assistantId
155
+ };
156
+ setMessages([...newMessages, finalMessage]);
157
+ onFinish?.(finalMessage);
158
+ } else {
159
+ const payload = await response.json();
160
+ const assistantMessage = {
161
+ role: "assistant",
162
+ content: extractAssistantContent(payload),
163
+ id: generateId()
164
+ };
165
+ setMessages([...newMessages, assistantMessage]);
166
+ onFinish?.(assistantMessage);
167
+ }
168
+ } catch (err) {
169
+ if (isAbortError(err)) {
170
+ return;
171
+ }
172
+ const error2 = err instanceof Error ? err : new Error(String(err));
173
+ setError(error2);
174
+ onError?.(error2);
175
+ } finally {
176
+ setIsLoading(false);
177
+ abortControllerRef.current = null;
178
+ }
179
+ },
180
+ [messages, api, customerId, feature, tags, customHeaders, onError, onFinish]
181
+ );
182
+ const handleSubmit = react.useCallback(
183
+ async (e) => {
184
+ e?.preventDefault();
185
+ if (!input.trim() || isLoading) return;
186
+ const userMessage = {
187
+ role: "user",
188
+ content: input
189
+ };
190
+ setInput("");
191
+ await append(userMessage);
192
+ },
193
+ [input, isLoading, append]
194
+ );
195
+ const reload = react.useCallback(async () => {
196
+ if (messages.length === 0 || isLoading) return;
197
+ const messagesWithoutLast = messages.slice(0, -1);
198
+ const lastUserMessage = [...messagesWithoutLast].reverse().find((m) => m.role === "user");
199
+ if (lastUserMessage) {
200
+ setMessages(messagesWithoutLast);
201
+ await append(lastUserMessage);
202
+ }
203
+ }, [messages, isLoading, append]);
204
+ const stop = react.useCallback(() => {
205
+ if (abortControllerRef.current) {
206
+ abortControllerRef.current.abort();
207
+ abortControllerRef.current = null;
208
+ setIsLoading(false);
209
+ }
210
+ }, []);
211
+ const clear = react.useCallback(() => {
212
+ setMessages([]);
213
+ setInput("");
214
+ setError(null);
215
+ }, []);
216
+ react.useEffect(() => {
217
+ return () => {
218
+ if (abortControllerRef.current) {
219
+ abortControllerRef.current.abort();
220
+ }
221
+ };
222
+ }, []);
223
+ return {
224
+ messages,
225
+ input,
226
+ setInput,
227
+ isLoading,
228
+ error,
229
+ append,
230
+ handleSubmit,
231
+ reload,
232
+ stop,
233
+ clear
234
+ };
235
+ }
236
+
237
+ exports.useChatWithUsage = useChatWithUsage;
238
+ //# sourceMappingURL=index.cjs.map
239
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/useChatWithUsage.ts"],"names":["useState","useRef","useCallback","error","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import { useState, useCallback, useRef, useEffect } from \"react\";\r\nimport type { FormEvent } from \"react\";\r\n\r\ntype JsonRecord = Record<string, unknown>;\r\n\r\nfunction isJsonRecord(value: unknown): value is JsonRecord {\r\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\r\n}\r\n\r\nfunction extractAssistantContent(payload: unknown): string {\r\n if (!isJsonRecord(payload)) {\r\n return \"\";\r\n }\r\n\r\n const choices = payload.choices;\r\n if (Array.isArray(choices)) {\r\n for (const choice of choices) {\r\n if (!isJsonRecord(choice)) continue;\r\n const message = choice.message;\r\n if (!isJsonRecord(message)) continue;\r\n const content = message.content;\r\n if (typeof content === \"string\") {\r\n return content;\r\n }\r\n }\r\n }\r\n\r\n const directContent = payload.content;\r\n return typeof directContent === \"string\" ? directContent : \"\";\r\n}\r\n\r\nfunction extractStreamingContent(payload: unknown): string {\r\n if (!isJsonRecord(payload)) {\r\n return \"\";\r\n }\r\n\r\n const choices = payload.choices;\r\n if (Array.isArray(choices)) {\r\n for (const choice of choices) {\r\n if (!isJsonRecord(choice)) continue;\r\n const delta = choice.delta;\r\n if (!isJsonRecord(delta)) continue;\r\n const content = delta.content;\r\n if (typeof content === \"string\") {\r\n return content;\r\n }\r\n\r\n if (Array.isArray(content)) {\r\n return content\r\n .map((entry) => {\r\n if (!entry) return \"\";\r\n if (typeof entry === \"string\") return entry;\r\n if (isJsonRecord(entry) && typeof entry.text === \"string\") return entry.text;\r\n return \"\";\r\n })\r\n .join(\"\");\r\n }\r\n }\r\n }\r\n\r\n const directContent = payload.content;\r\n return typeof directContent === \"string\" ? directContent : \"\";\r\n}\r\n\r\nfunction tryParseJson(text: string): unknown {\r\n try {\r\n return JSON.parse(text) as unknown;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\nfunction isAbortError(error: unknown): boolean {\r\n if (typeof DOMException !== \"undefined\" && error instanceof DOMException) {\r\n return error.name === \"AbortError\";\r\n }\r\n\r\n if (isJsonRecord(error) && typeof error.name === \"string\") {\r\n return error.name === \"AbortError\";\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport interface Message {\r\n role: \"user\" | \"assistant\" | \"system\";\r\n content: string;\r\n id?: string;\r\n}\r\n\r\nexport interface UseChatWithUsageOptions {\r\n /**\r\n * API endpoint that handles the chat completion.\r\n * Should be a server-side route that uses UsageTap.\r\n */\r\n api: string;\r\n \r\n /**\r\n * Customer ID to track usage for.\r\n * This will be sent to the API endpoint.\r\n */\r\n customerId: string;\r\n \r\n /**\r\n * Optional feature name for usage tracking.\r\n */\r\n feature?: string;\r\n \r\n /**\r\n * Optional tags for usage tracking.\r\n */\r\n tags?: string[];\r\n \r\n /**\r\n * Initial messages to populate the chat.\r\n */\r\n initialMessages?: Message[];\r\n \r\n /**\r\n * Called when an error occurs.\r\n */\r\n onError?: (error: Error) => void;\r\n \r\n /**\r\n * Called when a response is finished.\r\n */\r\n onFinish?: (message: Message) => void;\r\n \r\n /**\r\n * Additional headers to send with requests.\r\n */\r\n headers?: Record<string, string>;\r\n}\r\n\r\nexport interface UseChatWithUsageReturn {\r\n /**\r\n * Current messages in the chat.\r\n */\r\n messages: Message[];\r\n \r\n /**\r\n * Current input value.\r\n */\r\n input: string;\r\n \r\n /**\r\n * Set the input value.\r\n */\r\n setInput: (input: string) => void;\r\n \r\n /**\r\n * Whether a request is in progress.\r\n */\r\n isLoading: boolean;\r\n \r\n /**\r\n * Current error, if any.\r\n */\r\n error: Error | null;\r\n \r\n /**\r\n * Append a user message and get a response.\r\n */\r\n append: (message: Message) => Promise<void>;\r\n \r\n /**\r\n * Submit the current input as a user message.\r\n */\r\n handleSubmit: (e?: React.FormEvent<HTMLFormElement>) => Promise<void>;\r\n \r\n /**\r\n * Reload the last assistant response.\r\n */\r\n reload: () => Promise<void>;\r\n \r\n /**\r\n * Stop the current streaming response.\r\n */\r\n stop: () => void;\r\n \r\n /**\r\n * Clear all messages.\r\n */\r\n clear: () => void;\r\n}\r\n\r\n/**\r\n * React hook for chat interfaces with UsageTap integration.\r\n * \r\n * The API endpoint should be a server-side route that:\r\n * 1. Receives messages, customerId, feature, and tags\r\n * 2. Uses UsageTap SDK to wrap OpenAI calls\r\n * 3. Returns streaming or non-streaming responses\r\n * \r\n * @example\r\n * ```tsx\r\n * import { useChatWithUsage } from \"@usagetap/sdk/react\";\r\n *\r\n * function ChatComponent({ userId }) {\r\n * const { messages, input, setInput, handleSubmit, isLoading } = useChatWithUsage({\r\n * api: \"/api/chat\",\r\n * customerId: userId,\r\n * feature: \"chat.assistant\",\r\n * });\r\n * \r\n * return (\r\n * <div>\r\n * {messages.map((m) => (\r\n * <div key={m.id}>\r\n * <strong>{m.role}:</strong> {m.content}\r\n * </div>\r\n * ))}\r\n * <form onSubmit={handleSubmit}>\r\n * <input\r\n * value={input}\r\n * onChange={(e) => setInput(e.target.value)}\r\n * disabled={isLoading}\r\n * />\r\n * <button type=\"submit\" disabled={isLoading}>\r\n * Send\r\n * </button>\r\n * </form>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n * \r\n * Server route example:\r\n * ```ts\r\n * // app/api/chat/route.ts\r\n * import { streamOpenAIRoute } from \"@usagetap/sdk\";\r\n * \r\n * export const POST = streamOpenAIRoute(usageTap, openai, {\r\n * getRequest: async (req) => {\r\n * const body = await req.json();\r\n * return {\r\n * params: { model: \"gpt-4o-mini\", messages: body.messages },\r\n * context: {\r\n * customerId: body.customerId,\r\n * feature: body.feature,\r\n * tags: body.tags,\r\n * },\r\n * };\r\n * },\r\n * });\r\n * ```\r\n */\r\nexport function useChatWithUsage(options: UseChatWithUsageOptions): UseChatWithUsageReturn {\r\n const {\r\n api,\r\n customerId,\r\n feature,\r\n tags,\r\n initialMessages = [],\r\n onError,\r\n onFinish,\r\n headers: customHeaders = {},\r\n } = options;\r\n\r\n const [messages, setMessages] = useState<Message[]>(initialMessages);\r\n const [input, setInput] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n const abortControllerRef = useRef<AbortController | null>(null);\r\n\r\n const generateId = (): string => `msg_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;\r\n\r\n const append = useCallback(\r\n async (message: Message): Promise<void> => {\r\n setIsLoading(true);\r\n setError(null);\r\n\r\n const messageWithId = { ...message, id: message.id || generateId() };\r\n const newMessages = [...messages, messageWithId];\r\n setMessages(newMessages);\r\n\r\n abortControllerRef.current = new AbortController();\r\n\r\n try {\r\n const response = await fetch(api, {\r\n method: \"POST\",\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n ...customHeaders,\r\n },\r\n body: JSON.stringify({\r\n messages: newMessages,\r\n customerId,\r\n feature,\r\n tags,\r\n }),\r\n signal: abortControllerRef.current.signal,\r\n });\r\n\r\n if (!response.ok) {\r\n throw new Error(`HTTP error! status: ${response.status}`);\r\n }\r\n\r\n const contentType = response.headers.get(\"content-type\");\r\n const isStream = contentType?.includes(\"text/event-stream\") || contentType?.includes(\"text/plain\");\r\n\r\n if (isStream && response.body) {\r\n // Handle streaming response\r\n const reader = response.body.getReader();\r\n const decoder = new TextDecoder();\r\n let assistantMessage = \"\";\r\n const assistantId = generateId();\r\n\r\n while (true) {\r\n const readResult = await reader.read();\r\n if (readResult.done) break;\r\n\r\n const chunkValue: Uint8Array = readResult.value instanceof Uint8Array\r\n ? readResult.value\r\n : new Uint8Array();\r\n\r\n const chunk = decoder.decode(chunkValue, { stream: true });\r\n const lines = chunk.split(\"\\n\");\r\n\r\n for (const line of lines) {\r\n if (line.startsWith(\"data: \")) {\r\n const data = line.slice(6);\r\n if (data === \"[DONE]\") continue;\r\n\r\n const parsed = tryParseJson(data);\r\n if (parsed) {\r\n const content = extractStreamingContent(parsed);\r\n if (content) {\r\n assistantMessage += content;\r\n setMessages([\r\n ...newMessages,\r\n { role: \"assistant\", content: assistantMessage, id: assistantId },\r\n ]);\r\n }\r\n } else {\r\n // Plain text streaming\r\n assistantMessage += data;\r\n setMessages([\r\n ...newMessages,\r\n { role: \"assistant\", content: assistantMessage, id: assistantId },\r\n ]);\r\n }\r\n }\r\n }\r\n }\r\n\r\n const finalMessage: Message = {\r\n role: \"assistant\",\r\n content: assistantMessage,\r\n id: assistantId,\r\n };\r\n \r\n setMessages([...newMessages, finalMessage]);\r\n onFinish?.(finalMessage);\r\n } else {\r\n // Handle non-streaming response\r\n const payload = (await response.json()) as unknown;\r\n const assistantMessage: Message = {\r\n role: \"assistant\",\r\n content: extractAssistantContent(payload),\r\n id: generateId(),\r\n };\r\n \r\n setMessages([...newMessages, assistantMessage]);\r\n onFinish?.(assistantMessage);\r\n }\r\n } catch (err) {\r\n if (isAbortError(err)) {\r\n // Request was aborted, don't treat as error\r\n return;\r\n }\r\n \r\n const error = err instanceof Error ? err : new Error(String(err));\r\n setError(error);\r\n onError?.(error);\r\n } finally {\r\n setIsLoading(false);\r\n abortControllerRef.current = null;\r\n }\r\n },\r\n [messages, api, customerId, feature, tags, customHeaders, onError, onFinish],\r\n );\r\n\r\n const handleSubmit = useCallback(\r\n async (e?: FormEvent<HTMLFormElement>): Promise<void> => {\r\n e?.preventDefault();\r\n if (!input.trim() || isLoading) return;\r\n\r\n const userMessage: Message = {\r\n role: \"user\",\r\n content: input,\r\n };\r\n\r\n setInput(\"\");\r\n await append(userMessage);\r\n },\r\n [input, isLoading, append],\r\n );\r\n\r\n const reload = useCallback(async (): Promise<void> => {\r\n if (messages.length === 0 || isLoading) return;\r\n\r\n // Remove last assistant message and resend last user message\r\n const messagesWithoutLast = messages.slice(0, -1);\r\n const lastUserMessage = [...messagesWithoutLast].reverse().find((m) => m.role === \"user\");\r\n\r\n if (lastUserMessage) {\r\n setMessages(messagesWithoutLast);\r\n await append(lastUserMessage);\r\n }\r\n }, [messages, isLoading, append]);\r\n\r\n const stop = useCallback((): void => {\r\n if (abortControllerRef.current) {\r\n abortControllerRef.current.abort();\r\n abortControllerRef.current = null;\r\n setIsLoading(false);\r\n }\r\n }, []);\r\n\r\n const clear = useCallback((): void => {\r\n setMessages([]);\r\n setInput(\"\");\r\n setError(null);\r\n }, []);\r\n\r\n // Cleanup on unmount\r\n useEffect(() => {\r\n return () => {\r\n if (abortControllerRef.current) {\r\n abortControllerRef.current.abort();\r\n }\r\n };\r\n }, []);\r\n\r\n return {\r\n messages,\r\n input,\r\n setInput,\r\n isLoading,\r\n error,\r\n append,\r\n handleSubmit,\r\n reload,\r\n stop,\r\n clear,\r\n };\r\n}\r\n"]}