Skip to content

Commit 8822603

Browse files
committed
Validate Workers AI config & widen model types
Add early config validation for Workers AI (validateWorkersAiConfig) that throws a clear error when neither a binding nor credentials/gateway are provided, and wire it into adapter constructors and client builders. Widen many Workers AI model type unions to accept arbitrary string values (while preserving autocomplete for known models). Apply small runtime type casts for binding.run calls and response shapes in workers-ai-provider. Add unit tests for config detection, validation, and arbitrary model strings, plus .changeset notes.
1 parent a9f5af2 commit 8822603

22 files changed

+470
-42
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"workers-ai-provider": patch
3+
---
4+
5+
Add early config validation to `createWorkersAI` that throws a clear error when neither a binding nor credentials (accountId + apiKey) are provided. Widen all model type parameters (TextGenerationModels, ImageGenerationModels, EmbeddingModels, TranscriptionModels, SpeechModels, RerankingModels) to accept arbitrary strings while preserving autocomplete for known models.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/tanstack-ai": patch
3+
---
4+
5+
Add config validation to all Workers AI adapter constructors that throws a clear error when neither a binding, credentials (accountId + apiKey), nor a gateway configuration is provided. Widen all model type parameters (WorkersAiTextModel, WorkersAiImageModel, WorkersAiEmbeddingModel, WorkersAiTranscriptionModel, WorkersAiTTSModel, WorkersAiSummarizeModel) to accept arbitrary strings while preserving autocomplete for known models.

packages/tanstack-ai/src/adapters/workers-ai-embedding.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,19 @@ import {
1111
createGatewayFetch,
1212
isDirectBindingConfig,
1313
isDirectCredentialsConfig,
14+
validateWorkersAiConfig,
1415
} from "../utils/create-fetcher";
1516
import { workersAiRestFetch } from "../utils/workers-ai-rest";
1617

1718
// ---------------------------------------------------------------------------
1819
// Model type derived from @cloudflare/workers-types
1920
// ---------------------------------------------------------------------------
2021

21-
export type WorkersAiEmbeddingModel = {
22-
[K in keyof AiModels]: AiModels[K] extends BaseAiTextEmbeddings ? K : never;
23-
}[keyof AiModels];
22+
export type WorkersAiEmbeddingModel =
23+
| {
24+
[K in keyof AiModels]: AiModels[K] extends BaseAiTextEmbeddings ? K : never;
25+
}[keyof AiModels]
26+
| (string & {});
2427

2528
// ---------------------------------------------------------------------------
2629
// WorkersAiEmbeddingAdapter: embeddings via Workers AI
@@ -36,6 +39,7 @@ export class WorkersAiEmbeddingAdapter {
3639
private config: WorkersAiAdapterConfig;
3740

3841
constructor(config: WorkersAiAdapterConfig, model: WorkersAiEmbeddingModel) {
42+
validateWorkersAiConfig(config);
3943
this.model = model;
4044
this.config = config;
4145
}

packages/tanstack-ai/src/adapters/workers-ai-image.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createGatewayFetch,
99
isDirectBindingConfig,
1010
isDirectCredentialsConfig,
11+
validateWorkersAiConfig,
1112
} from "../utils/create-fetcher";
1213
import { workersAiRestFetch } from "../utils/workers-ai-rest";
1314
import { binaryToBase64, uint8ArrayToBase64 } from "../utils/binary";
@@ -17,9 +18,9 @@ import type { WorkersAiDirectCredentialsConfig } from "../utils/create-fetcher";
1718
// Model type derived from @cloudflare/workers-types
1819
// ---------------------------------------------------------------------------
1920

20-
export type WorkersAiImageModel = {
21-
[K in keyof AiModels]: AiModels[K] extends BaseAiTextToImage ? K : never;
22-
}[keyof AiModels];
21+
export type WorkersAiImageModel =
22+
| { [K in keyof AiModels]: AiModels[K] extends BaseAiTextToImage ? K : never }[keyof AiModels]
23+
| (string & {});
2324

2425
// ---------------------------------------------------------------------------
2526
// WorkersAiImageAdapter: image generation via Workers AI
@@ -32,6 +33,7 @@ export class WorkersAiImageAdapter extends BaseImageAdapter<WorkersAiImageModel>
3233

3334
constructor(config: WorkersAiAdapterConfig, model: WorkersAiImageModel) {
3435
super({}, model);
36+
validateWorkersAiConfig(config);
3537
this.adapterConfig = config;
3638
}
3739

packages/tanstack-ai/src/adapters/workers-ai-summarize.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createGatewayFetch,
99
isDirectBindingConfig,
1010
isDirectCredentialsConfig,
11+
validateWorkersAiConfig,
1112
} from "../utils/create-fetcher";
1213
import { workersAiRestFetch } from "../utils/workers-ai-rest";
1314

@@ -18,7 +19,7 @@ import { workersAiRestFetch } from "../utils/workers-ai-rest";
1819
/**
1920
* Workers AI models that support summarization.
2021
*/
21-
export type WorkersAiSummarizeModel = "@cf/facebook/bart-large-cnn";
22+
export type WorkersAiSummarizeModel = "@cf/facebook/bart-large-cnn" | (string & {});
2223

2324
// ---------------------------------------------------------------------------
2425
// WorkersAiSummarizeAdapter
@@ -30,6 +31,7 @@ export class WorkersAiSummarizeAdapter extends BaseSummarizeAdapter<WorkersAiSum
3031

3132
constructor(config: WorkersAiAdapterConfig, model: WorkersAiSummarizeModel) {
3233
super({}, model);
34+
validateWorkersAiConfig(config);
3335
this.adapterConfig = config;
3436
}
3537

packages/tanstack-ai/src/adapters/workers-ai-transcription.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createGatewayFetch,
99
isDirectBindingConfig,
1010
isDirectCredentialsConfig,
11+
validateWorkersAiConfig,
1112
} from "../utils/create-fetcher";
1213
import { workersAiRestFetch, workersAiRestFetchBinary } from "../utils/workers-ai-rest";
1314
import { uint8ArrayToBase64 } from "../utils/binary";
@@ -31,7 +32,8 @@ export type WorkersAiTranscriptionModel =
3132
| "@cf/openai/whisper"
3233
| "@cf/openai/whisper-tiny-en"
3334
| "@cf/openai/whisper-large-v3-turbo"
34-
| "@cf/deepgram/nova-3";
35+
| "@cf/deepgram/nova-3"
36+
| (string & {});
3537

3638
// ---------------------------------------------------------------------------
3739
// WorkersAiTranscriptionAdapter
@@ -43,6 +45,7 @@ export class WorkersAiTranscriptionAdapter extends BaseTranscriptionAdapter<Work
4345

4446
constructor(config: WorkersAiAdapterConfig, model: WorkersAiTranscriptionModel) {
4547
super({}, model);
48+
validateWorkersAiConfig(config);
4649
this.adapterConfig = config;
4750
}
4851

packages/tanstack-ai/src/adapters/workers-ai-tts.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createGatewayFetch,
99
isDirectBindingConfig,
1010
isDirectCredentialsConfig,
11+
validateWorkersAiConfig,
1112
} from "../utils/create-fetcher";
1213
import { workersAiRestFetch } from "../utils/workers-ai-rest";
1314
import { binaryToBase64, uint8ArrayToBase64 } from "../utils/binary";
@@ -26,7 +27,8 @@ import { binaryToBase64, uint8ArrayToBase64 } from "../utils/binary";
2627
export type WorkersAiTTSModel =
2728
| "@cf/deepgram/aura-1"
2829
| "@cf/deepgram/aura-2-en"
29-
| "@cf/deepgram/aura-2-es";
30+
| "@cf/deepgram/aura-2-es"
31+
| (string & {});
3032

3133
// ---------------------------------------------------------------------------
3234
// WorkersAiTTSAdapter
@@ -38,6 +40,7 @@ export class WorkersAiTTSAdapter extends BaseTTSAdapter<WorkersAiTTSModel> {
3840

3941
constructor(config: WorkersAiAdapterConfig, model: WorkersAiTTSModel) {
4042
super({}, model);
43+
validateWorkersAiConfig(config);
4144
this.adapterConfig = config;
4245
}
4346

packages/tanstack-ai/src/adapters/workers-ai.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,26 @@ import {
1313
createWorkersAiBindingFetch,
1414
isDirectBindingConfig,
1515
isDirectCredentialsConfig,
16+
validateWorkersAiConfig,
1617
} from "../utils/create-fetcher";
1718

1819
// ---------------------------------------------------------------------------
1920
// Model types derived from @cloudflare/workers-types
2021
// ---------------------------------------------------------------------------
2122

22-
export type WorkersAiTextModel = {
23-
[K in keyof AiModels]: AiModels[K] extends BaseAiTextGeneration ? K : never;
24-
}[keyof AiModels];
23+
export type WorkersAiTextModel =
24+
| {
25+
[K in keyof AiModels]: AiModels[K] extends BaseAiTextGeneration ? K : never;
26+
}[keyof AiModels]
27+
| (string & {});
2528

2629
// ---------------------------------------------------------------------------
2730
// Helpers: build the right OpenAI client depending on config mode
2831
// ---------------------------------------------------------------------------
2932

3033
function buildWorkersAiClient(config: WorkersAiAdapterConfig): OpenAI {
34+
validateWorkersAiConfig(config);
35+
3136
if (isDirectBindingConfig(config)) {
3237
// Plain binding mode: shim translates OpenAI fetch calls to env.AI.run()
3338
return new OpenAI({

packages/tanstack-ai/src/utils/create-fetcher.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,29 @@ export function isGatewayConfig(config: WorkersAiAdapterConfig): config is AiGat
136136
return "binding" in config && !isDirectBindingConfig(config);
137137
}
138138

139+
// ---------------------------------------------------------------------------
140+
// Config validation
141+
// ---------------------------------------------------------------------------
142+
143+
/**
144+
* Validates that a WorkersAiAdapterConfig contains a valid configuration.
145+
* Throws an error if neither a binding, credentials (accountId + apiKey),
146+
* nor a gateway configuration is provided.
147+
*/
148+
export function validateWorkersAiConfig(config: WorkersAiAdapterConfig): void {
149+
if (
150+
!isDirectBindingConfig(config) &&
151+
!isDirectCredentialsConfig(config) &&
152+
!isGatewayConfig(config)
153+
) {
154+
throw new Error(
155+
"Invalid Workers AI configuration: you must provide either a binding (e.g. { binding: env.AI }), " +
156+
"credentials ({ accountId, apiKey }), or a gateway configuration ({ binding: env.AI.gateway(id) } " +
157+
"or { accountId, gatewayId }).",
158+
);
159+
}
160+
}
161+
139162
// ---------------------------------------------------------------------------
140163
// createGatewayFetch -- for routing through AI Gateway
141164
// ---------------------------------------------------------------------------

packages/tanstack-ai/test/config-detection.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
isDirectBindingConfig,
44
isDirectCredentialsConfig,
55
isGatewayConfig,
6+
validateWorkersAiConfig,
67
type WorkersAiAdapterConfig,
78
} from "../src/utils/create-fetcher";
89

@@ -90,3 +91,71 @@ describe("config detection", () => {
9091
expect(isGatewayConfig(config)).toBe(false);
9192
});
9293
});
94+
95+
// ---------------------------------------------------------------------------
96+
// validateWorkersAiConfig
97+
// ---------------------------------------------------------------------------
98+
99+
describe("validateWorkersAiConfig", () => {
100+
it("accepts plain Workers AI binding config", () => {
101+
const binding = {
102+
run: (_model: string, _inputs: Record<string, unknown>) => Promise.resolve({}),
103+
gateway: (_id: string) => ({
104+
run: (_req: unknown) => Promise.resolve(new Response("ok")),
105+
}),
106+
};
107+
expect(() => validateWorkersAiConfig({ binding })).not.toThrow();
108+
});
109+
110+
it("accepts plain REST credentials config", () => {
111+
expect(() => validateWorkersAiConfig({ accountId: "abc", apiKey: "key" })).not.toThrow();
112+
});
113+
114+
it("accepts gateway binding config", () => {
115+
const binding = {
116+
run: (_request: unknown) => Promise.resolve(new Response("ok")),
117+
};
118+
expect(() => validateWorkersAiConfig({ binding })).not.toThrow();
119+
});
120+
121+
it("accepts gateway credentials config", () => {
122+
const config = {
123+
accountId: "abc",
124+
gatewayId: "gw-1",
125+
} as WorkersAiAdapterConfig;
126+
expect(() => validateWorkersAiConfig(config)).not.toThrow();
127+
});
128+
129+
it("throws for empty config", () => {
130+
expect(() => validateWorkersAiConfig({} as WorkersAiAdapterConfig)).toThrow(
131+
/Invalid Workers AI configuration/,
132+
);
133+
});
134+
135+
it("throws for config with only unrelated properties", () => {
136+
expect(() =>
137+
validateWorkersAiConfig({ foo: "bar" } as unknown as WorkersAiAdapterConfig),
138+
).toThrow(/Invalid Workers AI configuration/);
139+
});
140+
141+
it("throws for config with only accountId (missing apiKey)", () => {
142+
expect(() =>
143+
validateWorkersAiConfig({ accountId: "abc" } as unknown as WorkersAiAdapterConfig),
144+
).toThrow(/Invalid Workers AI configuration/);
145+
});
146+
147+
it("throws for config with only apiKey (missing accountId)", () => {
148+
expect(() =>
149+
validateWorkersAiConfig({ apiKey: "key" } as unknown as WorkersAiAdapterConfig),
150+
).toThrow(/Invalid Workers AI configuration/);
151+
});
152+
153+
it("error message mentions binding and credentials", () => {
154+
try {
155+
validateWorkersAiConfig({} as WorkersAiAdapterConfig);
156+
} catch (e) {
157+
expect((e as Error).message).toContain("binding");
158+
expect((e as Error).message).toContain("credentials");
159+
}
160+
});
161+
});

0 commit comments

Comments
 (0)