@struct-ai/sdk 0.3.17 → 0.4.3

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.
Files changed (54) hide show
  1. package/README.md +84 -17
  2. package/dist/commonjs/context.d.ts +19 -0
  3. package/dist/commonjs/context.js +58 -0
  4. package/dist/commonjs/core.js +104 -8
  5. package/dist/commonjs/events.d.ts +17 -6
  6. package/dist/commonjs/events.js +82 -59
  7. package/dist/commonjs/genai-content.d.ts +52 -0
  8. package/dist/commonjs/genai-content.js +143 -0
  9. package/dist/commonjs/instrument.d.ts +47 -0
  10. package/dist/commonjs/instrument.js +158 -0
  11. package/dist/commonjs/integrations/anthropic-content.js +18 -6
  12. package/dist/commonjs/integrations/anthropic.d.ts +6 -1
  13. package/dist/commonjs/integrations/anthropic.js +113 -125
  14. package/dist/commonjs/integrations/index.js +8 -0
  15. package/dist/commonjs/integrations/langchain-callback.d.ts +3 -0
  16. package/dist/commonjs/integrations/langchain-callback.js +84 -6
  17. package/dist/commonjs/integrations/langchain-content.js +1 -1
  18. package/dist/commonjs/integrations/openai-content.d.ts +34 -0
  19. package/dist/commonjs/integrations/openai-content.js +375 -0
  20. package/dist/commonjs/integrations/openai.d.ts +39 -0
  21. package/dist/commonjs/integrations/openai.js +305 -0
  22. package/dist/commonjs/semconv.d.ts +1 -0
  23. package/dist/commonjs/semconv.js +1 -0
  24. package/dist/commonjs/truncation.d.ts +29 -0
  25. package/dist/commonjs/truncation.js +184 -10
  26. package/dist/commonjs/version.d.ts +1 -1
  27. package/dist/commonjs/version.js +1 -1
  28. package/dist/esm/context.d.ts +19 -0
  29. package/dist/esm/context.js +56 -0
  30. package/dist/esm/core.js +104 -8
  31. package/dist/esm/events.d.ts +17 -6
  32. package/dist/esm/events.js +82 -61
  33. package/dist/esm/genai-content.d.ts +52 -0
  34. package/dist/esm/genai-content.js +137 -0
  35. package/dist/esm/instrument.d.ts +47 -0
  36. package/dist/esm/instrument.js +155 -0
  37. package/dist/esm/integrations/anthropic-content.js +19 -7
  38. package/dist/esm/integrations/anthropic.d.ts +6 -1
  39. package/dist/esm/integrations/anthropic.js +112 -126
  40. package/dist/esm/integrations/index.js +8 -0
  41. package/dist/esm/integrations/langchain-callback.d.ts +3 -0
  42. package/dist/esm/integrations/langchain-callback.js +85 -7
  43. package/dist/esm/integrations/langchain-content.js +1 -1
  44. package/dist/esm/integrations/openai-content.d.ts +34 -0
  45. package/dist/esm/integrations/openai-content.js +360 -0
  46. package/dist/esm/integrations/openai.d.ts +39 -0
  47. package/dist/esm/integrations/openai.js +296 -0
  48. package/dist/esm/semconv.d.ts +1 -0
  49. package/dist/esm/semconv.js +1 -0
  50. package/dist/esm/truncation.d.ts +29 -0
  51. package/dist/esm/truncation.js +182 -10
  52. package/dist/esm/version.d.ts +1 -1
  53. package/dist/esm/version.js +1 -1
  54. package/package.json +8 -2
@@ -0,0 +1,375 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.messageContentToParts = messageContentToParts;
4
+ exports.inputItemToEvent = inputItemToEvent;
5
+ exports.outputItemToChoiceParts = outputItemToChoiceParts;
6
+ exports.normalizeInput = normalizeInput;
7
+ exports.lastUserParts = lastUserParts;
8
+ exports.iterFunctionCalls = iterFunctionCalls;
9
+ exports.deriveFinishReason = deriveFinishReason;
10
+ exports.isTerminalResponse = isTerminalResponse;
11
+ exports.mapChoiceFinishReason = mapChoiceFinishReason;
12
+ exports.toInputMessages = toInputMessages;
13
+ exports.toSystemInstructions = toSystemInstructions;
14
+ exports.toOutputMessages = toOutputMessages;
15
+ exports.safeJsonForTool = safeJsonForTool;
16
+ const semconv_js_1 = require("../semconv.js");
17
+ const truncation_js_1 = require("../truncation.js");
18
+ /** Read `key` from a Responses item that may be a plain object or SDK object. */
19
+ function get(item, key, dflt) {
20
+ if (item == null || typeof item !== "object")
21
+ return dflt;
22
+ const v = item[key];
23
+ return v === undefined ? dflt : v;
24
+ }
25
+ /** Map a Responses `input_image.image_url` to a spec image part (never embeds full base64). */
26
+ function parseImageUrl(imageUrl) {
27
+ if (typeof imageUrl === "string" && imageUrl.startsWith("data:")) {
28
+ try {
29
+ const idx = imageUrl.indexOf(",");
30
+ const header = idx >= 0 ? imageUrl.slice(0, idx) : imageUrl; // "data:<mime>;base64"
31
+ const data = idx >= 0 ? imageUrl.slice(idx + 1) : "";
32
+ const mime = header.slice("data:".length).split(";")[0] || undefined;
33
+ return {
34
+ type: "blob",
35
+ modality: "image",
36
+ content: data.slice(0, 256) + "...",
37
+ mime_type: mime,
38
+ };
39
+ }
40
+ catch {
41
+ /* malformed data URL falls through to uri */
42
+ }
43
+ }
44
+ return { type: "uri", modality: "image", uri: imageUrl || "" };
45
+ }
46
+ /** Map ONE entry inside a Responses `message.content` list to a spec part. */
47
+ function contentEntryToPart(entry) {
48
+ if (typeof entry === "string")
49
+ return { type: "text", content: entry };
50
+ const entryType = get(entry, "type");
51
+ if (entryType === "input_text" || entryType === "output_text") {
52
+ return { type: "text", content: get(entry, "text", "") || "" };
53
+ }
54
+ if (entryType === "refusal") {
55
+ return { type: "text", content: get(entry, "refusal", "") || "" };
56
+ }
57
+ if (entryType === "input_image" || entryType === "output_image") {
58
+ // A Responses image can reference an uploaded file by `file_id` instead of
59
+ // an `image_url`. Preserve that identity as a file part (never embed file
60
+ // contents — we don't have them) rather than dropping it to an empty uri.
61
+ const fileId = get(entry, "file_id");
62
+ if (typeof fileId === "string" && fileId) {
63
+ return { type: "file", modality: "image", file_id: fileId };
64
+ }
65
+ const url = get(entry, "image_url", "") || "";
66
+ if (entryType === "input_image")
67
+ return parseImageUrl(url);
68
+ return url ? parseImageUrl(url) : { type: "output_image" };
69
+ }
70
+ if (entryType === "input_file") {
71
+ // Preserve safe identity fields; NEVER embed the base64 `file_data`.
72
+ const part = { type: "file" };
73
+ const fileId = get(entry, "file_id");
74
+ const filename = get(entry, "filename");
75
+ const fileUrl = get(entry, "file_url");
76
+ if (typeof fileId === "string" && fileId)
77
+ part.file_id = fileId;
78
+ if (typeof filename === "string" && filename)
79
+ part.filename = filename;
80
+ if (typeof fileUrl === "string" && fileUrl)
81
+ part.uri = fileUrl;
82
+ return part;
83
+ }
84
+ if (entryType === "input_audio") {
85
+ // Preserve only the audio format; NEVER embed the base64 `data`.
86
+ const part = { type: "audio", modality: "audio" };
87
+ const audio = get(entry, "input_audio");
88
+ const format = get(audio, "format") ?? get(entry, "format");
89
+ if (typeof format === "string" && format)
90
+ part.format = format;
91
+ return part;
92
+ }
93
+ return { type: entryType || "unknown" };
94
+ }
95
+ /** Map a Responses `message.content` (string or list) to spec parts. */
96
+ function messageContentToParts(content) {
97
+ if (content == null)
98
+ return [];
99
+ if (typeof content === "string")
100
+ return [{ type: "text", content }];
101
+ if (!Array.isArray(content))
102
+ return [];
103
+ return content.map(contentEntryToPart);
104
+ }
105
+ /** Map a top-level `function_call` item to a single `tool_call` part. */
106
+ function functionCallParts(item) {
107
+ const part = { type: "tool_call", name: get(item, "name", "") || "" };
108
+ const callId = get(item, "call_id");
109
+ if (callId)
110
+ part.id = callId;
111
+ const rawArgs = get(item, "arguments");
112
+ if (rawArgs !== undefined && rawArgs !== null) {
113
+ if (typeof rawArgs === "string") {
114
+ // JSON string on the wire; parse to a dict when we can, else keep raw.
115
+ try {
116
+ part.arguments = rawArgs ? JSON.parse(rawArgs) : {};
117
+ }
118
+ catch {
119
+ part.arguments = rawArgs;
120
+ }
121
+ }
122
+ else {
123
+ part.arguments = rawArgs;
124
+ }
125
+ }
126
+ return [part];
127
+ }
128
+ /**
129
+ * Map a function_call_output / custom_tool_call_output item to a single
130
+ * tool_call_response part. `output` may be a string OR an array of
131
+ * input-content items — arrays route through the safe content mapper so inline
132
+ * base64 (`file_data`, audio `data`) is never copied verbatim.
133
+ */
134
+ function toolResponseParts(item) {
135
+ const part = { type: "tool_call_response" };
136
+ const callId = get(item, "call_id");
137
+ if (callId)
138
+ part.id = callId;
139
+ const output = get(item, "output", "");
140
+ part.response = Array.isArray(output) ? messageContentToParts(output) : output;
141
+ return [part];
142
+ }
143
+ /** Map a `custom_tool_call` item to a tool_call part with its RAW string input. */
144
+ function customToolCallParts(item) {
145
+ const part = { type: "tool_call", name: get(item, "name", "") || "" };
146
+ const callId = get(item, "call_id");
147
+ if (callId)
148
+ part.id = callId;
149
+ const input = get(item, "input");
150
+ if (typeof input === "string")
151
+ part.arguments = input; // freeform — keep raw
152
+ return [part];
153
+ }
154
+ const EVENT_NAME_MAP = {
155
+ user: semconv_js_1.EVENT_NAMES.USER_MESSAGE,
156
+ assistant: semconv_js_1.EVENT_NAMES.ASSISTANT_MESSAGE,
157
+ system: semconv_js_1.EVENT_NAMES.SYSTEM_MESSAGE,
158
+ // OpenAI's `developer` role is system-level instruction; Struct renders only
159
+ // the known user/assistant/system/tool event names, so map it to the system
160
+ // event (the payload keeps `role: "developer"` for fidelity).
161
+ developer: semconv_js_1.EVENT_NAMES.SYSTEM_MESSAGE,
162
+ };
163
+ /**
164
+ * True for a Responses `input` item that is a chat message. Covers the explicit
165
+ * ``{"type": "message", ...}`` form AND OpenAI's ``EasyInputMessage`` shorthand,
166
+ * where ``type`` is OPTIONAL (``type?: "message"``) — e.g.
167
+ * ``{"role": "user", "content": "hi"}``. A typeless item counts only when it
168
+ * carries a string ``role`` (so we never treat function_call / reasoning / other
169
+ * typeless objects as messages).
170
+ */
171
+ function isMessageItem(item) {
172
+ const itemType = get(item, "type");
173
+ if (itemType === "message")
174
+ return true;
175
+ return ((itemType === undefined || itemType === null) &&
176
+ typeof get(item, "role") === "string");
177
+ }
178
+ /** Map ONE Responses `input` item to `[eventName, role, parts]`, or null. */
179
+ function inputItemToEvent(item) {
180
+ if (isMessageItem(item)) {
181
+ const role = get(item, "role", "user") || "user";
182
+ const parts = messageContentToParts(get(item, "content"));
183
+ const eventName = EVENT_NAME_MAP[role] ?? `gen_ai.${role}.message`;
184
+ return [eventName, role, parts];
185
+ }
186
+ const itemType = get(item, "type");
187
+ if (itemType === "function_call") {
188
+ return [semconv_js_1.EVENT_NAMES.ASSISTANT_MESSAGE, "assistant", functionCallParts(item)];
189
+ }
190
+ if (itemType === "custom_tool_call") {
191
+ // OpenAI custom tools: same semantics as function_call but `input` is a
192
+ // FREEFORM string — never JSON.parse it.
193
+ return [semconv_js_1.EVENT_NAMES.ASSISTANT_MESSAGE, "assistant", customToolCallParts(item)];
194
+ }
195
+ if (itemType === "function_call_output" || itemType === "custom_tool_call_output") {
196
+ return [semconv_js_1.EVENT_NAMES.TOOL_MESSAGE, "tool", toolResponseParts(item)];
197
+ }
198
+ // Deliberately NO message event for everything else:
199
+ // - reasoning / compaction / compaction_trigger (encrypted_content NEVER read)
200
+ // - builtin/agentic tool machinery — computer_call(+output), web_search_call,
201
+ // file_search_call, image_generation_call, code_interpreter_call,
202
+ // local_shell_call(+output), and the v6-era shell_call(+output),
203
+ // apply_patch_call(+output), tool_search_call/tool_search_output,
204
+ // additional_tools, program(+output), mcp_* —
205
+ // these are not chat messages, and their payloads (shell commands, patches,
206
+ // program code) are exactly the sensitive fields we must not serialize.
207
+ // - unknown future items.
208
+ // The shape-sweep test pins every documented member of this policy.
209
+ return null;
210
+ }
211
+ /** Map ONE `response.output` item to the assistant's choice parts. */
212
+ function outputItemToChoiceParts(item) {
213
+ const itemType = get(item, "type");
214
+ if (itemType === "message")
215
+ return messageContentToParts(get(item, "content"));
216
+ if (itemType === "function_call")
217
+ return functionCallParts(item);
218
+ if (itemType === "custom_tool_call")
219
+ return customToolCallParts(item);
220
+ // v6 ResponseOutputItem also carries tool RESULTS — capture them (with the
221
+ // same array base64-stripping as the input path). Other agentic *_output
222
+ // items (computer/shell/apply_patch/…) stay privacy-skipped below.
223
+ if (itemType === "function_call_output" || itemType === "custom_tool_call_output") {
224
+ return toolResponseParts(item);
225
+ }
226
+ return [];
227
+ }
228
+ /** Normalize the `input` kwarg to a list of items (bare string → one user message). */
229
+ function normalizeInput(input) {
230
+ if (typeof input === "string") {
231
+ return [
232
+ { type: "message", role: "user", content: [{ type: "input_text", text: input }] },
233
+ ];
234
+ }
235
+ if (Array.isArray(input))
236
+ return input;
237
+ return [];
238
+ }
239
+ /** Spec parts of the LAST `user` message in `input` (for parent propagation). */
240
+ function lastUserParts(input) {
241
+ const items = normalizeInput(input);
242
+ for (let i = items.length - 1; i >= 0; i--) {
243
+ const item = items[i];
244
+ if (isMessageItem(item) && get(item, "role") === "user") {
245
+ return messageContentToParts(get(item, "content"));
246
+ }
247
+ }
248
+ return undefined;
249
+ }
250
+ /** Every function_call `(name, call_id)` from a response.output (for tool linkage). */
251
+ function iterFunctionCalls(output) {
252
+ const pairs = [];
253
+ if (!Array.isArray(output))
254
+ return pairs;
255
+ for (const item of output) {
256
+ const itemType = get(item, "type");
257
+ // custom_tool_call is a tool invocation too — its call_id is what a
258
+ // custom_tool_call_output echoes, so @struct.tool() linkage needs it.
259
+ if (itemType !== "function_call" && itemType !== "custom_tool_call")
260
+ continue;
261
+ const name = get(item, "name", "") || "";
262
+ const callId = get(item, "call_id", "") || "";
263
+ if (name && callId)
264
+ pairs.push([name, callId]);
265
+ }
266
+ return pairs;
267
+ }
268
+ /** Derive a raw finish-reason string from a Responses response. */
269
+ function deriveFinishReason(response) {
270
+ const incomplete = get(response, "incomplete_details");
271
+ if (incomplete != null) {
272
+ const reason = get(incomplete, "reason", "") || "";
273
+ return reason ? `incomplete:${reason}` : "incomplete";
274
+ }
275
+ return get(response, "status") || undefined;
276
+ }
277
+ const TERMINAL_STATUSES = new Set([
278
+ "completed",
279
+ "incomplete",
280
+ "failed",
281
+ "cancelled",
282
+ ]);
283
+ /**
284
+ * True when a Responses response is in a terminal state (generation finished).
285
+ * A `background: true` request can return non-terminal (`"queued"` /
286
+ * `"in_progress"`) with no assistant message yet — we must NOT emit a terminal
287
+ * `gen_ai.choice` / finish reason for those. A response with no status is
288
+ * treated as terminal so we never silently drop telemetry for the common
289
+ * synchronous call (or minimal mocks).
290
+ */
291
+ function isTerminalResponse(response) {
292
+ if (get(response, "incomplete_details") != null)
293
+ return true;
294
+ const status = get(response, "status");
295
+ if (status == null)
296
+ return true;
297
+ return TERMINAL_STATUSES.has(status);
298
+ }
299
+ // Only terminal statuses map to a spec choice finish reason; non-terminal
300
+ // (queued/in_progress) responses never reach here — the caller skips the choice
301
+ // event entirely (see isTerminalResponse).
302
+ const FINISH_REASON_MAP = {
303
+ completed: "stop",
304
+ // "incomplete" is handled reason-aware in mapChoiceFinishReason (length vs
305
+ // content_filter), not here.
306
+ failed: "error",
307
+ cancelled: "stop",
308
+ };
309
+ /** Map a raw Responses status/derived reason to a spec choice finish reason. */
310
+ function mapChoiceFinishReason(raw) {
311
+ if (!raw)
312
+ return "stop";
313
+ const [base, detail] = raw.split(":"); // e.g. "incomplete:max_output_tokens"
314
+ if (base === "incomplete") {
315
+ // Documented incomplete reasons: max_output_tokens → length,
316
+ // content_filter → content_filter (its own spec value). Unknown future
317
+ // reasons fall back to length (the generic "cut short" semantics).
318
+ if (detail === "content_filter")
319
+ return "content_filter";
320
+ return "length";
321
+ }
322
+ return FINISH_REASON_MAP[base] ?? "stop";
323
+ }
324
+ /** Serialize `input` → spec `gen_ai.input.messages` JSON string. */
325
+ function toInputMessages(input) {
326
+ try {
327
+ const result = [];
328
+ for (const item of normalizeInput(input)) {
329
+ const mapped = inputItemToEvent(item);
330
+ if (!mapped)
331
+ continue;
332
+ const [, role, parts] = mapped;
333
+ result.push({ role, parts });
334
+ }
335
+ return (0, truncation_js_1.truncateAndSerialize)(result);
336
+ }
337
+ catch {
338
+ return "[]";
339
+ }
340
+ }
341
+ /** Serialize Responses `instructions` → spec `system_instructions` JSON string. */
342
+ function toSystemInstructions(instructions) {
343
+ try {
344
+ if (typeof instructions === "string") {
345
+ return (0, truncation_js_1.truncateAndSerialize)([{ type: "text", content: instructions }]);
346
+ }
347
+ if (instructions) {
348
+ return (0, truncation_js_1.truncateAndSerialize)([{ type: "text", content: String(instructions) }]);
349
+ }
350
+ return "[]";
351
+ }
352
+ catch {
353
+ return "[]";
354
+ }
355
+ }
356
+ /** Serialize `response.output` → spec `gen_ai.output.messages` JSON string. */
357
+ function toOutputMessages(output, finishReason) {
358
+ try {
359
+ const parts = [];
360
+ for (const item of Array.isArray(output) ? output : []) {
361
+ parts.push(...outputItemToChoiceParts(item));
362
+ }
363
+ const msg = { role: "assistant", parts };
364
+ if (finishReason)
365
+ msg.finish_reason = mapChoiceFinishReason(finishReason);
366
+ return (0, truncation_js_1.truncateAndSerialize)([msg]);
367
+ }
368
+ catch {
369
+ return "[]";
370
+ }
371
+ }
372
+ function safeJsonForTool(obj) {
373
+ return (0, truncation_js_1.serializeToolDefinitions)(obj);
374
+ }
375
+ //# sourceMappingURL=openai-content.js.map
@@ -0,0 +1,39 @@
1
+ import { type Span, type Tracer } from "@opentelemetry/api";
2
+ import type { Logger } from "@opentelemetry/api-logs";
3
+ import type { StructSDK } from "../core.js";
4
+ interface PatchContext {
5
+ tracer: Tracer;
6
+ sdk: StructSDK;
7
+ logger: Logger | undefined;
8
+ }
9
+ interface CreateParams {
10
+ model?: string;
11
+ max_output_tokens?: number;
12
+ temperature?: number;
13
+ top_p?: number;
14
+ input?: unknown;
15
+ instructions?: unknown;
16
+ tools?: unknown[];
17
+ stream?: boolean;
18
+ }
19
+ export declare function patch(sdk: StructSDK): Promise<void>;
20
+ export declare function unpatch(): Promise<void>;
21
+ type CreateMethod = (this: unknown, params: CreateParams, opts?: unknown) => unknown;
22
+ declare function detectProvider(resource: unknown): string;
23
+ /** @internal */
24
+ export declare const _detectProviderForTest: typeof detectProvider;
25
+ export declare function wrapCreate(original: CreateMethod): CreateMethod;
26
+ declare function setChatRequestAttrs(span: Span, params: CreateParams | undefined, sdk: StructSDK, logger: Logger | undefined, provider?: string): void;
27
+ /** @internal */
28
+ export type _PatchContextForTest = PatchContext;
29
+ /** @internal */
30
+ export declare function _wrapCreateForTest(original: CreateMethod): CreateMethod;
31
+ /** @internal */
32
+ export declare const _setChatRequestAttrsForTest: typeof setChatRequestAttrs;
33
+ export declare function _setActivePatchCtxForTest(ctx: PatchContext | undefined): void;
34
+ /** @internal — version-compat tests assert the patch surface across openai releases. */
35
+ export declare function _collectResponsesClassesForTest(mod: Record<string, unknown>): Array<{
36
+ prototype: Record<string, unknown>;
37
+ }>;
38
+ export {};
39
+ //# sourceMappingURL=openai.d.ts.map