From 8188b63f65473b4e1cc459bdcfc2e005158f655d Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Tue, 21 Apr 2026 12:44:31 -0400 Subject: [PATCH 1/4] fix(types): eliminate as-any gaps for serverTool mixing and fromChatMessages input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two consumer-facing type gaps in v0.4.0 required `as any`: 1. Mixing `tool()` + `serverTool()` results in a single array typed as `Array` failed because `ServerTool`'s `config: Extract<..., {type: T}>` is invariant over `T`, so a `ServerTool<'openrouter:datetime'>` did not assign to the bare `ServerTool` (= `ServerTool`). Split into: - `ServerToolBase` — structural base (also kept as the union member of `Tool`). - `ServerToolNarrow` — narrow form, returned by `serverTool()`, extends `ServerToolBase` via interface inheritance. - `ServerTool` — now a type alias for `ServerToolBase`, so `Array` accepts any narrow variant. 2. `fromChatMessages()` returns `InputsUnion`, but `callModel`'s `request.input` was typed `FieldOrAsyncFunction | string`, which is a narrower union. Widen `input` to also accept `FieldOrAsyncFunction` so the converter's output assigns directly without a cast — matching the docstring. Adds a type-level regression test (`consumer-type-ergonomics.test-d.ts`) covering both scenarios, and migrates the existing narrowing fixtures to `ServerToolNarrow`. --- .changeset/fix-server-tool-type-gaps.md | 24 ++++ packages/agent/src/index.ts | 2 + packages/agent/src/lib/async-params.ts | 13 ++- packages/agent/src/lib/stream-transformers.ts | 12 +- packages/agent/src/lib/tool-types.ts | 37 +++++-- packages/agent/src/lib/tool.ts | 4 +- .../unit/consumer-type-ergonomics.test-d.ts | 103 ++++++++++++++++++ .../server-tool-stream-narrowing.test-d.ts | 6 +- 8 files changed, 182 insertions(+), 19 deletions(-) create mode 100644 .changeset/fix-server-tool-type-gaps.md create mode 100644 packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts diff --git a/.changeset/fix-server-tool-type-gaps.md b/.changeset/fix-server-tool-type-gaps.md new file mode 100644 index 0000000..9679d3e --- /dev/null +++ b/.changeset/fix-server-tool-type-gaps.md @@ -0,0 +1,24 @@ +--- +"@openrouter/agent": patch +--- + +Fix two type gaps that forced consumers to use `as any` when wiring up +`callModel` with server tools and chat-format inputs: + +- **`ServerTool` is now a structural-base alias**, not a generic. The + factory returns a narrow `ServerToolNarrow` that extends + `ServerToolBase` via interface extension, so a specific + `ServerToolNarrow<'openrouter:datetime'>` flows into the public + `ServerTool` alias without a cast. Mixed arrays like + `Array` or `Tool[]` now accept any mix of + `tool()` and `serverTool()` results directly. New public types: + `ServerToolBase` (structural base) and `ServerToolNarrow` (narrow + form when the exact `config` shape matters). The old `ServerTool` + generic is replaced by `ServerToolNarrow`; code that only used + `ServerTool` without a type argument is unaffected. + +- **`callModel`'s `request.input` now accepts `InputsUnion`** (the SDK's + wider message shape returned by `fromChatMessages()`), alongside the + existing `Item[]` and plain `string` forms. The docstring on + `fromChatMessages()` already claims its output "can be passed directly + to `callModel()`"; the types now match. diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 0bd90cf..c4e84f8 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -151,7 +151,9 @@ export type { ResponseStreamEvent, ResponseStreamEvent as EnhancedResponseStreamEvent, ServerTool, + ServerToolBase, ServerToolConfig, + ServerToolNarrow, ServerToolResultItem, ServerToolType, StateAccessor, diff --git a/packages/agent/src/lib/async-params.ts b/packages/agent/src/lib/async-params.ts index 8856346..8662fe5 100644 --- a/packages/agent/src/lib/async-params.ts +++ b/packages/agent/src/lib/async-params.ts @@ -57,7 +57,18 @@ type BaseCallModelInput< models.ResponsesRequest[K] >; } & { - input: FieldOrAsyncFunction | string; + /** + * The input for the model turn. Accepts either: + * - A plain `string` prompt. + * - An array of typed `Item[]` (the narrow, local item union). + * - The SDK's `InputsUnion` shape (a `string` or broader item array) + * — this is what converters like `fromChatMessages()` return, so + * those results assign directly without a cast. + * + * When a function is provided, it is resolved once per call with the + * current turn context before the request is sent. + */ + input: FieldOrAsyncFunction | FieldOrAsyncFunction; tools?: TTools; stopWhen?: StopWhen; /** Typed context data passed to tools via contextSchema. Includes optional `shared` key. */ diff --git a/packages/agent/src/lib/stream-transformers.ts b/packages/agent/src/lib/stream-transformers.ts index b35b56b..e3d26e8 100644 --- a/packages/agent/src/lib/stream-transformers.ts +++ b/packages/agent/src/lib/stream-transformers.ts @@ -30,7 +30,13 @@ import { isURLCitationAnnotation, isWebSearchCallOutputItem, } from './stream-type-guards.js'; -import type { ClientTool, ParsedToolCall, ServerTool, Tool } from './tool-types.js'; +import type { + ClientTool, + ParsedToolCall, + ServerToolBase, + ServerToolNarrow, + Tool, +} from './tool-types.js'; /** * Extract text deltas from responses stream events @@ -263,7 +269,7 @@ type KnownServerToolOutputs = { * so the SDK's forward-compat variants flow through automatically. */ type InferServerToolOutput = - S extends ServerTool + S extends ServerToolNarrow ? K extends keyof KnownServerToolOutputs ? KnownServerToolOutputs[K] : OpenRouterServerToolOutput @@ -275,7 +281,7 @@ type InferServerToolOutput = * to every mapped output plus the generic fallback. Unused otherwise. */ type InferServerToolOutputsUnion = InferServerToolOutput< - Extract + Extract >; /** diff --git a/packages/agent/src/lib/tool-types.ts b/packages/agent/src/lib/tool-types.ts index f4794a8..a125c2d 100644 --- a/packages/agent/src/lib/tool-types.ts +++ b/packages/agent/src/lib/tool-types.ts @@ -346,12 +346,12 @@ export type ServerToolType = ServerToolConfig['type']; * Structural base type for every server tool. Interface extension (not a * distributive conditional) is used so the narrow-T subtype assigns cleanly * into the wide-T supertype via nominal inheritance — TypeScript treats - * `ServerTool<'web_search_2025_08_26'>` as a subtype of `ServerToolBase` + * `ServerToolNarrow<'web_search_2025_08_26'>` as a subtype of `ServerToolBase` * without needing to reason about variance through `Extract<..., {type: T}>`. * * `Tool` uses `ServerToolBase` as its union member (rather than a generic - * `ServerTool` parameterized on a union) so specific `ServerTool` values - * assign into `Tool[]` directly. + * `ServerTool` parameterized on a union) so specific `ServerToolNarrow` + * values assign into `Tool[]` directly. */ export interface ServerToolBase { readonly _brand: 'server-tool'; @@ -359,16 +359,21 @@ export interface ServerToolBase { } /** - * A server-executed tool. OpenRouter runs the tool and returns an output - * item in the response — no execute function lives on the client. When - * the type parameter `T` is a specific literal, `config` narrows to the - * SDK shape for that tool. Because this interface `extends ServerToolBase`, - * any `ServerTool` value is nominally assignable to `ServerToolBase` - * (and hence to `Tool`) regardless of `T`. + * A server-executed tool narrowed to a single `type` literal `T`. Because + * `config: Extract` makes `T` appear in both + * positions of the filter, the narrow form is not naturally assignable to + * a `ServerToolNarrow` bare union — so we keep the generic + * form separate from the public `ServerTool` alias. + * + * Consumers should use the `ServerTool` alias (which is `ServerToolBase`) + * when typing mixed arrays like `Array`, and use + * `ServerToolNarrow` (or simply `ReturnType>`) + * only when the specific config shape matters. * * @template T The specific server-tool type literal (narrows `config`). */ -export interface ServerTool extends ServerToolBase { +export interface ServerToolNarrow + extends ServerToolBase { readonly config: Extract< ServerToolConfig, { @@ -377,6 +382,18 @@ export interface ServerTool extends S >; } +/** + * Public alias for a server-executed tool that accepts any `type`. + * Structurally identical to `ServerToolBase`; kept as a distinct alias so + * that `Array` reads naturally at call sites and + * every `ServerToolNarrow` instance assigns into it via extension. + * + * For factory-call-site typing where the exact `T` matters — e.g. narrowing + * `config` to a specific SDK shape — use `ServerToolNarrow` or rely on + * `ReturnType>`. + */ +export type ServerTool = ServerToolBase; + /** * Union of every tool kind accepted by `callModel({ tools: [...] })`: * client function/generator/manual tools, or OpenRouter server tools. diff --git a/packages/agent/src/lib/tool.ts b/packages/agent/src/lib/tool.ts index 00ea165..8df9dd5 100644 --- a/packages/agent/src/lib/tool.ts +++ b/packages/agent/src/lib/tool.ts @@ -2,8 +2,8 @@ import type { $ZodObject, $ZodShape, $ZodType, infer as zodInfer } from 'zod/v4/ import type { ManualTool, NextTurnParamsFunctions, - ServerTool, ServerToolConfig, + ServerToolNarrow, ServerToolType, ToModelOutputFunction, Tool, @@ -370,7 +370,7 @@ export function serverTool( type: T; } >, -): ServerTool { +): ServerToolNarrow { return { _brand: 'server-tool', config, diff --git a/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts b/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts new file mode 100644 index 0000000..ac2848f --- /dev/null +++ b/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts @@ -0,0 +1,103 @@ +/** + * Regression coverage for two consumer-facing type gaps reported against + * v0.4.0 that previously required `as any`: + * + * 1. Mixing `tool()` + `serverTool()` results in a single array typed as + * `Array` must assign to `callModel`'s `tools` + * parameter without a cast. The factory return type is narrow + * (`ServerToolNarrow`) and must flow through interface extension up + * to the `ServerToolBase`-based `ServerTool` alias. + * + * 2. `fromChatMessages()` returns the SDK's `InputsUnion`, which must be + * directly assignable to `callModel`'s `request.input` without a cast. + * Previously the input was typed as `Item[] | string`, which is a + * narrower union that `InputsUnion` does not extend. + */ + +import type * as models from '@openrouter/sdk/models'; +import { expectTypeOf } from 'vitest'; +import { z } from 'zod/v4'; +import type { CallModelInput } from '../../src/lib/async-params.js'; +import { fromChatMessages } from '../../src/lib/chat-compat.js'; +import { serverTool, tool } from '../../src/lib/tool.js'; +import type { + ClientTool, + ServerTool, + ServerToolBase, + ServerToolNarrow, + Tool, +} from '../../src/lib/tool-types.js'; + +// --- Issue 1: mixed arrays assign without `as any` -------------------------- + +// Specific narrow factory return types must flow to the public `ServerTool` +// alias via interface extension. +expectTypeOf>().toExtend(); +expectTypeOf>().toExtend(); +expectTypeOf>().toExtend(); + +// ServerTool (bare, no generic) is the structural base — it should accept +// any narrow variant assigned to it. +const _dt: ServerTool = serverTool({ + type: 'openrouter:datetime', +}); +const _ws: ServerTool = serverTool({ + type: 'openrouter:web_search', +}); +void _dt; +void _ws; + +// Array accepts a mix without cast. +const _mixed: Array = [ + tool({ + name: 'save_note', + inputSchema: z.object({ + title: z.string(), + }), + execute: async () => ({ + ok: true, + }), + }), + serverTool({ + type: 'openrouter:datetime', + }), + serverTool({ + type: 'openrouter:web_search', + }), +]; +void _mixed; + +// Tool[] accepts the same mix. +const _asTool: Tool[] = [ + tool({ + name: 'save_note', + inputSchema: z.object({ + title: z.string(), + }), + execute: async () => ({ + ok: true, + }), + }), + serverTool({ + type: 'openrouter:datetime', + }), +]; +void _asTool; + +// --- Issue 2: fromChatMessages() output is assignable to input ------------- + +// A `CallModelInput`'s `input` field accepts `InputsUnion` directly. We use +// `Extract` instead of `toExtend` because `input` is a field-or-fn union; we +// just need the plain data variant to accept `InputsUnion`. +type _InputField = CallModelInput['input']; +expectTypeOf().toExtend<_InputField>(); + +// And the concrete return of `fromChatMessages()` must be assignable. +const _converted = fromChatMessages([ + { + role: 'user', + content: 'hi', + }, +]); +const _asInput: _InputField = _converted; +void _asInput; diff --git a/packages/agent/tests/unit/server-tool-stream-narrowing.test-d.ts b/packages/agent/tests/unit/server-tool-stream-narrowing.test-d.ts index 4599c6c..1c0ede0 100644 --- a/packages/agent/tests/unit/server-tool-stream-narrowing.test-d.ts +++ b/packages/agent/tests/unit/server-tool-stream-narrowing.test-d.ts @@ -13,7 +13,7 @@ import { expectTypeOf } from 'vitest'; import { z } from 'zod/v4'; import type { StreamableOutputItem } from '../../src/lib/stream-transformers.js'; import { serverTool, tool } from '../../src/lib/tool.js'; -import type { ServerTool, ServerToolType, Tool } from '../../src/lib/tool-types.js'; +import type { ServerToolNarrow, ServerToolType, Tool } from '../../src/lib/tool-types.js'; // --- Default (unconstrained TTools): widest possible union ------------------ @@ -37,7 +37,7 @@ expectTypeOf().toExtend(); type DatetimeOnly = StreamableOutputItem< readonly [ - ServerTool<'openrouter:datetime'>, + ServerToolNarrow<'openrouter:datetime'>, ] >; expectTypeOf().toExtend(); @@ -81,7 +81,7 @@ expectTypeOf().not.toExtend(); type FutureToolOnly = StreamableOutputItem< readonly [ - ServerTool, + ServerToolNarrow, ] >; // The widest `ServerToolType` includes every known literal; the inferred From 1c91662c9508d67cd325bee383b5ce508bf0ef75 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Tue, 21 Apr 2026 16:59:18 -0400 Subject: [PATCH 2/4] fix(types): keep ServerTool generic, drop ServerToolNarrow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores ServerTool as a conditional generic with a `never` default so bare `ServerTool` collapses to `ServerToolBase` — preserving backward compatibility for callers that used `ServerTool` while still allowing mixed `Array` to accept narrow variants via intersection subtyping. Infers the stream output's type from `config.type` directly so narrowing works through both the intersection form and the bare base. --- packages/agent/src/index.ts | 1 - packages/agent/src/lib/stream-transformers.ts | 27 ++++---- packages/agent/src/lib/tool-types.ts | 64 ++++++++----------- packages/agent/src/lib/tool.ts | 4 +- .../unit/consumer-type-ergonomics.test-d.ts | 18 +++--- .../server-tool-stream-narrowing.test-d.ts | 6 +- 6 files changed, 53 insertions(+), 67 deletions(-) diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index c4e84f8..f0057e0 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -153,7 +153,6 @@ export type { ServerTool, ServerToolBase, ServerToolConfig, - ServerToolNarrow, ServerToolResultItem, ServerToolType, StateAccessor, diff --git a/packages/agent/src/lib/stream-transformers.ts b/packages/agent/src/lib/stream-transformers.ts index e3d26e8..6b6d300 100644 --- a/packages/agent/src/lib/stream-transformers.ts +++ b/packages/agent/src/lib/stream-transformers.ts @@ -30,13 +30,7 @@ import { isURLCitationAnnotation, isWebSearchCallOutputItem, } from './stream-type-guards.js'; -import type { - ClientTool, - ParsedToolCall, - ServerToolBase, - ServerToolNarrow, - Tool, -} from './tool-types.js'; +import type { ClientTool, ParsedToolCall, ServerToolBase, Tool } from './tool-types.js'; /** * Extract text deltas from responses stream events @@ -267,13 +261,20 @@ type KnownServerToolOutputs = { * map via KnownServerToolOutputs; anything else falls back to the * provider-side server-tool output union (`OpenRouterServerToolOutput`) * so the SDK's forward-compat variants flow through automatically. + * + * Inference reads `config.type` directly rather than `ServerTool` + * so it works whether the source is the narrow intersection form or the + * wide `ServerToolBase` base — both expose the same `config.type` field. */ -type InferServerToolOutput = - S extends ServerToolNarrow - ? K extends keyof KnownServerToolOutputs - ? KnownServerToolOutputs[K] - : OpenRouterServerToolOutput - : never; +type InferServerToolOutput = S extends { + readonly config: { + readonly type: infer K; + }; +} + ? K extends keyof KnownServerToolOutputs + ? KnownServerToolOutputs[K] + : OpenRouterServerToolOutput + : never; /** * Union of output item shapes produced by the server tools present in diff --git a/packages/agent/src/lib/tool-types.ts b/packages/agent/src/lib/tool-types.ts index a125c2d..1d5dd3d 100644 --- a/packages/agent/src/lib/tool-types.ts +++ b/packages/agent/src/lib/tool-types.ts @@ -343,15 +343,13 @@ export type ServerToolConfig = Exclude< export type ServerToolType = ServerToolConfig['type']; /** - * Structural base type for every server tool. Interface extension (not a - * distributive conditional) is used so the narrow-T subtype assigns cleanly - * into the wide-T supertype via nominal inheritance — TypeScript treats - * `ServerToolNarrow<'web_search_2025_08_26'>` as a subtype of `ServerToolBase` - * without needing to reason about variance through `Extract<..., {type: T}>`. + * Structural base type for every server tool. Non-generic so that `Tool`'s + * union member doesn't carry a type parameter — this sidesteps the variance + * issue where `ServerTool<'openrouter:datetime'>` would fail to assign into + * `ServerTool` through `Extract<..., {type: T}>`. * - * `Tool` uses `ServerToolBase` as its union member (rather than a generic - * `ServerTool` parameterized on a union) so specific `ServerToolNarrow` - * values assign into `Tool[]` directly. + * `ServerTool` (bare, via its `never` default) collapses to this type, so + * `Array` accepts any narrow variant. */ export interface ServerToolBase { readonly _brand: 'server-tool'; @@ -359,46 +357,34 @@ export interface ServerToolBase { } /** - * A server-executed tool narrowed to a single `type` literal `T`. Because - * `config: Extract` makes `T` appear in both - * positions of the filter, the narrow form is not naturally assignable to - * a `ServerToolNarrow` bare union — so we keep the generic - * form separate from the public `ServerTool` alias. + * A server-executed tool. Without a type argument, it is the structural base + * (equivalent to `ServerToolBase`) — use it as-is in mixed arrays like + * `Array`. With a `type` literal argument, it + * narrows `config` to that specific SDK variant — returned by + * `serverTool()` and useful when the concrete config shape matters. * - * Consumers should use the `ServerTool` alias (which is `ServerToolBase`) - * when typing mixed arrays like `Array`, and use - * `ServerToolNarrow` (or simply `ReturnType>`) - * only when the specific config shape matters. + * The `[T] extends [never]` default branch keeps bare `ServerTool` equal to + * `ServerToolBase`, so specific narrow variants assign into the bare form + * via the intersection being a subtype of `ServerToolBase`. * * @template T The specific server-tool type literal (narrows `config`). */ -export interface ServerToolNarrow - extends ServerToolBase { - readonly config: Extract< - ServerToolConfig, - { - type: T; - } - >; -} - -/** - * Public alias for a server-executed tool that accepts any `type`. - * Structurally identical to `ServerToolBase`; kept as a distinct alias so - * that `Array` reads naturally at call sites and - * every `ServerToolNarrow` instance assigns into it via extension. - * - * For factory-call-site typing where the exact `T` matters — e.g. narrowing - * `config` to a specific SDK shape — use `ServerToolNarrow` or rely on - * `ReturnType>`. - */ -export type ServerTool = ServerToolBase; +export type ServerTool = [T] extends [never] + ? ServerToolBase + : ServerToolBase & { + readonly config: Extract< + ServerToolConfig, + { + type: T; + } + >; + }; /** * Union of every tool kind accepted by `callModel({ tools: [...] })`: * client function/generator/manual tools, or OpenRouter server tools. * The server branch is the structural base; specific `ServerTool` - * values flow in via interface extension. + * values flow in via the intersection being a subtype of `ServerToolBase`. */ export type Tool = ClientTool | ServerToolBase; diff --git a/packages/agent/src/lib/tool.ts b/packages/agent/src/lib/tool.ts index 8df9dd5..00ea165 100644 --- a/packages/agent/src/lib/tool.ts +++ b/packages/agent/src/lib/tool.ts @@ -2,8 +2,8 @@ import type { $ZodObject, $ZodShape, $ZodType, infer as zodInfer } from 'zod/v4/ import type { ManualTool, NextTurnParamsFunctions, + ServerTool, ServerToolConfig, - ServerToolNarrow, ServerToolType, ToModelOutputFunction, Tool, @@ -370,7 +370,7 @@ export function serverTool( type: T; } >, -): ServerToolNarrow { +): ServerTool { return { _brand: 'server-tool', config, diff --git a/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts b/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts index ac2848f..eb92c4d 100644 --- a/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts +++ b/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts @@ -4,9 +4,10 @@ * * 1. Mixing `tool()` + `serverTool()` results in a single array typed as * `Array` must assign to `callModel`'s `tools` - * parameter without a cast. The factory return type is narrow - * (`ServerToolNarrow`) and must flow through interface extension up - * to the `ServerToolBase`-based `ServerTool` alias. + * parameter without a cast. `serverTool()` returns the narrow + * `ServerTool` (a `ServerToolBase` intersection), which must flow + * into the bare `ServerTool` — bare `ServerTool` collapses to + * `ServerToolBase` via its `never` default. * * 2. `fromChatMessages()` returns the SDK's `InputsUnion`, which must be * directly assignable to `callModel`'s `request.input` without a cast. @@ -24,17 +25,16 @@ import type { ClientTool, ServerTool, ServerToolBase, - ServerToolNarrow, Tool, } from '../../src/lib/tool-types.js'; // --- Issue 1: mixed arrays assign without `as any` -------------------------- -// Specific narrow factory return types must flow to the public `ServerTool` -// alias via interface extension. -expectTypeOf>().toExtend(); -expectTypeOf>().toExtend(); -expectTypeOf>().toExtend(); +// Specific narrow factory return types must flow to the bare `ServerTool` +// form (which collapses to `ServerToolBase`) via intersection subtyping. +expectTypeOf>().toExtend(); +expectTypeOf>().toExtend(); +expectTypeOf>().toExtend(); // ServerTool (bare, no generic) is the structural base — it should accept // any narrow variant assigned to it. diff --git a/packages/agent/tests/unit/server-tool-stream-narrowing.test-d.ts b/packages/agent/tests/unit/server-tool-stream-narrowing.test-d.ts index 1c0ede0..4599c6c 100644 --- a/packages/agent/tests/unit/server-tool-stream-narrowing.test-d.ts +++ b/packages/agent/tests/unit/server-tool-stream-narrowing.test-d.ts @@ -13,7 +13,7 @@ import { expectTypeOf } from 'vitest'; import { z } from 'zod/v4'; import type { StreamableOutputItem } from '../../src/lib/stream-transformers.js'; import { serverTool, tool } from '../../src/lib/tool.js'; -import type { ServerToolNarrow, ServerToolType, Tool } from '../../src/lib/tool-types.js'; +import type { ServerTool, ServerToolType, Tool } from '../../src/lib/tool-types.js'; // --- Default (unconstrained TTools): widest possible union ------------------ @@ -37,7 +37,7 @@ expectTypeOf().toExtend(); type DatetimeOnly = StreamableOutputItem< readonly [ - ServerToolNarrow<'openrouter:datetime'>, + ServerTool<'openrouter:datetime'>, ] >; expectTypeOf().toExtend(); @@ -81,7 +81,7 @@ expectTypeOf().not.toExtend(); type FutureToolOnly = StreamableOutputItem< readonly [ - ServerToolNarrow, + ServerTool, ] >; // The widest `ServerToolType` includes every known literal; the inferred From 1e77b0ddbdbf451ec4d4bf7a8550051ea83f28b7 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Tue, 21 Apr 2026 16:59:45 -0400 Subject: [PATCH 3/4] chore(release): bump to minor and apply biome formatting Reclassifies the changeset as `minor` and spells out the type-level breaking change so downstream release notes surface the migration from `ServerTool` to `ServerToolNarrow`. Also applies biome formatting required by the pre-push lint hook. --- .changeset/fix-server-tool-type-gaps.md | 14 ++++++++++---- packages/agent/src/lib/tool-types.ts | 6 +++++- .../tests/unit/consumer-type-ergonomics.test-d.ts | 7 +------ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.changeset/fix-server-tool-type-gaps.md b/.changeset/fix-server-tool-type-gaps.md index 9679d3e..9bc36ef 100644 --- a/.changeset/fix-server-tool-type-gaps.md +++ b/.changeset/fix-server-tool-type-gaps.md @@ -1,9 +1,16 @@ --- -"@openrouter/agent": patch +"@openrouter/agent": minor --- Fix two type gaps that forced consumers to use `as any` when wiring up -`callModel` with server tools and chat-format inputs: +`callModel` with server tools and chat-format inputs. + +**Breaking (type-level):** `ServerTool` is no longer generic. Callers +writing `ServerTool<'openrouter:datetime'>` will fail to compile with +`Type 'ServerTool' is not generic.` Migrate to `ServerToolNarrow` +(or use `ReturnType>`). Code that only used +`ServerTool` without a type argument is unaffected — that remains the +recommended form for mixed-tool arrays. - **`ServerTool` is now a structural-base alias**, not a generic. The factory returns a narrow `ServerToolNarrow` that extends @@ -14,8 +21,7 @@ Fix two type gaps that forced consumers to use `as any` when wiring up `tool()` and `serverTool()` results directly. New public types: `ServerToolBase` (structural base) and `ServerToolNarrow` (narrow form when the exact `config` shape matters). The old `ServerTool` - generic is replaced by `ServerToolNarrow`; code that only used - `ServerTool` without a type argument is unaffected. + generic is replaced by `ServerToolNarrow`. - **`callModel`'s `request.input` now accepts `InputsUnion`** (the SDK's wider message shape returned by `fromChatMessages()`), alongside the diff --git a/packages/agent/src/lib/tool-types.ts b/packages/agent/src/lib/tool-types.ts index 1d5dd3d..7a4475a 100644 --- a/packages/agent/src/lib/tool-types.ts +++ b/packages/agent/src/lib/tool-types.ts @@ -369,7 +369,11 @@ export interface ServerToolBase { * * @template T The specific server-tool type literal (narrows `config`). */ -export type ServerTool = [T] extends [never] +export type ServerTool = [ + T, +] extends [ + never, +] ? ServerToolBase : ServerToolBase & { readonly config: Extract< diff --git a/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts b/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts index eb92c4d..d6c9e80 100644 --- a/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts +++ b/packages/agent/tests/unit/consumer-type-ergonomics.test-d.ts @@ -21,12 +21,7 @@ import { z } from 'zod/v4'; import type { CallModelInput } from '../../src/lib/async-params.js'; import { fromChatMessages } from '../../src/lib/chat-compat.js'; import { serverTool, tool } from '../../src/lib/tool.js'; -import type { - ClientTool, - ServerTool, - ServerToolBase, - Tool, -} from '../../src/lib/tool-types.js'; +import type { ClientTool, ServerTool, ServerToolBase, Tool } from '../../src/lib/tool-types.js'; // --- Issue 1: mixed arrays assign without `as any` -------------------------- From 491014fd97ffe8bb519152a7e38f3ed85f91493c Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Tue, 21 Apr 2026 17:00:09 -0400 Subject: [PATCH 4/4] docs(changeset): revise to reflect backward-compatible design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert classification to `patch` and rewrite the body to explain that both fixes are purely additive — `ServerTool` and `ServerTool` still compile exactly as before, so no consumer migration is required. --- .changeset/fix-server-tool-type-gaps.md | 36 ++++++++++++------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/.changeset/fix-server-tool-type-gaps.md b/.changeset/fix-server-tool-type-gaps.md index 9bc36ef..717fd3c 100644 --- a/.changeset/fix-server-tool-type-gaps.md +++ b/.changeset/fix-server-tool-type-gaps.md @@ -1,27 +1,25 @@ --- -"@openrouter/agent": minor +"@openrouter/agent": patch --- Fix two type gaps that forced consumers to use `as any` when wiring up -`callModel` with server tools and chat-format inputs. +`callModel` with server tools and chat-format inputs. Both fixes are +purely additive at the public-type level — `ServerTool` and +`ServerTool` continue to work exactly as before; no consumer code +needs to change. -**Breaking (type-level):** `ServerTool` is no longer generic. Callers -writing `ServerTool<'openrouter:datetime'>` will fail to compile with -`Type 'ServerTool' is not generic.` Migrate to `ServerToolNarrow` -(or use `ReturnType>`). Code that only used -`ServerTool` without a type argument is unaffected — that remains the -recommended form for mixed-tool arrays. - -- **`ServerTool` is now a structural-base alias**, not a generic. The - factory returns a narrow `ServerToolNarrow` that extends - `ServerToolBase` via interface extension, so a specific - `ServerToolNarrow<'openrouter:datetime'>` flows into the public - `ServerTool` alias without a cast. Mixed arrays like - `Array` or `Tool[]` now accept any mix of - `tool()` and `serverTool()` results directly. New public types: - `ServerToolBase` (structural base) and `ServerToolNarrow` (narrow - form when the exact `config` shape matters). The old `ServerTool` - generic is replaced by `ServerToolNarrow`. +- **Mixed `Array` now accepts narrow + `serverTool()` results without a cast.** Previously `ServerTool` + defined `config: Extract`, which made + it invariant over `T` — so `ServerTool<'openrouter:datetime'>` was + not assignable to the bare `ServerTool` (= `ServerTool`). + `ServerTool` is now a conditional generic with a `never` default that + collapses to a non-generic structural base (`ServerToolBase`, also + newly exported), and narrow variants are represented as an + intersection with that base — so any `serverTool(...)` result flows + into `Array` or `Tool[]` directly. + `ServerTool<'openrouter:datetime'>` (narrowing `config` at a call + site) still compiles as before. - **`callModel`'s `request.input` now accepts `InputsUnion`** (the SDK's wider message shape returned by `fromChatMessages()`), alongside the