ironside 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +58 -0
- package/README.md +91 -0
- package/dist/src/batcher.d.ts +51 -0
- package/dist/src/batcher.js +90 -0
- package/dist/src/batcher.js.map +1 -0
- package/dist/src/client.d.ts +70 -0
- package/dist/src/client.js +171 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/types.d.ts +64 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/wrappers/anthropic.d.ts +22 -0
- package/dist/src/wrappers/anthropic.js +188 -0
- package/dist/src/wrappers/anthropic.js.map +1 -0
- package/dist/src/wrappers/openai.d.ts +26 -0
- package/dist/src/wrappers/openai.js +162 -0
- package/dist/src/wrappers/openai.js.map +1 -0
- package/dist/src/wrappers/streaming.d.ts +17 -0
- package/dist/src/wrappers/streaming.js +86 -0
- package/dist/src/wrappers/streaming.js.map +1 -0
- package/dist/src/wrappers/vercel-ai.d.ts +29 -0
- package/dist/src/wrappers/vercel-ai.js +30 -0
- package/dist/src/wrappers/vercel-ai.js.map +1 -0
- package/package.json +55 -0
- package/src/batcher.ts +109 -0
- package/src/client.ts +267 -0
- package/src/index.ts +24 -0
- package/src/types.ts +75 -0
- package/src/wrappers/anthropic.ts +276 -0
- package/src/wrappers/openai.ts +283 -0
- package/src/wrappers/streaming.ts +92 -0
- package/src/wrappers/vercel-ai.ts +73 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IronsideClient, TraceHandle } from "../client.js";
|
|
2
|
+
export interface WrapAnthropicOptions {
|
|
3
|
+
/** Attach generations to an existing trace instead of creating a new standalone trace per call. */
|
|
4
|
+
trace?: TraceHandle;
|
|
5
|
+
}
|
|
6
|
+
interface MessagesLike {
|
|
7
|
+
create: (...args: never[]) => unknown;
|
|
8
|
+
}
|
|
9
|
+
interface AnthropicLike {
|
|
10
|
+
messages: MessagesLike;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Wraps an Anthropic client instance so every messages.create() call —
|
|
14
|
+
* streaming or not — is automatically recorded as a generation. Mutates
|
|
15
|
+
* client.messages in place and returns the same client reference.
|
|
16
|
+
*
|
|
17
|
+
* Safe to call more than once on the same client — re-wrapping is detected
|
|
18
|
+
* and is a no-op, rather than nesting a second wrapper around the first
|
|
19
|
+
* (which would silently double-record every call).
|
|
20
|
+
*/
|
|
21
|
+
export declare function wrapAnthropic<T extends AnthropicLike>(client: T, ironside: IronsideClient, options?: WrapAnthropicOptions): T;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { errorEndOptions, instrumentAsyncIterable } from "./streaming.js";
|
|
2
|
+
function extractModelParameters(body) {
|
|
3
|
+
if (!body)
|
|
4
|
+
return undefined;
|
|
5
|
+
const entries = ["temperature", "top_p", "top_k", "max_tokens"]
|
|
6
|
+
.filter((key) => body[key] !== undefined)
|
|
7
|
+
.map((key) => [key, body[key]]);
|
|
8
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
9
|
+
}
|
|
10
|
+
function usageDetailsFrom(usage) {
|
|
11
|
+
if (!usage)
|
|
12
|
+
return undefined;
|
|
13
|
+
const details = {
|
|
14
|
+
...(usage.input_tokens !== undefined && { input_tokens: usage.input_tokens }),
|
|
15
|
+
...(usage.output_tokens !== undefined && { output_tokens: usage.output_tokens })
|
|
16
|
+
};
|
|
17
|
+
return Object.keys(details).length > 0 ? details : undefined;
|
|
18
|
+
}
|
|
19
|
+
function createEventAccumulator() {
|
|
20
|
+
const blocks = [];
|
|
21
|
+
let responseModel;
|
|
22
|
+
let inputTokens;
|
|
23
|
+
let outputTokens;
|
|
24
|
+
let stopReason;
|
|
25
|
+
return {
|
|
26
|
+
onEvent(raw) {
|
|
27
|
+
const event = raw;
|
|
28
|
+
switch (event.type) {
|
|
29
|
+
case "message_start":
|
|
30
|
+
responseModel = event.message?.model;
|
|
31
|
+
inputTokens = event.message?.usage?.input_tokens;
|
|
32
|
+
outputTokens = event.message?.usage?.output_tokens;
|
|
33
|
+
break;
|
|
34
|
+
case "content_block_start": {
|
|
35
|
+
const block = event.content_block;
|
|
36
|
+
if (event.index === undefined || !block)
|
|
37
|
+
break;
|
|
38
|
+
if (block.type === "text") {
|
|
39
|
+
blocks[event.index] = { type: "text", text: block.text ?? "" };
|
|
40
|
+
}
|
|
41
|
+
else if (block.type === "tool_use") {
|
|
42
|
+
blocks[event.index] = {
|
|
43
|
+
type: "tool_use",
|
|
44
|
+
...(block.id && { id: block.id }),
|
|
45
|
+
...(block.name && { name: block.name }),
|
|
46
|
+
partialJson: ""
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
else if (block.type === "thinking") {
|
|
50
|
+
// Extended-thinking blocks stream as thinking_delta text plus a
|
|
51
|
+
// final signature_delta (the signature is required to round-trip
|
|
52
|
+
// the block in later turns — dropping it would make the recorded
|
|
53
|
+
// output unusable as a replay input). Review finding, PR #39.
|
|
54
|
+
blocks[event.index] = {
|
|
55
|
+
type: "thinking",
|
|
56
|
+
thinking: block.thinking ?? "",
|
|
57
|
+
signature: block.signature ?? ""
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
else if (block.type) {
|
|
61
|
+
// Other block kinds (redacted_thinking arrives complete at
|
|
62
|
+
// start; future API additions) are kept as-is rather than
|
|
63
|
+
// dropped — deltas for them aren't understood, but the block's
|
|
64
|
+
// existence is real data.
|
|
65
|
+
blocks[event.index] = { ...block, type: block.type };
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "content_block_delta": {
|
|
70
|
+
if (event.index === undefined)
|
|
71
|
+
break;
|
|
72
|
+
const block = blocks[event.index];
|
|
73
|
+
if (!block)
|
|
74
|
+
break;
|
|
75
|
+
if (event.delta?.type === "text_delta" && block.type === "text") {
|
|
76
|
+
block.text += event.delta.text ?? "";
|
|
77
|
+
}
|
|
78
|
+
else if (event.delta?.type === "input_json_delta" && block.type === "tool_use") {
|
|
79
|
+
block.partialJson += event.delta.partial_json ?? "";
|
|
80
|
+
}
|
|
81
|
+
else if (event.delta?.type === "thinking_delta" && block.type === "thinking") {
|
|
82
|
+
block.thinking += event.delta.thinking ?? "";
|
|
83
|
+
}
|
|
84
|
+
else if (event.delta?.type === "signature_delta" && block.type === "thinking") {
|
|
85
|
+
block.signature = event.delta.signature ?? "";
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "message_delta":
|
|
90
|
+
if (event.usage?.output_tokens !== undefined)
|
|
91
|
+
outputTokens = event.usage.output_tokens;
|
|
92
|
+
if (event.usage?.input_tokens !== undefined)
|
|
93
|
+
inputTokens = event.usage.input_tokens;
|
|
94
|
+
if (event.delta?.stop_reason)
|
|
95
|
+
stopReason = event.delta.stop_reason;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
endOptions() {
|
|
100
|
+
const content = blocks.filter(Boolean).map((block) => {
|
|
101
|
+
if (block.type === "tool_use" && "partialJson" in block) {
|
|
102
|
+
const { partialJson, ...rest } = block;
|
|
103
|
+
const rawJson = typeof partialJson === "string" ? partialJson : "";
|
|
104
|
+
let input;
|
|
105
|
+
try {
|
|
106
|
+
input = rawJson ? JSON.parse(rawJson) : {};
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Truncated stream (early break mid-tool-call): keep the raw
|
|
110
|
+
// fragment rather than losing it or throwing in a finalizer.
|
|
111
|
+
input = { __partial_json: rawJson };
|
|
112
|
+
}
|
|
113
|
+
return { ...rest, input };
|
|
114
|
+
}
|
|
115
|
+
return block;
|
|
116
|
+
});
|
|
117
|
+
const usage = {
|
|
118
|
+
...(inputTokens !== undefined && { input_tokens: inputTokens }),
|
|
119
|
+
...(outputTokens !== undefined && { output_tokens: outputTokens })
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
output: {
|
|
123
|
+
role: "assistant",
|
|
124
|
+
...(responseModel && { model: responseModel }),
|
|
125
|
+
content,
|
|
126
|
+
...(stopReason && { stop_reason: stopReason })
|
|
127
|
+
},
|
|
128
|
+
...(Object.keys(usage).length > 0 && { usageDetails: usage }),
|
|
129
|
+
metadata: { streamed: "true" }
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Wraps an Anthropic client instance so every messages.create() call —
|
|
136
|
+
* streaming or not — is automatically recorded as a generation. Mutates
|
|
137
|
+
* client.messages in place and returns the same client reference.
|
|
138
|
+
*
|
|
139
|
+
* Safe to call more than once on the same client — re-wrapping is detected
|
|
140
|
+
* and is a no-op, rather than nesting a second wrapper around the first
|
|
141
|
+
* (which would silently double-record every call).
|
|
142
|
+
*/
|
|
143
|
+
export function wrapAnthropic(client, ironside, options = {}) {
|
|
144
|
+
const messages = client.messages;
|
|
145
|
+
if (messages.__ironsideWrapped)
|
|
146
|
+
return client;
|
|
147
|
+
const originalCreate = messages.create.bind(messages);
|
|
148
|
+
const wrappedCreate = async (...args) => {
|
|
149
|
+
const requestBody = args[0];
|
|
150
|
+
const modelParameters = extractModelParameters(requestBody);
|
|
151
|
+
const trace = options.trace ?? ironside.trace({ name: "anthropic.messages.create" });
|
|
152
|
+
const generation = trace.generation({
|
|
153
|
+
name: "anthropic.messages.create",
|
|
154
|
+
...(requestBody?.model && { model: requestBody.model }),
|
|
155
|
+
...(modelParameters && { modelParameters }),
|
|
156
|
+
input: requestBody?.messages
|
|
157
|
+
});
|
|
158
|
+
try {
|
|
159
|
+
const result = await originalCreate(...args);
|
|
160
|
+
if (requestBody?.stream) {
|
|
161
|
+
const accumulator = createEventAccumulator();
|
|
162
|
+
return instrumentAsyncIterable(result, accumulator.onEvent, ({ error, consumed }) => {
|
|
163
|
+
if (error)
|
|
164
|
+
generation.end(errorEndOptions(error));
|
|
165
|
+
else if (consumed)
|
|
166
|
+
generation.end(accumulator.endOptions());
|
|
167
|
+
else
|
|
168
|
+
generation.end({ metadata: { streamed: "true" } });
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const message = result;
|
|
172
|
+
const usageDetails = usageDetailsFrom(message.usage);
|
|
173
|
+
generation.end({
|
|
174
|
+
output: message,
|
|
175
|
+
...(usageDetails && { usageDetails })
|
|
176
|
+
});
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
generation.end(errorEndOptions(error));
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
messages.create = wrappedCreate;
|
|
185
|
+
Object.defineProperty(messages, "__ironsideWrapped", { value: true, enumerable: false });
|
|
186
|
+
return client;
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../../src/wrappers/anthropic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { IronsideClient, TraceHandle } from "../client.js";
|
|
2
|
+
export interface WrapOpenAIOptions {
|
|
3
|
+
/** Attach generations to an existing trace instead of creating a new standalone trace per call. */
|
|
4
|
+
trace?: TraceHandle;
|
|
5
|
+
}
|
|
6
|
+
interface ChatCompletionsLike {
|
|
7
|
+
create: (...args: never[]) => unknown;
|
|
8
|
+
}
|
|
9
|
+
interface OpenAILike {
|
|
10
|
+
chat: {
|
|
11
|
+
completions: ChatCompletionsLike;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Wraps an OpenAI client instance so every chat.completions.create() call
|
|
16
|
+
* — streaming or not — is automatically recorded as a generation. Mutates
|
|
17
|
+
* client.chat.completions in place and returns the same client reference
|
|
18
|
+
* for convenient chaining (`const client = wrapOpenAI(new OpenAI(...), ironside)`).
|
|
19
|
+
*
|
|
20
|
+
* Safe to call more than once on the same client — re-wrapping is detected
|
|
21
|
+
* and is a no-op, rather than nesting a second wrapper around the first
|
|
22
|
+
* (which would silently double-record every call: two traces, two
|
|
23
|
+
* generations, per real API call).
|
|
24
|
+
*/
|
|
25
|
+
export declare function wrapOpenAI<T extends OpenAILike>(client: T, ironside: IronsideClient, options?: WrapOpenAIOptions): T;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { errorEndOptions, instrumentAsyncIterable } from "./streaming.js";
|
|
2
|
+
function extractModelParameters(body) {
|
|
3
|
+
if (!body)
|
|
4
|
+
return undefined;
|
|
5
|
+
const entries = ["temperature", "top_p", "max_tokens", "max_completion_tokens", "presence_penalty", "frequency_penalty", "seed"]
|
|
6
|
+
.filter((key) => body[key] !== undefined)
|
|
7
|
+
.map((key) => [key, body[key]]);
|
|
8
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
9
|
+
}
|
|
10
|
+
function usageDetailsFrom(usage) {
|
|
11
|
+
if (!usage)
|
|
12
|
+
return undefined;
|
|
13
|
+
const details = {
|
|
14
|
+
...(usage.prompt_tokens !== undefined && { input_tokens: usage.prompt_tokens }),
|
|
15
|
+
...(usage.completion_tokens !== undefined && { output_tokens: usage.completion_tokens })
|
|
16
|
+
};
|
|
17
|
+
return Object.keys(details).length > 0 ? details : undefined;
|
|
18
|
+
}
|
|
19
|
+
function assembleMessage(choice) {
|
|
20
|
+
const assembledToolCalls = choice.toolCalls.filter(Boolean);
|
|
21
|
+
return {
|
|
22
|
+
role: "assistant",
|
|
23
|
+
content: choice.sawContent ? choice.content : null,
|
|
24
|
+
...(assembledToolCalls.length > 0 && { tool_calls: assembledToolCalls })
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createChunkAccumulator() {
|
|
28
|
+
// Keyed by choice.index — an n>1 request streams every choice's deltas
|
|
29
|
+
// interleaved in the same chunk sequence, so reading only choices[0]
|
|
30
|
+
// would silently truncate the response to a fraction of one choice
|
|
31
|
+
// (review finding, PR #39).
|
|
32
|
+
const choices = new Map();
|
|
33
|
+
let usage;
|
|
34
|
+
let responseModel;
|
|
35
|
+
return {
|
|
36
|
+
onChunk(raw) {
|
|
37
|
+
const chunk = raw;
|
|
38
|
+
if (chunk.model)
|
|
39
|
+
responseModel = chunk.model;
|
|
40
|
+
if (chunk.usage)
|
|
41
|
+
usage = chunk.usage;
|
|
42
|
+
for (const choice of chunk.choices ?? []) {
|
|
43
|
+
const index = choice.index ?? 0;
|
|
44
|
+
let slot = choices.get(index);
|
|
45
|
+
if (!slot) {
|
|
46
|
+
slot = { content: "", sawContent: false, toolCalls: [] };
|
|
47
|
+
choices.set(index, slot);
|
|
48
|
+
}
|
|
49
|
+
if (choice.finish_reason)
|
|
50
|
+
slot.finishReason = choice.finish_reason;
|
|
51
|
+
if (typeof choice.delta?.content === "string") {
|
|
52
|
+
slot.content += choice.delta.content;
|
|
53
|
+
slot.sawContent = true;
|
|
54
|
+
}
|
|
55
|
+
for (const tc of choice.delta?.tool_calls ?? []) {
|
|
56
|
+
const toolSlot = (slot.toolCalls[tc.index] ??= { function: { arguments: "" } });
|
|
57
|
+
if (tc.id)
|
|
58
|
+
toolSlot.id = tc.id;
|
|
59
|
+
if (tc.type)
|
|
60
|
+
toolSlot.type = tc.type;
|
|
61
|
+
if (tc.function?.name)
|
|
62
|
+
toolSlot.function.name = tc.function.name;
|
|
63
|
+
if (tc.function?.arguments)
|
|
64
|
+
toolSlot.function.arguments += tc.function.arguments;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
endOptions() {
|
|
69
|
+
const sorted = [...choices.entries()].sort(([a], [b]) => a - b);
|
|
70
|
+
const usageDetails = usageDetailsFrom(usage);
|
|
71
|
+
// Single choice (the overwhelmingly common case): output is the
|
|
72
|
+
// assistant message itself. n>1: output mirrors the non-streaming
|
|
73
|
+
// completion's choices array so no choice is dropped.
|
|
74
|
+
const single = sorted.length === 0
|
|
75
|
+
? { content: "", sawContent: false, toolCalls: [] } // empty stream: a bare content-null message
|
|
76
|
+
: sorted.length === 1
|
|
77
|
+
? sorted[0][1]
|
|
78
|
+
: undefined;
|
|
79
|
+
const output = single
|
|
80
|
+
? assembleMessage(single)
|
|
81
|
+
: {
|
|
82
|
+
choices: sorted.map(([index, choice]) => ({
|
|
83
|
+
index,
|
|
84
|
+
message: assembleMessage(choice),
|
|
85
|
+
...(choice.finishReason && { finish_reason: choice.finishReason })
|
|
86
|
+
}))
|
|
87
|
+
};
|
|
88
|
+
const finishReason = single?.finishReason;
|
|
89
|
+
return {
|
|
90
|
+
output,
|
|
91
|
+
...(usageDetails && { usageDetails }),
|
|
92
|
+
metadata: {
|
|
93
|
+
streamed: "true",
|
|
94
|
+
...(responseModel && { response_model: responseModel }),
|
|
95
|
+
...(finishReason && { finish_reason: finishReason })
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Wraps an OpenAI client instance so every chat.completions.create() call
|
|
103
|
+
* — streaming or not — is automatically recorded as a generation. Mutates
|
|
104
|
+
* client.chat.completions in place and returns the same client reference
|
|
105
|
+
* for convenient chaining (`const client = wrapOpenAI(new OpenAI(...), ironside)`).
|
|
106
|
+
*
|
|
107
|
+
* Safe to call more than once on the same client — re-wrapping is detected
|
|
108
|
+
* and is a no-op, rather than nesting a second wrapper around the first
|
|
109
|
+
* (which would silently double-record every call: two traces, two
|
|
110
|
+
* generations, per real API call).
|
|
111
|
+
*/
|
|
112
|
+
export function wrapOpenAI(client, ironside, options = {}) {
|
|
113
|
+
const completions = client.chat.completions;
|
|
114
|
+
if (completions.__ironsideWrapped)
|
|
115
|
+
return client;
|
|
116
|
+
const originalCreate = completions.create.bind(completions);
|
|
117
|
+
const wrappedCreate = async (...args) => {
|
|
118
|
+
const requestBody = args[0];
|
|
119
|
+
const modelParameters = extractModelParameters(requestBody);
|
|
120
|
+
const trace = options.trace ?? ironside.trace({ name: "openai.chat.completions.create" });
|
|
121
|
+
const generation = trace.generation({
|
|
122
|
+
name: "openai.chat.completions.create",
|
|
123
|
+
...(requestBody?.model && { model: requestBody.model }),
|
|
124
|
+
...(modelParameters && { modelParameters }),
|
|
125
|
+
input: requestBody?.messages
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
const result = await originalCreate(...args);
|
|
129
|
+
if (requestBody?.stream) {
|
|
130
|
+
const accumulator = createChunkAccumulator();
|
|
131
|
+
return instrumentAsyncIterable(result, accumulator.onChunk, ({ error, consumed }) => {
|
|
132
|
+
if (error)
|
|
133
|
+
generation.end(errorEndOptions(error));
|
|
134
|
+
else if (consumed)
|
|
135
|
+
generation.end(accumulator.endOptions());
|
|
136
|
+
// !consumed: the result wasn't iterable at all (unexpected SDK
|
|
137
|
+
// shape) — end with what we know rather than dangle forever.
|
|
138
|
+
else
|
|
139
|
+
generation.end({ metadata: { streamed: "true" } });
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const completion = result;
|
|
143
|
+
const usageDetails = usageDetailsFrom(completion.usage);
|
|
144
|
+
generation.end({
|
|
145
|
+
output: completion,
|
|
146
|
+
...(usageDetails && { usageDetails })
|
|
147
|
+
});
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
generation.end(errorEndOptions(error));
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
completions.create = wrappedCreate;
|
|
156
|
+
Object.defineProperty(completions, "__ironsideWrapped", {
|
|
157
|
+
value: true,
|
|
158
|
+
enumerable: false
|
|
159
|
+
});
|
|
160
|
+
return client;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../../src/wrappers/openai.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patches `stream`'s async iterator in place so every yielded chunk feeds
|
|
3
|
+
* `onChunk` and exactly one of done/break/error triggers `onFinish`.
|
|
4
|
+
* Returns the same object. If the value isn't async-iterable at all
|
|
5
|
+
* (unexpected SDK shape), it is returned untouched and `onFinish` fires
|
|
6
|
+
* immediately with `{ consumed: false }` so the generation isn't left
|
|
7
|
+
* dangling by our own bug.
|
|
8
|
+
*/
|
|
9
|
+
export declare function instrumentAsyncIterable<T>(stream: T, onChunk: (chunk: unknown) => void, onFinish: (outcome: {
|
|
10
|
+
error?: unknown;
|
|
11
|
+
consumed: boolean;
|
|
12
|
+
}) => void): T;
|
|
13
|
+
/** Shared error→end mapping so streamed and non-streamed failures record identically. */
|
|
14
|
+
export declare function errorEndOptions(error: unknown): {
|
|
15
|
+
level: "error";
|
|
16
|
+
statusMessage: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Shared machinery for tracing streamed responses (M9-07). Both provider
|
|
2
|
+
// SDKs return a Stream object that is an async iterable with extra API
|
|
3
|
+
// surface (.tee(), .controller, .toReadableStream(), ...). Returning our
|
|
4
|
+
// own wrapper generator would silently break every caller using that
|
|
5
|
+
// surface, so instead the stream's [Symbol.asyncIterator] is patched IN
|
|
6
|
+
// PLACE and the SAME object is returned — identical to how the wrappers
|
|
7
|
+
// patch client.chat.completions.create itself.
|
|
8
|
+
//
|
|
9
|
+
// The generation can only be finalized when the caller actually consumes
|
|
10
|
+
// the stream (that's when the text/usage exists at all). Three exits all
|
|
11
|
+
// funnel into one finalize call: normal completion (done), early break
|
|
12
|
+
// (the iterator's return()), and a mid-stream error (next() rejecting or
|
|
13
|
+
// throw()). A stream the caller never iterates records its start event
|
|
14
|
+
// but never ends — visible in the UI as a dangling in-progress
|
|
15
|
+
// generation, which is the honest representation of what happened.
|
|
16
|
+
//
|
|
17
|
+
// KNOWN LIMIT — .tee(): each branch iterates through the patched
|
|
18
|
+
// asyncIterator, so chunks from both branches feed one accumulator
|
|
19
|
+
// (double-counted text). finalize still fires exactly once. tee'd +
|
|
20
|
+
// wrapped is rare enough that correct-single-stream beats a per-iterator
|
|
21
|
+
// accumulator design that couldn't merge usage sanely anyway.
|
|
22
|
+
/**
|
|
23
|
+
* Patches `stream`'s async iterator in place so every yielded chunk feeds
|
|
24
|
+
* `onChunk` and exactly one of done/break/error triggers `onFinish`.
|
|
25
|
+
* Returns the same object. If the value isn't async-iterable at all
|
|
26
|
+
* (unexpected SDK shape), it is returned untouched and `onFinish` fires
|
|
27
|
+
* immediately with `{ consumed: false }` so the generation isn't left
|
|
28
|
+
* dangling by our own bug.
|
|
29
|
+
*/
|
|
30
|
+
export function instrumentAsyncIterable(stream, onChunk, onFinish) {
|
|
31
|
+
const iterable = stream;
|
|
32
|
+
if (typeof iterable?.[Symbol.asyncIterator] !== "function") {
|
|
33
|
+
onFinish({ consumed: false });
|
|
34
|
+
return stream;
|
|
35
|
+
}
|
|
36
|
+
let finished = false;
|
|
37
|
+
const finishOnce = (outcome) => {
|
|
38
|
+
if (finished)
|
|
39
|
+
return;
|
|
40
|
+
finished = true;
|
|
41
|
+
onFinish({ ...outcome, consumed: true });
|
|
42
|
+
};
|
|
43
|
+
const originalFactory = iterable[Symbol.asyncIterator].bind(iterable);
|
|
44
|
+
iterable[Symbol.asyncIterator] = () => {
|
|
45
|
+
const inner = originalFactory();
|
|
46
|
+
return {
|
|
47
|
+
async next() {
|
|
48
|
+
try {
|
|
49
|
+
const result = await inner.next();
|
|
50
|
+
if (result.done)
|
|
51
|
+
finishOnce({});
|
|
52
|
+
else
|
|
53
|
+
onChunk(result.value);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
finishOnce({ error });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
// Called on `break`/`return` inside a for-await — the caller chose
|
|
62
|
+
// to stop early; what accumulated so far is the real output.
|
|
63
|
+
async return(value) {
|
|
64
|
+
finishOnce({});
|
|
65
|
+
if (inner.return)
|
|
66
|
+
return inner.return(value);
|
|
67
|
+
return { done: true, value: undefined };
|
|
68
|
+
},
|
|
69
|
+
async throw(error) {
|
|
70
|
+
finishOnce({ error });
|
|
71
|
+
if (inner.throw)
|
|
72
|
+
return inner.throw(error);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
return stream;
|
|
78
|
+
}
|
|
79
|
+
/** Shared error→end mapping so streamed and non-streamed failures record identically. */
|
|
80
|
+
export function errorEndOptions(error) {
|
|
81
|
+
return {
|
|
82
|
+
level: "error",
|
|
83
|
+
statusMessage: error instanceof Error ? error.message : String(error)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=streaming.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming.js","sourceRoot":"","sources":["../../../src/wrappers/streaming.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,uEAAuE;AACvE,yEAAyE;AACzE,qEAAqE;AACrE,wEAAwE;AACxE,wEAAwE;AACxE,+CAA+C;AAC/C,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AACzE,uEAAuE;AACvE,yEAAyE;AACzE,uEAAuE;AACvE,+DAA+D;AAC/D,mEAAmE;AACnE,EAAE;AACF,iEAAiE;AACjE,mEAAmE;AACnE,oEAAoE;AACpE,yEAAyE;AACzE,8DAA8D;AAE9D;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAS,EACT,OAAiC,EACjC,QAAmE;IAEnE,MAAM,QAAQ,GAAG,MAEhB,CAAC;IACF,IAAI,OAAO,QAAQ,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,UAAU,EAAE,CAAC;QAC3D,QAAQ,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9B,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,UAAU,GAAG,CAAC,OAA4B,EAAE,EAAE;QAClD,IAAI,QAAQ;YAAE,OAAO;QACrB,QAAQ,GAAG,IAAI,CAAC;QAChB,QAAQ,CAAC,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC,CAAC;IAEF,MAAM,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvE,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,GAAG,EAAE;QACpC,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC;QAChC,OAAO;YACL,KAAK,CAAC,IAAI;gBACR,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;oBAClC,IAAI,MAAM,CAAC,IAAI;wBAAE,UAAU,CAAC,EAAE,CAAC,CAAC;;wBAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;oBAC3B,OAAO,MAAM,CAAC;gBAChB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,UAAU,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;oBACtB,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;YACD,mEAAmE;YACnE,6DAA6D;YAC7D,KAAK,CAAC,MAAM,CAAC,KAAe;gBAC1B,UAAU,CAAC,EAAE,CAAC,CAAC;gBACf,IAAI,KAAK,CAAC,MAAM;oBAAE,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC7C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;YAC1C,CAAC;YACD,KAAK,CAAC,KAAK,CAAC,KAAe;gBACzB,UAAU,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;gBACtB,IAAI,KAAK,CAAC,KAAK;oBAAE,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC3C,MAAM,KAAK,CAAC;YACd,CAAC;SACF,CAAC;IACJ,CAAC,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,eAAe,CAAC,KAAc;IAI5C,OAAO;QACL,KAAK,EAAE,OAAO;QACd,aAAa,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;KACtE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { IronsideClient, TraceHandle } from "../client.js";
|
|
2
|
+
export interface RecordGenerateTextOptions {
|
|
3
|
+
trace?: TraceHandle;
|
|
4
|
+
/** Passed through to the generation's name; defaults to "ai.generateText". */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** The `model` id string you passed to generateText/streamText, since the result object doesn't always echo it back verbatim. */
|
|
7
|
+
model?: string;
|
|
8
|
+
/** The `prompt`/`messages` you passed in, for the recorded input. */
|
|
9
|
+
input?: unknown;
|
|
10
|
+
/** Sampling parameters (temperature, maxOutputTokens, ...) you passed to generateText/streamText — this recorder doesn't intercept the call, so these aren't available on the result and must be passed through explicitly. */
|
|
11
|
+
modelParameters?: Record<string, string | number | boolean | null>;
|
|
12
|
+
}
|
|
13
|
+
interface LanguageModelUsageLike {
|
|
14
|
+
inputTokens?: number;
|
|
15
|
+
outputTokens?: number;
|
|
16
|
+
}
|
|
17
|
+
interface GenerateTextResultLike {
|
|
18
|
+
text?: string;
|
|
19
|
+
usage?: LanguageModelUsageLike;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Records a completed generateText()/streamText() result as a generation.
|
|
23
|
+
* Call after awaiting the result (or after a stream finishes and its
|
|
24
|
+
* `usage` promise/property resolves) — this does not intercept the call
|
|
25
|
+
* itself, since the AI SDK's own `telemetry` option is the recommended
|
|
26
|
+
* hook for that.
|
|
27
|
+
*/
|
|
28
|
+
export declare function recordGenerateTextResult(ironside: IronsideClient, result: GenerateTextResultLike, options?: RecordGenerateTextOptions): void;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records a completed generateText()/streamText() result as a generation.
|
|
3
|
+
* Call after awaiting the result (or after a stream finishes and its
|
|
4
|
+
* `usage` promise/property resolves) — this does not intercept the call
|
|
5
|
+
* itself, since the AI SDK's own `telemetry` option is the recommended
|
|
6
|
+
* hook for that.
|
|
7
|
+
*/
|
|
8
|
+
export function recordGenerateTextResult(ironside, result, options = {}) {
|
|
9
|
+
const trace = options.trace ?? ironside.trace({ name: options.name ?? "ai.generateText" });
|
|
10
|
+
const generation = trace.generation({
|
|
11
|
+
name: options.name ?? "ai.generateText",
|
|
12
|
+
...(options.model && { model: options.model }),
|
|
13
|
+
...(options.modelParameters && { modelParameters: options.modelParameters }),
|
|
14
|
+
input: options.input
|
|
15
|
+
});
|
|
16
|
+
generation.end({
|
|
17
|
+
output: result.text,
|
|
18
|
+
...(result.usage && {
|
|
19
|
+
usageDetails: {
|
|
20
|
+
...(result.usage.inputTokens !== undefined && {
|
|
21
|
+
input_tokens: result.usage.inputTokens
|
|
22
|
+
}),
|
|
23
|
+
...(result.usage.outputTokens !== undefined && {
|
|
24
|
+
output_tokens: result.usage.outputTokens
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=vercel-ai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vercel-ai.js","sourceRoot":"","sources":["../../../src/wrappers/vercel-ai.ts"],"names":[],"mappings":"AAuCA;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CACtC,QAAwB,EACxB,MAA8B,EAC9B,UAAqC,EAAE;IAEvC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,iBAAiB,EAAE,CAAC,CAAC;IAC3F,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;QAClC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,iBAAiB;QACvC,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC9C,GAAG,CAAC,OAAO,CAAC,eAAe,IAAI,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC;QAC5E,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC,CAAC;IAEH,UAAU,CAAC,GAAG,CAAC;QACb,MAAM,EAAE,MAAM,CAAC,IAAI;QACnB,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI;YAClB,YAAY,EAAE;gBACZ,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,IAAI;oBAC5C,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,WAAW;iBACvC,CAAC;gBACF,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,KAAK,SAAS,IAAI;oBAC7C,aAAa,EAAE,MAAM,CAAC,KAAK,CAAC,YAAY;iBACzC,CAAC;aACH;SACF,CAAC;KACH,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ironside",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ironside client SDK — instrument LLM traces in a few lines.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"ai",
|
|
8
|
+
"llm",
|
|
9
|
+
"observability",
|
|
10
|
+
"opentelemetry",
|
|
11
|
+
"tracing"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"main": "./dist/src/index.js",
|
|
16
|
+
"types": "./dist/src/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/src/index.d.ts",
|
|
20
|
+
"import": "./dist/src/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/src",
|
|
25
|
+
"src",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE.md"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/luka-zivkovic/ironside.git",
|
|
35
|
+
"directory": "packages/sdk"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"provenance": false,
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"prepack": "npm run build",
|
|
45
|
+
"test:package": "node ./scripts/smoke-package.mjs",
|
|
46
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"ulid": "^3.0.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@anthropic-ai/sdk": "^0.111.0",
|
|
53
|
+
"openai": "^6.46.0"
|
|
54
|
+
}
|
|
55
|
+
}
|