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.
@@ -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
+ }