From 96f18a573b629d57c32e4c12fa054ec9dc6ad618 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Mon, 19 Jan 2026 14:27:30 -0500 Subject: [PATCH 1/2] test: add E2E test for generator tool streaming via getToolStream Adds a test that verifies generator tools emit preliminary results in real-time via getToolStream(). The test uses a search_with_progress tool that yields status/progress events before the final result. --- tests/e2e/call-model.test.ts | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/e2e/call-model.test.ts b/tests/e2e/call-model.test.ts index d562f46c..c4df04f4 100644 --- a/tests/e2e/call-model.test.ts +++ b/tests/e2e/call-model.test.ts @@ -827,6 +827,89 @@ describe('callModel E2E Tests', () => { expect(fullToolCall.length).toBeGreaterThan(0); } }, 30000); + + it('should stream preliminary results from generator tools via getToolStream', async () => { + const progressTool = { + type: ToolType.Function, + function: { + name: 'search_with_progress', + description: 'Search with progress updates', + inputSchema: z.object({ + query: z.string().describe('Search query'), + }), + eventSchema: z.object({ + status: z.string(), + progress: z.number().min(0).max(100), + }), + outputSchema: z.object({ + results: z.array(z.string()), + totalFound: z.number(), + }), + execute: async function* (params: { query: string }) { + // Preliminary event 1 + yield { status: 'Starting search...', progress: 0 }; + + await new Promise((r) => setTimeout(r, 100)); + + // Preliminary event 2 + yield { status: 'Searching...', progress: 50 }; + + await new Promise((r) => setTimeout(r, 100)); + + // Preliminary event 3 + yield { status: 'Almost done...', progress: 90 }; + + // Final result + yield { + results: [`Found: ${params.query}`], + totalFound: 1, + }; + }, + }, + }; + + const response = client.callModel({ + model: 'anthropic/claude-haiku-4.5', + input: fromChatMessages([ + { + role: 'user', + content: 'Search for TypeScript documentation using the search_with_progress tool.', + }, + ]), + tools: [progressTool], + toolChoice: 'required', + stopWhen: stepCountIs(1), // Limit to single turn to ensure one tool call + }); + + const preliminaryResults: Array<{ status: string; progress: number }> = []; + let toolCallId: string | undefined; + + for await (const event of response.getToolStream()) { + if (event.type === 'preliminary_result') { + if (!toolCallId) { + toolCallId = event.toolCallId; + } + expect(event.toolCallId).toBe(toolCallId); // Same tool call + preliminaryResults.push(event.result as { status: string; progress: number }); + } + } + + // Verify preliminary results were received + expect(preliminaryResults.length).toBeGreaterThanOrEqual(3); + + // Verify events came in order (progress should be increasing) + for (let i = 1; i < preliminaryResults.length; i++) { + const current = preliminaryResults[i]; + const previous = preliminaryResults[i - 1]; + if (current && previous) { + expect(current.progress).toBeGreaterThanOrEqual(previous.progress); + } + } + + // Verify final response + const finalResponse = await response.getResponse(); + expect(finalResponse).toBeDefined(); + }, 60000); }); describe('response.fullResponsesStream - Streaming all events', () => { From 902c5b34d3c2d7f427aed35de32beb7c8264d747 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Mon, 19 Jan 2026 14:40:13 -0500 Subject: [PATCH 2/2] chore: merge latest main with monorepo sync --- package.json | 25 +++++++++++-------------- pnpm-lock.yaml | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 3228aa13..2c719a2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/sdk", - "version": "0.3.15", + "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": [ @@ -64,19 +64,17 @@ "type": "git", "url": "https://github.com/OpenRouterTeam/typescript-sdk.git" }, - "scripts": { - "lint": "eslint --cache --max-warnings=0 src", - "build": "tsc", - "typecheck": "tsc --noEmit", - "prepublishOnly": "npm run build", - "postinstall": "node scripts/check-types.js || true", - "test": "vitest --run --project unit", - "test:e2e": "vitest --run --project e2e", - "test:watch": "vitest --watch --project unit" - }, - "peerDependencies": {}, + "scripts": { + "lint": "eslint --cache --max-warnings=0 src", + "build": "tsc", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build", + "test": "vitest --run", + "test:watch": "vitest" + }, "devDependencies": { "@eslint/js": "^9.19.0", + "@linear/sdk": "^69.0.0", "@types/node": "^22.13.12", "dotenv": "^16.4.7", "eslint": "^9.19.0", @@ -87,6 +85,5 @@ }, "dependencies": { "zod": "^3.25.0 || ^4.0.0" - }, - "packageManager": "pnpm@10.22.0" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10c27fb5..ec583b62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: '@eslint/js': specifier: ^9.19.0 version: 9.38.0 + '@linear/sdk': + specifier: ^69.0.0 + version: 69.0.0(graphql@16.12.0) '@types/node': specifier: ^22.13.12 version: 22.18.13 @@ -233,6 +236,11 @@ packages: resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -252,6 +260,10 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@linear/sdk@69.0.0': + resolution: {integrity: sha512-o9f+NAlEk/QqAwBQBkeoHripnNITh+IxgczXvb0QBK+eIjNpD9Sn3gP8LyolGTcMqsuGRQOgWBBp2XGhkMx+wQ==} + engines: {node: '>=18.x'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -697,6 +709,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1178,6 +1194,10 @@ snapshots: '@eslint/core': 0.16.0 levn: 0.4.1 + '@graphql-typed-document-node/core@3.2.0(graphql@16.12.0)': + dependencies: + graphql: 16.12.0 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -1191,6 +1211,12 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@linear/sdk@69.0.0(graphql@16.12.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) + transitivePeerDependencies: + - graphql + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1662,6 +1688,8 @@ snapshots: graphemer@1.4.0: {} + graphql@16.12.0: {} + has-flag@4.0.0: {} ignore@5.3.2: {}