From 855a1867d484c4b7d24114a84e1f2e29b7c6bbe7 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sat, 27 Jul 2024 14:24:06 -0700 Subject: [PATCH 1/4] Add wrapAISDKModel method for Vercel's AI SDK --- js/package.json | 7 +- js/scripts/create-entrypoints.js | 1 + js/src/tests/wrapped_ai_sdk.int.test.ts | 50 ++++++ js/src/traceable.ts | 93 +++++++++++- js/src/wrappers/generic.ts | 72 +++++++++ js/src/wrappers/index.ts | 1 + js/src/wrappers/openai.ts | 70 --------- js/src/wrappers/vercel.ts | 79 ++++++++++ js/yarn.lock | 193 ++++++++++++++++++++++++ 9 files changed, 493 insertions(+), 73 deletions(-) create mode 100644 js/src/tests/wrapped_ai_sdk.int.test.ts create mode 100644 js/src/wrappers/generic.ts create mode 100644 js/src/wrappers/vercel.ts diff --git a/js/package.json b/js/package.json index 8a1598cd6..606c2d3e2 100644 --- a/js/package.json +++ b/js/package.json @@ -101,10 +101,10 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@ai-sdk/anthropic": "^0.0.33", "@babel/preset-env": "^7.22.4", "@faker-js/faker": "^8.4.1", "@jest/globals": "^29.5.0", - "langchain": "^0.2.10", "@langchain/core": "^0.2.17", "@langchain/langgraph": "^0.0.29", "@langchain/openai": "^0.2.5", @@ -112,6 +112,7 @@ "@types/jest": "^29.5.1", "@typescript-eslint/eslint-plugin": "^5.59.8", "@typescript-eslint/parser": "^5.59.8", + "ai": "^3.2.37", "babel-jest": "^29.5.0", "cross-env": "^7.0.3", "dotenv": "^16.1.3", @@ -121,11 +122,13 @@ "eslint-plugin-no-instanceof": "^1.0.1", "eslint-plugin-prettier": "^4.2.1", "jest": "^29.5.0", + "langchain": "^0.2.10", "openai": "^4.38.5", "prettier": "^2.8.8", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "zod": "^3.23.8" }, "peerDependencies": { "@langchain/core": "*", diff --git a/js/scripts/create-entrypoints.js b/js/scripts/create-entrypoints.js index 61d7341ab..a3487f756 100644 --- a/js/scripts/create-entrypoints.js +++ b/js/scripts/create-entrypoints.js @@ -17,6 +17,7 @@ const entrypoints = { wrappers: "wrappers/index", anonymizer: "anonymizer/index", "wrappers/openai": "wrappers/openai", + "wrappers/vercel": "wrappers/vercel", "singletons/traceable": "singletons/traceable", }; diff --git a/js/src/tests/wrapped_ai_sdk.int.test.ts b/js/src/tests/wrapped_ai_sdk.int.test.ts new file mode 100644 index 000000000..80556b61e --- /dev/null +++ b/js/src/tests/wrapped_ai_sdk.int.test.ts @@ -0,0 +1,50 @@ +import { anthropic } from "@ai-sdk/anthropic"; +import { generateObject, generateText, streamObject, streamText } from "ai"; +import { z } from "zod"; +import { wrapAISDKModel } from "../wrappers/vercel.js"; + +test("AI SDK generateText", async () => { + const modelWithTracing = wrapAISDKModel(anthropic("claude-3-haiku-20240307")); + const { text } = await generateText({ + model: modelWithTracing, + prompt: "Write a vegetarian lasagna recipe for 4 people.", + }); + console.log(text); +}); + +test("AI SDK generateObject", async () => { + const modelWithTracing = wrapAISDKModel(anthropic("claude-3-haiku-20240307")); + const { object } = await generateObject({ + model: modelWithTracing, + prompt: "Write a vegetarian lasagna recipe for 4 people.", + schema: z.object({ + ingredients: z.array(z.string()), + }), + }); + console.log(object); +}); + +test("AI SDK streamText", async () => { + const modelWithTracing = wrapAISDKModel(anthropic("claude-3-haiku-20240307")); + const { textStream } = await streamText({ + model: modelWithTracing, + prompt: "Write a vegetarian lasagna recipe for 4 people.", + }); + for await (const chunk of textStream) { + console.log(chunk); + } +}); + +test("AI SDK streamObject", async () => { + const modelWithTracing = wrapAISDKModel(anthropic("claude-3-haiku-20240307")); + const { partialObjectStream } = await streamObject({ + model: modelWithTracing, + prompt: "Write a vegetarian lasagna recipe for 4 people.", + schema: z.object({ + ingredients: z.array(z.string()), + }), + }); + for await (const chunk of partialObjectStream) { + console.log(chunk); + } +}); diff --git a/js/src/traceable.ts b/js/src/traceable.ts index 1f009c680..f663a6de2 100644 --- a/js/src/traceable.ts +++ b/js/src/traceable.ts @@ -279,6 +279,7 @@ export function traceable any>( // eslint-disable-next-line @typescript-eslint/no-explicit-any aggregator?: (args: any[]) => any; argsConfigPath?: [number] | [number, string]; + __finalTracedIteratorKey?: string; /** * Extract invocation parameters from the arguments of the traced function. @@ -294,7 +295,12 @@ export function traceable any>( } ) { type Inputs = Parameters; - const { aggregator, argsConfigPath, ...runTreeConfig } = config ?? {}; + const { + aggregator, + __finalTracedIteratorKey, + argsConfigPath, + ...runTreeConfig + } = config ?? {}; const traceableFunc = ( ...args: Inputs | [RunTree, ...Inputs] | [RunnableConfigLike, ...Inputs] @@ -434,6 +440,47 @@ export function traceable any>( return chunks; } + function tapReadableStreamForTracing( + stream: ReadableStream, + snapshot: ReturnType | undefined + ) { + const reader = stream.getReader(); + let finished = false; + const chunks: unknown[] = []; + + const tappedStream = new ReadableStream({ + async start(controller) { + // eslint-disable-next-line no-constant-condition + while (true) { + const result = await (snapshot + ? snapshot(() => reader.read()) + : reader.read()); + if (result.done) { + finished = true; + await currentRunTree?.end( + handleRunOutputs(await handleChunks(chunks)) + ); + await handleEnd(); + controller.close(); + break; + } + chunks.push(result.value); + controller.enqueue(result.value); + } + }, + async cancel(reason) { + if (!finished) await currentRunTree?.end(undefined, "Cancelled"); + await currentRunTree?.end( + handleRunOutputs(await handleChunks(chunks)) + ); + await handleEnd(); + return reader.cancel(reason); + }, + }); + + return tappedStream; + } + async function* wrapAsyncIteratorForTracing( iterator: AsyncIterator, snapshot: ReturnType | undefined @@ -463,10 +510,14 @@ export function traceable any>( await handleEnd(); } } + function wrapAsyncGeneratorForTracing( iterable: AsyncIterable, snapshot: ReturnType | undefined ) { + if (isReadableStream(iterable)) { + return tapReadableStreamForTracing(iterable, snapshot); + } const iterator = iterable[Symbol.asyncIterator](); const wrappedIterator = wrapAsyncIteratorForTracing(iterator, snapshot); iterable[Symbol.asyncIterator] = () => wrappedIterator; @@ -512,6 +563,25 @@ export function traceable any>( return wrapAsyncGeneratorForTracing(returnValue, snapshot); } + if ( + !Array.isArray(returnValue) && + typeof returnValue === "object" && + returnValue != null && + __finalTracedIteratorKey !== undefined && + isAsyncIterable( + (returnValue as Record)[__finalTracedIteratorKey] + ) + ) { + const snapshot = AsyncLocalStorage.snapshot(); + return { + ...returnValue, + [__finalTracedIteratorKey]: wrapAsyncGeneratorForTracing( + (returnValue as Record)[__finalTracedIteratorKey], + snapshot + ), + }; + } + const tracedPromise = new Promise((resolve, reject) => { Promise.resolve(returnValue) .then( @@ -523,6 +593,27 @@ export function traceable any>( ); } + if ( + !Array.isArray(rawOutput) && + typeof rawOutput === "object" && + rawOutput != null && + __finalTracedIteratorKey !== undefined && + isAsyncIterable( + (rawOutput as Record)[__finalTracedIteratorKey] + ) + ) { + const snapshot = AsyncLocalStorage.snapshot(); + return { + ...rawOutput, + [__finalTracedIteratorKey]: wrapAsyncGeneratorForTracing( + (rawOutput as Record)[ + __finalTracedIteratorKey + ], + snapshot + ), + }; + } + if (isGenerator(wrappedFunc) && isIteratorLike(rawOutput)) { const chunks = gatherAll(rawOutput); diff --git a/js/src/wrappers/generic.ts b/js/src/wrappers/generic.ts new file mode 100644 index 000000000..3b62bc0f8 --- /dev/null +++ b/js/src/wrappers/generic.ts @@ -0,0 +1,72 @@ +import type { RunTreeConfig } from "../index.js"; +import { traceable } from "../traceable.js"; + +export const _wrapClient = ( + sdk: T, + runName: string, + options?: Omit +): T => { + return new Proxy(sdk, { + get(target, propKey, receiver) { + const originalValue = target[propKey as keyof T]; + if (typeof originalValue === "function") { + return traceable(originalValue.bind(target), { + run_type: "llm", + ...options, + name: [runName, propKey.toString()].join("."), + }); + } else if ( + originalValue != null && + !Array.isArray(originalValue) && + // eslint-disable-next-line no-instanceof/no-instanceof + !(originalValue instanceof Date) && + typeof originalValue === "object" + ) { + return _wrapClient( + originalValue, + [runName, propKey.toString()].join("."), + options + ); + } else { + return Reflect.get(target, propKey, receiver); + } + }, + }); +}; + +type WrapSDKOptions = Partial< + RunTreeConfig & { + /** + * @deprecated Use `name` instead. + */ + runName: string; + } +>; + +/** + * Wrap an arbitrary SDK, enabling automatic LangSmith tracing. + * Method signatures are unchanged. + * + * Note that this will wrap and trace ALL SDK methods, not just + * LLM completion methods. If the passed SDK contains other methods, + * we recommend using the wrapped instance for LLM calls only. + * @param sdk An arbitrary SDK instance. + * @param options LangSmith options. + * @returns + */ +export const wrapSDK = ( + sdk: T, + options?: WrapSDKOptions +): T => { + const traceableOptions = options ? { ...options } : undefined; + if (traceableOptions != null) { + delete traceableOptions.runName; + delete traceableOptions.name; + } + + return _wrapClient( + sdk, + options?.name ?? options?.runName ?? sdk.constructor?.name, + traceableOptions + ); +}; diff --git a/js/src/wrappers/index.ts b/js/src/wrappers/index.ts index e8f265647..6ff1385b0 100644 --- a/js/src/wrappers/index.ts +++ b/js/src/wrappers/index.ts @@ -1 +1,2 @@ export * from "./openai.js"; +export { wrapSDK } from "./generic.js"; diff --git a/js/src/wrappers/openai.ts b/js/src/wrappers/openai.ts index 23ff7d77e..05fae4d5d 100644 --- a/js/src/wrappers/openai.ts +++ b/js/src/wrappers/openai.ts @@ -276,73 +276,3 @@ export const wrapOpenAI = ( return openai as PatchedOpenAIClient; }; - -const _wrapClient = ( - sdk: T, - runName: string, - options?: Omit -): T => { - return new Proxy(sdk, { - get(target, propKey, receiver) { - const originalValue = target[propKey as keyof T]; - if (typeof originalValue === "function") { - return traceable(originalValue.bind(target), { - run_type: "llm", - ...options, - name: [runName, propKey.toString()].join("."), - }); - } else if ( - originalValue != null && - !Array.isArray(originalValue) && - // eslint-disable-next-line no-instanceof/no-instanceof - !(originalValue instanceof Date) && - typeof originalValue === "object" - ) { - return _wrapClient( - originalValue, - [runName, propKey.toString()].join("."), - options - ); - } else { - return Reflect.get(target, propKey, receiver); - } - }, - }); -}; - -type WrapSDKOptions = Partial< - RunTreeConfig & { - /** - * @deprecated Use `name` instead. - */ - runName: string; - } ->; - -/** - * Wrap an arbitrary SDK, enabling automatic LangSmith tracing. - * Method signatures are unchanged. - * - * Note that this will wrap and trace ALL SDK methods, not just - * LLM completion methods. If the passed SDK contains other methods, - * we recommend using the wrapped instance for LLM calls only. - * @param sdk An arbitrary SDK instance. - * @param options LangSmith options. - * @returns - */ -export const wrapSDK = ( - sdk: T, - options?: WrapSDKOptions -): T => { - const traceableOptions = options ? { ...options } : undefined; - if (traceableOptions != null) { - delete traceableOptions.runName; - delete traceableOptions.name; - } - - return _wrapClient( - sdk, - options?.name ?? options?.runName ?? sdk.constructor?.name, - traceableOptions - ); -}; diff --git a/js/src/wrappers/vercel.ts b/js/src/wrappers/vercel.ts new file mode 100644 index 000000000..0264f112f --- /dev/null +++ b/js/src/wrappers/vercel.ts @@ -0,0 +1,79 @@ +import type { RunTreeConfig } from "../index.js"; +import { traceable } from "../traceable.js"; +import { _wrapClient } from "./generic.js"; + +/** + * Wrap a Vercel AI SDK model, enabling automatic LangSmith tracing. + * After wrapping a model, you can use it with the Vercel AI SDK Core + * methods as normal. + * + * @example + * ```ts + * import { anthropic } from "@ai-sdk/anthropic"; + * import { streamText } from "ai"; + * import { wrapAISDKModel } from "langsmith/wrappers/vercel"; + * + * const anthropicModel = anthropic("claude-3-haiku-20240307"); + * + * const modelWithTracing = wrapAISDKModel(anthropicModel); + * + * const { textStream } = await streamText({ + * model: modelWithTracing, + * prompt: "Write a vegetarian lasagna recipe for 4 people.", + * }); + * + * for await (const chunk of textStream) { + * console.log(chunk); + * } + * ``` + * @param model An AI SDK model instance. + * @param options LangSmith options. + * @returns + */ +export const wrapAISDKModel = ( + model: T, + options?: Partial +): T => { + if ( + !("doStream" in model) || + typeof model.doStream !== "function" || + !("doGenerate" in model) || + typeof model.doGenerate !== "function" + ) { + throw new Error( + `Received invalid input. This version of wrapAISDKModel only supports Vercel LanguageModelV1 instances.` + ); + } + const runName = options?.name ?? model.constructor?.name; + return new Proxy(model, { + get(target, propKey, receiver) { + const originalValue = target[propKey as keyof T]; + if (typeof originalValue === "function") { + let __finalTracedIteratorKey; + if (propKey === "doStream") { + __finalTracedIteratorKey = "stream"; + } + return traceable(originalValue.bind(target), { + run_type: "llm", + name: runName, + ...options, + __finalTracedIteratorKey, + }); + } else if ( + originalValue != null && + !Array.isArray(originalValue) && + // eslint-disable-next-line no-instanceof/no-instanceof + !(originalValue instanceof Date) && + typeof originalValue === "object" + ) { + return _wrapClient( + originalValue, + [runName, propKey.toString()].join("."), + options + ); + } else { + return Reflect.get(target, propKey, receiver); + } + }, + }); +}; diff --git a/js/yarn.lock b/js/yarn.lock index cf459cb03..4f652fb3a 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -2,6 +2,74 @@ # yarn lockfile v1 +"@ai-sdk/anthropic@^0.0.33": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@ai-sdk/anthropic/-/anthropic-0.0.33.tgz#ab0d690e844965e0f54e6bbc85b91f0a90a4153d" + integrity sha512-xCgerb04tpVOYLL3CmaXUWXa+U8Dt8vflkat4m/0PKQdYGq06JLx/+vaRO8dEz+zU12sQl+3HTPrX53v/wVSxQ== + dependencies: + "@ai-sdk/provider" "0.0.14" + "@ai-sdk/provider-utils" "1.0.5" + +"@ai-sdk/provider-utils@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-1.0.5.tgz#765c60871019ded104d79b4cea0805ba563bb5aa" + integrity sha512-XfOawxk95X3S43arn2iQIFyWGMi0DTxsf9ETc6t7bh91RPWOOPYN1tsmS5MTKD33OGJeaDQ/gnVRzXUCRBrckQ== + dependencies: + "@ai-sdk/provider" "0.0.14" + eventsource-parser "1.1.2" + nanoid "3.3.6" + secure-json-parse "2.7.0" + +"@ai-sdk/provider@0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-0.0.14.tgz#a07569c39a8828aa8312cf1ac6f35ce6ee1b2fce" + integrity sha512-gaQ5Y033nro9iX1YUjEDFDRhmMcEiCk56LJdIUbX5ozEiCNCfpiBpEqrjSp/Gp5RzBS2W0BVxfG7UGW6Ezcrzg== + dependencies: + json-schema "0.4.0" + +"@ai-sdk/react@0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@ai-sdk/react/-/react-0.0.30.tgz#51d586141a81d7f9b76798922b206e8c6faf04dc" + integrity sha512-VnHYRzwhiM4bZdL9DXwJltN8Qnz1MkFdRTa1y7KdmHSJ18ebCNWmPO5XJhnZiQdEXHYmrzZ3WiVt2X6pxK07FA== + dependencies: + "@ai-sdk/provider-utils" "1.0.5" + "@ai-sdk/ui-utils" "0.0.20" + swr "2.2.5" + +"@ai-sdk/solid@0.0.23": + version "0.0.23" + resolved "https://registry.yarnpkg.com/@ai-sdk/solid/-/solid-0.0.23.tgz#712cf1a02bfc337806c5c1b486d16252bec57a15" + integrity sha512-GMojG2PsqwnOGfx7C1MyQPzPBIlC44qn3ykjp9OVnN2Fu47mcFp3QM6gwWoHwNqi7FQDjRy+s/p+8EqYIQcAwg== + dependencies: + "@ai-sdk/provider-utils" "1.0.5" + "@ai-sdk/ui-utils" "0.0.20" + +"@ai-sdk/svelte@0.0.24": + version "0.0.24" + resolved "https://registry.yarnpkg.com/@ai-sdk/svelte/-/svelte-0.0.24.tgz#2519b84a0c104c82d5e48d3b8e9350e9dd4af6cf" + integrity sha512-ZjzzvfYLE01VTO0rOZf6z9sTGhJhe6IYZMxQiM3P+zemufRYe57NDcLYEb6h+2qhvU6Z+k/Q+Nh/spAt0JzGUg== + dependencies: + "@ai-sdk/provider-utils" "1.0.5" + "@ai-sdk/ui-utils" "0.0.20" + sswr "2.1.0" + +"@ai-sdk/ui-utils@0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@ai-sdk/ui-utils/-/ui-utils-0.0.20.tgz#c68968185a7cc33f7d98d13999731e1c7b672cbb" + integrity sha512-6MRWigzXfuxUcAYEFMLP6cLbALJkg12Iz1Sl+wuPMpB6aw7di2ePiTuNakFUYjgP7TNsW4UxzpypBqqJ1KNB0A== + dependencies: + "@ai-sdk/provider-utils" "1.0.5" + secure-json-parse "2.7.0" + +"@ai-sdk/vue@0.0.24": + version "0.0.24" + resolved "https://registry.yarnpkg.com/@ai-sdk/vue/-/vue-0.0.24.tgz#2e72f7e755850ed51540f9a7b25dc6b228a8647a" + integrity sha512-0S+2dVSui6LFgaWoFx+3h5R7GIP9MxdJo63tFuLvgyKr2jmpo5S5kGcWl95vNdzKDqaesAXfOnky+tn5A2d49A== + dependencies: + "@ai-sdk/provider-utils" "1.0.5" + "@ai-sdk/ui-utils" "0.0.20" + swrv "1.0.4" + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz" @@ -1338,6 +1406,17 @@ zod "^3.22.4" zod-to-json-schema "^3.22.3" +"@langchain/openai@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.2.5.tgz#e85b983986a7415ea743d4c854bb0674134334d4" + integrity sha512-gQXS5VBFyAco0jgSnUVan6fYVSIxlffmDaeDGpXrAmz2nQPgiN/h24KYOt2NOZ1zRheRzRuO/CfRagMhyVUaFA== + dependencies: + "@langchain/core" ">=0.2.16 <0.3.0" + js-tiktoken "^1.0.12" + openai "^4.49.1" + zod "^3.22.4" + zod-to-json-schema "^3.22.3" + "@langchain/textsplitters@~0.0.0": version "0.0.2" resolved "https://registry.yarnpkg.com/@langchain/textsplitters/-/textsplitters-0.0.2.tgz#500baa8341fb7fc86fca531a4192665a319504a3" @@ -1367,6 +1446,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@opentelemetry/api@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" + integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== + "@sinclair/typebox@^0.25.16": version "0.25.24" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz" @@ -1444,6 +1528,11 @@ dependencies: "@babel/types" "^7.20.7" +"@types/diff-match-patch@^1.0.36": + version "1.0.36" + resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz#dcef10a69d357fe9d43ac4ff2eca6b85dbf466af" + integrity sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg== + "@types/graceful-fs@^4.1.3": version "4.1.6" resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz" @@ -1658,6 +1747,26 @@ agentkeepalive@^4.2.1: dependencies: humanize-ms "^1.2.1" +ai@^3.2.37: + version "3.2.37" + resolved "https://registry.yarnpkg.com/ai/-/ai-3.2.37.tgz#148ed3124e6b0a01c703597471718520ef1c498d" + integrity sha512-waqKYZOE1zJwKEHx69R4v/xNG0a1o0He8TDgX29hUu36Zk0yrBJoVSlXbC9KoFuxW4eRpt+gZv1kqd1nVc1CGg== + dependencies: + "@ai-sdk/provider" "0.0.14" + "@ai-sdk/provider-utils" "1.0.5" + "@ai-sdk/react" "0.0.30" + "@ai-sdk/solid" "0.0.23" + "@ai-sdk/svelte" "0.0.24" + "@ai-sdk/ui-utils" "0.0.20" + "@ai-sdk/vue" "0.0.24" + "@opentelemetry/api" "1.9.0" + eventsource-parser "1.1.2" + json-schema "0.4.0" + jsondiffpatch "0.6.0" + nanoid "3.3.6" + secure-json-parse "2.7.0" + zod-to-json-schema "3.22.5" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -1971,6 +2080,11 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" @@ -1986,6 +2100,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz" integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== +client-only@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + cliui@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" @@ -2136,6 +2255,11 @@ detect-newline@^3.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +diff-match-patch@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + diff-sequences@^29.4.3: version "29.4.3" resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz" @@ -2452,6 +2576,11 @@ eventemitter3@^4.0.4: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +eventsource-parser@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-1.1.2.tgz#ed6154a4e3dbe7cda9278e5e35d2ffc58b309f89" + integrity sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA== + execa@^5.0.0: version "5.1.1" resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" @@ -3462,6 +3591,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" @@ -3479,6 +3613,15 @@ json5@^2.2.2, json5@^2.2.3: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsondiffpatch@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz#daa6a25bedf0830974c81545568d5f671c82551f" + integrity sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ== + dependencies: + "@types/diff-match-patch" "^1.0.36" + chalk "^5.3.0" + diff-match-patch "^1.0.5" + jsonpointer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" @@ -3705,6 +3848,11 @@ mustache@^4.2.0: resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== +nanoid@3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" @@ -4128,6 +4276,11 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +secure-json-parse@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" + integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== + semver@7.x, semver@^7.3.5, semver@^7.3.7: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" @@ -4140,6 +4293,11 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.6.3: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -4194,6 +4352,13 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +sswr@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sswr/-/sswr-2.1.0.tgz#1eb64cd647cc9e11f871e7f43554abd8c64e1103" + integrity sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ== + dependencies: + swrev "^4.0.0" + stack-utils@^2.0.3: version "2.0.6" resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" @@ -4298,6 +4463,24 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swr@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b" + integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg== + dependencies: + client-only "^0.0.1" + use-sync-external-store "^1.2.0" + +swrev@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/swrev/-/swrev-4.0.0.tgz#83da6983c7ef9d71ac984a9b169fc197cbf18ff8" + integrity sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA== + +swrv@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/swrv/-/swrv-1.0.4.tgz#278b4811ed4acbb1ae46654972a482fd1847e480" + integrity sha512-zjEkcP8Ywmj+xOJW3lIT65ciY/4AL4e/Or7Gj0MzU3zBJNMdJiT8geVZhINavnlHRMMCcJLHhraLTAiDOTmQ9g== + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" @@ -4478,6 +4661,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-sync-external-store@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + uuid@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" @@ -4637,6 +4825,11 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zod-to-json-schema@3.22.5: + version "3.22.5" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.22.5.tgz#3646e81cfc318dbad2a22519e5ce661615418673" + integrity sha512-+akaPo6a0zpVCCseDed504KBJUQpEW5QZw7RMneNmKw+fGaML1Z9tUNLnHHAC8x6dzVRO1eB2oEMyZRnuBZg7Q== + zod-to-json-schema@^3.22.3: version "3.22.4" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.22.4.tgz#f8cc691f6043e9084375e85fb1f76ebafe253d70" From 0fe267804ba0ac2d153a2f02db8d91f1570fb240 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sat, 27 Jul 2024 14:40:05 -0700 Subject: [PATCH 2/4] Use OpenAI in int test for CI --- js/.gitignore | 4 ++++ js/package.json | 15 ++++++++++++++- js/src/tests/wrapped_ai_sdk.int.test.ts | 10 +++++----- js/tsconfig.json | 1 + js/yarn.lock | 8 ++++---- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/js/.gitignore b/js/.gitignore index 902b3f759..e758389d2 100644 --- a/js/.gitignore +++ b/js/.gitignore @@ -71,6 +71,10 @@ Chinook_Sqlite.sql /wrappers/openai.js /wrappers/openai.d.ts /wrappers/openai.d.cts +/wrappers/vercel.cjs +/wrappers/vercel.js +/wrappers/vercel.d.ts +/wrappers/vercel.d.cts /singletons/traceable.cjs /singletons/traceable.js /singletons/traceable.d.ts diff --git a/js/package.json b/js/package.json index 606c2d3e2..279a507ca 100644 --- a/js/package.json +++ b/js/package.json @@ -45,6 +45,10 @@ "wrappers/openai.js", "wrappers/openai.d.ts", "wrappers/openai.d.cts", + "wrappers/vercel.cjs", + "wrappers/vercel.js", + "wrappers/vercel.d.ts", + "wrappers/vercel.d.cts", "singletons/traceable.cjs", "singletons/traceable.js", "singletons/traceable.d.ts", @@ -101,7 +105,7 @@ "uuid": "^9.0.0" }, "devDependencies": { - "@ai-sdk/anthropic": "^0.0.33", + "@ai-sdk/openai": "^0.0.40", "@babel/preset-env": "^7.22.4", "@faker-js/faker": "^8.4.1", "@jest/globals": "^29.5.0", @@ -252,6 +256,15 @@ "import": "./wrappers/openai.js", "require": "./wrappers/openai.cjs" }, + "./wrappers/vercel": { + "types": { + "import": "./wrappers/vercel.d.ts", + "require": "./wrappers/vercel.d.cts", + "default": "./wrappers/vercel.d.ts" + }, + "import": "./wrappers/vercel.js", + "require": "./wrappers/vercel.cjs" + }, "./singletons/traceable": { "types": { "import": "./singletons/traceable.d.ts", diff --git a/js/src/tests/wrapped_ai_sdk.int.test.ts b/js/src/tests/wrapped_ai_sdk.int.test.ts index 80556b61e..7553423db 100644 --- a/js/src/tests/wrapped_ai_sdk.int.test.ts +++ b/js/src/tests/wrapped_ai_sdk.int.test.ts @@ -1,10 +1,10 @@ -import { anthropic } from "@ai-sdk/anthropic"; +import { openai } from "@ai-sdk/openai"; import { generateObject, generateText, streamObject, streamText } from "ai"; import { z } from "zod"; import { wrapAISDKModel } from "../wrappers/vercel.js"; test("AI SDK generateText", async () => { - const modelWithTracing = wrapAISDKModel(anthropic("claude-3-haiku-20240307")); + const modelWithTracing = wrapAISDKModel(openai("gpt-4o-mini")); const { text } = await generateText({ model: modelWithTracing, prompt: "Write a vegetarian lasagna recipe for 4 people.", @@ -13,7 +13,7 @@ test("AI SDK generateText", async () => { }); test("AI SDK generateObject", async () => { - const modelWithTracing = wrapAISDKModel(anthropic("claude-3-haiku-20240307")); + const modelWithTracing = wrapAISDKModel(openai("gpt-4o-mini")); const { object } = await generateObject({ model: modelWithTracing, prompt: "Write a vegetarian lasagna recipe for 4 people.", @@ -25,7 +25,7 @@ test("AI SDK generateObject", async () => { }); test("AI SDK streamText", async () => { - const modelWithTracing = wrapAISDKModel(anthropic("claude-3-haiku-20240307")); + const modelWithTracing = wrapAISDKModel(openai("gpt-4o-mini")); const { textStream } = await streamText({ model: modelWithTracing, prompt: "Write a vegetarian lasagna recipe for 4 people.", @@ -36,7 +36,7 @@ test("AI SDK streamText", async () => { }); test("AI SDK streamObject", async () => { - const modelWithTracing = wrapAISDKModel(anthropic("claude-3-haiku-20240307")); + const modelWithTracing = wrapAISDKModel(openai("gpt-4o-mini")); const { partialObjectStream } = await streamObject({ model: modelWithTracing, prompt: "Write a vegetarian lasagna recipe for 4 people.", diff --git a/js/tsconfig.json b/js/tsconfig.json index 92b1a3026..ab24d6247 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -42,6 +42,7 @@ "src/wrappers/index.ts", "src/anonymizer/index.ts", "src/wrappers/openai.ts", + "src/wrappers/vercel.ts", "src/singletons/traceable.ts" ] } diff --git a/js/yarn.lock b/js/yarn.lock index 4f652fb3a..28195c859 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@ai-sdk/anthropic@^0.0.33": - version "0.0.33" - resolved "https://registry.yarnpkg.com/@ai-sdk/anthropic/-/anthropic-0.0.33.tgz#ab0d690e844965e0f54e6bbc85b91f0a90a4153d" - integrity sha512-xCgerb04tpVOYLL3CmaXUWXa+U8Dt8vflkat4m/0PKQdYGq06JLx/+vaRO8dEz+zU12sQl+3HTPrX53v/wVSxQ== +"@ai-sdk/openai@^0.0.40": + version "0.0.40" + resolved "https://registry.yarnpkg.com/@ai-sdk/openai/-/openai-0.0.40.tgz#227df69c8edf8b26b17f78ae55daa03e58a58870" + integrity sha512-9Iq1UaBHA5ZzNv6j3govuKGXrbrjuWvZIgWNJv4xzXlDMHu9P9hnqlBr/Aiay54WwCuTVNhTzAUTfFgnTs2kbQ== dependencies: "@ai-sdk/provider" "0.0.14" "@ai-sdk/provider-utils" "1.0.5" From c60b0ce993aad2e1264c725f672df942f28241ca Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Tue, 30 Jul 2024 12:05:48 -0700 Subject: [PATCH 3/4] Add aggregation --- js/src/tests/wrapped_ai_sdk.int.test.ts | 29 ++++++++++++++++++++++- js/src/wrappers/vercel.ts | 31 +++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/js/src/tests/wrapped_ai_sdk.int.test.ts b/js/src/tests/wrapped_ai_sdk.int.test.ts index 7553423db..ddc221741 100644 --- a/js/src/tests/wrapped_ai_sdk.int.test.ts +++ b/js/src/tests/wrapped_ai_sdk.int.test.ts @@ -1,5 +1,11 @@ import { openai } from "@ai-sdk/openai"; -import { generateObject, generateText, streamObject, streamText } from "ai"; +import { + generateObject, + generateText, + streamObject, + streamText, + tool, +} from "ai"; import { z } from "zod"; import { wrapAISDKModel } from "../wrappers/vercel.js"; @@ -12,6 +18,27 @@ test("AI SDK generateText", async () => { console.log(text); }); +test("AI SDK generateText with a tool", async () => { + const modelWithTracing = wrapAISDKModel(openai("gpt-4o-mini")); + const { text } = await generateText({ + model: modelWithTracing, + prompt: + "Write a vegetarian lasagna recipe for 4 people. Get ingredients first.", + tools: { + getIngredients: tool({ + description: "get a list of ingredients", + parameters: z.object({ + ingredients: z.array(z.string()), + }), + execute: async () => + JSON.stringify(["pasta", "tomato", "cheese", "onions"]), + }), + }, + maxToolRoundtrips: 2, + }); + console.log(text); +}); + test("AI SDK generateObject", async () => { const modelWithTracing = wrapAISDKModel(openai("gpt-4o-mini")); const { object } = await generateObject({ diff --git a/js/src/wrappers/vercel.ts b/js/src/wrappers/vercel.ts index 0264f112f..1cd706d4e 100644 --- a/js/src/wrappers/vercel.ts +++ b/js/src/wrappers/vercel.ts @@ -50,14 +50,45 @@ export const wrapAISDKModel = ( const originalValue = target[propKey as keyof T]; if (typeof originalValue === "function") { let __finalTracedIteratorKey; + let aggregator; if (propKey === "doStream") { __finalTracedIteratorKey = "stream"; + aggregator = (chunks: any[]) => { + return chunks.reduce( + (aggregated, chunk) => { + console.log(chunk); + if (chunk.type === "text-delta") { + return { + ...aggregated, + text: aggregated.text + chunk.textDelta, + }; + } else if (chunk.type === "tool-call") { + return { + ...aggregated, + ...chunk, + }; + } else if (chunk.type === "finish") { + return { + ...aggregated, + usage: chunk.usage, + finishReason: chunk.finishReason, + }; + } else { + return aggregated; + } + }, + { + text: "", + } + ); + }; } return traceable(originalValue.bind(target), { run_type: "llm", name: runName, ...options, __finalTracedIteratorKey, + aggregator, }); } else if ( originalValue != null && From 31cb73b9c481cdd483e8bf99ee004a63c0d1affb Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Tue, 30 Jul 2024 12:09:06 -0700 Subject: [PATCH 4/4] Nits --- js/src/traceable.ts | 2 +- js/src/wrappers/vercel.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/js/src/traceable.ts b/js/src/traceable.ts index f663a6de2..dc43af0d3 100644 --- a/js/src/traceable.ts +++ b/js/src/traceable.ts @@ -297,8 +297,8 @@ export function traceable any>( type Inputs = Parameters; const { aggregator, - __finalTracedIteratorKey, argsConfigPath, + __finalTracedIteratorKey, ...runTreeConfig } = config ?? {}; diff --git a/js/src/wrappers/vercel.ts b/js/src/wrappers/vercel.ts index 1cd706d4e..dc022d7c8 100644 --- a/js/src/wrappers/vercel.ts +++ b/js/src/wrappers/vercel.ts @@ -56,7 +56,6 @@ export const wrapAISDKModel = ( aggregator = (chunks: any[]) => { return chunks.reduce( (aggregated, chunk) => { - console.log(chunk); if (chunk.type === "text-delta") { return { ...aggregated,