diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index 69ecac5..8cb7829 100644 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -5,15 +5,15 @@ management: docVersion: 1.0.0 speakeasyVersion: 1.680.0 generationVersion: 2.788.4 - releaseVersion: 0.3.10 - configChecksum: 29a887b679dad847b37964d7d8c2d87d + releaseVersion: 0.3.11 + configChecksum: cef53c8d1317cb3ef7f140d2e53bd7a0 repoURL: https://github.com/OpenRouterTeam/typescript-sdk.git installationURL: https://github.com/OpenRouterTeam/typescript-sdk published: true persistentEdits: - generation_id: cf2761d3-02ae-4b5c-b54f-2627801d3677 - pristine_commit_hash: 1ba3d6fe317aee62716df18c2f461b29c7d0e8e3 - pristine_tree_hash: 4f92002bb977564791b1296c0a552d5ae8f5b351 + generation_id: a2ada765-9f3b-4659-b8b6-ed40269ea430 + pristine_commit_hash: 984a52fad729045bce58eef97270470afb744196 + pristine_tree_hash: 5e4ab5e188946409ef6e4571fe523f276fe7105f features: typescript: acceptHeaders: 2.81.2 @@ -1960,12 +1960,12 @@ trackedFiles: pristine_git_object: 410efafd6a7f50d91ccb87131fedbe0c3d47e15a jsr.json: id: 7f6ab7767282 - last_write_checksum: sha1:077495983e847ab761e9361a4c0b11546b91d315 - pristine_git_object: 803816ad090244d157c67480c8b7141b4bce3a96 + last_write_checksum: sha1:f1ea0044f9cd1074d554da2a6f1d66366d57215b + pristine_git_object: cacd2f7942fa4dad0f007c80aa05e57f8af49b7c package.json: id: 7030d0b2f71b - last_write_checksum: sha1:86e4d4968cdb2ffe2e89ec4f7a22c4d04c0ea024 - pristine_git_object: 24f4ce534eb0c48a8354aba89c465afccace2710 + last_write_checksum: sha1:b2c8f1a9776d2997b4404711ee9aa80b79a065da + pristine_git_object: 0c7924af3d3a92ddf8286d18b5845d2ca0de020c src/core.ts: id: f431fdbcd144 last_write_checksum: sha1:5aa66b0b6a5964f3eea7f3098c2eb3c0ee9c0131 @@ -2088,8 +2088,8 @@ trackedFiles: pristine_git_object: a187e58707bdb726ca2aff74941efe7493422d4e src/lib/config.ts: id: 320761608fb3 - last_write_checksum: sha1:5608ed9fc6a1751b332d7a876a6d7a6a78670793 - pristine_git_object: c0bd2f341c2a834bc7bfd30d06213087cc7884e3 + last_write_checksum: sha1:351edbf3be387f1947550fa5d5eb5dedb4a41d9a + pristine_git_object: cde57f6466fbde060f4e4601264df4c9153dc794 src/lib/dlv.ts: id: b1988214835a last_write_checksum: sha1:eaac763b22717206a6199104e0403ed17a4e2711 diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index db1ce73..7d5fe12 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -33,7 +33,7 @@ generation: skipResponseBodyAssertions: false preApplyUnionDiscriminators: true typescript: - version: 0.3.10 + version: 0.3.11 acceptHeaderEnum: false additionalDependencies: dependencies: {} diff --git a/jsr.json b/jsr.json index 803816a..cacd2f7 100644 --- a/jsr.json +++ b/jsr.json @@ -2,7 +2,7 @@ { "name": "@openrouter/sdk", - "version": "0.3.10", + "version": "0.3.11", "exports": { ".": "./src/index.ts", "./models/errors": "./src/models/errors/index.ts", diff --git a/package.json b/package.json index f6e4974..da61ac4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/sdk", - "version": "0.3.10", + "version": "0.3.11", "author": "OpenRouter", "description": "The OpenRouter TypeScript SDK is a type-safe toolkit for building AI applications with access to 300+ language models through a unified API.", "keywords": [ diff --git a/src/lib/config.ts b/src/lib/config.ts index 082ee9c..1a5152e 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -72,7 +72,7 @@ export function serverURLFromOptions(options: SDKOptions): URL | null { export const SDK_METADATA = { language: "typescript", openapiDocVersion: "1.0.0", - sdkVersion: "0.3.10", + sdkVersion: "0.3.11", genVersion: "2.788.4", - userAgent: "speakeasy-sdk/typescript 0.3.10 2.788.4 1.0.0 @openrouter/sdk", + userAgent: "speakeasy-sdk/typescript 0.3.11 2.788.4 1.0.0 @openrouter/sdk", } as const; diff --git a/src/lib/tool-executor.ts b/src/lib/tool-executor.ts index 87901d3..48f150c 100644 --- a/src/lib/tool-executor.ts +++ b/src/lib/tool-executor.ts @@ -1,4 +1,4 @@ -import type { ZodType } from 'zod/v4'; +import type { ZodType } from 'zod'; import type { APITool, Tool, @@ -12,10 +12,12 @@ import { hasExecuteFunction, isGeneratorTool, isRegularExecuteTool } from './too /** * Convert a Zod schema to JSON Schema using Zod v4's toJSONSchema function + * Uses type assertion to bridge zod (user schemas) and zod/v4 (toJSONSchema) */ export function convertZodToJsonSchema(zodSchema: ZodType): Record { - const jsonSchema = toJSONSchema(zodSchema, { - target: 'openapi-3.0', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const jsonSchema = toJSONSchema(zodSchema as any, { + target: 'draft-7', }); return jsonSchema; } diff --git a/src/lib/tool-types.ts b/src/lib/tool-types.ts index 816be82..7998a20 100644 --- a/src/lib/tool-types.ts +++ b/src/lib/tool-types.ts @@ -1,4 +1,4 @@ -import type { ZodObject, ZodRawShape, ZodType, z } from 'zod/v4'; +import type { ZodObject, ZodRawShape, ZodType, z } from 'zod'; import type * as models from '../models/index.js'; import type { OpenResponsesStreamEvent } from '../models/index.js'; import type { ModelResult } from './model-result.js'; diff --git a/src/lib/tool.ts b/src/lib/tool.ts index 4823db7..f6ba6ba 100644 --- a/src/lib/tool.ts +++ b/src/lib/tool.ts @@ -1,4 +1,4 @@ -import type { ZodObject, ZodRawShape, ZodType, z } from "zod/v4"; +import type { ZodObject, ZodRawShape, ZodType, z } from "zod"; import { ToolType, type TurnContext, diff --git a/tests/e2e/call-model-tools.test.ts b/tests/e2e/call-model-tools.test.ts index f0fe324..47f605e 100644 --- a/tests/e2e/call-model-tools.test.ts +++ b/tests/e2e/call-model-tools.test.ts @@ -26,7 +26,7 @@ describe('Enhanced Tool Support for callModel', () => { }); const jsonSchema = toJSONSchema(schema, { - target: 'openapi-3.0', + target: 'draft-7', }); expect(jsonSchema).toHaveProperty('type', 'object'); @@ -48,7 +48,7 @@ describe('Enhanced Tool Support for callModel', () => { }); const jsonSchema = toJSONSchema(schema, { - target: 'openapi-3.0', + target: 'draft-7', }); expect(jsonSchema.properties?.user).toBeDefined(); @@ -61,7 +61,7 @@ describe('Enhanced Tool Support for callModel', () => { }); const jsonSchema = toJSONSchema(schema, { - target: 'openapi-3.0', + target: 'draft-7', }); expect(jsonSchema.properties?.location?.['description']).toBe( diff --git a/tests/e2e/call-model.test.ts b/tests/e2e/call-model.test.ts index e7943b9..2d47dad 100644 --- a/tests/e2e/call-model.test.ts +++ b/tests/e2e/call-model.test.ts @@ -160,7 +160,7 @@ describe('callModel E2E Tests', () => { it('should work with chat-style messages and chat-style tools together', async () => { const response = client.callModel({ - model: 'meta-llama/llama-3.1-8b-instruct', + model: 'anthropic/claude-sonnet-4', input: fromChatMessages([ { role: 'system', diff --git a/tests/unit/tool-type-compat.test.ts b/tests/unit/tool-type-compat.test.ts new file mode 100644 index 0000000..ddc4a12 --- /dev/null +++ b/tests/unit/tool-type-compat.test.ts @@ -0,0 +1,342 @@ +/** + * Type compatibility tests for tool() function + * Verifies all permutations of tool definitions work with plain 'zod' imports + */ +import { describe, it, expect } from 'vitest'; +import { z } from 'zod'; // Plain zod import (what users typically use) +import { tool } from '../../src/lib/tool.js'; + +describe('Tool Type Compatibility with plain zod imports', () => { + describe('Regular tools with execute function', () => { + it('should accept simple inputSchema with execute function', () => { + const simpleTool = tool({ + name: 'simple_tool', + description: 'A simple tool', + inputSchema: z.object({ + message: z.string(), + }), + execute: async (params) => { + return `Received: ${params.message}`; + }, + }); + + expect(simpleTool.type).toBe('function'); + expect(simpleTool.function.name).toBe('simple_tool'); + }); + + it('should accept complex nested inputSchema', () => { + const complexTool = tool({ + name: 'complex_tool', + inputSchema: z.object({ + user: z.object({ + name: z.string(), + age: z.number(), + email: z.string().email(), + }), + preferences: z.object({ + theme: z.enum(['light', 'dark']), + notifications: z.boolean(), + }), + tags: z.array(z.string()), + }), + execute: async (params) => { + return { processed: true, userName: params.user.name }; + }, + }); + + expect(complexTool.function.name).toBe('complex_tool'); + }); + + it('should accept tool with outputSchema', () => { + const typedOutputTool = tool({ + name: 'typed_output_tool', + inputSchema: z.object({ + query: z.string(), + }), + outputSchema: z.object({ + result: z.string(), + count: z.number(), + }), + execute: async (params) => { + return { result: params.query.toUpperCase(), count: params.query.length }; + }, + }); + + expect(typedOutputTool.function.name).toBe('typed_output_tool'); + }); + + it('should accept tool with sync execute function', () => { + const syncTool = tool({ + name: 'sync_tool', + inputSchema: z.object({ + value: z.number(), + }), + execute: (params) => { + return params.value * 2; + }, + }); + + expect(syncTool.function.name).toBe('sync_tool'); + }); + + it('should accept tool with optional fields in schema', () => { + const optionalFieldsTool = tool({ + name: 'optional_fields_tool', + inputSchema: z.object({ + required: z.string(), + optional: z.string().optional(), + nullable: z.string().nullable(), + defaulted: z.string().default('default'), + }), + execute: async (params) => params.required, + }); + + expect(optionalFieldsTool.function.name).toBe('optional_fields_tool'); + }); + + it('should accept tool with union types in schema', () => { + const unionTool = tool({ + name: 'union_tool', + inputSchema: z.object({ + value: z.union([z.string(), z.number()]), + status: z.enum(['active', 'inactive', 'pending']), + }), + execute: async (params) => String(params.value), + }); + + expect(unionTool.function.name).toBe('union_tool'); + }); + + it('should accept tool with array types', () => { + const arrayTool = tool({ + name: 'array_tool', + inputSchema: z.object({ + items: z.array(z.object({ + id: z.number(), + name: z.string(), + })), + counts: z.array(z.number()), + }), + execute: async (params) => params.items.length, + }); + + expect(arrayTool.function.name).toBe('array_tool'); + }); + + it('should accept tool with descriptions on schema fields', () => { + const describedTool = tool({ + name: 'described_tool', + description: 'A tool with field descriptions', + inputSchema: z.object({ + location: z.string().describe('City and country e.g. Tokyo, Japan'), + units: z.enum(['celsius', 'fahrenheit']).describe('Temperature units'), + }), + execute: async (params) => ({ temp: 22, location: params.location }), + }); + + expect(describedTool.function.name).toBe('described_tool'); + }); + }); + + describe('Manual tools (execute: false)', () => { + it('should accept manual tool with simple schema', () => { + const manualTool = tool({ + name: 'manual_tool', + description: 'A manual tool that requires external handling', + inputSchema: z.object({ + action: z.string(), + target: z.string(), + }), + execute: false, + }); + + expect(manualTool.type).toBe('function'); + expect(manualTool.function.name).toBe('manual_tool'); + expect('execute' in manualTool.function).toBe(false); + }); + + it('should accept manual tool with complex schema', () => { + const complexManualTool = tool({ + name: 'complex_manual_tool', + inputSchema: z.object({ + admin_access: z.boolean().describe('Whether to request admin access'), + permissions: z.array(z.enum(['read', 'write', 'delete'])), + metadata: z.object({ + reason: z.string(), + timestamp: z.string(), + }).optional(), + }), + execute: false, + }); + + expect(complexManualTool.function.name).toBe('complex_manual_tool'); + }); + }); + + describe('Generator tools (with eventSchema)', () => { + it('should accept generator tool with event and output schemas', () => { + const generatorTool = tool({ + name: 'generator_tool', + description: 'A tool that streams progress updates', + inputSchema: z.object({ + task: z.string(), + }), + eventSchema: z.object({ + progress: z.number(), + status: z.string(), + }), + outputSchema: z.object({ + result: z.string(), + totalSteps: z.number(), + }), + execute: async function* (params) { + yield { progress: 0, status: 'Starting...' }; + yield { progress: 50, status: 'Processing...' }; + yield { progress: 100, status: 'Done' }; + yield { result: `Completed: ${params.task}`, totalSteps: 3 }; + }, + }); + + expect(generatorTool.type).toBe('function'); + expect(generatorTool.function.name).toBe('generator_tool'); + expect('eventSchema' in generatorTool.function).toBe(true); + }); + + it('should accept generator tool with complex event types', () => { + const complexGeneratorTool = tool({ + name: 'complex_generator_tool', + inputSchema: z.object({ + files: z.array(z.string()), + }), + eventSchema: z.object({ + type: z.enum(['progress', 'warning', 'info']), + message: z.string(), + data: z.record(z.unknown()).optional(), + }), + outputSchema: z.object({ + processedFiles: z.array(z.string()), + errors: z.array(z.string()), + }), + execute: async function* (params) { + for (const file of params.files) { + yield { type: 'progress' as const, message: `Processing ${file}` }; + } + yield { processedFiles: params.files, errors: [] }; + }, + }); + + expect(complexGeneratorTool.function.name).toBe('complex_generator_tool'); + }); + }); + + describe('Tools with nextTurnParams', () => { + it('should accept tool with nextTurnParams functions', () => { + const adaptiveTool = tool({ + name: 'adaptive_tool', + inputSchema: z.object({ + complexity: z.enum(['low', 'medium', 'high']), + }), + nextTurnParams: { + temperature: (params) => { + switch (params.complexity) { + case 'low': return 0.3; + case 'medium': return 0.7; + case 'high': return 1.0; + } + }, + maxOutputTokens: (params) => params.complexity === 'high' ? 4000 : 2000, + }, + execute: async (params) => `Processed with ${params.complexity} complexity`, + }); + + expect(adaptiveTool.function.name).toBe('adaptive_tool'); + expect(adaptiveTool.function.nextTurnParams).toBeDefined(); + }); + + it('should accept manual tool with nextTurnParams', () => { + const manualWithParams = tool({ + name: 'manual_with_params', + inputSchema: z.object({ + model_preference: z.string(), + }), + nextTurnParams: { + model: (params) => params.model_preference, + }, + execute: false, + }); + + expect(manualWithParams.function.name).toBe('manual_with_params'); + expect(manualWithParams.function.nextTurnParams).toBeDefined(); + }); + }); + + describe('Edge cases', () => { + it('should handle empty object schema', () => { + const emptyInputTool = tool({ + name: 'empty_input_tool', + inputSchema: z.object({}), + execute: async () => 'No input needed', + }); + + expect(emptyInputTool.function.name).toBe('empty_input_tool'); + }); + + it('should handle deeply nested schemas', () => { + const deeplyNestedTool = tool({ + name: 'deeply_nested_tool', + inputSchema: z.object({ + level1: z.object({ + level2: z.object({ + level3: z.object({ + level4: z.object({ + value: z.string(), + }), + }), + }), + }), + }), + execute: async (params) => params.level1.level2.level3.level4.value, + }); + + expect(deeplyNestedTool.function.name).toBe('deeply_nested_tool'); + }); + + it('should handle schema with all Zod primitive types', () => { + const allPrimitivesTool = tool({ + name: 'all_primitives_tool', + inputSchema: z.object({ + str: z.string(), + num: z.number(), + bool: z.boolean(), + bigint: z.bigint(), + date: z.date(), + undef: z.undefined(), + nullVal: z.null(), + any: z.any(), + unknown: z.unknown(), + }), + execute: async (params) => ({ received: true }), + }); + + expect(allPrimitivesTool.function.name).toBe('all_primitives_tool'); + }); + + it('should handle schema with refinements', () => { + const refinedTool = tool({ + name: 'refined_tool', + inputSchema: z.object({ + email: z.string().email(), + url: z.string().url(), + uuid: z.string().uuid(), + positiveNumber: z.number().positive(), + integerOnly: z.number().int(), + minMax: z.number().min(0).max(100), + lengthConstrained: z.string().min(1).max(255), + }), + execute: async (params) => params.email, + }); + + expect(refinedTool.function.name).toBe('refined_tool'); + }); + }); +});