opik-vercel 1.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/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # Opik Vercel AI SDK Integration
2
+
3
+ [![npm version](https://img.shields.io/npm/v/opik-vercel.svg)](https://www.npmjs.com/package/opik-vercel)
4
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/comet-ml/opik/blob/main/LICENSE)
5
+
6
+ Seamlessly integrate [Opik](https://www.comet.com/docs/opik/) observability with your [Vercel AI SDK](https://sdk.vercel.ai/docs) applications to trace, monitor, and debug your AI workflows.
7
+
8
+ ## Features
9
+
10
+ - 🔍 **Comprehensive Tracing**: Automatically trace AI SDK calls and completions
11
+ - 📊 **Hierarchical Visualization**: View your AI execution as a structured trace with parent-child relationships
12
+ - 📝 **Detailed Metadata Capture**: Record model names, prompts, completions, token usage, and custom metadata
13
+ - 🚨 **Error Handling**: Capture and visualize errors in your AI API interactions
14
+ - 🏷️ **Custom Tagging**: Add custom tags to organize and filter your traces
15
+ - 🔄 **Streaming Support**: Full support for streamed completions and chat responses
16
+
17
+ ## Installation
18
+
19
+ ### Node.js
20
+
21
+ ```bash
22
+ npm install opik-vercel ai @ai-sdk/openai @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
23
+ ```
24
+
25
+ ### Next.js
26
+
27
+ ```bash
28
+ npm install opik-vercel @vercel/otel @opentelemetry/api-logs @opentelemetry/instrumentation @opentelemetry/sdk-logs
29
+ ```
30
+
31
+ ### Requirements
32
+
33
+ - Node.js ≥ 18
34
+ - Vercel AI SDK (`ai` ≥ 3.0.0)
35
+ - Opik SDK (automatically installed as a peer dependency)
36
+ - OpenTelemetry packages (see installation commands above)
37
+
38
+ ## Configuration
39
+
40
+ Set your environment variables:
41
+
42
+ ```bash
43
+ OPIK_API_KEY="<your-api-key>"
44
+ OPIK_URL_OVERRIDE="https://www.comet.com/opik/api" # Cloud version
45
+ OPIK_PROJECT_NAME="<custom-project-name>"
46
+ OPIK_WORKSPACE="<your-workspace>"
47
+ OPENAI_API_KEY="<your-openai-api-key>" # If using OpenAI models
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ### Node.js
53
+
54
+ ```typescript
55
+ import { openai } from "@ai-sdk/openai";
56
+ import { generateText } from "ai";
57
+ import { NodeSDK } from "@opentelemetry/sdk-node";
58
+ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
59
+ import { OpikExporter } from "opik-vercel";
60
+
61
+ const sdk = new NodeSDK({
62
+ traceExporter: new OpikExporter(),
63
+ instrumentations: [getNodeAutoInstrumentations()],
64
+ });
65
+
66
+ sdk.start();
67
+
68
+ async function main() {
69
+ const result = await generateText({
70
+ model: openai("gpt-4o"),
71
+ maxTokens: 50,
72
+ prompt: "What is love?",
73
+ experimental_telemetry: OpikExporter.getSettings({
74
+ name: "opik-nodejs-example",
75
+ }),
76
+ });
77
+
78
+ console.log(result.text);
79
+
80
+ await sdk.shutdown(); // Flushes the trace to Opik
81
+ }
82
+
83
+ main().catch(console.error);
84
+ ```
85
+
86
+ ### Next.js
87
+
88
+ For Next.js applications, use the framework's built-in OpenTelemetry support:
89
+
90
+ ```typescript
91
+ // instrumentation.ts
92
+ import { registerOTel } from "@vercel/otel";
93
+ import { OpikExporter } from "opik-vercel";
94
+
95
+ export function register() {
96
+ registerOTel({
97
+ serviceName: "opik-vercel-ai-nextjs-example",
98
+ traceExporter: new OpikExporter(),
99
+ });
100
+ }
101
+ ```
102
+
103
+ Then use the AI SDK with telemetry enabled:
104
+
105
+ ```typescript
106
+ import { openai } from "@ai-sdk/openai";
107
+ import { generateText } from "ai";
108
+
109
+ const result = await generateText({
110
+ model: openai("gpt-4o"),
111
+ prompt: "What is love?",
112
+ experimental_telemetry: { isEnabled: true },
113
+ });
114
+ ```
115
+
116
+ ## Advanced Configuration
117
+
118
+ ### Custom Tags and Metadata
119
+
120
+ You can add custom tags and metadata to all traces generated by the OpikExporter:
121
+
122
+ ```typescript
123
+ const exporter = new OpikExporter({
124
+ // Optional: add custom tags to all traces
125
+ tags: ["production", "gpt-4o"],
126
+ // Optional: add custom metadata to all traces
127
+ metadata: {
128
+ environment: "production",
129
+ version: "1.0.0",
130
+ team: "ai-team",
131
+ },
132
+ });
133
+ ```
134
+
135
+ Tags are useful for filtering and grouping traces, while metadata adds additional context for debugging and analysis.
136
+
137
+ ### Telemetry Settings
138
+
139
+ Use `OpikExporter.getSettings()` to configure telemetry for individual AI SDK calls:
140
+
141
+ ```typescript
142
+ const result = await generateText({
143
+ model: openai("gpt-4o"),
144
+ prompt: "Tell a joke",
145
+ experimental_telemetry: OpikExporter.getSettings({
146
+ name: "custom-trace-name",
147
+ }),
148
+ });
149
+ ```
150
+
151
+ Or use the basic telemetry settings:
152
+
153
+ ```typescript
154
+ const result = await generateText({
155
+ model: openai("gpt-4o"),
156
+ prompt: "Tell a joke",
157
+ experimental_telemetry: { isEnabled: true },
158
+ });
159
+ ```
160
+
161
+ ## Viewing Traces
162
+
163
+ To view your traces:
164
+
165
+ 1. Sign in to your [Comet account](https://www.comet.com/signin)
166
+ 2. Navigate to the Opik section
167
+ 3. Select your project to view all traces
168
+ 4. Click on a specific trace to see the detailed execution flow
169
+
170
+ ## Debugging
171
+
172
+ To enable more verbose logging for troubleshooting:
173
+
174
+ ```bash
175
+ OPIK_LOG_LEVEL=DEBUG
176
+ ```
177
+
178
+ ## Learn More
179
+
180
+ - [Opik Vercel AI SDK Integration Guide](https://www.comet.com/docs/opik/tracing/integrations/vercel-ai-sdk)
181
+ - [Opik Documentation](https://www.comet.com/docs/opik/)
182
+ - [Vercel AI SDK Documentation](https://sdk.vercel.ai/docs)
183
+ - [Opik TypeScript SDK](https://github.com/comet-ml/opik/tree/main/sdks/typescript)
184
+
185
+ ## License
186
+
187
+ Apache 2.0
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ 'use strict';var opik=require('opik');var E=new opik.Opik,f=class{constructor({client:e=E,tags:t=[],metadata:n={}}={}){this.traces=new Map;this.spans=new Map;this.getSpanInput=e=>{let t={},{attributes:n}=e;return Object.keys(n).forEach(o=>{if(o==="ai.prompt"||o==="gen_ai.request"){let a=C(n[o]);a&&(t={...t,...a});}if(o.startsWith("ai.prompt.")){let a=o.replace("ai.prompt.","");t[a]=m(n[o]);}if(o.startsWith("gen_ai.request.")){let a=o.replace("gen_ai.request.","");t[a]=m(n[o]);}}),Object.keys(t).length>0||("ai.toolCall.name"in n&&(t.toolName=n["ai.toolCall.name"]),"ai.toolCall.args"in n&&(t.args=n["ai.toolCall.args"])),t};this.getSpanOutput=e=>{let{attributes:t}=e;return t["ai.response.text"]?{text:t["ai.response.text"]}:t["ai.toolCall.result"]?{result:t["ai.toolCall.result"]}:t["ai.response.toolCalls"]?{toolCalls:m(t["ai.response.toolCalls"])}:{}};this.getSpanMetadata=e=>{let{attributes:t}=e,n={};return t["gen_ai.response.model"]&&(n.model=t["gen_ai.response.model"]),t["gen_ai.system"]&&(n.system=t["gen_ai.system"]),n};this.getSpanUsage=e=>{let{attributes:t}=e,n={};return "ai.usage.promptTokens"in t&&(n.prompt_tokens=t["ai.usage.promptTokens"]),"gen_ai.usage.input_tokens"in t&&(n.prompt_tokens=t["gen_ai.usage.input_tokens"]),"ai.usage.completionTokens"in t&&(n.completion_tokens=t["ai.usage.completionTokens"]),"gen_ai.usage.output_tokens"in t&&(n.completion_tokens=t["gen_ai.usage.output_tokens"]),("prompt_tokens"in n||"completion_tokens"in n)&&(n.total_tokens=(n.prompt_tokens||0)+(n.completion_tokens||0)),n};this.processSpan=({otelSpan:e,parentSpan:t,trace:n})=>n.span({name:e.name,startTime:new Date(l(e.startTime)),endTime:new Date(l(e.endTime)),parentSpanId:t==null?void 0:t.data.id,input:this.getSpanInput(e),output:this.getSpanOutput(e),metadata:this.getSpanMetadata(e),usage:this.getSpanUsage(e),type:"llm"});this.shutdown=async()=>{await this.client.flush();};this.forceFlush=async()=>{await this.client.flush();};this.export=async(e,t)=>{let n=e.filter(a=>a.instrumentationScope.name==="ai"),i=e.length-n.length;if(i>0&&opik.logger.debug(`Ignored ${i} non-AI SDK spans`),n.length===0){opik.logger.debug("No AI SDK spans found"),t({code:0});return}let o=w(n);opik.logger.debug("Exporting spans",n),Object.entries(o).forEach(([a,c])=>{var S,h;let[r,...u]=c,p=this.client.trace({startTime:new Date(l(r.startTime)),endTime:new Date(l(r.endTime)),name:(h=(S=r.attributes["ai.telemetry.metadata.traceName"])==null?void 0:S.toString())!=null?h:r.name,input:this.getSpanInput(r),output:this.getSpanOutput(r),metadata:{...this.getSpanMetadata(r),...this.metadata},tags:this.tags,usage:this.getSpanUsage(r)});this.traces.set(a,p),u.forEach(g=>{var b,y;let R=this.spans.get((y=(b=g.parentSpanContext)==null?void 0:b.spanId)!=null?y:""),x=this.processSpan({parentSpan:R,otelSpan:g,trace:p});this.spans.set(g.spanContext().spanId,x);});});try{await this.client.flush(),t({code:0});}catch(a){opik.logger.error("Error exporting spans",a),t({code:1,error:a instanceof Error?a:new Error("Unknown error")});}};this.client=e,this.tags=[...t],this.metadata={...n};}static getSettings(e){var n,i,o;let t={...e.metadata};return e.name&&(t.traceName=e.name),{isEnabled:(n=e.isEnabled)!=null?n:true,recordInputs:(i=e.recordInputs)!=null?i:true,recordOutputs:(o=e.recordOutputs)!=null?o:true,functionId:e.functionId,metadata:t}}};function w(s){let e={};return s.forEach(t=>{let n=t.spanContext();e[n.traceId]||(e[n.traceId]=[]),e[n.traceId].push(t);}),Object.entries(e).forEach(([t,n])=>{e[t]=I(n);}),e}function l(s){return s[0]*1e3+s[1]/1e6}function m(s){try{return JSON.parse(s)}catch{return s}}function C(s){if(typeof s=="string")try{let e=JSON.parse(s);if(e!==null&&typeof e=="object"&&!Array.isArray(e))return e}catch{return}}function I(s){var a,c;let e=new Map,t=new Map;for(let r of s){let{spanId:u}=r.spanContext(),p=(c=(a=r.parentSpanContext)==null?void 0:a.spanId)!=null?c:"";e.set(u,r),p&&(t.has(p)||t.set(p,[]),t.get(p).push(r));}let n=s.filter(r=>{var u;return !((u=r.parentSpanContext)!=null&&u.spanId)||!e.has(r.parentSpanContext.spanId)}),i=[],o=[...n];for(;o.length>0;){let r=o.shift();i.push(r);let u=r.spanContext().spanId,p=t.get(u)||[];o.push(...p);}return i}exports.OpikExporter=f;
@@ -0,0 +1,46 @@
1
+ import { AttributeValue, Tracer } from '@opentelemetry/api';
2
+ import { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
3
+ import { Opik, Span, Trace } from 'opik';
4
+
5
+ type SpanExporter = NodeSDKConfiguration["traceExporter"];
6
+ type ExportFunction = SpanExporter["export"];
7
+ type ReadableSpan = Parameters<ExportFunction>[0][0];
8
+ type TelemetrySettings = {
9
+ isEnabled?: boolean;
10
+ recordInputs?: boolean;
11
+ recordOutputs?: boolean;
12
+ functionId?: string;
13
+ metadata?: Record<string, AttributeValue>;
14
+ tracer?: Tracer;
15
+ };
16
+ type OpikExporterSettings = TelemetrySettings & {
17
+ name?: string;
18
+ };
19
+ type OpikExporterOptions = {
20
+ client?: Opik;
21
+ tags?: string[];
22
+ metadata?: Record<string, AttributeValue>;
23
+ };
24
+ declare class OpikExporter implements SpanExporter {
25
+ private readonly traces;
26
+ private readonly spans;
27
+ private readonly client;
28
+ private readonly tags;
29
+ private readonly metadata;
30
+ constructor({ client, tags, metadata, }?: OpikExporterOptions);
31
+ private getSpanInput;
32
+ private getSpanOutput;
33
+ private getSpanMetadata;
34
+ private getSpanUsage;
35
+ processSpan: ({ otelSpan, parentSpan, trace, }: {
36
+ otelSpan: ReadableSpan;
37
+ parentSpan?: Span;
38
+ trace: Trace;
39
+ }) => Span;
40
+ shutdown: () => Promise<void>;
41
+ forceFlush: () => Promise<void>;
42
+ export: ExportFunction;
43
+ static getSettings(settings: OpikExporterSettings): TelemetrySettings;
44
+ }
45
+
46
+ export { OpikExporter };
@@ -0,0 +1,46 @@
1
+ import { AttributeValue, Tracer } from '@opentelemetry/api';
2
+ import { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
3
+ import { Opik, Span, Trace } from 'opik';
4
+
5
+ type SpanExporter = NodeSDKConfiguration["traceExporter"];
6
+ type ExportFunction = SpanExporter["export"];
7
+ type ReadableSpan = Parameters<ExportFunction>[0][0];
8
+ type TelemetrySettings = {
9
+ isEnabled?: boolean;
10
+ recordInputs?: boolean;
11
+ recordOutputs?: boolean;
12
+ functionId?: string;
13
+ metadata?: Record<string, AttributeValue>;
14
+ tracer?: Tracer;
15
+ };
16
+ type OpikExporterSettings = TelemetrySettings & {
17
+ name?: string;
18
+ };
19
+ type OpikExporterOptions = {
20
+ client?: Opik;
21
+ tags?: string[];
22
+ metadata?: Record<string, AttributeValue>;
23
+ };
24
+ declare class OpikExporter implements SpanExporter {
25
+ private readonly traces;
26
+ private readonly spans;
27
+ private readonly client;
28
+ private readonly tags;
29
+ private readonly metadata;
30
+ constructor({ client, tags, metadata, }?: OpikExporterOptions);
31
+ private getSpanInput;
32
+ private getSpanOutput;
33
+ private getSpanMetadata;
34
+ private getSpanUsage;
35
+ processSpan: ({ otelSpan, parentSpan, trace, }: {
36
+ otelSpan: ReadableSpan;
37
+ parentSpan?: Span;
38
+ trace: Trace;
39
+ }) => Span;
40
+ shutdown: () => Promise<void>;
41
+ forceFlush: () => Promise<void>;
42
+ export: ExportFunction;
43
+ static getSettings(settings: OpikExporterSettings): TelemetrySettings;
44
+ }
45
+
46
+ export { OpikExporter };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import {Opik,logger}from'opik';var E=new Opik,f=class{constructor({client:e=E,tags:t=[],metadata:n={}}={}){this.traces=new Map;this.spans=new Map;this.getSpanInput=e=>{let t={},{attributes:n}=e;return Object.keys(n).forEach(o=>{if(o==="ai.prompt"||o==="gen_ai.request"){let a=C(n[o]);a&&(t={...t,...a});}if(o.startsWith("ai.prompt.")){let a=o.replace("ai.prompt.","");t[a]=m(n[o]);}if(o.startsWith("gen_ai.request.")){let a=o.replace("gen_ai.request.","");t[a]=m(n[o]);}}),Object.keys(t).length>0||("ai.toolCall.name"in n&&(t.toolName=n["ai.toolCall.name"]),"ai.toolCall.args"in n&&(t.args=n["ai.toolCall.args"])),t};this.getSpanOutput=e=>{let{attributes:t}=e;return t["ai.response.text"]?{text:t["ai.response.text"]}:t["ai.toolCall.result"]?{result:t["ai.toolCall.result"]}:t["ai.response.toolCalls"]?{toolCalls:m(t["ai.response.toolCalls"])}:{}};this.getSpanMetadata=e=>{let{attributes:t}=e,n={};return t["gen_ai.response.model"]&&(n.model=t["gen_ai.response.model"]),t["gen_ai.system"]&&(n.system=t["gen_ai.system"]),n};this.getSpanUsage=e=>{let{attributes:t}=e,n={};return "ai.usage.promptTokens"in t&&(n.prompt_tokens=t["ai.usage.promptTokens"]),"gen_ai.usage.input_tokens"in t&&(n.prompt_tokens=t["gen_ai.usage.input_tokens"]),"ai.usage.completionTokens"in t&&(n.completion_tokens=t["ai.usage.completionTokens"]),"gen_ai.usage.output_tokens"in t&&(n.completion_tokens=t["gen_ai.usage.output_tokens"]),("prompt_tokens"in n||"completion_tokens"in n)&&(n.total_tokens=(n.prompt_tokens||0)+(n.completion_tokens||0)),n};this.processSpan=({otelSpan:e,parentSpan:t,trace:n})=>n.span({name:e.name,startTime:new Date(l(e.startTime)),endTime:new Date(l(e.endTime)),parentSpanId:t==null?void 0:t.data.id,input:this.getSpanInput(e),output:this.getSpanOutput(e),metadata:this.getSpanMetadata(e),usage:this.getSpanUsage(e),type:"llm"});this.shutdown=async()=>{await this.client.flush();};this.forceFlush=async()=>{await this.client.flush();};this.export=async(e,t)=>{let n=e.filter(a=>a.instrumentationScope.name==="ai"),i=e.length-n.length;if(i>0&&logger.debug(`Ignored ${i} non-AI SDK spans`),n.length===0){logger.debug("No AI SDK spans found"),t({code:0});return}let o=w(n);logger.debug("Exporting spans",n),Object.entries(o).forEach(([a,c])=>{var S,h;let[r,...u]=c,p=this.client.trace({startTime:new Date(l(r.startTime)),endTime:new Date(l(r.endTime)),name:(h=(S=r.attributes["ai.telemetry.metadata.traceName"])==null?void 0:S.toString())!=null?h:r.name,input:this.getSpanInput(r),output:this.getSpanOutput(r),metadata:{...this.getSpanMetadata(r),...this.metadata},tags:this.tags,usage:this.getSpanUsage(r)});this.traces.set(a,p),u.forEach(g=>{var b,y;let R=this.spans.get((y=(b=g.parentSpanContext)==null?void 0:b.spanId)!=null?y:""),x=this.processSpan({parentSpan:R,otelSpan:g,trace:p});this.spans.set(g.spanContext().spanId,x);});});try{await this.client.flush(),t({code:0});}catch(a){logger.error("Error exporting spans",a),t({code:1,error:a instanceof Error?a:new Error("Unknown error")});}};this.client=e,this.tags=[...t],this.metadata={...n};}static getSettings(e){var n,i,o;let t={...e.metadata};return e.name&&(t.traceName=e.name),{isEnabled:(n=e.isEnabled)!=null?n:true,recordInputs:(i=e.recordInputs)!=null?i:true,recordOutputs:(o=e.recordOutputs)!=null?o:true,functionId:e.functionId,metadata:t}}};function w(s){let e={};return s.forEach(t=>{let n=t.spanContext();e[n.traceId]||(e[n.traceId]=[]),e[n.traceId].push(t);}),Object.entries(e).forEach(([t,n])=>{e[t]=I(n);}),e}function l(s){return s[0]*1e3+s[1]/1e6}function m(s){try{return JSON.parse(s)}catch{return s}}function C(s){if(typeof s=="string")try{let e=JSON.parse(s);if(e!==null&&typeof e=="object"&&!Array.isArray(e))return e}catch{return}}function I(s){var a,c;let e=new Map,t=new Map;for(let r of s){let{spanId:u}=r.spanContext(),p=(c=(a=r.parentSpanContext)==null?void 0:a.spanId)!=null?c:"";e.set(u,r),p&&(t.has(p)||t.set(p,[]),t.get(p).push(r));}let n=s.filter(r=>{var u;return !((u=r.parentSpanContext)!=null&&u.spanId)||!e.has(r.parentSpanContext.spanId)}),i=[],o=[...n];for(;o.length>0;){let r=o.shift();i.push(r);let u=r.spanContext().spanId,p=t.get(u)||[];o.push(...p);}return i}export{f as OpikExporter};
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "opik-vercel",
3
+ "description": "Opik TypeScript and JavaScript SDK integration with Vercel AI SDK",
4
+ "version": "1.0.1",
5
+ "engines": {
6
+ "node": ">=18"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/comet-ml/opik.git",
11
+ "directory": "sdks/typescript/src/opik/integrations/opik-vercel"
12
+ },
13
+ "homepage": "https://www.comet.com/docs/opik/",
14
+ "author": {
15
+ "name": "Comet",
16
+ "email": "support@comet.com",
17
+ "url": "https://github.com/comet-ml"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/comet-ml/opik/issues",
21
+ "email": "support@comet.com"
22
+ },
23
+ "license": "Apache-2.0",
24
+ "keywords": [
25
+ "opik",
26
+ "vercel",
27
+ "vercel-ai-sdk",
28
+ "vercel-integration",
29
+ "sdk",
30
+ "javascript",
31
+ "javascript-sdk",
32
+ "typescript",
33
+ "typescript-sdk",
34
+ "comet"
35
+ ],
36
+ "exports": {
37
+ "./package.json": "./package.json",
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "import": "./dist/index.js",
41
+ "require": "./dist/index.cjs"
42
+ }
43
+ },
44
+ "main": "dist/index.cjs",
45
+ "module": "dist/index.js",
46
+ "types": "dist/index.d.ts",
47
+ "type": "module",
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "watch": "tsup --watch",
51
+ "lint": "eslint '**/*.{ts,tsx}'",
52
+ "typecheck": "tsc --noEmit",
53
+ "format": "prettier --write 'src/**/*.{ts,tsx,js,jsx,json,md}'",
54
+ "test": "vitest run"
55
+ },
56
+ "files": [
57
+ "dist/**/*",
58
+ "README.md"
59
+ ],
60
+ "peerDependencies": {
61
+ "opik": "^1.8.75",
62
+ "@opentelemetry/api": "^1.0.0",
63
+ "@opentelemetry/core": "^2.0.0",
64
+ "@opentelemetry/sdk-node": ">=0.56.0"
65
+ },
66
+ "devDependencies": {
67
+ "@eslint/js": "^9.38.0",
68
+ "@opentelemetry/auto-instrumentations-node": "^0.66.0",
69
+ "ai": "^5.0.76",
70
+ "eslint": "^9.38.0",
71
+ "globals": "^16.4.0",
72
+ "msw": "^2.7.0",
73
+ "prettier": "^3.6.2",
74
+ "tsup": "^8.5.0",
75
+ "typescript": "^5.9.3",
76
+ "typescript-eslint": "^8.46.2",
77
+ "vitest": "^3.2.4",
78
+ "zod": "^3.25.55"
79
+ }
80
+ }