From 19c0f09418c2b9058ea9abb978754a18412f2761 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:29:59 +0100 Subject: [PATCH 1/5] feat: make AI SDK an optional peer dependency - Move 'ai' from peerDependencies to peerDependenciesOptional - Use dynamic import for AI SDK in toAISDK() method - Add proper error handling when AI SDK is not installed - Update all toAISDK() calls to be async with await - Keep type imports for ToolSet (removed at compile time) --- README.md | 4 +- examples/ai-sdk-integration.ts | 2 +- examples/human-in-the-loop.ts | 2 +- examples/meta-tools.ts | 2 +- examples/planning.ts | 2 +- package.json | 4 +- src/tests/json-schema.spec.ts | 8 ++-- src/tests/meta-tools.spec.ts | 6 +-- src/tests/tool.spec.ts | 24 ++++++------ src/tool.ts | 72 +++++++++++++++++++--------------- 10 files changed, 68 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 2fe153b..f437750 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ import { StackOneToolSet } from "@stackone/ai"; const toolset = new StackOneToolSet(); -const aiSdkTools = toolset.getTools("hris_*").toAISDK(); +const aiSdkTools = await toolset.getTools("hris_*").toAISDK(); await generateText({ model: openai("gpt-5"), tools: aiSdkTools, @@ -274,7 +274,7 @@ const metaTools = await tools.metaTools(); const openAITools = metaTools.toOpenAI(); // Use with AI SDK -const aiSdkTools = metaTools.toAISDK(); +const aiSdkTools = await metaTools.toAISDK(); ``` #### Example: Dynamic Tool Discovery with AI SDK diff --git a/examples/ai-sdk-integration.ts b/examples/ai-sdk-integration.ts index 790bb3e..990b2ce 100644 --- a/examples/ai-sdk-integration.ts +++ b/examples/ai-sdk-integration.ts @@ -17,7 +17,7 @@ const aiSdkIntegration = async (): Promise => { const tools = toolset.getStackOneTools('hris_get_*', accountId); // Convert to AI SDK tools - const aiSdkTools = tools.toAISDK(); + const aiSdkTools = await tools.toAISDK(); // Use max steps to automatically call the tool if it's needed const { text } = await generateText({ diff --git a/examples/human-in-the-loop.ts b/examples/human-in-the-loop.ts index 30b584b..6d4e100 100644 --- a/examples/human-in-the-loop.ts +++ b/examples/human-in-the-loop.ts @@ -33,7 +33,7 @@ const humanInTheLoopExample = async (): Promise => { } // Get the AI SDK version of the tool without the execute function - const tool = createEmployeeTool.toAISDK({ + const tool = await createEmployeeTool.toAISDK({ executable: false, }); diff --git a/examples/meta-tools.ts b/examples/meta-tools.ts index adb2659..c77b0b0 100644 --- a/examples/meta-tools.ts +++ b/examples/meta-tools.ts @@ -27,7 +27,7 @@ const metaToolsWithAISDK = async (): Promise => { // Get meta tools for dynamic discovery and execution const metaTools = await allTools.metaTools(); - const aiSdkMetaTools = metaTools.toAISDK(); + const aiSdkMetaTools = await metaTools.toAISDK(); // Use meta tools to dynamically find and execute relevant tools const { text, toolCalls } = await generateText({ diff --git a/examples/planning.ts b/examples/planning.ts index e7f5a07..39b21f6 100644 --- a/examples/planning.ts +++ b/examples/planning.ts @@ -31,7 +31,7 @@ export const planningModule = async (): Promise => { await generateText({ model: openai('gpt-5'), prompt: 'You are a workplace agent, onboard the latest hires to our systems', - tools: onboardWorkflow.toAISDK(), + tools: await onboardWorkflow.toAISDK(), maxSteps: 3, }); }; diff --git a/package.json b/package.json index 5fbe369..44def64 100644 --- a/package.json +++ b/package.json @@ -64,9 +64,11 @@ "zod": "^3.23.8" }, "peerDependencies": { - "ai": "4.x|5.x", "openai": "5.x|6.x" }, + "peerDependenciesOptional": { + "ai": "4.x|5.x" + }, "repository": { "type": "git", "url": "git+https://github.com/StackOneHQ/stackone-ai-node.git" diff --git a/src/tests/json-schema.spec.ts b/src/tests/json-schema.spec.ts index a17237e..d3d67f6 100644 --- a/src/tests/json-schema.spec.ts +++ b/src/tests/json-schema.spec.ts @@ -248,9 +248,9 @@ describe('Schema Validation', () => { }); describe('AI SDK Integration', () => { - it('should convert to AI SDK tool format', () => { + it('should convert to AI SDK tool format', async () => { const tool = createArrayTestTool(); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); expect(aiSdkTool).toBeDefined(); // The AI SDK tool is an object with the tool name as the key @@ -278,7 +278,7 @@ describe('Schema Validation', () => { expect(arrayWithItems.items.type).toBe('string'); }); - it('should handle the problematic nested array case', () => { + it('should handle the problematic nested array case', async () => { const tool = createNestedArrayTestTool(); const openAIFormat = tool.toOpenAI(); const parameters = openAIFormat.function.parameters; @@ -305,7 +305,7 @@ describe('Schema Validation', () => { expect(aiSchema).toBeDefined(); // Generate the SDK tool and verify its structure - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); expect(aiSdkTool).toBeDefined(); const toolObj = aiSdkTool[tool.name]; diff --git a/src/tests/meta-tools.spec.ts b/src/tests/meta-tools.spec.ts index d3e13aa..efdb5db 100644 --- a/src/tests/meta-tools.spec.ts +++ b/src/tests/meta-tools.spec.ts @@ -414,8 +414,8 @@ describe('Meta Search Tools', () => { }); describe('AI SDK format', () => { - it('should convert meta tools to AI SDK format', () => { - const aiSdkTools = metaTools.toAISDK(); + it('should convert meta tools to AI SDK format', async () => { + const aiSdkTools = await metaTools.toAISDK(); expect(aiSdkTools).toHaveProperty('meta_search_tools'); expect(aiSdkTools).toHaveProperty('meta_execute_tool'); @@ -425,7 +425,7 @@ describe('Meta Search Tools', () => { }); it('should execute through AI SDK format', async () => { - const aiSdkTools = metaTools.toAISDK(); + const aiSdkTools = await metaTools.toAISDK(); const result = await aiSdkTools.meta_search_tools.execute?.( { query: 'ATS candidates', limit: 2 }, diff --git a/src/tests/tool.spec.ts b/src/tests/tool.spec.ts index e80e441..4923fbe 100644 --- a/src/tests/tool.spec.ts +++ b/src/tests/tool.spec.ts @@ -94,10 +94,10 @@ describe('StackOneTool', () => { ).toBe('string'); }); - it('should convert to AI SDK tool format', () => { + it('should convert to AI SDK tool format', async () => { const tool = createMockTool(); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); // Test the basic structure expect(aiSdkTool).toBeDefined(); @@ -114,10 +114,10 @@ describe('StackOneTool', () => { expect(schema.properties.id.type).toBe('string'); }); - it('should include execution metadata by default in AI SDK conversion', () => { + it('should include execution metadata by default in AI SDK conversion', async () => { const tool = createMockTool(); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); const execution = aiSdkTool.test_tool.execution; expect(execution).toBeDefined(); @@ -126,15 +126,15 @@ describe('StackOneTool', () => { expect(execution?.headers).toEqual({}); }); - it('should allow disabling execution metadata exposure for AI SDK conversion', () => { + it('should allow disabling execution metadata exposure for AI SDK conversion', async () => { const tool = createMockTool().setExposeExecutionMetadata(false); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); expect(aiSdkTool.test_tool.execution).toBeUndefined(); }); - it('should convert complex parameter types to zod schema', () => { + it('should convert complex parameter types to zod schema', async () => { const complexTool = new BaseTool( 'complex_tool', 'Complex tool', @@ -165,7 +165,7 @@ describe('StackOneTool', () => { } ); - const aiSdkTool = complexTool.toAISDK(); + const aiSdkTool = await complexTool.toAISDK(); // Check that the tool is defined expect(aiSdkTool).toBeDefined(); @@ -190,7 +190,7 @@ describe('StackOneTool', () => { it('should execute AI SDK tool with parameters', async () => { const tool = createMockTool(); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); if (!aiSdkTool.test_tool.execute) { throw new Error('test_tool.execute is undefined'); @@ -212,7 +212,7 @@ describe('StackOneTool', () => { throw mockError; }); - const aiSdkTool = tool.toAISDK(); + const aiSdkTool = await tool.toAISDK(); if (!aiSdkTool.test_tool.execute) { throw new Error('test_tool.execute is undefined'); @@ -300,7 +300,7 @@ describe('Tools', () => { expect(openAITools[1].function.name).toBe('tool2'); }); - it('should convert all tools to AI SDK tools', () => { + it('should convert all tools to AI SDK tools', async () => { const tool1 = createMockTool(); const tool2 = new StackOneTool( 'another_tool', @@ -329,7 +329,7 @@ describe('Tools', () => { const tools = new Tools([tool1, tool2]); - const aiSdkTools = tools.toAISDK(); + const aiSdkTools = await tools.toAISDK(); expect(Object.keys(aiSdkTools).length).toBe(2); expect(aiSdkTools.test_tool).toBeDefined(); diff --git a/src/tool.ts b/src/tool.ts index ec42403..66fd2ed 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,5 +1,4 @@ import * as orama from '@orama/orama'; -import { jsonSchema } from 'ai'; import type { ChatCompletionTool } from 'openai/resources/chat/completions'; import { RequestBuilder } from './modules/requestBuilder'; import type { @@ -181,7 +180,7 @@ export class BaseTool { /** * Convert the tool to AI SDK format */ - toAISDK( + async toAISDK( options: { executable?: boolean; execution?: ToolExecution | false } = { executable: true, } @@ -193,39 +192,48 @@ export class BaseTool { additionalProperties: false, }; - const schemaObject = jsonSchema(schema); - const toolDefinition: Record = { - inputSchema: schemaObject, // v5 - parameters: schemaObject, // v4 (backward compatibility) - description: this.description, - }; + /** AI SDK is optional dependency, import only when needed */ + try { + const { jsonSchema } = await import('ai'); - const executionOption = - options.execution !== undefined - ? options.execution - : this.#exposeExecutionMetadata - ? this.createExecutionMetadata() - : false; + const schemaObject = jsonSchema(schema); + const toolDefinition: Record = { + inputSchema: schemaObject, // v5 + parameters: schemaObject, // v4 (backward compatibility) + description: this.description, + }; - if (executionOption !== false) { - toolDefinition.execution = executionOption; - } + const executionOption = + options.execution !== undefined + ? options.execution + : this.#exposeExecutionMetadata + ? this.createExecutionMetadata() + : false; - if (options.executable ?? true) { - toolDefinition.execute = async (args: Record) => { - try { - return await this.execute(args as JsonDict); - } catch (error) { - return `Error executing tool: ${error instanceof Error ? error.message : String(error)}`; - } + if (executionOption !== false) { + toolDefinition.execution = executionOption; + } + + if (options.executable ?? true) { + toolDefinition.execute = async (args: Record) => { + try { + return await this.execute(args as JsonDict); + } catch (error) { + return `Error executing tool: ${error instanceof Error ? error.message : String(error)}`; + } + }; + } + + return { + [this.name]: { + ...toolDefinition, + }, }; + } catch { + throw new StackOneError( + 'AI SDK is not installed. Please install it with: npm install ai@4.x|5.x or bun add ai@4.x|5.x' + ); } - - return { - [this.name]: { - ...toolDefinition, - }, - }; } } @@ -346,14 +354,14 @@ export class Tools implements Iterable { /** * Convert all tools to AI SDK format */ - toAISDK( + async toAISDK( options: { executable?: boolean; execution?: ToolExecution | false } = { executable: true, } ) { const result: Record = {}; for (const tool of this.tools) { - Object.assign(result, tool.toAISDK(options)); + Object.assign(result, await tool.toAISDK(options)); } return result; } From 707f6469c3842d73257cdb7b4a17588c88bdafb3 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:31:18 +0100 Subject: [PATCH 2/5] refactor: narrow try-catch scope to import only --- src/tool.ts | 70 +++++++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/src/tool.ts b/src/tool.ts index 66fd2ed..c0425ab 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -193,47 +193,49 @@ export class BaseTool { }; /** AI SDK is optional dependency, import only when needed */ + let jsonSchema: typeof import('ai').jsonSchema; try { - const { jsonSchema } = await import('ai'); - - const schemaObject = jsonSchema(schema); - const toolDefinition: Record = { - inputSchema: schemaObject, // v5 - parameters: schemaObject, // v4 (backward compatibility) - description: this.description, - }; + const ai = await import('ai'); + jsonSchema = ai.jsonSchema; + } catch { + throw new StackOneError( + 'AI SDK is not installed. Please install it with: npm install ai@4.x|5.x or bun add ai@4.x|5.x' + ); + } - const executionOption = - options.execution !== undefined - ? options.execution - : this.#exposeExecutionMetadata - ? this.createExecutionMetadata() - : false; + const schemaObject = jsonSchema(schema); + const toolDefinition: Record = { + inputSchema: schemaObject, // v5 + parameters: schemaObject, // v4 (backward compatibility) + description: this.description, + }; - if (executionOption !== false) { - toolDefinition.execution = executionOption; - } + const executionOption = + options.execution !== undefined + ? options.execution + : this.#exposeExecutionMetadata + ? this.createExecutionMetadata() + : false; - if (options.executable ?? true) { - toolDefinition.execute = async (args: Record) => { - try { - return await this.execute(args as JsonDict); - } catch (error) { - return `Error executing tool: ${error instanceof Error ? error.message : String(error)}`; - } - }; - } + if (executionOption !== false) { + toolDefinition.execution = executionOption; + } - return { - [this.name]: { - ...toolDefinition, - }, + if (options.executable ?? true) { + toolDefinition.execute = async (args: Record) => { + try { + return await this.execute(args as JsonDict); + } catch (error) { + return `Error executing tool: ${error instanceof Error ? error.message : String(error)}`; + } }; - } catch { - throw new StackOneError( - 'AI SDK is not installed. Please install it with: npm install ai@4.x|5.x or bun add ai@4.x|5.x' - ); } + + return { + [this.name]: { + ...toolDefinition, + }, + }; } } From 7466d5ed72dd147b3f908a98a9983cec26ac064d Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:32:44 +0100 Subject: [PATCH 3/5] docs: add AI SDK optional installation instructions --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index f437750..d720e7e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,21 @@ yarn add @stackone/ai bun add @stackone/ai ``` +### Optional: AI SDK Integration + +If you plan to use the AI SDK integration (Vercel AI SDK), install it separately: + +```bash +# Using npm +npm install ai + +# Using yarn +yarn add ai + +# Using bun +bun add ai +``` + ## Integrations The OpenAPIToolSet and StackOneToolSet make it super easy to use these APIs as tools in your AI applications. From 3faa2075c404ac9906dbdbadaf17f2bb6a75325a Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:01:15 +0100 Subject: [PATCH 4/5] fix: add await to toAISDK() calls in tests --- src/toolsets/tests/stackone.mcp-fetch.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/toolsets/tests/stackone.mcp-fetch.spec.ts b/src/toolsets/tests/stackone.mcp-fetch.spec.ts index 33f4949..54bfe80 100644 --- a/src/toolsets/tests/stackone.mcp-fetch.spec.ts +++ b/src/toolsets/tests/stackone.mcp-fetch.spec.ts @@ -107,14 +107,14 @@ describe('ToolSet.fetchTools (MCP + RPC integration)', () => { const tool = tools.toArray()[0]; expect(tool.name).toBe('dummy_action'); - const aiTools = tool.toAISDK({ executable: false }); + const aiTools = await tool.toAISDK({ executable: false }); const aiToolDefinition = aiTools.dummy_action; expect(aiToolDefinition).toBeDefined(); expect(aiToolDefinition.description).toBe('Dummy tool'); expect(aiToolDefinition.inputSchema.jsonSchema.properties.foo.type).toBe('string'); expect(aiToolDefinition.execution).toBeUndefined(); - const executableTool = tool.toAISDK().dummy_action; + const executableTool = (await tool.toAISDK()).dummy_action; const result = await executableTool.execute({ foo: 'bar' }); expect(stackOneClient.actions.rpcAction).toHaveBeenCalledWith({ From 26af072f0d393b681285ce54bb5e14eab832a97d Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:05:13 +0100 Subject: [PATCH 5/5] feat: make OpenAI SDK an optional peer dependency - Move both ai and openai to peerDependenciesMeta with optional: true - Update from deprecated peerDependenciesOptional to peerDependenciesMeta - openai is already type-only import, no runtime dependency --- bun.lock | 4 ++++ package.json | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index d47d1d9..937dc06 100644 --- a/bun.lock +++ b/bun.lock @@ -35,6 +35,10 @@ "ai": "4.x|5.x", "openai": "5.x|6.x", }, + "optionalPeers": [ + "ai", + "openai", + ], }, }, "packages": { diff --git a/package.json b/package.json index 44def64..bf111e2 100644 --- a/package.json +++ b/package.json @@ -64,10 +64,16 @@ "zod": "^3.23.8" }, "peerDependencies": { + "ai": "4.x|5.x", "openai": "5.x|6.x" }, - "peerDependenciesOptional": { - "ai": "4.x|5.x" + "peerDependenciesMeta": { + "ai": { + "optional": true + }, + "openai": { + "optional": true + } }, "repository": { "type": "git",