@fluxgate/anthropic 0.0.1

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,7 @@
1
+ Copyright (c) 2026 [Your Name or Organization]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # @fluxgate/anthropic
2
+
3
+ ![Status: In Development](https://img.shields.io/badge/status-in%20development-orange)
4
+
5
+ Anthropic SDK wrapper for FluxGate token tracking. Automatically track token usage, costs, and latency for Anthropic Messages API calls.
6
+
7
+ ## 📦 Installation
8
+
9
+ ```bash
10
+ npm install @fluxgate/sdk @fluxgate/anthropic @anthropic-ai/sdk
11
+ ```
12
+
13
+ ## 🚀 Quick Start
14
+
15
+ ```typescript
16
+ import Anthropic from "@anthropic-ai/sdk";
17
+ import { FluxGate } from "@fluxgate/sdk";
18
+ import { createAnthropicCostTracker } from "@fluxgate/anthropic";
19
+
20
+ const client = new Anthropic({
21
+ apiKey: process.env.ANTHROPIC_API_KEY,
22
+ });
23
+
24
+ const fluxgate = new FluxGate({
25
+ apiKey: process.env.FLUXGATE_API_KEY,
26
+ });
27
+
28
+ const anthropic = createAnthropicCostTracker(client, fluxgate);
29
+
30
+ const message = await anthropic
31
+ .withContext({
32
+ feature: "chatbot",
33
+ user: "user-123",
34
+ })
35
+ .messages.create({
36
+ model: "claude-sonnet-4-6",
37
+ max_tokens: 1024,
38
+ messages: [{ role: "user", content: "Hello, Claude" }],
39
+ });
40
+
41
+ console.log(message.content);
42
+ console.log(message.fluxGateCostTrackingResponse);
43
+ // {
44
+ // status: "SUCCESS",
45
+ // cost: 0.003,
46
+ // trackingId: "evt_...",
47
+ // createdAt: "2026-05-12T..."
48
+ // }
49
+ ```
50
+
51
+ ## 📖 API Reference
52
+
53
+ ### `createAnthropicCostTracker(client, tracker)`
54
+
55
+ Creates a tracked Anthropic client with context support.
56
+
57
+ **Parameters:**
58
+
59
+ - `client`: Anthropic client instance
60
+ - `fluxgate`: FluxGate instance
61
+
62
+ **Returns:** Object with:
63
+
64
+ - `withContext(metadata)`: Returns tracked client with context
65
+ - `client`: Default tracked client (no context)
66
+
67
+ ### Tracked Methods
68
+
69
+ - ✅ `messages.create()` — non-streaming responses
70
+ - ✅ `messages.create({ stream: true })` — streaming responses
71
+
72
+ ## 💡 Usage Examples
73
+
74
+ ### Non-Streaming
75
+
76
+ ```typescript
77
+ import Anthropic from "@anthropic-ai/sdk";
78
+ import { FluxGate } from "@fluxgate/sdk";
79
+ import { createAnthropicCostTracker } from "@fluxgate/anthropic";
80
+
81
+ const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
82
+ const fluxgate = new FluxGate({ apiKey: process.env.FLUXGATE_API_KEY });
83
+ const anthropic = createAnthropicCostTracker(client, fluxgate);
84
+
85
+ const message = await anthropic
86
+ .withContext({ feature: "summarization", user: "user-123" })
87
+ .messages.create({
88
+ model: "claude-sonnet-4-6",
89
+ max_tokens: 1024,
90
+ messages: [
91
+ { role: "user", content: "Summarize the French Revolution in 3 sentences." },
92
+ ],
93
+ });
94
+
95
+ console.log(message.content[0].text);
96
+ console.log(message.fluxGateCostTrackingResponse);
97
+ ```
98
+
99
+ ### Streaming
100
+
101
+ ```typescript
102
+ const stream = await anthropic
103
+ .withContext({ feature: "streaming-chat" })
104
+ .messages.create({
105
+ model: "claude-sonnet-4-6",
106
+ max_tokens: 1024,
107
+ messages: [{ role: "user", content: "Tell me a story" }],
108
+ stream: true,
109
+ });
110
+
111
+ for await (const event of stream) {
112
+ if (
113
+ event.type === "content_block_delta" &&
114
+ event.delta.type === "text_delta"
115
+ ) {
116
+ process.stdout.write(event.delta.text);
117
+ }
118
+ }
119
+
120
+ // Access tracking data after stream completes
121
+ console.log(stream.fluxGateCostTrackingResponse);
122
+ ```
123
+
124
+ ### Rich User Context
125
+
126
+ ```typescript
127
+ const message = await anthropic
128
+ .withContext({
129
+ feature: "premium-chat",
130
+ user: {
131
+ id: "user-123",
132
+ name: "Jane Smith",
133
+ email: "jane@example.com",
134
+ monthlyRevenue: 49.99,
135
+ },
136
+ sessionId: "sess-abc123",
137
+ conversationId: "conv-xyz789",
138
+ })
139
+ .messages.create({
140
+ model: "claude-sonnet-4-6",
141
+ max_tokens: 1024,
142
+ messages: [{ role: "user", content: "Hello!" }],
143
+ });
144
+ ```
145
+
146
+ ### Error Handling
147
+
148
+ Errors are automatically tracked with `status: "ERROR"`:
149
+
150
+ ```typescript
151
+ try {
152
+ const message = await anthropic
153
+ .withContext({ feature: "chat" })
154
+ .messages.create({
155
+ model: "claude-sonnet-4-6",
156
+ max_tokens: 1024,
157
+ messages: [{ role: "user", content: "Hello!" }],
158
+ });
159
+ } catch (error) {
160
+ // Error was tracked automatically with status: "ERROR"
161
+ console.error(error);
162
+ }
163
+ ```
164
+
165
+ ### Without Context (Default)
166
+
167
+ ```typescript
168
+ // Use client property for default tracking without metadata
169
+ const message = await anthropic.client.messages.create({
170
+ model: "claude-sonnet-4-6",
171
+ max_tokens: 1024,
172
+ messages: [{ role: "user", content: "Hello!" }],
173
+ });
174
+
175
+ console.log(message.fluxGateCostTrackingResponse);
176
+ ```
177
+
178
+ ## 📊 Tracking Data Structure
179
+
180
+ Each response includes a `fluxGateCostTrackingResponse` property:
181
+
182
+ ```typescript
183
+ interface FluxGateCostTrackingResponse {
184
+ status: AiEventStatus;
185
+ cost: number | null;
186
+ trackingId: string | null;
187
+ createdAt: string | null;
188
+ errorMessage?: string;
189
+ }
190
+
191
+ type AiEventStatus =
192
+ | "SUCCESS"
193
+ | "ERROR"
194
+ | "BLOCKED"
195
+ | "MAX_TOKENS"
196
+ | "CONTENT_FILTER"
197
+ | "RECITATION"
198
+ | "MALFORMED_REQUEST";
199
+ ```
200
+
201
+ Tracked metrics include:
202
+
203
+ - ✅ Input tokens (prompt)
204
+ - ✅ Output tokens (completion)
205
+ - ✅ Model name
206
+ - ✅ Latency (milliseconds)
207
+ - ✅ Stream duration (for streaming)
208
+ - ✅ Stop reason (end_turn, max_tokens, content_filter, etc.)
209
+
210
+ ## 🎯 Type Safety
211
+
212
+ Full TypeScript support with enhanced types:
213
+
214
+ ```typescript
215
+ import type {
216
+ TrackedAnthropic,
217
+ WithTracking,
218
+ AiEventMetadata,
219
+ TrackedUser,
220
+ FluxGateCostTrackingResponse,
221
+ } from "@fluxgate/anthropic";
222
+
223
+ // TrackedAnthropic includes all Anthropic methods with tracking
224
+ const anthropic: TrackedAnthropic = trackedClient.client;
225
+
226
+ // WithTracking adds fluxGateCostTrackingResponse to any type
227
+ type Message = WithTracking<Anthropic.Message>;
228
+ ```
229
+
230
+ ## 🔗 Related Packages
231
+
232
+ - [@fluxgate/sdk](../sdk) - Core tracking library
233
+ - [@fluxgate/openai](../openai) - OpenAI SDK wrapper
234
+ - [@fluxgate/gemini](../gemini) - Gemini SDK wrapper
235
+
236
+ ## 📝 License
237
+
238
+ MIT
@@ -0,0 +1,11 @@
1
+ import { AiEventMetadata, FluxGate } from "@fluxgate/sdk";
2
+ import type Anthropic from "@anthropic-ai/sdk";
3
+ import { TrackedAnthropic } from "./types/types.js";
4
+ export type AnthropicTracker = {
5
+ withContext: (ctx: AiEventMetadata) => TrackedAnthropic;
6
+ get client(): TrackedAnthropic;
7
+ };
8
+ export declare function createAnthropicCostTracker(client: Anthropic, instance: FluxGate): AnthropicTracker;
9
+ export type { AiEventMetadata, TrackedUser, FluxGateCostTrackingResponse, WithTracking, AiEventStatus, } from "@fluxgate/sdk";
10
+ export * from "./types/types.js";
11
+ export { TrackedStream } from "./wrappers/TrackedStream.js";
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import { withAnthropicTracking } from "./wrappers/createWrappedClient.js";
2
+ export function createAnthropicCostTracker(client, instance) {
3
+ return {
4
+ withContext(ctx) {
5
+ return withAnthropicTracking(client, instance, ctx);
6
+ },
7
+ get client() {
8
+ return withAnthropicTracking(client, instance);
9
+ },
10
+ };
11
+ }
12
+ export * from "./types/types.js";
13
+ export { TrackedStream } from "./wrappers/TrackedStream.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { createAnthropicCostTracker } from "./index.js";
3
+ import { FluxGate } from "@fluxgate/sdk";
4
+ import Anthropic from "@anthropic-ai/sdk";
5
+ describe("createAnthropicCostTracker", () => {
6
+ let mockClient;
7
+ let mockFluxGate;
8
+ beforeEach(() => {
9
+ mockClient = new Anthropic({
10
+ apiKey: "test-key",
11
+ });
12
+ mockFluxGate = new FluxGate({
13
+ apiKey: "tracker-key",
14
+ endpoint: "https://test.example.com",
15
+ });
16
+ });
17
+ describe("initialization", () => {
18
+ it("should create a tracker with withContext method", () => {
19
+ const tracker = createAnthropicCostTracker(mockClient, mockFluxGate);
20
+ expect(tracker).toHaveProperty("withContext");
21
+ expect(tracker).toHaveProperty("client");
22
+ expect(typeof tracker.withContext).toBe("function");
23
+ });
24
+ it("should return wrapped client with context", () => {
25
+ const tracker = createAnthropicCostTracker(mockClient, mockFluxGate);
26
+ const contextClient = tracker.withContext({ feature: "test-feature" });
27
+ expect(contextClient).toBeDefined();
28
+ expect(contextClient.messages).toBeDefined();
29
+ expect(contextClient.messages.create).toBeDefined();
30
+ expect(typeof contextClient.messages.create).toBe("function");
31
+ });
32
+ it("should return wrapped client without context", () => {
33
+ const tracker = createAnthropicCostTracker(mockClient, mockFluxGate);
34
+ const defaultClient = tracker.client;
35
+ expect(defaultClient).toBeDefined();
36
+ expect(defaultClient.messages).toBeDefined();
37
+ expect(defaultClient.messages.create).toBeDefined();
38
+ expect(typeof defaultClient.messages.create).toBe("function");
39
+ });
40
+ });
41
+ describe("context handling", () => {
42
+ it("should accept user context with string user", () => {
43
+ const tracker = createAnthropicCostTracker(mockClient, mockFluxGate);
44
+ const contextClient = tracker.withContext({
45
+ feature: "chat",
46
+ user: "user-123",
47
+ sessionId: "session-456",
48
+ });
49
+ expect(contextClient).toBeDefined();
50
+ });
51
+ it("should accept user context with TrackedUser object", () => {
52
+ const tracker = createAnthropicCostTracker(mockClient, mockFluxGate);
53
+ const contextClient = tracker.withContext({
54
+ feature: "chat",
55
+ user: {
56
+ id: "user-123",
57
+ name: "Test User",
58
+ email: "test@example.com",
59
+ monthlyRevenue: 99.99,
60
+ },
61
+ conversationId: "conv-789",
62
+ });
63
+ expect(contextClient).toBeDefined();
64
+ });
65
+ it("should accept metadata with custom fields", () => {
66
+ const tracker = createAnthropicCostTracker(mockClient, mockFluxGate);
67
+ const contextClient = tracker.withContext({
68
+ feature: "chat",
69
+ step: "generation",
70
+ customField: "custom value",
71
+ anotherField: 123,
72
+ });
73
+ expect(contextClient).toBeDefined();
74
+ });
75
+ });
76
+ describe("multiple contexts", () => {
77
+ it("should allow creating multiple wrapped clients with different contexts", () => {
78
+ const tracker = createAnthropicCostTracker(mockClient, mockFluxGate);
79
+ const chatContext = tracker.withContext({ feature: "chat" });
80
+ const summaryContext = tracker.withContext({ feature: "summary" });
81
+ expect(chatContext).toBeDefined();
82
+ expect(summaryContext).toBeDefined();
83
+ expect(chatContext).not.toBe(summaryContext);
84
+ });
85
+ });
86
+ });
@@ -0,0 +1,13 @@
1
+ import type Anthropic from "@anthropic-ai/sdk";
2
+ import type { Message, MessageCreateParams, MessageCreateParamsNonStreaming, MessageCreateParamsStreaming, RawMessageStreamEvent } from "@anthropic-ai/sdk/resources/messages";
3
+ import type { WithTracking } from "@fluxgate/sdk";
4
+ import type { TrackedStream } from "../wrappers/TrackedStream.js";
5
+ type MessageCreateOptions = Parameters<Anthropic["messages"]["create"]>[1];
6
+ export type TrackedAnthropic = Omit<Anthropic, "messages"> & {
7
+ messages: Omit<Anthropic["messages"], "create"> & {
8
+ create(body: MessageCreateParamsNonStreaming, options?: MessageCreateOptions): Promise<WithTracking<Message>>;
9
+ create(body: MessageCreateParamsStreaming, options?: MessageCreateOptions): Promise<TrackedStream<RawMessageStreamEvent>>;
10
+ create(body: MessageCreateParams, options?: MessageCreateOptions): Promise<WithTracking<Message> | TrackedStream<RawMessageStreamEvent>>;
11
+ };
12
+ };
13
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { ExtractedUsage } from "@fluxgate/sdk";
2
+ type AnthropicUsage = {
3
+ input_tokens: number | null;
4
+ output_tokens: number | null;
5
+ cache_creation_input_tokens?: number | null;
6
+ cache_read_input_tokens?: number | null;
7
+ };
8
+ export declare function extractAnthropicUsage(usage: AnthropicUsage | null | undefined): ExtractedUsage;
9
+ export {};
@@ -0,0 +1,18 @@
1
+ export function extractAnthropicUsage(usage) {
2
+ if (!usage) {
3
+ return {
4
+ inputTokens: 0,
5
+ outputTokens: 0,
6
+ cachedTokens: 0,
7
+ totalTokens: 0,
8
+ };
9
+ }
10
+ const cachedTokens = (usage.cache_creation_input_tokens ?? 0) +
11
+ (usage.cache_read_input_tokens ?? 0);
12
+ return {
13
+ inputTokens: usage.input_tokens ?? 0,
14
+ outputTokens: usage.output_tokens ?? 0,
15
+ cachedTokens,
16
+ totalTokens: (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0),
17
+ };
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { extractAnthropicUsage } from "./extractUsage.js";
3
+ describe("extractAnthropicUsage", () => {
4
+ describe("valid usage data", () => {
5
+ it("should extract usage from complete usage object", () => {
6
+ const usage = {
7
+ input_tokens: 100,
8
+ output_tokens: 50,
9
+ cache_creation_input_tokens: 20,
10
+ cache_read_input_tokens: 10,
11
+ };
12
+ const result = extractAnthropicUsage(usage);
13
+ expect(result).toEqual({
14
+ inputTokens: 100,
15
+ outputTokens: 50,
16
+ cachedTokens: 30,
17
+ totalTokens: 150,
18
+ });
19
+ });
20
+ it("should handle missing cached token fields", () => {
21
+ const usage = {
22
+ input_tokens: 100,
23
+ output_tokens: 50,
24
+ };
25
+ const result = extractAnthropicUsage(usage);
26
+ expect(result).toEqual({
27
+ inputTokens: 100,
28
+ outputTokens: 50,
29
+ cachedTokens: 0,
30
+ totalTokens: 150,
31
+ });
32
+ });
33
+ it("should handle null cached token fields", () => {
34
+ const usage = {
35
+ input_tokens: 120,
36
+ output_tokens: 30,
37
+ cache_creation_input_tokens: null,
38
+ cache_read_input_tokens: null,
39
+ };
40
+ const result = extractAnthropicUsage(usage);
41
+ expect(result).toEqual({
42
+ inputTokens: 120,
43
+ outputTokens: 30,
44
+ cachedTokens: 0,
45
+ totalTokens: 150,
46
+ });
47
+ });
48
+ });
49
+ describe("missing or incomplete data", () => {
50
+ it("should return zero values for null usage", () => {
51
+ const result = extractAnthropicUsage(null);
52
+ expect(result).toEqual({
53
+ inputTokens: 0,
54
+ outputTokens: 0,
55
+ cachedTokens: 0,
56
+ totalTokens: 0,
57
+ });
58
+ });
59
+ it("should return zero values for undefined usage", () => {
60
+ const result = extractAnthropicUsage(undefined);
61
+ expect(result).toEqual({
62
+ inputTokens: 0,
63
+ outputTokens: 0,
64
+ cachedTokens: 0,
65
+ totalTokens: 0,
66
+ });
67
+ });
68
+ it("should handle partial usage object", () => {
69
+ const usage = {
70
+ input_tokens: 80,
71
+ output_tokens: undefined,
72
+ };
73
+ const result = extractAnthropicUsage(usage);
74
+ expect(result).toEqual({
75
+ inputTokens: 80,
76
+ outputTokens: 0,
77
+ cachedTokens: 0,
78
+ totalTokens: 80,
79
+ });
80
+ });
81
+ });
82
+ describe("edge cases", () => {
83
+ it("should handle zero tokens", () => {
84
+ const usage = {
85
+ input_tokens: 0,
86
+ output_tokens: 0,
87
+ cache_creation_input_tokens: 0,
88
+ cache_read_input_tokens: 0,
89
+ };
90
+ const result = extractAnthropicUsage(usage);
91
+ expect(result).toEqual({
92
+ inputTokens: 0,
93
+ outputTokens: 0,
94
+ cachedTokens: 0,
95
+ totalTokens: 0,
96
+ });
97
+ });
98
+ it("should handle very large token counts", () => {
99
+ const usage = {
100
+ input_tokens: 1000000,
101
+ output_tokens: 500000,
102
+ cache_creation_input_tokens: 200000,
103
+ cache_read_input_tokens: 100000,
104
+ };
105
+ const result = extractAnthropicUsage(usage);
106
+ expect(result).toEqual({
107
+ inputTokens: 1000000,
108
+ outputTokens: 500000,
109
+ cachedTokens: 300000,
110
+ totalTokens: 1500000,
111
+ });
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,16 @@
1
+ import { AiEventMetadata, AiEventStatus, ExtractedUsage, FluxGate, FluxGateCostTrackingResponse } from "@fluxgate/sdk";
2
+ export declare function stopReasonToStatus(stopReason: string | null | undefined): AiEventStatus;
3
+ export declare function recordUsage(params: {
4
+ instance: FluxGate;
5
+ model: string;
6
+ latencyMs: number;
7
+ streaming: boolean;
8
+ context: AiEventMetadata | undefined;
9
+ usage: ExtractedUsage;
10
+ status: AiEventStatus;
11
+ errorMessage?: string;
12
+ }): Promise<FluxGateCostTrackingResponse>;
13
+ export declare function extractResponseStatus(stopReason: string | null | undefined): {
14
+ status: AiEventStatus;
15
+ errorMessage?: string;
16
+ };
@@ -0,0 +1,58 @@
1
+ function normalizeMetadata(context) {
2
+ const { user, ...rest } = context ?? {};
3
+ const normalized = { ...rest };
4
+ if (typeof user === "string") {
5
+ normalized.user = user;
6
+ }
7
+ else if (user != null) {
8
+ normalized.user = user;
9
+ }
10
+ return normalized;
11
+ }
12
+ export function stopReasonToStatus(stopReason) {
13
+ if (!stopReason ||
14
+ stopReason === "end_turn" ||
15
+ stopReason === "stop_sequence") {
16
+ return "SUCCESS";
17
+ }
18
+ if (stopReason === "max_tokens") {
19
+ return "MAX_TOKENS";
20
+ }
21
+ if (stopReason === "content_filter") {
22
+ return "BLOCKED";
23
+ }
24
+ if (stopReason === "tool_use") {
25
+ return "SUCCESS";
26
+ }
27
+ return "ERROR";
28
+ }
29
+ export async function recordUsage(params) {
30
+ const { context, latencyMs, model, streaming, instance, usage, status, errorMessage, } = params;
31
+ const trackingData = await instance.recordEvent({
32
+ metadata: normalizeMetadata(context),
33
+ status: {
34
+ status,
35
+ errorMessage,
36
+ },
37
+ usage: {
38
+ inputTokens: usage.inputTokens,
39
+ outputTokens: usage.outputTokens,
40
+ cachedTokens: usage.cachedTokens,
41
+ model,
42
+ isStreamed: streaming,
43
+ latencyInMs: latencyMs,
44
+ provider: "anthropic",
45
+ streamingDurationInMs: streaming ? latencyMs : undefined,
46
+ },
47
+ });
48
+ return {
49
+ status,
50
+ errorMessage,
51
+ cost: trackingData?.cost ?? null,
52
+ trackingId: trackingData?.id ?? null,
53
+ createdAt: trackingData?.createdAt ?? null,
54
+ };
55
+ }
56
+ export function extractResponseStatus(stopReason) {
57
+ return { status: stopReasonToStatus(stopReason) };
58
+ }
@@ -0,0 +1 @@
1
+ export declare function isAsyncIterable<T>(obj: any): obj is AsyncIterable<T>;
@@ -0,0 +1,3 @@
1
+ export function isAsyncIterable(obj) {
2
+ return obj && typeof obj[Symbol.asyncIterator] === "function";
3
+ }
@@ -0,0 +1,8 @@
1
+ import { FluxGateCostTrackingResponse } from "@fluxgate/sdk";
2
+ export declare class TrackedStream<T> implements AsyncIterable<T> {
3
+ private readonly source;
4
+ private readonly finalize;
5
+ fluxGateCostTrackingResponse: FluxGateCostTrackingResponse | undefined;
6
+ constructor(source: AsyncIterable<T>, finalize: (lastItem: T | undefined, error: Error | undefined) => Promise<FluxGateCostTrackingResponse>);
7
+ [Symbol.asyncIterator](): AsyncGenerator<T>;
8
+ }
@@ -0,0 +1,23 @@
1
+ export class TrackedStream {
2
+ constructor(source, finalize) {
3
+ this.source = source;
4
+ this.finalize = finalize;
5
+ }
6
+ async *[Symbol.asyncIterator]() {
7
+ let last;
8
+ let streamError;
9
+ try {
10
+ for await (const item of this.source) {
11
+ last = item;
12
+ yield item;
13
+ }
14
+ }
15
+ catch (err) {
16
+ streamError = err;
17
+ throw err;
18
+ }
19
+ finally {
20
+ this.fluxGateCostTrackingResponse = await this.finalize(last, streamError);
21
+ }
22
+ }
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,168 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { TrackedStream } from "./TrackedStream.js";
3
+ describe("TrackedStream (Anthropic)", () => {
4
+ async function* createMockStream(items) {
5
+ for (const item of items) {
6
+ yield item;
7
+ }
8
+ }
9
+ async function* createErrorStream(items, errorAt) {
10
+ for (let i = 0; i < items.length; i++) {
11
+ if (i === errorAt) {
12
+ throw new Error("Stream error");
13
+ }
14
+ yield items[i];
15
+ }
16
+ }
17
+ describe("successful streams", () => {
18
+ it("should iterate through all items", async () => {
19
+ const items = [1, 2, 3, 4, 5];
20
+ const source = createMockStream(items);
21
+ const finalize = vi.fn().mockResolvedValue({
22
+ status: "SUCCESS",
23
+ cost: 0.001,
24
+ trackingId: "track-123",
25
+ createdAt: "2026-05-05T00:00:00Z",
26
+ });
27
+ const trackedStream = new TrackedStream(source, finalize);
28
+ const collected = [];
29
+ for await (const item of trackedStream) {
30
+ collected.push(item);
31
+ }
32
+ expect(collected).toEqual(items);
33
+ });
34
+ it("should call finalize with last item after stream completes", async () => {
35
+ const items = ["a", "b", "c"];
36
+ const source = createMockStream(items);
37
+ const finalize = vi.fn().mockResolvedValue({
38
+ status: "SUCCESS",
39
+ cost: 0.001,
40
+ trackingId: "track-123",
41
+ createdAt: "2026-05-05T00:00:00Z",
42
+ });
43
+ const trackedStream = new TrackedStream(source, finalize);
44
+ for await (const _ of trackedStream) {
45
+ // consume stream
46
+ }
47
+ expect(finalize).toHaveBeenCalledWith("c", undefined);
48
+ });
49
+ it("should set fluxGateCostTrackingResponse after stream completes", async () => {
50
+ const items = [1, 2, 3];
51
+ const source = createMockStream(items);
52
+ const mockResponse = {
53
+ status: "SUCCESS",
54
+ cost: 0.002,
55
+ trackingId: "track-456",
56
+ createdAt: "2026-05-05T00:00:00Z",
57
+ };
58
+ const finalize = vi.fn().mockResolvedValue(mockResponse);
59
+ const trackedStream = new TrackedStream(source, finalize);
60
+ expect(trackedStream.fluxGateCostTrackingResponse).toBeUndefined();
61
+ for await (const _ of trackedStream) {
62
+ // consume stream
63
+ }
64
+ expect(trackedStream.fluxGateCostTrackingResponse).toBeDefined();
65
+ expect(trackedStream.fluxGateCostTrackingResponse).toEqual(mockResponse);
66
+ });
67
+ it("should handle empty stream", async () => {
68
+ const source = createMockStream([]);
69
+ const finalize = vi.fn().mockResolvedValue({
70
+ status: "SUCCESS",
71
+ cost: 0,
72
+ trackingId: "track-789",
73
+ createdAt: "2026-05-05T00:00:00Z",
74
+ });
75
+ const trackedStream = new TrackedStream(source, finalize);
76
+ const collected = [];
77
+ for await (const item of trackedStream) {
78
+ collected.push(item);
79
+ }
80
+ expect(collected).toEqual([]);
81
+ expect(finalize).toHaveBeenCalledWith(undefined, undefined);
82
+ });
83
+ });
84
+ describe("error handling", () => {
85
+ it("should call finalize with error if stream throws", async () => {
86
+ const items = [1, 2, 3, 4, 5];
87
+ const source = createErrorStream(items, 2);
88
+ const finalize = vi.fn().mockResolvedValue({
89
+ status: "ERROR",
90
+ cost: null,
91
+ trackingId: "track-error",
92
+ createdAt: "2026-05-05T00:00:00Z",
93
+ errorMessage: "Stream error",
94
+ });
95
+ const trackedStream = new TrackedStream(source, finalize);
96
+ const collected = [];
97
+ try {
98
+ for await (const item of trackedStream) {
99
+ collected.push(item);
100
+ }
101
+ }
102
+ catch (error) {
103
+ expect(error.message).toBe("Stream error");
104
+ }
105
+ expect(collected).toEqual([1, 2]);
106
+ expect(finalize).toHaveBeenCalledWith(2, expect.objectContaining({ message: "Stream error" }));
107
+ });
108
+ it("should set fluxGateCostTrackingResponse even when stream errors", async () => {
109
+ const items = [1, 2, 3];
110
+ const source = createErrorStream(items, 1);
111
+ const mockResponse = {
112
+ status: "ERROR",
113
+ cost: null,
114
+ trackingId: "track-error-2",
115
+ createdAt: "2026-05-05T00:00:00Z",
116
+ errorMessage: "Stream error",
117
+ };
118
+ const finalize = vi.fn().mockResolvedValue(mockResponse);
119
+ const trackedStream = new TrackedStream(source, finalize);
120
+ try {
121
+ for await (const _ of trackedStream) {
122
+ // consume stream
123
+ }
124
+ }
125
+ catch (error) {
126
+ // expected error
127
+ }
128
+ expect(trackedStream.fluxGateCostTrackingResponse).toEqual(mockResponse);
129
+ });
130
+ it("should propagate the error after calling finalize", async () => {
131
+ const items = [1, 2, 3];
132
+ const source = createErrorStream(items, 1);
133
+ const finalize = vi.fn().mockResolvedValue({
134
+ status: "ERROR",
135
+ cost: null,
136
+ trackingId: null,
137
+ createdAt: null,
138
+ errorMessage: "Stream error",
139
+ });
140
+ const trackedStream = new TrackedStream(source, finalize);
141
+ await expect(async () => {
142
+ for await (const _ of trackedStream) {
143
+ // consume stream
144
+ }
145
+ }).rejects.toThrow("Stream error");
146
+ expect(finalize).toHaveBeenCalled();
147
+ });
148
+ });
149
+ describe("multiple iterations", () => {
150
+ it("should track last item across full iteration", async () => {
151
+ const items = ["first", "second", "third", "last"];
152
+ const source = createMockStream(items);
153
+ const finalize = vi.fn().mockResolvedValue({
154
+ status: "SUCCESS",
155
+ cost: 0.003,
156
+ trackingId: "track-multi",
157
+ createdAt: "2026-05-05T00:00:00Z",
158
+ });
159
+ const trackedStream = new TrackedStream(source, finalize);
160
+ let lastSeen;
161
+ for await (const item of trackedStream) {
162
+ lastSeen = item;
163
+ }
164
+ expect(lastSeen).toBe("last");
165
+ expect(finalize).toHaveBeenCalledWith("last", undefined);
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,9 @@
1
+ import { AiEventMetadata, FluxGate, WithTracking } from "@fluxgate/sdk";
2
+ import type Anthropic from "@anthropic-ai/sdk";
3
+ import type { Message, RawMessageStreamEvent } from "@anthropic-ai/sdk/resources/messages";
4
+ import { TrackedStream } from "./TrackedStream.js";
5
+ import { TrackedAnthropic } from "../types/types.js";
6
+ type OrigCreate = Anthropic["messages"]["create"];
7
+ export declare function withAnthropicTracking(client: Anthropic, instance: FluxGate, context?: AiEventMetadata): TrackedAnthropic;
8
+ export declare function createMessagesWrapper(original: OrigCreate, instance: FluxGate, context: AiEventMetadata | undefined): (params: Parameters<OrigCreate>[0], options?: Parameters<OrigCreate>[1]) => Promise<WithTracking<Message> | TrackedStream<RawMessageStreamEvent>>;
9
+ export {};
@@ -0,0 +1,76 @@
1
+ import { extractAnthropicUsage } from "../utils/extractUsage.js";
2
+ import { isAsyncIterable } from "../utils/utils.js";
3
+ import { TrackedStream } from "./TrackedStream.js";
4
+ import { extractResponseStatus, recordUsage } from "../utils/recordUsage.js";
5
+ export function withAnthropicTracking(client, instance, context) {
6
+ const wrappedClient = Object.create(Object.getPrototypeOf(client), Object.getOwnPropertyDescriptors(client));
7
+ wrappedClient.messages = Object.create(Object.getPrototypeOf(client.messages), Object.getOwnPropertyDescriptors(client.messages));
8
+ wrappedClient.messages.create = createMessagesWrapper(client.messages.create.bind(client.messages), instance, context);
9
+ return wrappedClient;
10
+ }
11
+ export function createMessagesWrapper(original, instance, context) {
12
+ return async function wrappedMessagesCreate(params, options) {
13
+ const start = performance.now();
14
+ let res;
15
+ try {
16
+ res = await original(params, options);
17
+ }
18
+ catch (err) {
19
+ await recordUsage({
20
+ instance,
21
+ model: params.model.toString(),
22
+ latencyMs: performance.now() - start,
23
+ streaming: !!params.stream,
24
+ context,
25
+ usage: extractAnthropicUsage(undefined),
26
+ status: "ERROR",
27
+ errorMessage: err.message,
28
+ });
29
+ throw err;
30
+ }
31
+ if (params.stream && isAsyncIterable(res)) {
32
+ let latestUsage;
33
+ let latestStopReason;
34
+ const trackingSource = (async function* () {
35
+ for await (const event of res) {
36
+ if (event.type === "message_delta") {
37
+ latestUsage = event.usage;
38
+ latestStopReason = event.delta.stop_reason;
39
+ }
40
+ yield event;
41
+ }
42
+ })();
43
+ return new TrackedStream(trackingSource, (_last, streamError) => {
44
+ const { status, errorMessage } = streamError
45
+ ? {
46
+ status: "ERROR",
47
+ errorMessage: streamError.message,
48
+ }
49
+ : extractResponseStatus(latestStopReason);
50
+ return recordUsage({
51
+ instance,
52
+ model: params.model.toString(),
53
+ latencyMs: performance.now() - start,
54
+ streaming: true,
55
+ context,
56
+ usage: extractAnthropicUsage(latestUsage),
57
+ status,
58
+ errorMessage,
59
+ });
60
+ });
61
+ }
62
+ const message = res;
63
+ const { status, errorMessage } = extractResponseStatus(message.stop_reason);
64
+ const fluxGateCostTrackingResponse = await recordUsage({
65
+ instance,
66
+ model: params.model.toString(),
67
+ latencyMs: performance.now() - start,
68
+ streaming: false,
69
+ context,
70
+ usage: extractAnthropicUsage(message.usage),
71
+ status,
72
+ errorMessage,
73
+ });
74
+ return Object.assign(message, { fluxGateCostTrackingResponse });
75
+ };
76
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@fluxgate/anthropic",
3
+ "version": "0.0.1",
4
+ "description": "Anthropic wrapper for FluxGate token usage, latency, and cost monitoring.",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./types": {
13
+ "types": "./dist/types/types.d.ts"
14
+ }
15
+ },
16
+ "type": "module",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "sideEffects": false,
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "dev": "tsc --watch",
24
+ "prepublishOnly": "npm run build",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run --root ../../",
27
+ "test:watch": "vitest --root ../../"
28
+ },
29
+ "keywords": [
30
+ "llm",
31
+ "tokens",
32
+ "tracker",
33
+ "anthropic",
34
+ "claude",
35
+ "monitoring",
36
+ "costs"
37
+ ],
38
+ "author": "FluxGate Team",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/yehova73/fluxgate-npm.git",
43
+ "directory": "packages/anthropic"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/yehova73/fluxgate-npm/issues"
47
+ },
48
+ "homepage": "https://fluxgate.app",
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "dependencies": {
53
+ "@fluxgate/sdk": "^0.0.2-dev.0"
54
+ },
55
+ "peerDependencies": {
56
+ "@anthropic-ai/sdk": "^0.50.0"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^20.11.0",
60
+ "typescript": "^5.3.3"
61
+ },
62
+ "engines": {
63
+ "node": ">=18.0.0"
64
+ }
65
+ }