@boldare/askdare 0.0.0-stage → 0.1.0-rc.0
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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/cli.js +1461 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +193 -0
- package/dist/next.d.ts +86 -0
- package/dist/next.js +2864 -0
- package/dist/store.d.ts +413 -0
- package/dist/version.js +641 -0
- package/docs/attachments.md +41 -0
- package/docs/cli.md +227 -0
- package/docs/configuration.md +357 -0
- package/docs/costs.md +181 -0
- package/docs/custom-store.md +259 -0
- package/docs/deployment.md +76 -0
- package/docs/embedding.md +258 -0
- package/docs/getting-started.md +116 -0
- package/docs/going-public.md +81 -0
- package/docs/identity.md +109 -0
- package/docs/knowledge-base.md +65 -0
- package/docs/migrating-from-openai-assistant.md +43 -0
- package/docs/privacy.md +175 -0
- package/docs/protocol.md +174 -0
- package/docs/styling.md +166 -0
- package/docs/support.md +48 -0
- package/docs/texts.md +176 -0
- package/docs/themes.md +172 -0
- package/docs/tools.md +52 -0
- package/docs/troubleshooting.md +345 -0
- package/docs/voice-input.md +31 -0
- package/llms.txt +32 -0
- package/package.json +98 -3
- package/skills/askdare/SKILL.md +36 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { C as VisitorIdentity, S as ThemeVariants, _ as ColorScheme, a as BotContext, b as PresetName, c as BotSecurity, d as WidgetConfig, f as defineBot, g as defineTool, h as ToolDefinition, i as Bot, l as KnowledgeConfig, m as ToolContext, n as RateLimitStore, o as BotDefinition, p as Tool, r as AttachmentConfig, s as BotLimits, t as CounterHit, u as VoiceConfig, v as ThemeDefinition, x as ThemeTokens, y as defineTheme } from "./store.js";
|
|
2
|
+
/**
|
|
3
|
+
* Version of the SSE wire protocol between the askdare server and its clients.
|
|
4
|
+
* Bumped on any breaking change to event names or payloads (ADR-0006).
|
|
5
|
+
*/
|
|
6
|
+
export declare const PROTOCOL_VERSION: 1;
|
|
7
|
+
export { type AttachmentConfig, type Bot, type BotContext, type BotDefinition, type BotLimits, type BotSecurity, type ColorScheme, type CounterHit, type KnowledgeConfig, type PresetName, type RateLimitStore, type ThemeDefinition, type ThemeTokens, type ThemeVariants, type Tool, type ToolContext, type ToolDefinition, type VisitorIdentity, type VoiceConfig, type WidgetConfig, defineBot, defineTheme, defineTool };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { a as themeDefinitionSchema, c as TEXT_KEYS, f as textsFor, i as resolveTheme, n as PROTOCOL_VERSION, o as themeWarnings, r as defineTheme, s as PRESET_NAMES, u as canonicalLocale } from "./version.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// src/bot/define-bot.ts
|
|
4
|
+
const DEFAULT_LIMITS = {
|
|
5
|
+
maxOutputTokens: 1024,
|
|
6
|
+
maxInputChars: 4e3,
|
|
7
|
+
maxSteps: 5,
|
|
8
|
+
turnTimeoutMs: 12e4,
|
|
9
|
+
dailyTokens: 2e6,
|
|
10
|
+
dailyTokensPerIp: 2e5,
|
|
11
|
+
sessionsPerIpPerHour: 30,
|
|
12
|
+
conversationsPerIpPerHour: 20,
|
|
13
|
+
messagesPerVisitorPerMinute: 10,
|
|
14
|
+
messagesPerIpPerMinute: 30,
|
|
15
|
+
maxTurns: 50,
|
|
16
|
+
uploadsPerVisitorPerHour: 20,
|
|
17
|
+
uploadsPerIpPerHour: 60,
|
|
18
|
+
uploadsPerDay: 500,
|
|
19
|
+
transcriptionsPerVisitorPerHour: 30,
|
|
20
|
+
voiceSecondsPerDay: 3600,
|
|
21
|
+
voiceSecondsPerIpPerDay: 600
|
|
22
|
+
};
|
|
23
|
+
/** An https URL or a path on your app (no protocol-relative URLs). */
|
|
24
|
+
const LINK = /^(https:\/\/[^\s"'<>\\]{1,500}|\/(?![/\\])[^\s"'<>\\]{0,500})$/;
|
|
25
|
+
/** A same-origin path (served by your app, allowed by the iframe CSP) or a small raster data URL. */
|
|
26
|
+
const AVATAR = /^(\/(?!\/)[\w\-./%]{1,200}|data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/=]{1,90000})$/;
|
|
27
|
+
const origin = z.string().refine((value) => {
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(value);
|
|
30
|
+
return (url.protocol === "https:" || url.protocol === "http:" && /^(localhost|127\.0\.0\.1|\[::1\]|.+\.localhost)$/.test(url.hostname)) && url.origin === value && /^[a-z0-9.-]+$|^\[[0-9a-f:]+\]$/i.test(url.hostname);
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}, "must be an https origin like https://www.example.com (no path or wildcard; http only for localhost)");
|
|
35
|
+
const definitionSchema = z.object({
|
|
36
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,47}$/, "use 1-48 lowercase letters, digits or dashes"),
|
|
37
|
+
model: z.string().trim().min(1, "is required"),
|
|
38
|
+
instructions: z.union([z.string().trim().min(1, "must not be empty"), z.function()]),
|
|
39
|
+
limits: z.object({
|
|
40
|
+
maxOutputTokens: z.number().int().min(16).max(128e3).optional(),
|
|
41
|
+
maxInputChars: z.number().int().min(1).max(1e5).optional(),
|
|
42
|
+
maxSteps: z.number().int().min(1).max(20).optional(),
|
|
43
|
+
turnTimeoutMs: z.number().int().min(1e3).max(9e5).optional(),
|
|
44
|
+
dailyTokens: z.number().int().min(1e3).optional(),
|
|
45
|
+
dailyTokensPerIp: z.number().int().min(100).optional(),
|
|
46
|
+
sessionsPerIpPerHour: z.number().int().min(1).max(1e4).optional(),
|
|
47
|
+
conversationsPerIpPerHour: z.number().int().min(1).max(1e4).optional(),
|
|
48
|
+
messagesPerVisitorPerMinute: z.number().int().min(1).max(1e3).optional(),
|
|
49
|
+
messagesPerIpPerMinute: z.number().int().min(1).max(1e4).optional(),
|
|
50
|
+
maxTurns: z.number().int().min(1).max(1e3).optional(),
|
|
51
|
+
uploadsPerVisitorPerHour: z.number().int().min(1).max(1e3).optional(),
|
|
52
|
+
uploadsPerIpPerHour: z.number().int().min(1).max(1e4).optional(),
|
|
53
|
+
uploadsPerDay: z.number().int().min(1).max(1e6).optional(),
|
|
54
|
+
transcriptionsPerVisitorPerHour: z.number().int().min(1).max(1e3).optional(),
|
|
55
|
+
voiceSecondsPerDay: z.number().int().min(60).optional(),
|
|
56
|
+
voiceSecondsPerIpPerDay: z.number().int().min(60).optional()
|
|
57
|
+
}).optional(),
|
|
58
|
+
tools: z.array(z.custom((value) => typeof value === "object" && value !== null && "execute" in value, "must be created with defineTool()")).max(64).refine((tools) => new Set(tools.map((tool) => tool.name)).size === tools.length, "must have unique names").refine((tools) => tools.every((tool) => tool.name !== "file_search"), "must not use the reserved name file_search").optional(),
|
|
59
|
+
pageContext: z.boolean().optional(),
|
|
60
|
+
knowledge: z.object({
|
|
61
|
+
vectorStoreId: z.string().regex(/^vs_[A-Za-z0-9_-]+$/, "must be a vector store ID like vs_…"),
|
|
62
|
+
maxResults: z.number().int().min(1).max(50).optional()
|
|
63
|
+
}).optional(),
|
|
64
|
+
attachments: z.object({
|
|
65
|
+
images: z.boolean().optional(),
|
|
66
|
+
pdf: z.boolean().optional(),
|
|
67
|
+
maxBytes: z.number().int().min(1e3).max(2e7).optional(),
|
|
68
|
+
maxPerMessage: z.number().int().min(1).max(10).optional(),
|
|
69
|
+
maxPdfPages: z.number().int().min(1).max(500).optional()
|
|
70
|
+
}).refine((value) => value.images !== false || value.pdf !== false, "must allow images or pdf").optional(),
|
|
71
|
+
voice: z.object({
|
|
72
|
+
model: z.string().trim().min(1, "is required"),
|
|
73
|
+
maxSeconds: z.number().int().min(5).max(300).optional()
|
|
74
|
+
}).optional(),
|
|
75
|
+
security: z.object({ allowedOrigins: z.array(origin).max(50).optional() }).optional(),
|
|
76
|
+
widget: z.object({
|
|
77
|
+
title: z.string().trim().min(1).max(80).optional(),
|
|
78
|
+
greeting: z.string().trim().min(1).max(500).optional(),
|
|
79
|
+
placeholder: z.string().trim().min(1).max(120).optional(),
|
|
80
|
+
locale: z.string().refine((value) => value === "auto" || canonicalLocale(value) !== void 0, "must be auto or a BCP 47 tag like en or pl-PL").optional(),
|
|
81
|
+
texts: z.partialRecord(z.enum(TEXT_KEYS), z.string().trim().min(1).max(300)).optional(),
|
|
82
|
+
disclosure: z.union([z.literal(false), z.object({ privacyUrl: z.string().regex(LINK, "must be an https URL or a path like /privacy").optional() }).strict()]).optional(),
|
|
83
|
+
css: z.string().max(5e4).optional(),
|
|
84
|
+
subtitle: z.string().trim().min(1).max(120).optional(),
|
|
85
|
+
starters: z.array(z.string().trim().min(1).max(80)).max(4).optional(),
|
|
86
|
+
avatar: z.string().regex(AVATAR, "must be a path like /avatar.png or a PNG, JPEG, WebP or GIF data URL").optional(),
|
|
87
|
+
launcherLabel: z.string().trim().min(1).max(40).optional(),
|
|
88
|
+
theme: z.union([z.enum(PRESET_NAMES), themeDefinitionSchema]).optional(),
|
|
89
|
+
colorScheme: z.enum([
|
|
90
|
+
"auto",
|
|
91
|
+
"light",
|
|
92
|
+
"dark"
|
|
93
|
+
]).optional()
|
|
94
|
+
}).optional()
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* Declares a chatbot. Validates eagerly so misconfiguration fails at startup, not on the first
|
|
98
|
+
* visitor's message.
|
|
99
|
+
*/
|
|
100
|
+
function defineBot(definition) {
|
|
101
|
+
const parsed = definitionSchema.safeParse(definition);
|
|
102
|
+
if (!parsed.success) {
|
|
103
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"} ${issue.message}`).join("; ");
|
|
104
|
+
const hint = parsed.error.issues.some((issue) => issue.path.join(".") === "model") ? " Set the model in the bot definition, usually as model: process.env.ASKDARE_MODEL with ASKDARE_MODEL in .env.local and in every environment that builds or runs the app (on Vercel: Production and Preview)." : "";
|
|
105
|
+
throw new TypeError(`[askdare] Invalid bot "${String(definition.id)}": ${issues}.${hint}`);
|
|
106
|
+
}
|
|
107
|
+
const { instructions } = definition;
|
|
108
|
+
const widget = definition.widget ?? {};
|
|
109
|
+
const locale = widget.locale ?? "en";
|
|
110
|
+
const overrides = Object.freeze({
|
|
111
|
+
...widget.texts,
|
|
112
|
+
...widget.title ? { "header.title": widget.title } : {},
|
|
113
|
+
...widget.greeting ? { "welcome.greeting": widget.greeting } : {},
|
|
114
|
+
...widget.placeholder ? { "composer.placeholder": widget.placeholder } : {},
|
|
115
|
+
...widget.launcherLabel ? { "launcher.label": widget.launcherLabel } : {}
|
|
116
|
+
});
|
|
117
|
+
const texts = textsFor(locale === "auto" ? "en" : locale, overrides);
|
|
118
|
+
const theme = resolveTheme(definition.widget?.theme, definition.widget?.colorScheme ?? "auto");
|
|
119
|
+
if (typeof definition.widget?.theme === "object" && globalThis.process?.env?.["NODE_ENV"] !== "production") for (const warning of themeWarnings(theme)) console.warn(`[askdare] Bot "${definition.id}" theme: ${warning}.`);
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
id: definition.id,
|
|
122
|
+
model: definition.model.trim(),
|
|
123
|
+
instructions: typeof instructions === "string" ? () => instructions : instructions,
|
|
124
|
+
limits: Object.freeze({
|
|
125
|
+
...DEFAULT_LIMITS,
|
|
126
|
+
...definition.limits
|
|
127
|
+
}),
|
|
128
|
+
tools: Object.freeze([...definition.tools ?? []]),
|
|
129
|
+
pageContext: definition.pageContext ?? false,
|
|
130
|
+
knowledge: definition.knowledge ? Object.freeze({
|
|
131
|
+
vectorStoreId: definition.knowledge.vectorStoreId,
|
|
132
|
+
maxResults: definition.knowledge.maxResults ?? 5
|
|
133
|
+
}) : void 0,
|
|
134
|
+
attachments: definition.attachments ? Object.freeze({
|
|
135
|
+
images: definition.attachments.images ?? true,
|
|
136
|
+
pdf: definition.attachments.pdf ?? true,
|
|
137
|
+
maxBytes: definition.attachments.maxBytes ?? 4e6,
|
|
138
|
+
maxPerMessage: definition.attachments.maxPerMessage ?? 3,
|
|
139
|
+
maxPdfPages: definition.attachments.maxPdfPages ?? 50
|
|
140
|
+
}) : void 0,
|
|
141
|
+
voice: definition.voice ? Object.freeze({
|
|
142
|
+
model: definition.voice.model.trim(),
|
|
143
|
+
maxSeconds: definition.voice.maxSeconds ?? 60
|
|
144
|
+
}) : void 0,
|
|
145
|
+
security: Object.freeze({ allowedOrigins: Object.freeze([...new Set(definition.security?.allowedOrigins ?? [])]) }),
|
|
146
|
+
widget: Object.freeze({
|
|
147
|
+
title: texts["header.title"] ?? "",
|
|
148
|
+
subtitle: widget.subtitle ?? "",
|
|
149
|
+
greeting: texts["welcome.greeting"] ?? "",
|
|
150
|
+
placeholder: texts["composer.placeholder"] ?? "",
|
|
151
|
+
locale,
|
|
152
|
+
starters: Object.freeze([...widget.starters ?? []]),
|
|
153
|
+
avatar: widget.avatar,
|
|
154
|
+
launcherLabel: texts["launcher.label"] ?? "",
|
|
155
|
+
texts: overrides,
|
|
156
|
+
disclosure: widget.disclosure === false ? false : Object.freeze({ privacyUrl: widget.disclosure?.privacyUrl }),
|
|
157
|
+
css: widget.css ?? "",
|
|
158
|
+
theme
|
|
159
|
+
})
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// src/tools/define-tool.ts
|
|
163
|
+
const TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
164
|
+
/** Finds fields OpenAI strict mode would reject, so they fail at startup instead of mid-chat. */
|
|
165
|
+
function strictModeProblems(schema, path) {
|
|
166
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodDefault) return [`${path || "(root)"} is optional; use .nullable() so the field is always present`];
|
|
167
|
+
if (schema instanceof z.ZodNullable) return strictModeProblems(schema.unwrap(), path);
|
|
168
|
+
if (schema instanceof z.ZodArray) return strictModeProblems(schema.element, `${path}[]`);
|
|
169
|
+
if (schema instanceof z.ZodObject) return Object.entries(schema.shape).flatMap(([key, value]) => strictModeProblems(value, path ? `${path}.${key}` : key));
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Declares a server-side function the model may call. Arguments are validated with the schema
|
|
174
|
+
* before `execute` runs; failures are reported to the model, never to the visitor.
|
|
175
|
+
*/
|
|
176
|
+
function defineTool(definition) {
|
|
177
|
+
const problems = [];
|
|
178
|
+
if (!TOOL_NAME.test(definition.name)) problems.push("name must be 1-64 letters, digits, \"_\" or \"-\"");
|
|
179
|
+
if (definition.description.trim() === "") problems.push("description must not be empty");
|
|
180
|
+
if (!(definition.input instanceof z.ZodObject)) problems.push("input must be a z.object schema");
|
|
181
|
+
else problems.push(...strictModeProblems(definition.input, ""));
|
|
182
|
+
const timeoutMs = definition.timeoutMs ?? 1e4;
|
|
183
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 12e4) problems.push("timeoutMs must be an integer between 100 and 120000");
|
|
184
|
+
if (problems.length > 0) throw new TypeError(`[askdare] Invalid tool "${definition.name}": ${problems.join("; ")}`);
|
|
185
|
+
return Object.freeze({
|
|
186
|
+
name: definition.name,
|
|
187
|
+
description: definition.description.trim(),
|
|
188
|
+
input: definition.input,
|
|
189
|
+
execute: definition.execute,
|
|
190
|
+
timeoutMs
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
export { PROTOCOL_VERSION, defineBot, defineTheme, defineTool };
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { i as Bot, n as RateLimitStore } from "./store.js";
|
|
2
|
+
/**
|
|
3
|
+
* Which proxy to trust when reading the client IP:
|
|
4
|
+
* - `vercel`: Vercel overwrites `x-forwarded-for` and sets `x-real-ip`, so both are reliable.
|
|
5
|
+
* - `heroku`: Heroku's router appends the client IP, so the last entry is reliable and anything to
|
|
6
|
+
* its left may be spoofed.
|
|
7
|
+
* - a number `n`: there are `n` trusted proxies; take the n-th entry from the right.
|
|
8
|
+
* - `auto` (default): `vercel` when `VERCEL` is set, otherwise one trusted proxy.
|
|
9
|
+
*
|
|
10
|
+
* Never expose `next start` without a proxy in front: the client then controls these headers.
|
|
11
|
+
*/
|
|
12
|
+
type TrustProxy = "auto" | "vercel" | "heroku" | number;
|
|
13
|
+
/** Stable error codes sent to clients. The accompanying message never contains internals. */
|
|
14
|
+
declare const ERROR_CODES: readonly ["invalid_input", "unauthorized", "session_expired", "not_found", "rate_limited", "budget_exceeded", "bot_unavailable", "content_blocked", "timeout", "upstream_error", "internal_error"];
|
|
15
|
+
type ErrorCode = (typeof ERROR_CODES)[number];
|
|
16
|
+
interface ChatHandlerOptions {
|
|
17
|
+
readonly bots: readonly Bot[];
|
|
18
|
+
/** Path of the catch-all route. Default `/api/chat` (route file `app/api/chat/[...path]/route.ts`). */
|
|
19
|
+
readonly basePath?: string;
|
|
20
|
+
/** Default `process.env.OPENAI_API_KEY`. */
|
|
21
|
+
readonly openaiApiKey?: string;
|
|
22
|
+
/** Token secret, at least 32 characters. Default `process.env.CHAT_SECRET`. */
|
|
23
|
+
readonly secret?: string;
|
|
24
|
+
/** Former secrets still accepted during rotation. Default: comma-separated `process.env.CHAT_SECRET_PREVIOUS`. */
|
|
25
|
+
readonly previousSecrets?: readonly string[];
|
|
26
|
+
/**
|
|
27
|
+
* Secret the site's backend signs identity tokens with, for `askdare('identify')` (ADR-0009).
|
|
28
|
+
* At least 32 random characters, different from `secret`. Default `process.env.IDENTITY_SECRET`;
|
|
29
|
+
* without one, identity is off.
|
|
30
|
+
*/
|
|
31
|
+
readonly identitySecret?: string;
|
|
32
|
+
readonly limits?: ChatHandlerLimitsOptions;
|
|
33
|
+
/**
|
|
34
|
+
* Called for every server error (status 500 and up), for an error that ends a streamed answer,
|
|
35
|
+
* and once a day when a bot-wide or the global budget runs out, for your monitoring, such as Sentry.
|
|
36
|
+
* Defaults to one `console.error` line with a pointer to the troubleshooting guide. Never
|
|
37
|
+
* receives message contents or API keys.
|
|
38
|
+
*/
|
|
39
|
+
readonly onError?: (report: ChatErrorReport) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Serves the state gallery: `/embed/<bot>?gallery=<state>&preset=<preset>&scheme=<scheme>`
|
|
42
|
+
* renders fixture states without calling the model. For development and visual tests; leave
|
|
43
|
+
* it off in production.
|
|
44
|
+
*/
|
|
45
|
+
readonly gallery?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** One server error, as `onError` receives it. */
|
|
48
|
+
interface ChatErrorReport {
|
|
49
|
+
/** The stable code the widget also receives, such as `bot_unavailable` or `upstream_error`. */
|
|
50
|
+
readonly code: ErrorCode;
|
|
51
|
+
/** What went wrong, for you: a missing key, a model the project can't use. Never shown to visitors. */
|
|
52
|
+
readonly message: string;
|
|
53
|
+
/** The underlying error, with any API key cut down to its prefix. */
|
|
54
|
+
readonly cause?: unknown;
|
|
55
|
+
}
|
|
56
|
+
interface ChatHandlerLimitsOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Counter storage for rate limits and budgets. Defaults to per-process memory, which is weak when
|
|
59
|
+
* several instances serve traffic; use `@boldare/askdare-upstash` there.
|
|
60
|
+
*/
|
|
61
|
+
readonly store?: RateLimitStore;
|
|
62
|
+
/** How to read the client IP behind your proxy. Default `auto` (Vercel, otherwise one proxy). */
|
|
63
|
+
readonly trustProxy?: TrustProxy;
|
|
64
|
+
/** Input plus output tokens per UTC day across all bots. Unlimited by default. */
|
|
65
|
+
readonly globalDailyTokens?: number;
|
|
66
|
+
}
|
|
67
|
+
type RouteHandler = (request: Request) => Promise<Response>;
|
|
68
|
+
interface ChatHandlers {
|
|
69
|
+
readonly GET: RouteHandler;
|
|
70
|
+
readonly POST: RouteHandler;
|
|
71
|
+
readonly DELETE: RouteHandler;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Creates the Next.js route handlers for askdare. Mount them in a catch-all route:
|
|
75
|
+
*
|
|
76
|
+
* ```ts
|
|
77
|
+
* // app/api/chat/[...path]/route.ts
|
|
78
|
+
* export const maxDuration = 300;
|
|
79
|
+
* export const { GET, POST, DELETE } = createChatHandler({ bots });
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* Secrets are read on the first request, so importing the route (for example during
|
|
83
|
+
* `next build`) never requires them.
|
|
84
|
+
*/
|
|
85
|
+
export declare function createChatHandler(options: ChatHandlerOptions): ChatHandlers;
|
|
86
|
+
export type { ChatErrorReport, ChatHandlerLimitsOptions, ChatHandlerOptions, ChatHandlers, RouteHandler, TrustProxy };
|