From acac6b99f0f00a6e39e2553dd6262666317a2ef9 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:01:19 +0000 Subject: [PATCH 01/10] refactor(types): replace json-schema library with custom JSONSchema type - Define comprehensive JSONSchema interface in types.ts covering all JSON Schema draft-07 properties - Add toJsonSchema() method to BaseTool and Tools classes for framework-agnostic schema export - Refactor toOpenAI(), toAnthropic(), and toOpenAIResponses() to use toJsonSchema() internally, reducing code duplication - Use type-fest OverrideProperties for ObjectJSONSchema type to ensure type: 'object' is always set - Remove json-schema and @types/json-schema dependencies This reduces external dependencies while providing a more flexible JSONSchema type that works seamlessly with OpenAI, Anthropic, and other LLM providers. --- package.json | 2 -- pnpm-lock.yaml | 20 ----------------- pnpm-workspace.yaml | 2 -- src/tool.test.ts | 21 ++++++++++-------- src/tool.ts | 49 ++++++++++++++++++++++++++++++------------ src/types.ts | 52 +++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 96 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 0b11c50..e2e5147 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,11 @@ "@modelcontextprotocol/sdk": "catalog:prod", "@orama/orama": "catalog:prod", "defu": "catalog:prod", - "json-schema": "catalog:prod", "zod": "catalog:dev" }, "devDependencies": { "@ai-sdk/provider-utils": "catalog:dev", "@hono/mcp": "catalog:dev", - "@types/json-schema": "catalog:dev", "@types/node": "catalog:dev", "@typescript/native-preview": "catalog:dev", "@vitest/coverage-v8": "catalog:dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 935fb0b..07a8f3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,9 +18,6 @@ catalogs: '@hono/mcp': specifier: ^0.1.4 version: 0.1.5 - '@types/json-schema': - specifier: ^7.0.15 - version: 7.0.15 '@types/node': specifier: ^22.13.5 version: 22.19.1 @@ -89,9 +86,6 @@ catalogs: defu: specifier: ^6.1.4 version: 6.1.4 - json-schema: - specifier: ^0.4.0 - version: 0.4.0 importers: .: @@ -108,9 +102,6 @@ importers: defu: specifier: catalog:prod version: 6.1.4 - json-schema: - specifier: catalog:prod - version: 0.4.0 zod: specifier: catalog:dev version: 4.1.13 @@ -121,9 +112,6 @@ importers: '@hono/mcp': specifier: catalog:dev version: 0.1.5(@modelcontextprotocol/sdk@1.24.3(zod@4.1.13))(hono@4.10.7) - '@types/json-schema': - specifier: catalog:dev - version: 7.0.15 '@types/node': specifier: catalog:dev version: 22.19.1 @@ -1676,12 +1664,6 @@ packages: integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, } - '@types/json-schema@7.0.15': - resolution: - { - integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, - } - '@types/node@22.19.1': resolution: { @@ -4330,8 +4312,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/json-schema@7.0.15': {} - '@types/node@22.19.1': dependencies: undici-types: 6.21.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f2c4844..49c0e40 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,7 +10,6 @@ catalogs: '@clack/prompts': ^0.11.0 '@ai-sdk/provider-utils': ^3.0.18 '@hono/mcp': ^0.1.4 - '@types/json-schema': ^7.0.15 '@types/node': ^22.13.5 '@typescript/native-preview': ^7.0.0-dev.20251209.1 '@vitest/coverage-v8': ^4.0.15 @@ -35,7 +34,6 @@ catalogs: '@modelcontextprotocol/sdk': ^1.24.3 '@orama/orama': ^3.1.11 defu: ^6.1.4 - json-schema: ^0.4.0 enablePrePostScripts: true diff --git a/src/tool.test.ts b/src/tool.test.ts index 9a70e44..b1961da 100644 --- a/src/tool.test.ts +++ b/src/tool.test.ts @@ -1,7 +1,11 @@ import { jsonSchema } from 'ai'; -import type { JSONSchema7 } from 'json-schema'; import { BaseTool, type MetaToolSearchResult, StackOneTool, Tools } from './tool'; -import { type ExecuteConfig, ParameterLocation, type ToolParameters } from './types'; +import { + type ExecuteConfig, + type JSONSchema, + ParameterLocation, + type ToolParameters, +} from './types'; import { StackOneAPIError } from './utils/errors'; // Create a mock tool for testing @@ -1107,11 +1111,10 @@ describe('Schema Validation', () => { ); const parameters = tool.toOpenAI().function.parameters; - expect(parameters).toBeDefined(); - const properties = parameters?.properties as Record; + const properties = parameters?.properties as Record; expect(properties.arrayWithItems.items).toBeDefined(); - expect((properties.arrayWithItems.items as JSONSchema7).type).toBe('number'); + expect((properties.arrayWithItems.items as JSONSchema).type).toBe('number'); }); it('should handle nested object structure', () => { @@ -1144,7 +1147,7 @@ describe('Schema Validation', () => { const parameters = tool.toOpenAI().function.parameters; expect(parameters).toBeDefined(); - const properties = parameters?.properties as Record; + const properties = parameters?.properties as Record; const nestedObject = properties.nestedObject; expect(nestedObject.type).toBe('object'); @@ -1185,7 +1188,7 @@ describe('Schema Validation', () => { // @ts-ignore - jsonSchema is available on Schema wrapper from ai sdk const arrayWithItems = toolObj.inputSchema.jsonSchema.properties?.arrayWithItems; expect(arrayWithItems?.type).toBe('array'); - expect((arrayWithItems?.items as JSONSchema7)?.type).toBe('string'); + expect((arrayWithItems?.items as JSONSchema)?.type).toBe('string'); }); it('should handle nested filter object for AI SDK', async () => { @@ -1220,14 +1223,14 @@ describe('Schema Validation', () => { const parameters = tool.toOpenAI().function.parameters; expect(parameters).toBeDefined(); - const aiSchema = jsonSchema(parameters as JSONSchema7); + const aiSchema = jsonSchema(parameters as JSONSchema); expect(aiSchema).toBeDefined(); const aiSdkTool = await tool.toAISDK(); // TODO: Remove ts-ignore once AISDKToolDefinition properly types inputSchema.jsonSchema // @ts-ignore - jsonSchema is available on Schema wrapper from ai sdk const filterProp = aiSdkTool[tool.name].inputSchema.jsonSchema.properties?.filter as - | (JSONSchema7 & { properties: Record }) + | (JSONSchema & { properties: Record }) | undefined; expect(filterProp?.type).toBe('object'); diff --git a/src/tool.ts b/src/tool.ts index 221e121..03b4fbf 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -2,6 +2,7 @@ import type { Tool as AnthropicTool } from '@anthropic-ai/sdk/resources'; import * as orama from '@orama/orama'; import type { ChatCompletionFunctionTool } from 'openai/resources/chat/completions'; import type { FunctionTool as OpenAIResponsesFunctionTool } from 'openai/resources/responses/responses'; +import type { OverrideProperties } from 'type-fest'; import { DEFAULT_HYBRID_ALPHA } from './consts'; import { RequestBuilder } from './requestBuilder'; import type { @@ -13,14 +14,22 @@ import type { Experimental_ToolCreationOptions, HttpExecuteConfig, JsonDict, + JSONSchema, LocalExecuteConfig, RpcExecuteConfig, ToolExecution, ToolParameters, } from './types'; + import { StackOneError } from './utils/errors'; import { TfidfIndex } from './utils/tfidf-index'; +/** + * JSON Schema with type narrowed to 'object' + * Used for tool parameter schemas which are always objects + */ +type ObjectJSONSchema = OverrideProperties; + /** * Base class for all tools. Provides common functionality for executing API calls * and converting to various formats (OpenAI, AI SDK) @@ -165,6 +174,18 @@ export class BaseTool { } } + /** + * Convert the tool parameters to a pure JSON Schema format + * This is framework-agnostic and can be used with any LLM that accepts JSON Schema + */ + toJsonSchema(): ObjectJSONSchema { + return { + type: 'object', + properties: this.parameters.properties, + required: this.parameters.required, + }; + } + /** * Convert the tool to OpenAI Chat Completions API format */ @@ -174,11 +195,7 @@ export class BaseTool { function: { name: this.name, description: this.description, - parameters: { - type: 'object', - properties: this.parameters.properties, - required: this.parameters.required, - }, + parameters: this.toJsonSchema(), }, }; } @@ -191,11 +208,7 @@ export class BaseTool { return { name: this.name, description: this.description, - input_schema: { - type: 'object', - properties: this.parameters.properties, - required: this.parameters.required, - }, + input_schema: this.toJsonSchema(), }; } @@ -211,9 +224,7 @@ export class BaseTool { description: this.description, strict, parameters: { - type: 'object', - properties: this.parameters.properties, - required: this.parameters.required, + ...this.toJsonSchema(), ...(strict ? { additionalProperties: false } : {}), }, }; @@ -389,6 +400,18 @@ export class Tools implements Iterable { return this.tools.filter((tool): tool is StackOneTool => tool instanceof StackOneTool); } + /** + * Convert all tools to pure JSON Schema format + * Returns an array of objects with name, description, and schema + */ + toJsonSchema(): Array<{ name: string; description: string; parameters: JSONSchema }> { + return this.tools.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: tool.toJsonSchema(), + })); + } + /** * Convert all tools to OpenAI Chat Completions API format */ diff --git a/src/types.ts b/src/types.ts index 0645de4..4c256d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,7 +4,6 @@ import type { Tool } from '@ai-sdk/provider-utils'; import type { ToolSet } from 'ai'; -import type { JSONSchema7, JSONSchema7Definition } from 'json-schema'; import type { ValueOf } from 'type-fest'; /** @@ -17,15 +16,60 @@ export type JsonDict = Record; */ type Headers = Record; +/** + * JSON Schema type for defining tool input/output schemas as raw JSON Schema objects. + * This allows tools to be defined without Zod when you have JSON Schema definitions available. + */ +export interface JSONSchema { + type?: string | Array; + properties?: Record; + items?: JSONSchema | Array; + required?: Array; + enum?: Array; + const?: unknown; + description?: string; + default?: unknown; + $ref?: string; + $defs?: Record; + definitions?: Record; + allOf?: Array; + anyOf?: Array; + oneOf?: Array; + not?: JSONSchema; + if?: JSONSchema; + then?: JSONSchema; + else?: JSONSchema; + minimum?: number; + maximum?: number; + exclusiveMinimum?: number; + exclusiveMaximum?: number; + minLength?: number; + maxLength?: number; + pattern?: string; + format?: string; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + additionalProperties?: boolean | JSONSchema; + additionalItems?: boolean | JSONSchema; + patternProperties?: Record; + propertyNames?: JSONSchema; + minProperties?: number; + maxProperties?: number; + title?: string; + examples?: Array; + [key: string]: unknown; // Allow additional properties for extensibility +} + /** * JSON Schema properties type */ -export type JsonSchemaProperties = Record; +export type JsonSchemaProperties = Record; /** - * JSON Schema type + * JSON Schema type union */ -type JsonSchemaType = JSONSchema7['type']; +type JsonSchemaType = JSONSchema['type']; /** * EXPERIMENTAL: Function to override the tool schema at creation time From ddd849526db2783a3d0c6dc4d4c58cc2e245c722 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:03:30 +0000 Subject: [PATCH 02/10] refactor(tool): use toJsonSchema() in toAISDK method Consolidate schema generation by reusing toJsonSchema() instead of manually constructing the schema object. This reduces duplication and ensures consistency across all conversion methods. --- src/tool.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tool.ts b/src/tool.ts index 03b4fbf..aab6638 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -239,9 +239,7 @@ export class BaseTool { }, ): Promise { const schema = { - type: 'object' as const, - properties: this.parameters.properties || {}, - required: this.parameters.required || [], + ...this.toJsonSchema(), additionalProperties: false, }; From 5e9caa2ec9ee579947531f0b361af077dd1ad121 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:07:25 +0000 Subject: [PATCH 03/10] refactor(tool): add type-safe schema validation for AI SDK - Import JSONSchema7 type from @ai-sdk/provider as AISDKJSONSchema - Use satisfies AISDKJSONSchema to validate schema at compile time - Move jsonSchema type import to top-level for cleaner code - Add @ai-sdk/provider as dev dependency for type checking --- package.json | 1 + pnpm-lock.yaml | 57 +++++++++++++++++++++++++-------------------- pnpm-workspace.yaml | 5 +++- src/tool.ts | 10 ++++---- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index e2e5147..ce62cb3 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "zod": "catalog:dev" }, "devDependencies": { + "@ai-sdk/provider": "catalog:", "@ai-sdk/provider-utils": "catalog:dev", "@hono/mcp": "catalog:dev", "@types/node": "catalog:dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07a8f3d..67bd065 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,10 @@ settings: excludeLinksFromLockfile: false catalogs: + default: + '@ai-sdk/provider': + specifier: ^2.0.0 + version: 2.0.0 dev: '@ai-sdk/openai': specifier: ^2.0.80 @@ -106,6 +110,9 @@ importers: specifier: catalog:dev version: 4.1.13 devDependencies: + '@ai-sdk/provider': + specifier: 'catalog:' + version: 2.0.0 '@ai-sdk/provider-utils': specifier: catalog:dev version: 3.0.18(zod@4.1.13) @@ -138,7 +145,7 @@ importers: version: 2.12.3(@types/node@22.19.1)(typescript@5.9.3) node: specifier: runtime:^24.11.0 - version: runtime:24.11.1 + version: runtime:24.12.0 openai: specifier: catalog:peer version: 6.9.1(zod@4.1.13) @@ -190,7 +197,7 @@ importers: version: 5.0.108(zod@4.1.13) node: specifier: runtime:^24.11.0 - version: runtime:24.11.1 + version: runtime:24.12.0 openai: specifier: catalog:peer version: 6.9.1(zod@4.1.13) @@ -2771,94 +2778,94 @@ packages: } engines: { node: '>= 0.6' } - node@runtime:24.11.1: + node@runtime:24.12.0: resolution: type: variations variants: - resolution: archive: tarball bin: bin/node - integrity: sha256-mLqRmgOQ2MQi1LsxBexbd3I89UFDtMHkudd2kPoHUgY= + integrity: sha256-MfPQZbuE8GnlIuVdDTA1vTZMul2KIiM/9oAF0oHou3g= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-aix-ppc64.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-aix-ppc64.tar.gz targets: - cpu: ppc64 os: aix - resolution: archive: tarball bin: bin/node - integrity: sha256-sFqjpm7+aAAj+TC9WvP9u9VCeU2lZEyirXEdaMvU3DU= + integrity: sha256-MZ8iGtxeRP8O1X6KRBsihPArjcb8h7jrkqapNkP9gIA= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-darwin-arm64.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-darwin-arm64.tar.gz targets: - cpu: arm64 os: darwin - resolution: archive: tarball bin: bin/node - integrity: sha256-CWCBttb83T9boPXx1EpH6DA3rS546tomZxwlL+ZN0RE= + integrity: sha256-uC6kxi/QjiUMq1nWJeddd8xbCj1gxmmOvuRUXIihacU= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-darwin-x64.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-darwin-x64.tar.gz targets: - cpu: x64 os: darwin - resolution: archive: tarball bin: bin/node - integrity: sha256-Dck+xceYsNNH8GjbbSBdA96ppxdl5qU5IraCuRJl1x8= + integrity: sha256-myou65io6zc2EiTiodBgMArS3RQ69Y39sW3nhd8PEig= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-arm64.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-linux-arm64.tar.gz targets: - cpu: arm64 os: linux - resolution: archive: tarball bin: bin/node - integrity: sha256-zUFAfzNS3i8GbqJsXF0OqbY2I3TWthg4Wp8una0iBhY= + integrity: sha256-Zux5tNZPQQmu34IhCHFdC2CXEY35FZwvYyFHfaTqF6o= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-ppc64le.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-linux-ppc64le.tar.gz targets: - cpu: ppc64le os: linux - resolution: archive: tarball bin: bin/node - integrity: sha256-XUyLyl+PJZP5CB3uOYNHYOhaFvphyVDz6G7IWZbwBVA= + integrity: sha256-jclgolVdsap3/RMcJb5XG594RLyLJ454cyufWA/n1YA= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-s390x.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-linux-s390x.tar.gz targets: - cpu: s390x os: linux - resolution: archive: tarball bin: bin/node - integrity: sha256-WKX/XMjyIA5Fi+oi4ynVwZlKobER1JnKRuwkEdWCOco= + integrity: sha256-YVkifgr318PGuy+pAEUrBKbLiEGnAqeazGEyCdcLBNA= type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-x64.tar.gz + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-linux-x64.tar.gz targets: - cpu: x64 os: linux - resolution: archive: zip bin: node.exe - integrity: sha256-zp7k5Ufr3/NVvrSOMJsWbCTfa+ApHJ6vEDzhXz3p5bQ= - prefix: node-v24.11.1-win-arm64 + integrity: sha256-sF5+Bm+BPTWtPNnCTu2u4HTAEqx+AAcSl2CP3S6UiuM= + prefix: node-v24.12.0-win-arm64 type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-win-arm64.zip + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-win-arm64.zip targets: - cpu: arm64 os: win32 - resolution: archive: zip bin: node.exe - integrity: sha256-U1WubXxJ7dz959NKw0hoIGAKgxv4HcO9ylyNtqm7DnY= - prefix: node-v24.11.1-win-x64 + integrity: sha256-nBJfYa6Ue1LneQlYMPnKwmeEagQ+9xkhg8hAFqqtKBI= + prefix: node-v24.12.0-win-x64 type: binary - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-win-x64.zip + url: https://nodejs.org/download/release/v24.12.0/node-v24.12.0-win-x64.zip targets: - cpu: x64 os: win32 - version: 24.11.1 + version: 24.12.0 hasBin: true object-assign@4.1.1: @@ -4981,7 +4988,7 @@ snapshots: negotiator@1.0.0: {} - node@runtime:24.11.1: {} + node@runtime:24.12.0: {} object-assign@4.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 49c0e40..cdb88da 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,13 +2,16 @@ packages: - . - examples +catalog: + '@ai-sdk/provider': ^2.0.0 + catalogMode: strict catalogs: dev: '@ai-sdk/openai': ^2.0.80 - '@clack/prompts': ^0.11.0 '@ai-sdk/provider-utils': ^3.0.18 + '@clack/prompts': ^0.11.0 '@hono/mcp': ^0.1.4 '@types/node': ^22.13.5 '@typescript/native-preview': ^7.0.0-dev.20251209.1 diff --git a/src/tool.ts b/src/tool.ts index aab6638..6cd7ef8 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,3 +1,5 @@ +import type { jsonSchema } from 'ai'; +import type { JSONSchema7 as AISDKJSONSchema } from '@ai-sdk/provider'; import type { Tool as AnthropicTool } from '@anthropic-ai/sdk/resources'; import * as orama from '@orama/orama'; import type { ChatCompletionFunctionTool } from 'openai/resources/chat/completions'; @@ -241,20 +243,20 @@ export class BaseTool { const schema = { ...this.toJsonSchema(), additionalProperties: false, - }; + } satisfies AISDKJSONSchema; /** AI SDK is optional dependency, import only when needed */ - let jsonSchema: typeof import('ai').jsonSchema; + let jsonSchemaFn: typeof jsonSchema; try { const ai = await import('ai'); - jsonSchema = ai.jsonSchema; + jsonSchemaFn = ai.jsonSchema; } catch { throw new StackOneError( 'AI SDK is not installed. Please install it with: npm install ai@4.x|5.x or pnpm add ai@4.x|5.x', ); } - const schemaObject = jsonSchema(schema); + const schemaObject = jsonSchemaFn(schema); // TODO: Remove ts-ignore once AISDKToolDefinition properly types the inputSchema and parameters // We avoid defining our own types as much as possible, so we use the AI SDK Tool type // but need to suppress errors for backward compatibility properties From 3ce894951a1884079afc1fc1811f678a927bf23b Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:10:51 +0000 Subject: [PATCH 04/10] refactor(utils): add tryImport utility for optional dependencies - Create tryImport() helper function for dynamic imports with friendly error messages when optional dependencies are not installed - Refactor toAISDK() to use tryImport() for cleaner code - Remove unused jsonSchema type import from top-level --- src/tool.ts | 18 ++++++------------ src/utils/try-import.ts | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 src/utils/try-import.ts diff --git a/src/tool.ts b/src/tool.ts index 6cd7ef8..7c85e12 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,4 +1,3 @@ -import type { jsonSchema } from 'ai'; import type { JSONSchema7 as AISDKJSONSchema } from '@ai-sdk/provider'; import type { Tool as AnthropicTool } from '@anthropic-ai/sdk/resources'; import * as orama from '@orama/orama'; @@ -25,6 +24,7 @@ import type { import { StackOneError } from './utils/errors'; import { TfidfIndex } from './utils/tfidf-index'; +import { tryImport } from './utils/try-import'; /** * JSON Schema with type narrowed to 'object' @@ -246,17 +246,11 @@ export class BaseTool { } satisfies AISDKJSONSchema; /** AI SDK is optional dependency, import only when needed */ - let jsonSchemaFn: typeof jsonSchema; - try { - const ai = await import('ai'); - jsonSchemaFn = ai.jsonSchema; - } catch { - throw new StackOneError( - 'AI SDK is not installed. Please install it with: npm install ai@4.x|5.x or pnpm add ai@4.x|5.x', - ); - } - - const schemaObject = jsonSchemaFn(schema); + const ai = await tryImport( + 'ai', + 'npm install ai@4.x|5.x or pnpm add ai@4.x|5.x', + ); + const schemaObject = ai.jsonSchema(schema); // TODO: Remove ts-ignore once AISDKToolDefinition properly types the inputSchema and parameters // We avoid defining our own types as much as possible, so we use the AI SDK Tool type // but need to suppress errors for backward compatibility properties diff --git a/src/utils/try-import.ts b/src/utils/try-import.ts new file mode 100644 index 0000000..338c32d --- /dev/null +++ b/src/utils/try-import.ts @@ -0,0 +1,25 @@ +import { StackOneError } from './errors'; + +/** + * Dynamically import an optional dependency with a friendly error message + * + * @param moduleName - The name of the module to import + * @param installHint - Installation instructions shown in error message + * @returns The imported module + * @throws StackOneError if the module is not installed + * + * @example + * ```ts + * const ai = await tryImport('ai', 'npm install ai@4.x|5.x'); + * const { jsonSchema } = ai; + * ``` + */ +export async function tryImport(moduleName: string, installHint: string): Promise { + try { + return await import(moduleName); + } catch { + throw new StackOneError( + `${moduleName} is not installed. Please install it with: ${installHint}`, + ); + } +} From 4be98cfcff7c2e590e3944eed2256f9f97ab7fb8 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:11:34 +0000 Subject: [PATCH 05/10] test(utils): add tests for tryImport utility - Test successful import of existing modules - Test StackOneError is thrown for non-existent modules - Verify error message includes module name and install hint --- src/utils/try-import.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/utils/try-import.test.ts diff --git a/src/utils/try-import.test.ts b/src/utils/try-import.test.ts new file mode 100644 index 0000000..1bdbbe0 --- /dev/null +++ b/src/utils/try-import.test.ts @@ -0,0 +1,23 @@ +import { StackOneError } from './errors'; +import { tryImport } from './try-import'; + +describe('tryImport', () => { + it('should successfully import an existing module', async () => { + const result = await tryImport('node:path', 'n/a'); + expect(result).toHaveProperty('join'); + expect(typeof result.join).toBe('function'); + }); + + it('should throw StackOneError for non-existent module', async () => { + await expect( + tryImport('non-existent-module-xyz', 'npm install non-existent-module-xyz'), + ).rejects.toThrow(StackOneError); + }); + + it('should include module name and install hint in error message', async () => { + const installHint = 'npm install my-package or pnpm add my-package'; + await expect(tryImport('non-existent-module-xyz', installHint)).rejects.toThrow( + 'non-existent-module-xyz is not installed. Please install it with: npm install my-package or pnpm add my-package', + ); + }); +}); From 3b9ced9b1f21d54e13acd01f4af65578b1b8dd59 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:13:26 +0000 Subject: [PATCH 06/10] refactor(tool): clean up toAISDK method - Remove deprecated v4 parameters property - Use satisfies for type-safe tool definition - Remove outdated TODO comment about ts-ignore - Simplify tool definition by constructing all properties upfront --- src/tool.ts | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/tool.ts b/src/tool.ts index 7c85e12..2264a47 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -251,14 +251,6 @@ export class BaseTool { 'npm install ai@4.x|5.x or pnpm add ai@4.x|5.x', ); const schemaObject = ai.jsonSchema(schema); - // TODO: Remove ts-ignore once AISDKToolDefinition properly types the inputSchema and parameters - // We avoid defining our own types as much as possible, so we use the AI SDK Tool type - // but need to suppress errors for backward compatibility properties - const toolDefinition = { - inputSchema: schemaObject, - parameters: schemaObject, // v4 (backward compatibility) - description: this.description, - } as AISDKToolDefinition; const executionOption = options.execution !== undefined @@ -267,19 +259,21 @@ export class BaseTool { ? this.createExecutionMetadata() : false; - 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)}`; - } - }; - } + const toolDefinition = { + inputSchema: schemaObject, + description: this.description, + execution: executionOption !== false ? executionOption : undefined, + execute: + (options.executable ?? true) + ? async (args: Record) => { + try { + return await this.execute(args as JsonDict); + } catch (error) { + return `Error executing tool: ${error instanceof Error ? error.message : String(error)}`; + } + } + : undefined, + } satisfies AISDKToolDefinition; return { [this.name]: toolDefinition, From 91d6c7947e28aa1bb7a7848e58e53e99553dee8a Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:14:57 +0000 Subject: [PATCH 07/10] chore(oxfmt): ignore .claude/settings.local.json --- .oxfmtrc.jsonc | 1 + 1 file changed, 1 insertion(+) diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index 758949e..8219399 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -3,4 +3,5 @@ "useTabs": true, "semi": true, "singleQuote": true, + "ignorePatterns": [".claude/settings.local.json"], } From 463e366be346f17a15746d6a90520449c1639392 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:17:38 +0000 Subject: [PATCH 08/10] chore(deps): move @ai-sdk/provider to catalog:dev --- package.json | 2 +- pnpm-lock.yaml | 9 ++++----- pnpm-workspace.yaml | 4 +--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index ce62cb3..40b75ca 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "zod": "catalog:dev" }, "devDependencies": { - "@ai-sdk/provider": "catalog:", + "@ai-sdk/provider": "catalog:dev", "@ai-sdk/provider-utils": "catalog:dev", "@hono/mcp": "catalog:dev", "@types/node": "catalog:dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67bd065..b7cbcfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,14 +5,13 @@ settings: excludeLinksFromLockfile: false catalogs: - default: - '@ai-sdk/provider': - specifier: ^2.0.0 - version: 2.0.0 dev: '@ai-sdk/openai': specifier: ^2.0.80 version: 2.0.80 + '@ai-sdk/provider': + specifier: ^2.0.0 + version: 2.0.0 '@ai-sdk/provider-utils': specifier: ^3.0.18 version: 3.0.18 @@ -111,7 +110,7 @@ importers: version: 4.1.13 devDependencies: '@ai-sdk/provider': - specifier: 'catalog:' + specifier: catalog:dev version: 2.0.0 '@ai-sdk/provider-utils': specifier: catalog:dev diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cdb88da..5aa9cf9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,14 +2,12 @@ packages: - . - examples -catalog: - '@ai-sdk/provider': ^2.0.0 - catalogMode: strict catalogs: dev: '@ai-sdk/openai': ^2.0.80 + '@ai-sdk/provider': ^2.0.0 '@ai-sdk/provider-utils': ^3.0.18 '@clack/prompts': ^0.11.0 '@hono/mcp': ^0.1.4 From f296b4277b5f32cbdfa76817d7bafe93151168ba Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:21:06 +0000 Subject: [PATCH 09/10] docs: tanstack ai jsonschema --- src/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types.ts b/src/types.ts index 4c256d5..fa4a22a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,8 @@ type Headers = Record; /** * JSON Schema type for defining tool input/output schemas as raw JSON Schema objects. * This allows tools to be defined without Zod when you have JSON Schema definitions available. + * + * @see https://github.com/TanStack/ai/blob/049eb8acd83e6d566c6040c0c4cb53dbe222d46a/packages/typescript/ai/src/types.ts#L5C1-L49C1 */ export interface JSONSchema { type?: string | Array; From fafd7ff00094d2b3325fe73de666430d84f72bd7 Mon Sep 17 00:00:00 2001 From: ryoppippi <1560508+ryoppippi@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:09:38 +0000 Subject: [PATCH 10/10] Revert "chore(oxfmt): ignore .claude/settings.local.json" This reverts commit 91d6c7947e28aa1bb7a7848e58e53e99553dee8a. --- .oxfmtrc.jsonc | 1 - 1 file changed, 1 deletion(-) diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index 8219399..758949e 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -3,5 +3,4 @@ "useTabs": true, "semi": true, "singleQuote": true, - "ignorePatterns": [".claude/settings.local.json"], }