Skip to content

Commit c0af426

Browse files
liyownclaude
andauthored
fix(types): fix the type definitions for factory models and config, a… (#408)
* fix(types): fix the type definitions for factory models and config, and add InferRestArgs utility type * refactor(types): simplify type definitions for factory models by removing unused InferConfig type * fix(types): handle optional model param, restore explicit return type, add regression test Review follow-ups for #408: - InferRestArgs now matches [any?, ...] so factories with an optional first parameter don't silently drop their trailing args - Extract the extended signature into a named ExtendedFactory type and restore an explicit return type on extendAdapter (no cast needed) - Add a regression test for 3-parameter factories like createAnthropicChat(model, apiKey, config?) (#407) - Add a patch changeset for @tanstack/ai Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(types): remove all any from extend-adapter type utilities Replace any-typed function shapes with a sound AnyAdapterFactory constraint built on never params / unknown return (parameters are contravariant, so never accepts every factory). The deliberate model-union widening now lives in an overload signature instead of an unchecked assignment, so no cast or any remains anywhere in the file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(types): assert invalid model names are rejected for 3-param factories Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 00e0c93 commit c0af426

3 files changed

Lines changed: 79 additions & 24 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/ai': patch
3+
---
4+
5+
Fix `extendAdapter` dropping required parameters after the model (e.g. `apiKey` in `createAnthropicChat`). All factory parameters after the model are now preserved, including labels and optionality.

packages/ai/src/extend-adapter.ts

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -144,39 +144,59 @@ type ExtractCustomModelNames<TDefs extends ReadonlyArray<ExtendedModelDef>> =
144144
// Factory Type Inference
145145
// ===========================
146146

147+
/**
148+
* The widest factory shape `extendAdapter` accepts: any function taking a
149+
* model as its first parameter. Parameters are contravariant, so `never`
150+
* params and an `unknown` return accept every factory without resorting
151+
* to `any`.
152+
*/
153+
type AnyAdapterFactory = (model: never, ...args: Array<never>) => unknown
154+
147155
/**
148156
* Infer the model parameter type from an adapter factory function.
149157
* For generic functions like `<T extends Union>(model: T)`, this gets `T` which
150158
* TypeScript treats as the constraint union when used in parameter position.
151159
*/
152160
type InferFactoryModels<TFactory> = TFactory extends (
153161
model: infer TModel,
154-
...args: Array<any>
155-
) => any
162+
...args: Array<never>
163+
) => unknown
156164
? TModel extends string
157165
? TModel
158166
: string
159167
: string
160168

161-
/**
162-
* Infer the config parameter type from an adapter factory function.
163-
*/
164-
type InferConfig<TFactory> = TFactory extends (
165-
model: any,
166-
config?: infer TConfig,
167-
) => any
168-
? TConfig
169-
: undefined
170-
171169
/**
172170
* Infer the adapter return type from a factory function.
173171
*/
174172
type InferAdapterReturn<TFactory> = TFactory extends (
175-
...args: Array<any>
173+
...args: Array<never>
176174
) => infer TReturn
177175
? TReturn
178176
: never
179177

178+
/**
179+
* Extracts all parameter types after the model parameter from a factory,
180+
* preserving labels and optionality (e.g. `[apiKey: string, config?: C]`).
181+
* Note: overloaded factories resolve against their last overload (a
182+
* `Parameters` limitation).
183+
*/
184+
type InferRestArgs<TFactory extends AnyAdapterFactory> =
185+
Parameters<TFactory> extends [unknown?, ...infer TRest] ? TRest : []
186+
187+
/**
188+
* The factory signature produced by `extendAdapter`: accepts both original
189+
* and custom model names while preserving all remaining parameters and the
190+
* return type of the original factory.
191+
*/
192+
type ExtendedFactory<
193+
TFactory extends AnyAdapterFactory,
194+
TDefs extends ReadonlyArray<ExtendedModelDef>,
195+
> = (
196+
model: InferFactoryModels<TFactory> | ExtractCustomModelNames<TDefs>,
197+
...args: InferRestArgs<TFactory>
198+
) => InferAdapterReturn<TFactory>
199+
180200
// ===========================
181201
// extendAdapter Function
182202
// ===========================
@@ -225,19 +245,17 @@ type InferAdapterReturn<TFactory> = TFactory extends (
225245
* ```
226246
*/
227247
export function extendAdapter<
228-
TFactory extends (...args: Array<any>) => any,
248+
TFactory extends AnyAdapterFactory,
229249
const TDefs extends ReadonlyArray<ExtendedModelDef>,
230-
>(
231-
factory: TFactory,
232-
_customModels: TDefs,
233-
): (
234-
model: InferFactoryModels<TFactory> | ExtractCustomModelNames<TDefs>,
235-
...args: InferConfig<TFactory> extends undefined
236-
? []
237-
: [config?: InferConfig<TFactory>]
238-
) => InferAdapterReturn<TFactory> {
250+
>(factory: TFactory, _customModels: TDefs): ExtendedFactory<TFactory, TDefs>
251+
// The implementation signature stays at the honest `AnyAdapterFactory` width;
252+
// the overload above performs the deliberate model-union widening.
253+
export function extendAdapter(
254+
factory: AnyAdapterFactory,
255+
_customModels: ReadonlyArray<ExtendedModelDef>,
256+
): AnyAdapterFactory {
239257
// At runtime, we simply pass through to the original factory.
240258
// The _customModels parameter is only used for type inference.
241259
// No runtime validation - users are trusted to pass valid model names.
242-
return factory as any
260+
return factory
243261
}

packages/ai/tests/extend-adapter.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,38 @@ describe('extendAdapter', () => {
243243
})
244244
})
245245

246+
describe('Factories with required args after model (#407)', () => {
247+
// Mimics createAnthropicChat(model, apiKey, config?)
248+
function mockChat<TModel extends MockModel>(
249+
model: TModel,
250+
apiKey: string,
251+
config?: MockAdapterConfig,
252+
): MockTextAdapter<TModel> {
253+
void apiKey
254+
return new MockTextAdapter(model, config)
255+
}
256+
257+
it('should preserve required apiKey and optional config parameters', () => {
258+
const extendedMock = extendAdapter(mockChat, customModels)
259+
260+
expectTypeOf(extendedMock).parameter(1).toEqualTypeOf<string>()
261+
262+
const adapter = extendedMock('my-fine-tuned-model', 'sk-test', {
263+
baseURL: 'https://custom.api.com',
264+
})
265+
expect(adapter.model).toBe('my-fine-tuned-model')
266+
267+
// config stays optional
268+
void extendedMock('mock-gpt-4', 'sk-test')
269+
270+
// @ts-expect-error - apiKey is required
271+
void extendedMock('mock-gpt-4')
272+
273+
// @ts-expect-error - invalid model names still rejected
274+
void extendedMock('not-a-model', 'sk-test')
275+
})
276+
})
277+
246278
describe('Empty custom models', () => {
247279
it('should work with empty custom models array', () => {
248280
const extendedMock = extendAdapter(mockText, [] as const)

0 commit comments

Comments
 (0)