void 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/cli.mjs CHANGED
@@ -1568,7 +1568,7 @@ else if (command === "deploy") {
1568
1568
  } else if (command === "auth") runSubcommandGroup("auth", authSubcommands, parseAuthArgs, (root, parsed) => import("../auth-cmd-CPkWrQR9.mjs").then(({ runAuthCommand }) => runAuthCommand(root, parsed)));
1569
1569
  else if (command === "project") runSubcommandGroup("project", projectSubcommands, parseProjectArgs, (root, parsed) => import("../project-cmd-CLrqSbqj.mjs").then(({ runProjectCommand }) => runProjectCommand(root, parsed)));
1570
1570
  else if (command === "secret") runSubcommandGroup("secret", secretSubcommands, parseSecretArgs, (root, parsed) => import("../secret-C_uTY7xx.mjs").then(({ runSecretCommand }) => runSecretCommand(root, parsed)));
1571
- else if (command === "domain") runSubcommandGroup("domain", domainSubcommands, parseDomainArgs, (root, parsed) => import("../domain-DxTc6HDn.mjs").then(({ runDomainCommand }) => runDomainCommand(root, parsed)));
1571
+ else if (command === "domain") runSubcommandGroup("domain", domainSubcommands, parseDomainArgs, (root, parsed) => import("../domain-B7RntQ1r.mjs").then(({ runDomainCommand }) => runDomainCommand(root, parsed)));
1572
1572
  else if (command === "db") runSubcommandGroup("db", dbSubcommands, parseDbArgs, (root, parsed) => import("../db-DiddQC2v.mjs").then(({ runDbCommand }) => runDbCommand(root, parsed)));
1573
1573
  else if (command === "env") runSubcommandGroup("env", envSubcommands, parseEnvArgs, (root, parsed) => import("../env-Bvw0wMTI.mjs").then(({ runEnvCommand }) => runEnvCommand(root, parsed)));
1574
1574
  else if (command === "gen") runSubcommandGroup("gen", genSubcommands, parseGenArgs, (root, parsed) => import("../gen-CZdNWIaA.mjs").then(({ runGenCommand }) => runGenCommand(root, parsed)));
@@ -1695,7 +1695,7 @@ else if (command === "-v" || command === "--version") {
1695
1695
  case "auth": return runSubcommandGroup("auth", authSubcommands, parseAuthArgs, (r, p) => import("../auth-cmd-CPkWrQR9.mjs").then(({ runAuthCommand }) => runAuthCommand(r, p)));
1696
1696
  case "project": return runSubcommandGroup("project", projectSubcommands, parseProjectArgs, (r, p) => import("../project-cmd-CLrqSbqj.mjs").then(({ runProjectCommand }) => runProjectCommand(r, p)));
1697
1697
  case "secret": return runSubcommandGroup("secret", secretSubcommands, parseSecretArgs, (r, p) => import("../secret-C_uTY7xx.mjs").then(({ runSecretCommand }) => runSecretCommand(r, p)));
1698
- case "domain": return runSubcommandGroup("domain", domainSubcommands, parseDomainArgs, (r, p) => import("../domain-DxTc6HDn.mjs").then(({ runDomainCommand }) => runDomainCommand(r, p)));
1698
+ case "domain": return runSubcommandGroup("domain", domainSubcommands, parseDomainArgs, (r, p) => import("../domain-B7RntQ1r.mjs").then(({ runDomainCommand }) => runDomainCommand(r, p)));
1699
1699
  case "db": return runSubcommandGroup("db", dbSubcommands, parseDbArgs, (r, p) => import("../db-DiddQC2v.mjs").then(({ runDbCommand }) => runDbCommand(r, p)));
1700
1700
  case "env": return runSubcommandGroup("env", envSubcommands, parseEnvArgs, (r, p) => import("../env-Bvw0wMTI.mjs").then(({ runEnvCommand }) => runEnvCommand(r, p)));
1701
1701
  case "gen": return runSubcommandGroup("gen", genSubcommands, parseGenArgs, (r, p) => import("../gen-CZdNWIaA.mjs").then(({ runGenCommand }) => runGenCommand(r, p)));
@@ -39,20 +39,13 @@ async function domainAdd(client, projectId, hostname) {
39
39
  R.success(`Domain ${hostname} added (status: ${domain.status})`);
40
40
  let cnameSection;
41
41
  let ownershipSection;
42
- let sslSection;
43
42
  if (domain.cname_target) cnameSection = `CNAME (traffic)\n ${hostname} → ${domain.cname_target}`;
44
43
  if (domain.ownership_verification) ownershipSection = `TXT (ownership verification)\n ${domain.ownership_verification.name} → ${domain.ownership_verification.value}`;
45
- const sslRecords = (domain.ssl_validation_records ?? []).filter((r) => Boolean(r.txt_name) && Boolean(r.txt_value));
46
- if (sslRecords.length > 0) sslSection = `TXT (SSL validation)\n${sslRecords.map((r) => ` ${r.txt_name} → ${r.txt_value}`).join("\n")}`;
47
- else sslSection = `SSL validation records will appear in \`void domain status\` shortly.`;
48
- const summary = `Add the records above to your DNS, then run \`void domain status ${hostname}\` to check progress.`;
49
- const sections = [
50
- cnameSection,
51
- ownershipSection,
52
- sslSection,
53
- summary
54
- ].filter((s) => s !== void 0);
55
- if (sections.length > 0) Se(sections.join("\n\n"), "DNS Setup");
44
+ const recordSections = [cnameSection, ownershipSection].filter((s) => s !== void 0);
45
+ if (recordSections.length > 0) {
46
+ const summary = `Add ${recordSections.length === 1 ? "the record above" : "the records above"} to your DNS, then run \`void domain status ${hostname}\` to check progress.`;
47
+ Se([...recordSections, summary].join("\n\n"), "DNS Setup");
48
+ }
56
49
  }
57
50
  async function domainRemove(client, projectId, hostname) {
58
51
  const domain = (await client.listDomains(projectId)).find((d) => d.hostname === hostname);
@@ -93,7 +86,7 @@ function computeRollup(status) {
93
86
  };
94
87
  if (status.cf_status === "pending_issuance" || status.cf_status === "active" && status.ssl_status !== "active") return {
95
88
  state: "issuing_cert",
96
- diagnostic: "Cloudflare is issuing the SSL certificate (typically <5 minutes)."
89
+ diagnostic: status.cname_target ? "Cloudflare is issuing the SSL certificate automatically (typically <5 minutes). If this persists, confirm the traffic CNAME below is published." : "Cloudflare is issuing the SSL certificate automatically (typically <5 minutes)."
97
90
  };
98
91
  if (status.cf_status === "pending_deployment") return {
99
92
  state: "deploying_cert",
@@ -125,20 +118,12 @@ async function domainStatus(client, projectId, hostname, verbose) {
125
118
  if (status.verification_errors && status.verification_errors.length > 0) rollupLines.push(` Errors: ${status.verification_errors.join(", ")}`);
126
119
  }
127
120
  Se(rollupLines.join("\n"), "Domain Status");
128
- const ownershipPending = Boolean(status.ownership_verification);
129
- const sslPending = status.ssl_status !== "active";
130
- if (ownershipPending || sslPending) {
131
- const pendingSections = [];
132
- if (ownershipPending && status.ownership_verification) pendingSections.push(`TXT (ownership verification)\n ${status.ownership_verification.name} → ${status.ownership_verification.value}`);
133
- if (sslPending) {
134
- const sslRecords = (status.ssl_validation_records ?? []).filter((r) => Boolean(r.txt_name) && Boolean(r.txt_value) && r.status !== "active");
135
- if (sslRecords.length > 0) {
136
- const lines = sslRecords.map((r) => ` ${r.txt_name} → ${r.txt_value}`).join("\n");
137
- pendingSections.push(`TXT (SSL validation)\n${lines}`);
138
- }
139
- }
140
- if (pendingSections.length > 0) Se(pendingSections.join("\n\n"), "Add these DNS records to your provider");
141
- }
121
+ let cnameSection;
122
+ let ownershipSection;
123
+ if (status.cname_target && status.ssl_status !== "active") cnameSection = `CNAME (traffic)\n ${status.hostname} → ${status.cname_target}`;
124
+ if (status.ownership_verification) ownershipSection = `TXT (ownership verification)\n ${status.ownership_verification.name} → ${status.ownership_verification.value}`;
125
+ const pendingRecords = [cnameSection, ownershipSection].filter((s) => s !== void 0);
126
+ if (pendingRecords.length > 0) Se(pendingRecords.join("\n\n"), pendingRecords.length === 1 ? "Add this DNS record to your provider" : "Add these DNS records to your provider");
142
127
  }
143
128
  //#endregion
144
129
  export { runDomainCommand };
@@ -1,102 +1,20 @@
1
1
  //#region src/runtime/ai-types.d.ts
2
- type ThirdPartyProvider = "openai" | "anthropic" | "google-ai-studio" | "groq" | "mistral" | "grok" | "deepseek" | "openrouter" | "perplexity" | "cohere" | "cerebras" | "huggingface" | "replicate" | "baseten" | "cartesia" | "deepgram" | "elevenlabs" | "fal" | "ideogram" | "parallel";
3
- type ThirdPartyModel = `${ThirdPartyProvider}/${string}`;
4
- type ChatContent = string | Array<{
5
- type: "text";
6
- text: string;
7
- } | {
8
- type: "image_url";
9
- image_url: {
10
- url: string;
11
- detail?: "auto" | "low" | "high";
12
- };
13
- }>;
14
- type ChatMessage = {
15
- role: "system" | "user" | "assistant";
16
- content: ChatContent;
17
- };
18
- type ChatCompletionInputs = {
19
- messages: Array<ChatMessage>;
20
- temperature?: number;
21
- max_tokens?: number;
22
- top_p?: number;
23
- frequency_penalty?: number;
24
- presence_penalty?: number;
25
- stop?: string | Array<string>;
26
- stream?: boolean;
27
- };
28
- type ChatCompletionResponse = {
29
- id: string;
30
- object: string;
31
- created: number;
32
- model: string;
33
- choices: Array<ChatCompletionChoice>;
34
- usage: ChatCompletionUsage;
35
- };
36
- type ChatCompletionChoice = {
37
- index: number;
38
- message: ChatMessage;
39
- finish_reason: string;
40
- };
41
- type ChatCompletionUsage = {
42
- prompt_tokens: number;
43
- completion_tokens: number;
44
- total_tokens: number;
45
- };
46
- type ChatCompletionChunk = {
47
- id: string;
48
- choices: Array<{
49
- index: number;
50
- delta: {
51
- role?: string;
52
- content?: string;
53
- };
54
- finish_reason: string | null;
55
- }>;
56
- };
57
- type ImageGatewayEndpoint = "images/generations" | "images/edits";
2
+ type AiProviderName = "openai" | "anthropic" | "google-ai-studio" | "groq" | "mistral" | "grok" | "deepseek" | "openrouter" | "perplexity" | "cohere" | "cerebras" | "huggingface" | "replicate" | "baseten" | "cartesia" | "deepgram" | "elevenlabs" | "fal" | "ideogram" | "parallel";
3
+ type AiProvider = AiProviderName | (string & {});
58
4
  type ImageFileInput = Blob | ArrayBuffer | ArrayBufferView | {
59
5
  data: string;
60
6
  name?: string;
61
7
  type?: string;
62
8
  };
63
- type ImageGenerationInputs = {
64
- prompt: string;
65
- n?: number;
66
- size?: string;
67
- quality?: string;
68
- response_format?: "url" | "b64_json";
69
- user?: string;
70
- [key: string]: unknown;
71
- };
72
- type ImageGenerationData = {
73
- url?: string;
74
- b64_json?: string;
75
- revised_prompt?: string;
76
- };
77
- type ImageGenerationResponse = {
78
- created?: number;
79
- data: Array<ImageGenerationData>;
80
- usage?: unknown;
81
- [key: string]: unknown;
82
- };
83
- type ImageGenerationOptions = {
84
- endpoint?: "images/generations";
85
- [key: string]: unknown;
86
- };
87
- type ImageEditOptions = {
88
- endpoint: "images/edits";
89
- [key: string]: unknown;
9
+ type AiProviderOptions = {
10
+ apiKeyEnv?: string;
11
+ apiKeyHeader?: string;
12
+ apiKeyPrefix?: string;
90
13
  };
91
- type ImageEditInputs = {
92
- prompt: string;
93
- image: ImageFileInput | Array<ImageFileInput>;
94
- mask?: ImageFileInput;
95
- n?: number;
96
- size?: string;
97
- response_format?: "url" | "b64_json";
98
- user?: string;
99
- [key: string]: unknown;
14
+ type AiProviderFetchInit = {
15
+ method?: string;
16
+ headers?: HeadersInit;
17
+ body?: unknown;
100
18
  };
101
19
  //#endregion
102
20
  //#region src/runtime/ai.d.ts
@@ -107,14 +25,16 @@ type ImageEditInputs = {
107
25
  * in a `Response` with SSE headers — ready to return from a route handler.
108
26
  */
109
27
  type VoidAi = Omit<Ai, "run"> & {
110
- /** Workers AI inference (existing) */run<Name extends keyof AiModels>(model: Name, inputs: AiModels[Name]["inputs"], options?: AiOptions): Promise<AiModels[Name]["postProcessedOutputs"]>; /** Third-party chat completions (OpenAI, Anthropic, etc.) */
111
- run(model: ThirdPartyModel, inputs: ChatCompletionInputs, options?: AiOptions): Promise<ChatCompletionResponse>; /** Image generation. Returns the proxy response so callers can return or parse it. */
112
- image<Name extends keyof AiModels>(model: Name, inputs: AiModels[Name]["inputs"], options?: ImageGenerationOptions): Promise<Response>; /** Third-party image generation or editing through AI Gateway. */
113
- image(model: ThirdPartyModel, inputs: ImageEditInputs, options: ImageEditOptions): Promise<Response>; /** Third-party image generation through AI Gateway. */
114
- image(model: ThirdPartyModel, inputs: ImageGenerationInputs, options?: ImageGenerationOptions): Promise<Response>; /** Stream Workers AI response as SSE */
115
- stream<Name extends keyof AiModels>(model: Name, inputs: Omit<AiModels[Name]["inputs"], "stream">, options?: AiOptions): Promise<Response>; /** Stream third-party chat completions as SSE */
116
- stream(model: ThirdPartyModel, inputs: Omit<ChatCompletionInputs, "stream">, options?: AiOptions): Promise<Response>;
28
+ /** Workers AI inference (existing) */run<Name extends keyof AiModels>(model: Name, inputs: AiModels[Name]["inputs"], options?: AiOptions): Promise<AiModels[Name]["postProcessedOutputs"]>; /** Cloudflare AI Gateway model inference. Accepts Cloudflare model IDs unchanged. */
29
+ run(model: string, inputs: unknown, options?: AiOptions): Promise<unknown>; /** Image generation. Returns the proxy response so callers can return or parse it. */
30
+ image<Name extends keyof AiModels>(model: Name, inputs: AiModels[Name]["inputs"], options?: AiOptions): Promise<Response>; /** Cloudflare AI Gateway model inference returned as a Response. */
31
+ image(model: string, inputs: unknown, options?: AiOptions): Promise<Response>; /** Provider-native AI Gateway requests using a project secret for the provider API key. */
32
+ provider(provider: AiProvider, options?: AiProviderOptions): {
33
+ fetch(path: string, init?: AiProviderFetchInit): Promise<Response>;
34
+ }; /** Stream Workers AI response as SSE */
35
+ stream<Name extends keyof AiModels>(model: Name, inputs: Omit<AiModels[Name]["inputs"], "stream">, options?: AiOptions): Promise<Response>; /** Stream any Cloudflare AI Gateway model response as SSE */
36
+ stream(model: string, inputs: Record<string, unknown>, options?: AiOptions): Promise<Response>;
117
37
  };
118
38
  declare const ai: VoidAi;
119
39
  //#endregion
120
- export { type ChatCompletionChoice, type ChatCompletionChunk, type ChatCompletionInputs, type ChatCompletionResponse, type ChatCompletionUsage, type ChatContent, type ChatMessage, type ImageEditInputs, type ImageEditOptions, type ImageFileInput, type ImageGatewayEndpoint, type ImageGenerationData, type ImageGenerationInputs, type ImageGenerationOptions, type ImageGenerationResponse, type ThirdPartyModel, type ThirdPartyProvider, VoidAi, ai };
40
+ export { type AiProvider, type AiProviderFetchInit, type AiProviderName, type AiProviderOptions, type ImageFileInput, VoidAi, ai };
@@ -32,9 +32,6 @@ const VOID_FILE_MARKER = "__voidFile";
32
32
  function hasStreamFlag(value) {
33
33
  return typeof value === "object" && value !== null && "stream" in value;
34
34
  }
35
- function isThirdPartyModel(model) {
36
- return model.split("/")[0] in PROVIDER_KEY_MAP;
37
- }
38
35
  function mediaTypeFromContentType(contentType) {
39
36
  if (!contentType) return null;
40
37
  const [mediaType] = contentType.split(";", 1);
@@ -109,8 +106,70 @@ async function serializeProxyValue(value, serializeDataObjectAsFile = false) {
109
106
  }
110
107
  return value;
111
108
  }
112
- async function serializeImageInputs(inputs, endpoint) {
113
- return endpoint === "images/edits" ? serializeProxyValue(inputs) : inputs;
109
+ function formFileName(value, fallback) {
110
+ return fileNameFromBlob(value) ?? fallback;
111
+ }
112
+ async function serializeFormData(form) {
113
+ const entries = [];
114
+ for (const [name, value] of form.entries()) {
115
+ if (typeof value === "string") {
116
+ entries.push({
117
+ name,
118
+ value
119
+ });
120
+ continue;
121
+ }
122
+ entries.push({
123
+ name,
124
+ value: {
125
+ [VOID_FILE_MARKER]: true,
126
+ data: arrayBufferToBase64(await value.arrayBuffer()),
127
+ name: formFileName(value, name),
128
+ type: value.type || void 0
129
+ }
130
+ });
131
+ }
132
+ return {
133
+ type: "form",
134
+ entries
135
+ };
136
+ }
137
+ async function serializeProviderBody(body) {
138
+ if (body === void 0 || body === null) return { type: "none" };
139
+ if (typeof body === "string") return {
140
+ type: "text",
141
+ data: body
142
+ };
143
+ if (body instanceof URLSearchParams) return {
144
+ type: "text",
145
+ data: body.toString(),
146
+ contentType: "application/x-www-form-urlencoded;charset=UTF-8"
147
+ };
148
+ if (body instanceof FormData) return serializeFormData(body);
149
+ if (body instanceof Blob) return {
150
+ type: "base64",
151
+ data: arrayBufferToBase64(await body.arrayBuffer()),
152
+ contentType: body.type || void 0
153
+ };
154
+ if (body instanceof ArrayBuffer) return {
155
+ type: "base64",
156
+ data: arrayBufferToBase64(body)
157
+ };
158
+ if (isBinaryView(body)) return {
159
+ type: "base64",
160
+ data: arrayBufferToBase64(copyBinaryView(body))
161
+ };
162
+ return {
163
+ type: "json",
164
+ value: await serializeProxyValue(body)
165
+ };
166
+ }
167
+ function headersToRecord(headers) {
168
+ if (!headers) return;
169
+ const result = {};
170
+ const normalized = new Headers(headers);
171
+ for (const [key, value] of normalized.entries()) result[key] = value;
172
+ return result;
114
173
  }
115
174
  async function responseFromResult(result) {
116
175
  if (result instanceof Response) return result;
@@ -132,9 +191,8 @@ function splitImageOptions(options) {
132
191
  runOptions: Object.keys(runOptions).length > 0 ? runOptions : void 0
133
192
  };
134
193
  }
135
- function resolveProviderKey(model, runtimeEnv) {
136
- const provider = model.split("/")[0];
137
- const envVar = PROVIDER_KEY_MAP[provider];
194
+ function resolveProviderKey(provider, runtimeEnv, options) {
195
+ const envVar = options?.apiKeyEnv ?? PROVIDER_KEY_MAP[provider];
138
196
  if (!envVar) throw new Error(`ai: Unknown provider '${provider}'. Use one of: ${Object.keys(PROVIDER_KEY_MAP).join(", ")}.`);
139
197
  const key = runtimeEnv[envVar];
140
198
  if (typeof key !== "string" || key.length === 0) throw new Error(`ai: Missing ${envVar} secret. Add it as a project secret for provider '${provider}'.`);
@@ -159,6 +217,7 @@ function resolveBackend() {
159
217
  const { runOptions } = splitImageOptions(options);
160
218
  return responseFromResult(await directAi.run(model, inputs, runOptions));
161
219
  },
220
+ providerFetch: unsupportedProviderFetch,
162
221
  models: directAi.models.bind(directAi),
163
222
  toMarkdown: directAi.toMarkdown?.bind(directAi) ?? unsupported("toMarkdown")
164
223
  };
@@ -169,8 +228,23 @@ function unsupported(method) {
169
228
  throw new Error(`ai: ai.${method}() is not supported in this environment.`);
170
229
  };
171
230
  }
231
+ function unsupportedProviderFetch() {
232
+ throw new Error("ai: ai.provider().fetch() is not supported in this environment.");
233
+ }
234
+ async function providerRequestBody(provider, path, init, options, runtimeEnv) {
235
+ return {
236
+ provider,
237
+ path,
238
+ method: init?.method,
239
+ headers: headersToRecord(init?.headers),
240
+ body: await serializeProviderBody(init?.body),
241
+ providerKey: resolveProviderKey(provider, runtimeEnv, options),
242
+ apiKeyHeader: options?.apiKeyHeader,
243
+ apiKeyPrefix: options?.apiKeyPrefix
244
+ };
245
+ }
172
246
  function makeServiceBindingBackend(proxy, runtimeEnv) {
173
- async function proxyFetch(action, body) {
247
+ async function proxyFetch(action, body, options = {}) {
174
248
  const res = await proxy.fetch(`https://ai-proxy/ai/${action}`, {
175
249
  method: "POST",
176
250
  headers: { "content-type": "application/json" },
@@ -179,7 +253,7 @@ function makeServiceBindingBackend(proxy, runtimeEnv) {
179
253
  ...body
180
254
  })
181
255
  });
182
- if (!res.ok) {
256
+ if (options.throwOnError !== false && !res.ok) {
183
257
  const text = await res.text();
184
258
  throw new Error(`ai: Proxy request failed with ${res.status}: '${text}'.`);
185
259
  }
@@ -189,17 +263,6 @@ function makeServiceBindingBackend(proxy, runtimeEnv) {
189
263
  async run(model, inputs, options) {
190
264
  const projectId = runtimeEnv.__PROJECT_ID;
191
265
  const aiToken = runtimeEnv.__VOID_PROXY_TOKEN;
192
- if (isThirdPartyModel(model)) {
193
- const res = await proxyFetch("gateway", {
194
- model,
195
- inputs,
196
- providerKey: resolveProviderKey(model, runtimeEnv),
197
- projectId,
198
- aiToken
199
- });
200
- if (hasStreamFlag(inputs) && inputs.stream) return res.body;
201
- return readProxyResult(res);
202
- }
203
266
  const res = await proxyFetch("run", {
204
267
  model,
205
268
  inputs,
@@ -213,18 +276,7 @@ function makeServiceBindingBackend(proxy, runtimeEnv) {
213
276
  async image(model, inputs, options) {
214
277
  const projectId = runtimeEnv.__PROJECT_ID;
215
278
  const aiToken = runtimeEnv.__VOID_PROXY_TOKEN;
216
- const { endpoint, runOptions } = splitImageOptions(options);
217
- if (isThirdPartyModel(model)) {
218
- const providerKey = resolveProviderKey(model, runtimeEnv);
219
- return proxyFetch("gateway", {
220
- model,
221
- endpoint,
222
- inputs: await serializeImageInputs(inputs, endpoint),
223
- providerKey,
224
- projectId,
225
- aiToken
226
- });
227
- }
279
+ const { runOptions } = splitImageOptions(options);
228
280
  return proxyFetch("run", {
229
281
  model,
230
282
  inputs,
@@ -233,6 +285,15 @@ function makeServiceBindingBackend(proxy, runtimeEnv) {
233
285
  aiToken
234
286
  });
235
287
  },
288
+ async providerFetch(provider, path, init, options) {
289
+ const projectId = runtimeEnv.__PROJECT_ID;
290
+ const aiToken = runtimeEnv.__VOID_PROXY_TOKEN;
291
+ return proxyFetch("provider", {
292
+ ...await providerRequestBody(provider, path, init, options, runtimeEnv),
293
+ projectId,
294
+ aiToken
295
+ }, { throwOnError: false });
296
+ },
236
297
  async models(params) {
237
298
  const projectId = runtimeEnv.__PROJECT_ID;
238
299
  const aiToken = runtimeEnv.__VOID_PROXY_TOKEN;
@@ -261,7 +322,7 @@ function makeHttpBackend(token, projectId, runtimeEnv) {
261
322
  authorization: `Bearer ${token}`,
262
323
  ...cfAccessHeaders(runtimeEnv)
263
324
  };
264
- async function proxyFetch(action, body) {
325
+ async function proxyFetch(action, body, options = {}) {
265
326
  const res = await fetch(`${base}/${action}`, {
266
327
  method: "POST",
267
328
  headers,
@@ -271,7 +332,7 @@ function makeHttpBackend(token, projectId, runtimeEnv) {
271
332
  projectId
272
333
  })
273
334
  });
274
- if (!res.ok) {
335
+ if (options.throwOnError !== false && !res.ok) {
275
336
  const text = await res.text();
276
337
  throw new Error(`ai: Proxy request failed with ${res.status}: '${text}'.`);
277
338
  }
@@ -279,15 +340,6 @@ function makeHttpBackend(token, projectId, runtimeEnv) {
279
340
  }
280
341
  return {
281
342
  async run(model, inputs, options) {
282
- if (isThirdPartyModel(model)) {
283
- const res = await proxyFetch("gateway", {
284
- model,
285
- inputs,
286
- providerKey: resolveProviderKey(model, runtimeEnv)
287
- });
288
- if (hasStreamFlag(inputs) && inputs.stream) return res.body;
289
- return readProxyResult(res);
290
- }
291
343
  const res = await proxyFetch("run", {
292
344
  model,
293
345
  inputs,
@@ -297,22 +349,16 @@ function makeHttpBackend(token, projectId, runtimeEnv) {
297
349
  return readProxyResult(res);
298
350
  },
299
351
  async image(model, inputs, options) {
300
- const { endpoint, runOptions } = splitImageOptions(options);
301
- if (isThirdPartyModel(model)) {
302
- const providerKey = resolveProviderKey(model, runtimeEnv);
303
- return proxyFetch("gateway", {
304
- model,
305
- endpoint,
306
- inputs: await serializeImageInputs(inputs, endpoint),
307
- providerKey
308
- });
309
- }
352
+ const { runOptions } = splitImageOptions(options);
310
353
  return proxyFetch("run", {
311
354
  model,
312
355
  inputs,
313
356
  options: runOptions
314
357
  });
315
358
  },
359
+ async providerFetch(provider, path, init, options) {
360
+ return proxyFetch("provider", await providerRequestBody(provider, path, init, options, runtimeEnv), { throwOnError: false });
361
+ },
316
362
  async models(params) {
317
363
  return (await proxyFetch("models", { inputs: params })).json();
318
364
  },
@@ -341,6 +387,7 @@ const ai = new Proxy({}, { get(_, prop) {
341
387
  };
342
388
  if (prop === "run") return backend().run;
343
389
  if (prop === "image") return backend().image;
390
+ if (prop === "provider") return (provider, options) => ({ fetch: (path, init) => backend().providerFetch(provider, path, init, options) });
344
391
  if (prop === "models") return backend().models;
345
392
  if (prop === "toMarkdown") return backend().toMarkdown;
346
393
  } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "void",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -353,7 +353,7 @@
353
353
  "valibot": ">=1.0.0-beta.7",
354
354
  "vite": "^8.0.0",
355
355
  "zod": "^3.25.0 || ^4.0.0",
356
- "@void/md": "0.9.1"
356
+ "@void/md": "0.9.2"
357
357
  },
358
358
  "peerDependenciesMeta": {
359
359
  "@void/md": {
@@ -4,7 +4,7 @@ outline: deep
4
4
 
5
5
  # AI
6
6
 
7
- Void provides a typed AI client powered by Cloudflare's [AI Gateway](https://developers.cloudflare.com/ai-gateway/). It supports both [Workers AI](https://developers.cloudflare.com/workers-ai/) models and third-party providers such as OpenAI, Anthropic, and Google through bring-your-own-key credentials. Import `ai` from `void/ai` and run inference directly from your route handlers. Usage is metered through Void.
7
+ Void provides a typed AI client powered by Cloudflare's [AI Gateway](https://developers.cloudflare.com/ai-gateway/). Import `ai` from `void/ai` and run inference directly from your route handlers. Usage is metered through Void.
8
8
 
9
9
  ```ts
10
10
  import { ai } from 'void/ai';
@@ -29,7 +29,7 @@ export const POST = defineHandler(async (c) => {
29
29
  });
30
30
  ```
31
31
 
32
- You can use any model available on [Workers AI](https://developers.cloudflare.com/workers-ai/models/), including text generation, image classification, image-to-text, text-to-image, embeddings, translation, and more. Models that return binary data, such as generated images, are returned as a `Blob` from `ai.run()`.
32
+ You can use any model available through Cloudflare's AI binding, including Workers AI models such as `@cf/meta/llama-3.1-8b-instruct` and Cloudflare Gateway models such as `google/gemini-2.5-flash` or `openai/gpt-4.1-mini`. The input object must match the selected Cloudflare model's schema. Models that return binary data, such as generated images, are returned as a `Blob` from `ai.run()`.
33
33
 
34
34
  ## Streaming
35
35
 
@@ -87,11 +87,52 @@ Workers AI usage is metered in [**neurons**](https://developers.cloudflare.com/w
87
87
 
88
88
  On the **free tier**, AI requests return a `429` error once the limit is reached. On **paid tiers**, usage beyond the included allowance is tracked as overage on your monthly bill.
89
89
 
90
- ## Third-Party Providers (BYOK)
90
+ ## Cloudflare Gateway Models
91
91
 
92
- You can use models from OpenAI, Anthropic, Google, and other providers by passing a `provider/model` identifier. Bring your own API key, add it as a project secret, and Void handles the rest. Third-party inference is billed directly by the provider, and Void does not mark up API calls. Requests still route through AI Gateway so usage shows up on the dashboard.
92
+ `ai.run()` mirrors Cloudflare's `env.AI.run()` model naming and input schemas. Third-party models use Cloudflare model IDs and Cloudflare-managed credentials.
93
93
 
94
- ### Usage
94
+ ```ts
95
+ const result = await ai.run('google/gemini-2.5-flash', {
96
+ contents: [
97
+ {
98
+ role: 'user',
99
+ parts: [{ text: 'Explain Durable Objects in one paragraph.' }],
100
+ },
101
+ ],
102
+ });
103
+ ```
104
+
105
+ OpenAI-compatible models use OpenAI-style `messages`:
106
+
107
+ ```ts
108
+ const result = await ai.run('openai/gpt-4.1-mini', {
109
+ messages: [{ role: 'user', content: 'Summarize this deploy.' }],
110
+ });
111
+ ```
112
+
113
+ Pass Cloudflare AI Gateway options as the third argument:
114
+
115
+ ```ts
116
+ const result = await ai.run(
117
+ 'openai/gpt-4.1-mini',
118
+ {
119
+ messages: [{ role: 'user', content: 'Summarize this deploy.' }],
120
+ },
121
+ {
122
+ gateway: {
123
+ skipCache: true,
124
+ },
125
+ },
126
+ );
127
+ ```
128
+
129
+ Void always injects the `void` gateway ID and project metadata for metering.
130
+
131
+ ## Provider-Native Requests
132
+
133
+ Use `ai.provider(provider).fetch(path, init)` when you want to call a provider-native API with your own provider key. The request still routes through Cloudflare AI Gateway and Void metering, but the request shape is the provider's native HTTP API.
134
+
135
+ ### OpenAI
95
136
 
96
137
  ```ts
97
138
  import { defineHandler } from 'void';
@@ -100,85 +141,99 @@ import { ai } from 'void/ai';
100
141
  export const POST = defineHandler(async (c) => {
101
142
  const { prompt } = await c.req.json();
102
143
 
103
- const result = await ai.run('openai/gpt-4o', {
104
- messages: [{ role: 'user', content: prompt }],
105
- max_tokens: 512,
144
+ const response = await ai.provider('openai').fetch('/chat/completions', {
145
+ body: {
146
+ model: 'gpt-4o',
147
+ messages: [{ role: 'user', content: prompt }],
148
+ max_tokens: 512,
149
+ },
106
150
  });
107
151
 
152
+ const result = await response.json();
108
153
  return c.json(result);
109
154
  });
110
155
  ```
111
156
 
112
- The same `ai.run()` and `ai.stream()` methods work for both Workers AI and third-party models. The framework detects the provider from the model string and routes the request through [AI Gateway](https://developers.cloudflare.com/ai-gateway/) automatically.
113
-
114
- The options follow each provider's conventions. All third-party providers use the OpenAI-compatible chat completions format, including `messages`, `max_tokens`, and `temperature`. TypeScript narrows the input and return types based on the model string: Workers AI models get per-model typed inputs from `@cloudflare/workers-types`, while third-party models such as `"provider/model"` get `ChatCompletionInputs` and `ChatCompletionResponse`.
115
-
116
- ### Vision
117
-
118
- Third-party chat models that accept image inputs can receive OpenAI-compatible multimodal message content:
157
+ ### Google AI Studio
119
158
 
120
159
  ```ts
121
- const result = await ai.run('openai/gpt-4o', {
122
- messages: [
123
- {
124
- role: 'user',
125
- content: [
126
- { type: 'text', text: 'What is in this image?' },
160
+ const response = await ai
161
+ .provider('google-ai-studio')
162
+ .fetch('/v1/models/gemini-2.5-flash:generateContent', {
163
+ body: {
164
+ contents: [
127
165
  {
128
- type: 'image_url',
129
- image_url: { url: 'data:image/png;base64,...', detail: 'high' },
166
+ role: 'user',
167
+ parts: [{ text: 'What is Cloudflare?' }],
130
168
  },
131
169
  ],
132
170
  },
133
- ],
134
- });
171
+ });
172
+
173
+ const result = await response.json();
174
+ ```
175
+
176
+ ### Custom Providers
177
+
178
+ For providers that are not in Void's default key map, pass the secret name and API-key header:
179
+
180
+ ```ts
181
+ const response = await ai
182
+ .provider('custom-provider', {
183
+ apiKeyEnv: 'CUSTOM_PROVIDER_API_KEY',
184
+ apiKeyHeader: 'x-api-key',
185
+ apiKeyPrefix: '',
186
+ })
187
+ .fetch('/v1/respond', {
188
+ body: { prompt: 'Hello' },
189
+ });
135
190
  ```
136
191
 
137
192
  ### Image Generation
138
193
 
139
- Use `ai.image()` for image-generation providers or Workers AI image models. It returns a `Response`, so route handlers can return it directly or parse it as JSON depending on the provider:
194
+ Use `ai.run()` or `ai.image()` for Cloudflare-native image models:
140
195
 
141
196
  ```ts
142
197
  export const POST = defineHandler(async (c) => {
143
198
  const { prompt } = await c.req.json();
144
-
145
- return ai.image('openai/gpt-image-1.5', {
146
- prompt,
147
- size: '1024x1024',
148
- response_format: 'b64_json',
149
- });
199
+ return ai.image('@cf/black-forest-labs/flux-1-schnell', { prompt });
150
200
  });
151
201
  ```
152
202
 
153
- For Workers AI text-to-image models, pass the model name and input shape from Cloudflare's model docs:
203
+ Use `ai.provider().fetch()` for provider-native image APIs:
154
204
 
155
205
  ```ts
156
206
  export const POST = defineHandler(async (c) => {
157
207
  const { prompt } = await c.req.json();
158
- return ai.image('@cf/black-forest-labs/flux-1-schnell', { prompt });
208
+
209
+ return ai.provider('openai').fetch('/images/generations', {
210
+ body: {
211
+ model: 'gpt-image-1.5',
212
+ prompt,
213
+ size: '1024x1024',
214
+ response_format: 'b64_json',
215
+ },
216
+ });
159
217
  });
160
218
  ```
161
219
 
162
- For image edits, pass the file-like input and select the edit endpoint. Void serializes the file through the proxy and forwards it to AI Gateway as multipart form data:
220
+ For multipart provider APIs, pass a `FormData` body. Void serializes the body through the proxy and reconstructs it before forwarding to AI Gateway:
163
221
 
164
222
  ```ts
165
223
  export const POST = defineHandler(async (c) => {
166
224
  const body = await c.req.parseBody();
225
+ const form = new FormData();
226
+ form.set('model', 'gpt-image-1.5');
227
+ form.set('prompt', String(body.prompt));
228
+ form.set('image', body.image as Blob, 'source.png');
167
229
 
168
- return ai.image(
169
- 'openai/gpt-image-1.5',
170
- {
171
- prompt: String(body.prompt),
172
- image: body.image as Blob,
173
- },
174
- { endpoint: 'images/edits' },
175
- );
230
+ return ai.provider('openai').fetch('/images/edits', { body: form });
176
231
  });
177
232
  ```
178
233
 
179
234
  ### Provider Key Convention
180
235
 
181
- Each provider requires an API key set as a project secret. The env var name is automatically derived from the provider prefix:
236
+ Provider-native requests require an API key set as a project secret. The env var name is automatically derived from the provider name:
182
237
 
183
238
  | Provider prefix | Env var |
184
239
  | ------------------ | --------------------- |
@@ -203,7 +258,7 @@ Each provider requires an API key set as a project secret. The env var name is a
203
258
  | `ideogram` | `IDEOGRAM_API_KEY` |
204
259
  | `parallel` | `PARALLEL_API_KEY` |
205
260
 
206
- All [AI Gateway providers](https://developers.cloudflare.com/ai-gateway/usage/providers/) that accept a Bearer API key are supported. Providers with non-standard auth (Amazon Bedrock, Azure OpenAI, Google Vertex) are not yet supported.
261
+ OpenAI-style providers use `Authorization: Bearer <key>`. Google AI Studio uses `x-goog-api-key`. Use `apiKeyHeader` and `apiKeyPrefix` for custom providers.
207
262
 
208
263
  For production, add your API key as a project secret:
209
264
 
@@ -217,19 +272,22 @@ For local development, add it to `.env.local` in your project root:
217
272
  OPENAI_API_KEY=sk-...
218
273
  ```
219
274
 
220
- If the key is missing at runtime, `ai.run()` throws a descriptive error telling you which env var to set.
275
+ If the key is missing at runtime, `ai.provider().fetch()` throws a descriptive error telling you which env var to set.
221
276
 
222
- ### Streaming with Third-Party Models
277
+ ### Streaming with Provider-Native APIs
223
278
 
224
- `ai.stream()` works the same way. The response is always SSE regardless of provider:
279
+ Provider-native streaming APIs return the provider response directly:
225
280
 
226
281
  ```ts
227
282
  export const POST = defineHandler(async (c) => {
228
283
  const { prompt } = await c.req.json();
229
284
 
230
- return ai.stream('anthropic/claude-sonnet-4-20250514', {
231
- messages: [{ role: 'user', content: prompt }],
232
- max_tokens: 512,
285
+ return ai.provider('openai').fetch('/chat/completions', {
286
+ body: {
287
+ model: 'gpt-4o',
288
+ messages: [{ role: 'user', content: prompt }],
289
+ stream: true,
290
+ },
233
291
  });
234
292
  });
235
293
  ```
@@ -101,7 +101,7 @@ src/
101
101
  | `void/ws` | `defineRoom()`, `defineWebSocket()`, typed `connect()`, and WebSocket context types |
102
102
  | `void/db` | `db` Drizzle D1 instance (auto-wired with user schema) + `createDb()` for custom D1 bindings |
103
103
  | `void/queues` | `queues` typed proxy + `QueueMap` stub interface (augmented by generated `queues.d.ts`) |
104
- | `void/ai` | `ai` proxy — `ai.run()`, `ai.stream()`, `ai.models()` with auto-detected backend (service binding / HTTPS / direct) |
104
+ | `void/ai` | `ai` proxy — Cloudflare-native `ai.run()`/`ai.stream()`, provider-native `ai.provider().fetch()`, `ai.models()` |
105
105
  | `void/log` | `logger.error/warn/info(msg, fields?)` — emits stringified JSON to `console.*` so Cloudflare Tail captures level + msg |
106
106
  | `void/env` | `defineEnv()`, typed `env` proxy, built-in schema helpers (`string`, `number`, `oneOf`, …) + global Cloudflare types |
107
107
 
@@ -675,7 +675,7 @@ Imported from `"void/ai"`.
675
675
 
676
676
  ### `ai`
677
677
 
678
- Typed AI client for Workers AI models and AI Gateway-backed third-party providers.
678
+ Typed AI client for Cloudflare AI models and provider-native AI Gateway requests.
679
679
 
680
680
  ```ts
681
681
  import { ai } from 'void/ai';
@@ -683,9 +683,16 @@ import { ai } from 'void/ai';
683
683
  const result = await ai.run('@cf/meta/llama-3.1-8b-instruct', {
684
684
  messages: [{ role: 'user', content: 'Summarize this release note.' }],
685
685
  });
686
+
687
+ const response = await ai.provider('openai').fetch('/chat/completions', {
688
+ body: {
689
+ model: 'gpt-4o',
690
+ messages: [{ role: 'user', content: 'Summarize this release note.' }],
691
+ },
692
+ });
686
693
  ```
687
694
 
688
- **Key exports:** `ai`, `VoidAi`, model input/response types. See [AI](../guide/ai.md) for provider setup and streaming examples.
695
+ **Key exports:** `ai`, `VoidAi`, provider request types. See [AI](../guide/ai.md) for provider setup and streaming examples.
689
696
 
690
697
  ## ISR
691
698
 
@@ -572,7 +572,9 @@ See [Environment Variables](../guide/env-vars.md) for the full guide.
572
572
  void domain add <hostname> [--project <name>]
573
573
  ```
574
574
 
575
- Add a custom domain to a project. Prints the CNAME target you need to add in your DNS provider.
575
+ Add a custom domain to a project. Prints the two DNS records to add at your DNS provider: a traffic **CNAME** pointing `<hostname>` at the CNAME target shown in the command output, and a non-rotating `_cf-custom-hostname` ownership **TXT**. Certificates are validated over HTTP at Cloudflare's edge and renew automatically — there are no `_acme-challenge` records to publish, at first issuance or ever. After adding the records the domain activates automatically (no polling required); run `void domain status <hostname>` to check progress.
576
+
577
+ > Wildcard custom hostnames (`*.example.com`) are not supported — register each subdomain individually.
576
578
 
577
579
  ### `void domain delete`
578
580
 
@@ -596,7 +598,7 @@ List all custom domains and their status (active/pending).
596
598
  void domain status <hostname> [--project <name>] [--verbose]
597
599
  ```
598
600
 
599
- Check verification and SSL status for a specific domain. Prints a rolled-up state (`awaiting_dns`, `verifying_dns`, `issuing_cert`, `deploying_cert`, `awaiting_deployment`, `active`, `error`, or `pending`) with a one-line diagnostic explaining what Cloudflare is doing and any user action required. When DNS records are still pending, the command also prints the exact TXT records to add at your DNS provider.
601
+ Check verification and SSL status for a specific domain. Prints a rolled-up state (`awaiting_dns`, `verifying_dns`, `issuing_cert`, `deploying_cert`, `awaiting_deployment`, `active`, `error`, or `pending`) with a one-line diagnostic explaining what Cloudflare is doing and any user action required. While the certificate is not yet active, the command also surfaces the records to configure — the traffic **CNAME** and the `_cf-custom-hostname` ownership **TXT** — so you can verify them. Activation is automatic: a background job reconciles pending domains (about every 2 minutes for the first 30 minutes after adding, then hourly), so this command is for instant feedback rather than required polling.
600
602
 
601
603
  Pass `--verbose` to additionally print the raw multi-line status breakdown (DB status, SSL status, ownership state, verification errors) underneath the rollup.
602
604