void 0.9.1 → 0.9.3
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/README.md +1 -0
- package/dist/{better-auth-shared-ChVbrq52.mjs → better-auth-shared-QAfk8CAS.mjs} +1 -1
- package/dist/cli/cli.mjs +14 -14
- package/dist/{config-BVEC0lti.mjs → config-8dLIngKW.mjs} +1 -1
- package/dist/{db-DiddQC2v.mjs → db-UW4IYQFw.mjs} +4 -4
- package/dist/{deploy-CRU9fGjE.mjs → deploy-RQXpVBL-.mjs} +27 -10
- package/dist/{domain-DxTc6HDn.mjs → domain-B7RntQ1r.mjs} +12 -27
- package/dist/{drizzle-2fs1qTgy.mjs → drizzle-DYstYz0j.mjs} +1 -1
- package/dist/{env-Bvw0wMTI.mjs → env-C_ZcxxED.mjs} +2 -2
- package/dist/{env-types-D6qI1ThV.mjs → env-types-CaJaIRXU.mjs} +1 -1
- package/dist/{env-validation-DBJsxZLz.mjs → env-validation-LH6eoyW-.mjs} +3 -3
- package/dist/{gen-CZdNWIaA.mjs → gen-O5mXZ76K.mjs} +2 -2
- package/dist/{handler-xflsMwWM.d.mts → handler-hm9KldPP.d.mts} +21 -1
- package/dist/{headers-Y0jshugF.mjs → headers-VkAnACKX.mjs} +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +48 -43
- package/dist/{init-BHupO7Fm.mjs → init-DGrLx2ls.mjs} +5 -5
- package/dist/{node-CHWVVjO6.mjs → node-Df8UjQ2H.mjs} +2 -2
- package/dist/pages/client.d.mts +1 -1
- package/dist/pages/index.d.mts +7 -5
- package/dist/pages/index.mjs +3 -3
- package/dist/pages/islands-plugin.d.mts +1 -1
- package/dist/pages/islands-plugin.mjs +1 -1
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/pages/protocol.mjs +29 -11
- package/dist/{plugin-inference-BUBhHt_1.mjs → plugin-inference--7geW5Z2.mjs} +1 -1
- package/dist/{prepare-CrlAVbWS.mjs → prepare-DT-7QII_.mjs} +14 -19
- package/dist/{preset-CIJG2O7a.mjs → preset-2aBxMfY1.mjs} +1 -1
- package/dist/{project-tsconfig-HAGjfY6w.mjs → project-tsconfig-BTNuNoJ0.mjs} +18 -4
- package/dist/{protocol-DYca39yJ.d.mts → protocol-B9GZA4Bn.d.mts} +7 -4
- package/dist/{route-types-jxRfWuCb.mjs → route-types-D03ryMXz.mjs} +3 -2
- package/dist/runtime/ai.d.mts +20 -100
- package/dist/runtime/ai.mjs +101 -54
- package/dist/runtime/better-auth-pg.mjs +1 -1
- package/dist/runtime/better-auth.mjs +1 -1
- package/dist/runtime/handler.d.mts +2 -2
- package/dist/runtime/handler.mjs +67 -1
- package/dist/runtime/live.d.mts +1 -1
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws.d.mts +1 -1
- package/dist/{scan-BNC_1OsY.mjs → scan-B2H1Vo2C.mjs} +3 -3
- package/dist/{scan-2YmJkYAf.mjs → scan-VCAM1oh3.mjs} +55 -29
- package/dist/{types-Dm9kep2X.d.mts → types-DEWCqHtl.d.mts} +12 -3
- package/package.json +2 -2
- package/skills/void/docs/guide/ai.md +109 -51
- package/skills/void/docs/guide/pages-routing/layouts.md +8 -7
- package/skills/void/docs/guide/pages-routing/loaders.md +1 -1
- package/skills/void/docs/guide/pages-routing/overview.md +32 -5
- package/skills/void/docs/guide/server-routing.md +28 -0
- package/skills/void/docs/node_modules/void/AGENTS.md +15 -14
- package/skills/void/docs/node_modules/void/README.md +1 -0
- package/skills/void/docs/reference/api.md +53 -3
- package/skills/void/docs/reference/cli.md +4 -2
- package/skills/void/docs/reference/config.md +1 -1
- /package/dist/{dist-DR9sIMbM.mjs → dist-5cGIJHQQ.mjs} +0 -0
- /package/dist/{dotenv-Bkoqyq9r.mjs → dotenv-lS94ymhM.mjs} +0 -0
- /package/dist/{log-CWWZV4V1.mjs → log-BdD_Fpms.mjs} +0 -0
- /package/dist/{providers-uC0PJg1c.mjs → providers-BwPbdHdi.mjs} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as join, n as dirname, r as extname, t as basename } from "./pathe.M-eThtNZ-BrPhGF_K.mjs";
|
|
2
2
|
import { n as readProjectPaths } from "./project-paths-CCMrHYQm.mjs";
|
|
3
|
-
import { t as glob } from "./dist-
|
|
3
|
+
import { t as glob } from "./dist-5cGIJHQQ.mjs";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
//#region src/router/parse-filename.ts
|
|
6
6
|
const EXTENSIONS = [
|
|
@@ -83,30 +83,29 @@ function toLayoutId(layout) {
|
|
|
83
83
|
if (isNamedLayout(layout)) return layout.directory ? `${layout.directory}/_layouts/${layout.name}` : `_layouts/${layout.name}`;
|
|
84
84
|
return layout.directory ? `${layout.directory}/layout` : "layout";
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
function resolveLayoutDirectories(componentId, componentPath) {
|
|
87
|
+
if (componentPath) {
|
|
88
|
+
const segments = componentPath.split("/");
|
|
89
|
+
segments.pop();
|
|
90
|
+
return segments.map((_, index) => segments.slice(0, index + 1).join("/"));
|
|
91
|
+
}
|
|
88
92
|
const parts = componentId.split("/");
|
|
93
|
+
return parts.slice(0, -1).map((_, index) => parts.slice(0, index + 1).join("/"));
|
|
94
|
+
}
|
|
95
|
+
/** Resolve the layout chain for a component (innermost last) */
|
|
96
|
+
function resolveLayoutChain(componentId, layouts, componentPath) {
|
|
89
97
|
const chain = [];
|
|
90
98
|
const root = layouts.find((layout) => layout.directory === "");
|
|
91
99
|
if (root) chain.push(root);
|
|
92
|
-
|
|
93
|
-
for (let partIndex = 0; partIndex < parts.length - 1; partIndex++) {
|
|
94
|
-
dir = dir ? `${dir}/${parts[partIndex]}` : parts[partIndex];
|
|
100
|
+
for (const dir of resolveLayoutDirectories(componentId, componentPath)) {
|
|
95
101
|
const layout = layouts.find((candidate) => candidate.directory === dir);
|
|
96
102
|
if (layout) chain.push(layout);
|
|
97
103
|
}
|
|
98
104
|
return chain;
|
|
99
105
|
}
|
|
100
106
|
/** Walk up directory tree to find closest _layouts/<name> */
|
|
101
|
-
function resolveNamedLayout(name, componentId, namedLayouts) {
|
|
102
|
-
const
|
|
103
|
-
const dirs = [];
|
|
104
|
-
let dir = "";
|
|
105
|
-
for (let partIndex = 0; partIndex < parts.length - 1; partIndex++) {
|
|
106
|
-
dir = dir ? `${dir}/${parts[partIndex]}` : parts[partIndex];
|
|
107
|
-
dirs.push(dir);
|
|
108
|
-
}
|
|
109
|
-
dirs.reverse();
|
|
107
|
+
function resolveNamedLayout(name, componentId, namedLayouts, componentPath) {
|
|
108
|
+
const dirs = resolveLayoutDirectories(componentId, componentPath).reverse();
|
|
110
109
|
dirs.push("");
|
|
111
110
|
for (const directory of dirs) {
|
|
112
111
|
const found = namedLayouts.find((namedLayout) => namedLayout.name === name && namedLayout.directory === directory);
|
|
@@ -116,7 +115,7 @@ function resolveNamedLayout(name, componentId, namedLayouts) {
|
|
|
116
115
|
}
|
|
117
116
|
function resolveEffectiveLayoutEntries(page, layouts, namedLayouts) {
|
|
118
117
|
if (page.layout === false) return [];
|
|
119
|
-
const defaultChain = resolveLayoutChain(page.componentId, layouts).map((layout) => ({
|
|
118
|
+
const defaultChain = resolveLayoutChain(page.componentId, layouts, page.componentPath).map((layout) => ({
|
|
120
119
|
id: toLayoutId(layout),
|
|
121
120
|
definition: layout
|
|
122
121
|
}));
|
|
@@ -124,7 +123,7 @@ function resolveEffectiveLayoutEntries(page, layouts, namedLayouts) {
|
|
|
124
123
|
const isExclusive = page.layout.startsWith("!");
|
|
125
124
|
const name = isExclusive ? page.layout.slice(1) : page.layout;
|
|
126
125
|
if (name.startsWith(".") || name.startsWith("/")) return defaultChain;
|
|
127
|
-
const resolved = resolveNamedLayout(name, page.componentId, namedLayouts);
|
|
126
|
+
const resolved = resolveNamedLayout(name, page.componentId, namedLayouts, page.componentPath);
|
|
128
127
|
if (!resolved) return defaultChain;
|
|
129
128
|
const namedEntry = {
|
|
130
129
|
id: toLayoutId(resolved),
|
|
@@ -160,13 +159,31 @@ const SERVER_EXTENSIONS = [
|
|
|
160
159
|
"mts"
|
|
161
160
|
];
|
|
162
161
|
const PAGES_EXPORT_RE = /export\s+(?:const|function|async\s+function)\s+(loader|actions?)\b/g;
|
|
163
|
-
const REVALIDATE_RE = /export\s+const\s+revalidate\s*=\s*(\d+)\s
|
|
164
|
-
const PRERENDER_RE = /export\s+const\s+prerender\s*=\s*true\s
|
|
165
|
-
const PRERENDER_FALSE_RE = /export\s+const\s+prerender\s*=\s*false\s
|
|
162
|
+
const REVALIDATE_RE = /export\s+const\s+revalidate\s*=\s*(\d+)\s*;?/;
|
|
163
|
+
const PRERENDER_RE = /export\s+const\s+prerender\s*=\s*true\s*;?/;
|
|
164
|
+
const PRERENDER_FALSE_RE = /export\s+const\s+prerender\s*=\s*false\s*;?/;
|
|
165
|
+
const SSR_TRUE_RE = /export\s+const\s+ssr\s*=\s*true\s*;?/;
|
|
166
|
+
const SSR_FALSE_RE = /export\s+const\s+ssr\s*=\s*false\s*;?/;
|
|
166
167
|
const GET_PRERENDER_PATHS_RE = /export\s+(?:async\s+)?function\s+getPrerenderPaths\b/;
|
|
167
168
|
const LAYOUT_STRING_RE = /export\s+const\s+layout\s*=\s*["']([^"']+)["']\s*;?/;
|
|
168
169
|
const LAYOUT_FALSE_RE = /export\s+const\s+layout\s*=\s*false\s*;?/;
|
|
169
170
|
const MAX_REVALIDATE = 31536e3;
|
|
171
|
+
function inferAutoPrerender(page, options) {
|
|
172
|
+
const isDynamic = page.params.length > 0 || page.catchAll;
|
|
173
|
+
if (options?.output === "static") return !isDynamic || page.hasGetPrerenderPaths === true;
|
|
174
|
+
return page.island === true && !page.methods.includes("loader") && !isDynamic;
|
|
175
|
+
}
|
|
176
|
+
function resolvePageRendering(page, options) {
|
|
177
|
+
if (page.island && page.ssr === false) throw new Error(`pages: ssr = false is not valid for island page '${page.componentPath}'.`);
|
|
178
|
+
const ssrMode = page.ssr === true ? "server" : page.ssr === false ? "client" : "auto";
|
|
179
|
+
const prerenderMode = page.prerender === true ? "force" : page.prerender === false ? "disabled" : "auto";
|
|
180
|
+
return {
|
|
181
|
+
ssrMode,
|
|
182
|
+
prerenderMode,
|
|
183
|
+
renderMode: page.island === true ? "island" : ssrMode === "client" ? "client" : "server",
|
|
184
|
+
shouldPrerender: prerenderMode === "force" || prerenderMode === "auto" && inferAutoPrerender(page, options)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
170
187
|
async function scanPages(root, options) {
|
|
171
188
|
const pagesDir = options?.pagesDir ?? options?.paths?.pagesDir ?? readProjectPaths(root).pagesDir;
|
|
172
189
|
if (!existsSync(pagesDir)) return {
|
|
@@ -219,7 +236,7 @@ async function scanPages(root, options) {
|
|
|
219
236
|
const methods = [];
|
|
220
237
|
let revalidate;
|
|
221
238
|
let prerender;
|
|
222
|
-
let
|
|
239
|
+
let ssr;
|
|
223
240
|
let hasGetPrerenderPaths;
|
|
224
241
|
const serverBase = hasIslandSuffix ? file.replace(`.island${ext}`, "") : file.replace(ext, "");
|
|
225
242
|
for (const serverExt of SERVER_EXTENSIONS) {
|
|
@@ -231,17 +248,24 @@ async function scanPages(root, options) {
|
|
|
231
248
|
const revalidateMatch = content.match(REVALIDATE_RE);
|
|
232
249
|
if (revalidateMatch) revalidate = Number(revalidateMatch[1]);
|
|
233
250
|
if (PRERENDER_RE.test(content)) prerender = true;
|
|
234
|
-
if (PRERENDER_FALSE_RE.test(content))
|
|
251
|
+
else if (PRERENDER_FALSE_RE.test(content)) prerender = false;
|
|
252
|
+
if (SSR_TRUE_RE.test(content)) ssr = true;
|
|
253
|
+
else if (SSR_FALSE_RE.test(content)) ssr = false;
|
|
235
254
|
if (GET_PRERENDER_PATHS_RE.test(content)) hasGetPrerenderPaths = true;
|
|
236
255
|
break;
|
|
237
256
|
}
|
|
238
257
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
258
|
+
const rendering = resolvePageRendering({
|
|
259
|
+
componentPath: file,
|
|
260
|
+
island,
|
|
261
|
+
ssr,
|
|
262
|
+
prerender,
|
|
263
|
+
methods,
|
|
264
|
+
params: route.params,
|
|
265
|
+
catchAll: route.catchAll,
|
|
266
|
+
hasGetPrerenderPaths
|
|
267
|
+
}, options);
|
|
268
|
+
if (rendering.shouldPrerender && revalidate === void 0) revalidate = MAX_REVALIDATE;
|
|
245
269
|
let metadata;
|
|
246
270
|
let layout;
|
|
247
271
|
if (ext === ".md") {
|
|
@@ -264,6 +288,8 @@ async function scanPages(root, options) {
|
|
|
264
288
|
componentId,
|
|
265
289
|
revalidate,
|
|
266
290
|
prerender,
|
|
291
|
+
ssr,
|
|
292
|
+
...rendering,
|
|
267
293
|
hasGetPrerenderPaths,
|
|
268
294
|
island,
|
|
269
295
|
metadata,
|
|
@@ -291,7 +317,7 @@ async function scanPages(root, options) {
|
|
|
291
317
|
if (page.layout === void 0 || page.layout === false) continue;
|
|
292
318
|
const name = page.layout.startsWith("!") ? page.layout.slice(1) : page.layout;
|
|
293
319
|
if (name.startsWith(".") || name.startsWith("/")) continue;
|
|
294
|
-
if (!resolveNamedLayout(name, page.componentId, namedLayouts)) throw new Error(`pages: Page '${page.componentPath}' specifies layout '${page.layout}', but _layouts/${name}.vue was not found in any ancestor directory.`);
|
|
320
|
+
if (!resolveNamedLayout(name, page.componentId, namedLayouts, page.componentPath)) throw new Error(`pages: Page '${page.componentPath}' specifies layout '${page.layout}', but _layouts/${name}.vue was not found in any ancestor directory.`);
|
|
295
321
|
}
|
|
296
322
|
return {
|
|
297
323
|
pages,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
//#region src/pages/types.d.ts
|
|
2
|
+
type ExplicitBoolean = true | false | undefined;
|
|
3
|
+
type SsrMode = "auto" | "server" | "client";
|
|
4
|
+
type PrerenderMode = "auto" | "force" | "disabled";
|
|
5
|
+
type PageRenderMode = "server" | "client" | "island";
|
|
2
6
|
type PageDefinition = {
|
|
3
7
|
/** URL pattern, e.g. "/users/:id" */pattern: string; /** Route params, e.g. ["id"] */
|
|
4
8
|
params: Array<string>; /** Whether this is a catch-all route */
|
|
@@ -7,8 +11,13 @@ type PageDefinition = {
|
|
|
7
11
|
serverPath: string | null; /** HTTP methods exported by server handler */
|
|
8
12
|
methods: Array<string>; /** Component ID used in page object, e.g. "users/[id]" */
|
|
9
13
|
componentId: string; /** Edge caching revalidate TTL in seconds, from .server.ts export */
|
|
10
|
-
revalidate?: number; /** Whether `export const prerender = true` is set in server handler */
|
|
11
|
-
prerender?:
|
|
14
|
+
revalidate?: number; /** Whether `export const prerender = true/false` is set in server handler */
|
|
15
|
+
prerender?: ExplicitBoolean; /** Whether `export const ssr = true/false` is set in server handler */
|
|
16
|
+
ssr?: ExplicitBoolean; /** Normalized SSR mode from the public `ssr` export */
|
|
17
|
+
ssrMode: SsrMode; /** Normalized prerender mode from the public `prerender` export */
|
|
18
|
+
prerenderMode: PrerenderMode; /** Normalized page render mode */
|
|
19
|
+
renderMode: PageRenderMode; /** Whether this page should participate in prerender collection */
|
|
20
|
+
shouldPrerender: boolean; /** Whether `export function getPrerenderPaths` is defined in server handler */
|
|
12
21
|
hasGetPrerenderPaths?: boolean; /** Whether this is an island page (.island.tsx/.island.vue) */
|
|
13
22
|
island?: boolean; /** Frontmatter metadata extracted from .md files (title, description) */
|
|
14
23
|
metadata?: {
|
|
@@ -39,4 +48,4 @@ type DevCssAsset = {
|
|
|
39
48
|
css: string;
|
|
40
49
|
};
|
|
41
50
|
//#endregion
|
|
42
|
-
export {
|
|
51
|
+
export { PageRenderMode as a, PageDefinition as i, LayoutDefinition as n, PageScanResult as o, NamedLayoutDefinition as r, DevCssAsset as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "void",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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.
|
|
356
|
+
"@void/md": "0.9.3"
|
|
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/).
|
|
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
|
|
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
|
-
##
|
|
90
|
+
## Cloudflare Gateway Models
|
|
91
91
|
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
277
|
+
### Streaming with Provider-Native APIs
|
|
223
278
|
|
|
224
|
-
|
|
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.
|
|
231
|
-
|
|
232
|
-
|
|
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
|
```
|
|
@@ -149,7 +149,7 @@ import { useShared, Link } from '@void/solid';
|
|
|
149
149
|
import type { JSX } from 'solid-js';
|
|
150
150
|
|
|
151
151
|
export default function Layout(props: { children: JSX.Element }) {
|
|
152
|
-
const shared = useShared
|
|
152
|
+
const shared = useShared();
|
|
153
153
|
return (
|
|
154
154
|
<>
|
|
155
155
|
<nav>
|
|
@@ -307,15 +307,16 @@ Middleware can inject data available on every page via `c.set("shared", {...})`.
|
|
|
307
307
|
```ts
|
|
308
308
|
// middleware/01.auth.ts
|
|
309
309
|
import { defineMiddleware } from 'void';
|
|
310
|
+
import { getUser, type AuthUser } from 'void/auth';
|
|
310
311
|
|
|
311
312
|
declare module 'void' {
|
|
312
313
|
interface CloudContextVariables {
|
|
313
|
-
shared: { auth: { user:
|
|
314
|
+
shared: { auth: { user: AuthUser | null } };
|
|
314
315
|
}
|
|
315
316
|
}
|
|
316
317
|
|
|
317
318
|
export default defineMiddleware(async (c, next) => {
|
|
318
|
-
const user =
|
|
319
|
+
const user = getUser();
|
|
319
320
|
c.set('shared', { auth: { user } });
|
|
320
321
|
await next();
|
|
321
322
|
});
|
|
@@ -329,7 +330,7 @@ Access it on the client with `useShared()`. The return type is inferred from you
|
|
|
329
330
|
import { useShared } from '@void/react';
|
|
330
331
|
|
|
331
332
|
export default function Page() {
|
|
332
|
-
const { auth } = useShared(); // { auth: { user:
|
|
333
|
+
const { auth } = useShared(); // { auth: { user: AuthUser | null } }
|
|
333
334
|
return <p>Hello, {auth?.user?.name}</p>;
|
|
334
335
|
}
|
|
335
336
|
```
|
|
@@ -337,14 +338,14 @@ export default function Page() {
|
|
|
337
338
|
```vue [Vue]
|
|
338
339
|
<script setup lang="ts">
|
|
339
340
|
import { useShared } from '@void/vue';
|
|
340
|
-
const { auth } = useShared(); // { auth: { user:
|
|
341
|
+
const { auth } = useShared(); // { auth: { user: AuthUser | null } }
|
|
341
342
|
</script>
|
|
342
343
|
```
|
|
343
344
|
|
|
344
345
|
```svelte [Svelte]
|
|
345
346
|
<script>
|
|
346
347
|
import { useShared } from "@void/svelte";
|
|
347
|
-
const { auth } = useShared(); // { auth: { user:
|
|
348
|
+
const { auth } = useShared(); // { auth: { user: AuthUser | null } }
|
|
348
349
|
</script>
|
|
349
350
|
```
|
|
350
351
|
|
|
@@ -352,7 +353,7 @@ const { auth } = useShared(); // { auth: { user: { name: string } | null } }
|
|
|
352
353
|
import { useShared } from '@void/solid';
|
|
353
354
|
|
|
354
355
|
export default function Page() {
|
|
355
|
-
const shared = useShared(); // { auth: { user:
|
|
356
|
+
const shared = useShared(); // { auth: { user: AuthUser | null } }
|
|
356
357
|
return <p>Hello, {shared.auth?.user?.name}</p>;
|
|
357
358
|
}
|
|
358
359
|
```
|
|
@@ -237,7 +237,7 @@ export default function Dashboard(props: Props) {
|
|
|
237
237
|
|
|
238
238
|
### How Streaming Works
|
|
239
239
|
|
|
240
|
-
On the initial page load (SSR), React uses React 19 streaming SSR and renders the nearest Suspense fallback for deferred props; the other adapters render their loading state. As each deferred function resolves, the server streams an inline `<script>` tag that delivers the data, so no extra HTTP request is needed. On SPA navigation, deferred data streams via NDJSON over the same response.
|
|
240
|
+
On the initial page load (SSR), React uses React 19 streaming SSR and renders the nearest Suspense fallback for deferred props; the other adapters render their loading state. As each deferred function resolves, the server streams an inline `<script>` tag that delivers the data, so no extra HTTP request is needed. Routes with `export const ssr = false` skip server-rendered component HTML but still stream deferred resolution scripts after the client-mounted shell. On SPA navigation, deferred data streams via NDJSON over the same response.
|
|
241
241
|
|
|
242
242
|
### Deferred Props After Mutations
|
|
243
243
|
|
|
@@ -100,6 +100,7 @@ Each page can have a companion `.server.ts` file that runs exclusively on the se
|
|
|
100
100
|
|
|
101
101
|
- A [**loader**](./loaders), which runs on `GET` and returns the data that becomes the page component's props
|
|
102
102
|
- [**Actions**](./actions-and-forms), which handle mutations from forms and programmatic calls. Export a single `action` or multiple [named actions](./actions-and-forms#named-actions) when a page has several mutations
|
|
103
|
+
- `ssr = false` to opt a route out of server-rendered component HTML while keeping server loaders and client-side routing
|
|
103
104
|
|
|
104
105
|
File-based routing rules are the same as [server routing](../server-routing.md): `[param]` for dynamic segments, `[...param]` for catch-all, `(group)/` for route groups.
|
|
105
106
|
|
|
@@ -107,14 +108,40 @@ File-based routing rules are the same as [server routing](../server-routing.md):
|
|
|
107
108
|
|
|
108
109
|
Pages uses an Inertia-style protocol under the hood:
|
|
109
110
|
|
|
110
|
-
| Request | Response
|
|
111
|
-
| --------------------- |
|
|
112
|
-
| Initial page load | Full SSR HTML. Client hydrates automatically.
|
|
113
|
-
| Subsequent navigation | JSON with component name + props. Client component swap or re-render.
|
|
114
|
-
| Form submission | Runs action, then returns fresh props or a redirect.
|
|
111
|
+
| Request | Response |
|
|
112
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
113
|
+
| Initial page load | Full SSR HTML. Client hydrates automatically. Routes with `ssr = false` return a client-mounted shell instead. |
|
|
114
|
+
| Subsequent navigation | JSON with component name + props. Client component swap or re-render. |
|
|
115
|
+
| Form submission | Runs action, then returns fresh props or a redirect. |
|
|
115
116
|
|
|
116
117
|
This means the first page load is server-rendered for SEO and performance, while later navigations stay fast without full page reloads.
|
|
117
118
|
|
|
119
|
+
To opt a specific route out of server-rendered component HTML, export `ssr = false` from its companion `.server.ts` file:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// pages/dashboard.server.ts
|
|
123
|
+
import { defineHandler } from 'void';
|
|
124
|
+
|
|
125
|
+
export const ssr = false;
|
|
126
|
+
|
|
127
|
+
export const loader = defineHandler(async () => {
|
|
128
|
+
return { title: 'Dashboard' };
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The loader still runs on the first request, and its props are embedded in the HTML shell. The page component mounts in the browser instead of hydrating server-rendered markup.
|
|
133
|
+
|
|
134
|
+
Render and prerender flags combine like this:
|
|
135
|
+
|
|
136
|
+
| Page exports | Behavior |
|
|
137
|
+
| -------------------------------------- | ------------------------------------------------------------------------- |
|
|
138
|
+
| `ssr` unset or `true` | Server-render component HTML on request. |
|
|
139
|
+
| `ssr = false` | Return a client-mounted shell on request. |
|
|
140
|
+
| `ssr = false` + `prerender = true` | Prerender a client-mounted shell with embedded loader data. |
|
|
141
|
+
| `ssr = false` + `prerender = false` | Return the client-mounted shell only on request; never prerender it. |
|
|
142
|
+
| Island page + `ssr = false` | Invalid. Island pages already use the island renderer. |
|
|
143
|
+
| `output: "static"` + `prerender` unset | Auto-prerender pages that have known paths, including client-only shells. |
|
|
144
|
+
|
|
118
145
|
Use the `Link` component for SPA navigation between pages. It renders an `<a>` tag that intercepts clicks and navigates without a full page reload:
|
|
119
146
|
|
|
120
147
|
::: code-group
|
|
@@ -191,6 +191,34 @@ export default defineMiddleware(async (c, next) => {
|
|
|
191
191
|
|
|
192
192
|
`defineMiddleware` uses Hono middleware semantics: `(c, next) => Promise<void> | void`.
|
|
193
193
|
|
|
194
|
+
For a temporary full-site gate, use the built-in `basicAuth()` middleware with credentials from `void/env`. Void internal endpoints under `/__void` are excluded automatically so deploy migrations and dev tooling continue to work. Wrap `void/env` reads in functions so they are resolved per request after Void has bound the runtime env.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// env.ts
|
|
198
|
+
import { defineEnv, string } from 'void/env';
|
|
199
|
+
|
|
200
|
+
export default defineEnv({
|
|
201
|
+
BASIC_AUTH_USERNAME: string(),
|
|
202
|
+
BASIC_AUTH_PASSWORD: string(),
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// middleware/01.basic-auth.ts
|
|
208
|
+
import { basicAuth } from 'void';
|
|
209
|
+
import { env } from 'void/env';
|
|
210
|
+
|
|
211
|
+
export default basicAuth({
|
|
212
|
+
username: () => env.BASIC_AUTH_USERNAME,
|
|
213
|
+
password: () => env.BASIC_AUTH_PASSWORD,
|
|
214
|
+
realm: 'Preview',
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Set `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` as local environment variables for development and production secrets before deploy.
|
|
219
|
+
|
|
220
|
+
For app-specific bypasses such as health checks or public webhooks, compose that logic in your own middleware before calling `basicAuth()`.
|
|
221
|
+
|
|
194
222
|
Middleware can set typed context variables using `c.set()`. Augment the `CloudContextVariables` interface so downstream handlers get full type safety:
|
|
195
223
|
|
|
196
224
|
```ts
|