@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 +7 -0
- package/README.md +238 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +13 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +86 -0
- package/dist/types/types.d.ts +13 -0
- package/dist/types/types.js +1 -0
- package/dist/utils/extractUsage.d.ts +9 -0
- package/dist/utils/extractUsage.js +18 -0
- package/dist/utils/extractUsage.test.d.ts +1 -0
- package/dist/utils/extractUsage.test.js +114 -0
- package/dist/utils/recordUsage.d.ts +16 -0
- package/dist/utils/recordUsage.js +58 -0
- package/dist/utils/utils.d.ts +1 -0
- package/dist/utils/utils.js +3 -0
- package/dist/wrappers/TrackedStream.d.ts +8 -0
- package/dist/wrappers/TrackedStream.js +23 -0
- package/dist/wrappers/TrackedStream.test.d.ts +1 -0
- package/dist/wrappers/TrackedStream.test.js +168 -0
- package/dist/wrappers/createWrappedClient.d.ts +9 -0
- package/dist/wrappers/createWrappedClient.js +76 -0
- package/package.json +65 -0
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
|
+

|
|
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
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|