@gedatou/cadenza-ai 0.7.1

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.
Files changed (57) hide show
  1. package/README.md +71 -0
  2. package/dist/anthropic-DdrANeO1.mjs +236 -0
  3. package/dist/bedrock-534H9yfk.mjs +75 -0
  4. package/dist/catalog-CvvS2JaW.mjs +32 -0
  5. package/dist/deepseek-C1b74T2F.mjs +55 -0
  6. package/dist/gemini-DcJ5Kr3h.mjs +229 -0
  7. package/dist/grok-CffQuBd6.mjs +77 -0
  8. package/dist/groq-Ba6TTYLp.mjs +98 -0
  9. package/dist/index.d.mts +1056 -0
  10. package/dist/index.mjs +2809 -0
  11. package/dist/llmgateway-Pt5CS6aQ.mjs +211 -0
  12. package/dist/mistral-BGMwwbU9.mjs +107 -0
  13. package/dist/mock/index.d.mts +168 -0
  14. package/dist/mock/index.mjs +467 -0
  15. package/dist/ollama-BBJs-3mK.mjs +80 -0
  16. package/dist/openai--FIeahIH.mjs +153 -0
  17. package/dist/openrouter-q-lx-2Cb.mjs +167 -0
  18. package/dist/preset-CEyIPxfS.mjs +157 -0
  19. package/dist/preset-DRT0T14q.d.mts +19 -0
  20. package/dist/providers/anthropic.d.mts +6 -0
  21. package/dist/providers/anthropic.mjs +11 -0
  22. package/dist/providers/bedrock.d.mts +6 -0
  23. package/dist/providers/bedrock.mjs +11 -0
  24. package/dist/providers/byteplus.d.mts +6 -0
  25. package/dist/providers/byteplus.mjs +21 -0
  26. package/dist/providers/deepseek.d.mts +35 -0
  27. package/dist/providers/deepseek.mjs +283 -0
  28. package/dist/providers/gemini.d.mts +6 -0
  29. package/dist/providers/gemini.mjs +11 -0
  30. package/dist/providers/grok.d.mts +6 -0
  31. package/dist/providers/grok.mjs +11 -0
  32. package/dist/providers/groq.d.mts +6 -0
  33. package/dist/providers/groq.mjs +11 -0
  34. package/dist/providers/llmgateway.d.mts +6 -0
  35. package/dist/providers/llmgateway.mjs +11 -0
  36. package/dist/providers/mistral.d.mts +6 -0
  37. package/dist/providers/mistral.mjs +11 -0
  38. package/dist/providers/ollama.d.mts +9 -0
  39. package/dist/providers/ollama.mjs +28 -0
  40. package/dist/providers/openai-compatible.d.mts +32 -0
  41. package/dist/providers/openai-compatible.mjs +45 -0
  42. package/dist/providers/openai.d.mts +6 -0
  43. package/dist/providers/openai.mjs +11 -0
  44. package/dist/providers/openrouter.d.mts +6 -0
  45. package/dist/providers/openrouter.mjs +11 -0
  46. package/dist/providers/vercel-gateway.d.mts +6 -0
  47. package/dist/providers/vercel-gateway.mjs +11 -0
  48. package/dist/providers/vertex.d.mts +6 -0
  49. package/dist/providers/vertex.mjs +11 -0
  50. package/dist/server/index.d.mts +173 -0
  51. package/dist/server/index.mjs +440 -0
  52. package/dist/thinking-CH2uL1dr.mjs +26 -0
  53. package/dist/types-56Hn7Qq2.d.mts +46 -0
  54. package/dist/vercel-gateway-DW3Z0Os1.mjs +109 -0
  55. package/dist/vertex-hg4Dt54O.mjs +20 -0
  56. package/package.json +114 -0
  57. package/styles.css +3 -0
@@ -0,0 +1,173 @@
1
+ import { o as ThinkingLevel, r as Model } from "../types-56Hn7Qq2.mjs";
2
+ import { n as definePreset, t as ProviderPreset } from "../preset-DRT0T14q.mjs";
3
+ import { ByokProvider, defineByokProvider } from "@tanstack/ai/byok";
4
+ import { byokMissing, getByokKey } from "@tanstack/ai/byok/server";
5
+ import { AgentLoopStrategy, AnySummarizeAdapter, AnyTool, AnyTranscriptionAdapter, ChatMiddleware, DebugOption, StreamDurability, SystemPrompt, chat, chatParamsFromRequest, maxIterations, memoryStream, mergeAgentTools, toServerSentEventsResponse, toolDefinition } from "@tanstack/ai";
6
+ //#region src/server/catalog-handler.d.ts
7
+ /**
8
+ * `GET /api/ai/catalog`: the pure-data side of each preset plus a coverage map
9
+ * the browser feeds into `byok.setServerCoverage()`.
10
+ * `GET ?refresh=1&provider=<id>`: `preset.discoverModels(key)` with the same
11
+ * header-then-env key the chat handler would use → `{ provider, models }`.
12
+ */
13
+ interface CatalogHandlerOptions {
14
+ /**
15
+ * Hosts an `x-byok-ollama` header may point at; default: loopback and RFC 1918
16
+ * ranges. Same option and same meaning as on `createChatHandler` — set both,
17
+ * because `?refresh=1&provider=ollama` fetches that host too.
18
+ */
19
+ ollamaHosts?: readonly string[];
20
+ }
21
+ declare function createCatalogHandler(presets: readonly ProviderPreset[], options?: CatalogHandlerOptions): {
22
+ GET: (request: Request) => Promise<Response>;
23
+ };
24
+ //#endregion
25
+ //#region src/server/selection.d.ts
26
+ interface Selection {
27
+ preset: ProviderPreset;
28
+ model: Model;
29
+ thinking: ThinkingLevel;
30
+ /** 客户端要的联网搜索,且模型确实声明了这个能力。 */
31
+ search: boolean;
32
+ }
33
+ /**
34
+ * Read `provider` / `model` / `thinking` off `forwardedProps` — nothing else on
35
+ * that object is trusted — and resolve them against the server-side presets.
36
+ */
37
+ declare function pickSelection(fp: Record<string, unknown>, presets: readonly ProviderPreset[], options: {
38
+ defaultModel?: string;
39
+ }): Selection | Response;
40
+ //#endregion
41
+ //#region src/server/chat-handler.d.ts
42
+ interface ChatHandlerOptions<TContext = unknown> {
43
+ providers: readonly ProviderPreset[];
44
+ /** `provider/model` used when the request carries no selection. */
45
+ defaultModel?: string;
46
+ systemPrompts?: SystemPrompt[] | ((selection: Selection) => SystemPrompt[]);
47
+ /**
48
+ * Client tools always merge in on top. Prefer the function form whenever any
49
+ * entry is a provider tool (`webSearchTool()` and friends): those are branded
50
+ * per vendor, and an adapter that does not recognise the brand silently
51
+ * degrades the tool into a schema-less function call nothing can execute.
52
+ */
53
+ tools?: ReadonlyArray<AnyTool> | ((selection: Selection) => ReadonlyArray<AnyTool>);
54
+ middleware?: Array<ChatMiddleware<TContext>>;
55
+ context?: (request: Request) => TContext | Promise<TContext>;
56
+ agentLoopStrategy?: AgentLoopStrategy;
57
+ /** Inspect or replace the resolved selection; return a `Response` to refuse the request. */
58
+ onSelect?: (selection: Selection, request: Request) => Selection | Response;
59
+ /**
60
+ * An `AIPersistence<ChatTranscriptStores>` from the optional peer
61
+ * `@tanstack/ai-persistence`. Typed as `unknown` so this entry's declarations
62
+ * never reference that package; the peer is imported lazily at request time.
63
+ */
64
+ persistence?: unknown;
65
+ /** `ReconstructChatOptions['authorize']` from `@tanstack/ai-persistence`; only read together with `persistence`. */
66
+ authorize?: unknown;
67
+ durability?: (request: Request) => StreamDurability;
68
+ /** Default 4 MiB. */
69
+ maxBodyBytes?: number;
70
+ /** Hosts an `x-byok-ollama` header may point at; default: loopback and RFC 1918 ranges. */
71
+ ollamaHosts?: readonly string[];
72
+ debug?: DebugOption;
73
+ }
74
+ interface ChatHandler {
75
+ POST: (request: Request) => Promise<Response>;
76
+ GET: (request: Request) => Promise<Response>;
77
+ }
78
+ /**
79
+ * Route-handler pair for `/api/ai/chat`. The request lifecycle (spec §服务端):
80
+ * size guard → parse → pick selection → onSelect → BYOK key → adapter →
81
+ * thinking → chat() → SSE. `GET` serves persistence reconstruction and
82
+ * durable-stream resumption when those options are present.
83
+ */
84
+ declare function createChatHandler<TContext = unknown>(options: ChatHandlerOptions<TContext>): ChatHandler;
85
+ //#endregion
86
+ //#region src/server/generation-handlers.d.ts
87
+ interface GenerationHandlerOptions<TAdapter> {
88
+ /** Builds the adapter with the BYOK / env key; `null` when the provider needs no key. */
89
+ adapter: (model: string, key: string | null) => TAdapter;
90
+ /** Which BYOK header / env holds the key; omit for keyless providers. */
91
+ byok?: ByokProvider;
92
+ /** Model used unless `forwardedProps.model` names one. */
93
+ defaultModel: string;
94
+ /** Default 8 MiB (base64 audio). */
95
+ maxBodyBytes?: number;
96
+ debug?: DebugOption;
97
+ }
98
+ interface GenerationHandler {
99
+ POST: (request: Request) => Promise<Response>;
100
+ }
101
+ /** `POST /api/ai/transcription` for `useTranscription({ connection })`: the body is the generation envelope, the reply an SSE run ending in `generation:result`. */
102
+ declare function createTranscriptionHandler(options: GenerationHandlerOptions<AnyTranscriptionAdapter>): GenerationHandler;
103
+ /** `POST /api/ai/summarize` for `useSummarize({ connection })`; same lifecycle as the transcription handler. */
104
+ declare function createSummarizeHandler(options: GenerationHandlerOptions<AnySummarizeAdapter>): GenerationHandler;
105
+ //#endregion
106
+ //#region src/server/ollama-host.d.ts
107
+ /**
108
+ * `x-byok-ollama` carries a host URL, not a key: every handler that hands it to
109
+ * `fetch` has to check it first, or it is a server-side request forgery hole.
110
+ *
111
+ * It lives in its own file so the check cannot drift between callers again —
112
+ * the chat handler had one while the catalog handler's `discoverModels` path
113
+ * did not, and the only thing recording that dependency was a comment in
114
+ * `providers/ollama.ts` saying the chat handler had already checked.
115
+ */
116
+ /**
117
+ * Whether `host` — the raw `x-byok-ollama` value — may be fetched.
118
+ *
119
+ * `allow` is the deployer's own list (`ollamaHosts`), matched exactly against
120
+ * `host` or `hostname`. Without one, only loopback and the private ranges pass.
121
+ */
122
+ declare function ollamaHostAllowed(host: string, allow?: readonly string[]): boolean;
123
+ //#endregion
124
+ //#region src/server/thinking.d.ts
125
+ type Fragment = Record<string, unknown>;
126
+ type Effort3 = 'low' | 'medium' | 'high';
127
+ /** 三档 effort 的 provider(grok / groq / vercel-gateway / ollama gpt-oss)共用的折叠表。 */
128
+ declare const EFFORT_3: Record<Exclude<ThinkingLevel, 'off'>, Effort3>;
129
+ /** clamp 到模型支持的档位后再交给 preset;非推理模型关着时不发任何片段。 */
130
+ declare function resolveThinking(preset: ProviderPreset, model: Model, level: ThinkingLevel): Fragment;
131
+ declare function openaiThinking(level: ThinkingLevel, model: Model): Fragment;
132
+ declare function anthropicThinking(level: ThinkingLevel, model: Model): Fragment;
133
+ declare function geminiThinking(level: ThinkingLevel, model: Model): Fragment;
134
+ declare function openrouterThinking(level: ThinkingLevel, _model: Model): Fragment;
135
+ declare function grokThinking(level: ThinkingLevel, model: Model): Fragment;
136
+ declare function groqThinking(level: ThinkingLevel, model: Model): Fragment;
137
+ declare function vercelGatewayThinking(level: ThinkingLevel, _model: Model): Fragment;
138
+ declare function llmgatewayThinking(level: ThinkingLevel, _model: Model): Fragment;
139
+ declare function ollamaThinking(level: ThinkingLevel, model: Model): Fragment;
140
+ declare function openaiCompatibleThinking(level: ThinkingLevel, model: Model): Fragment;
141
+ declare function deepseekThinking(level: ThinkingLevel, model: Model): Fragment;
142
+ declare function deepseekResponsesThinking(level: ThinkingLevel, model: Model): Fragment;
143
+ declare function noThinking(_level: ThinkingLevel, _model: Model): Fragment;
144
+ //#endregion
145
+ //#region src/server/title-handler.d.ts
146
+ interface TitleHandlerOptions {
147
+ providers: readonly ProviderPreset[];
148
+ /** `vendor/model` used when `forwardedProps` names none; normally the conversation's own selection arrives. */
149
+ defaultModel?: string;
150
+ /** Upper bound the instruction asks for. Default 6 — a sidebar row, not a sentence. */
151
+ maxWords?: number;
152
+ /** Replace the instruction; receives `maxWords`. */
153
+ prompt?: (maxWords: number) => string;
154
+ /** Default 1 MiB. */
155
+ maxBodyBytes?: number;
156
+ debug?: DebugOption;
157
+ }
158
+ /**
159
+ * The ChatGPT rule: name the conversation from its first exchange, in the
160
+ * user's language, a handful of words, nothing around it.
161
+ */
162
+ declare function defaultTitlePrompt(maxWords: number): string;
163
+ /** Strip what models add anyway — fences, a `Title:` label, quotes, trailing punctuation — and keep one line. */
164
+ declare function cleanTitle(raw: string, maxLength?: number): string;
165
+ /**
166
+ * `POST /api/ai/title` for `useSummarize({ connection })`: the same envelope and
167
+ * SSE reply as the summarize handler, but the model is whichever the
168
+ * conversation selected (`forwardedProps.provider` / `model`, thinking off), and
169
+ * the reply is a title, not a summary.
170
+ */
171
+ declare function createTitleHandler(options: TitleHandlerOptions): GenerationHandler;
172
+ //#endregion
173
+ export { CatalogHandlerOptions, ChatHandler, ChatHandlerOptions, EFFORT_3, GenerationHandler, GenerationHandlerOptions, ProviderPreset, Selection, TitleHandlerOptions, anthropicThinking, byokMissing, chat, chatParamsFromRequest, cleanTitle, createCatalogHandler, createChatHandler, createSummarizeHandler, createTitleHandler, createTranscriptionHandler, deepseekResponsesThinking, deepseekThinking, defaultTitlePrompt, defineByokProvider, definePreset, geminiThinking, getByokKey, grokThinking, groqThinking, llmgatewayThinking, maxIterations, memoryStream, mergeAgentTools, noThinking, ollamaHostAllowed, ollamaThinking, openaiCompatibleThinking, openaiThinking, openrouterThinking, pickSelection, resolveThinking, toServerSentEventsResponse, toolDefinition, vercelGatewayThinking };
@@ -0,0 +1,440 @@
1
+ import { r as parseModelRef } from "../catalog-CvvS2JaW.mjs";
2
+ import { n as clampThinkingLevel, t as THINKING_LEVELS } from "../thinking-CH2uL1dr.mjs";
3
+ import { a as deepseekThinking, c as groqThinking, d as ollamaThinking, f as openaiCompatibleThinking, g as vercelGatewayThinking, h as resolveThinking, i as deepseekResponsesThinking, l as llmgatewayThinking, m as openrouterThinking, n as EFFORT_3, o as geminiThinking, p as openaiThinking, r as anthropicThinking, s as grokThinking, t as definePreset, u as noThinking } from "../preset-CEyIPxfS.mjs";
4
+ import { defineByokProvider } from "@tanstack/ai/byok";
5
+ import { byokMissing, byokMissing as byokMissing$1, getByokKey, getByokKey as getByokKey$1 } from "@tanstack/ai/byok/server";
6
+ import { chat, chat as chat$1, chatParamsFromRequest, chatParamsFromRequest as chatParamsFromRequest$1, generateTranscription, generationParamsFromRequest, maxIterations, memoryStream, mergeAgentTools, mergeAgentTools as mergeAgentTools$1, summarize, toServerSentEventsResponse, toServerSentEventsResponse as toServerSentEventsResponse$1, toolDefinition } from "@tanstack/ai";
7
+ //#region src/server/ollama-host.ts
8
+ /**
9
+ * `x-byok-ollama` carries a host URL, not a key: every handler that hands it to
10
+ * `fetch` has to check it first, or it is a server-side request forgery hole.
11
+ *
12
+ * It lives in its own file so the check cannot drift between callers again —
13
+ * the chat handler had one while the catalog handler's `discoverModels` path
14
+ * did not, and the only thing recording that dependency was a comment in
15
+ * `providers/ollama.ts` saying the chat handler had already checked.
16
+ */
17
+ /**
18
+ * Loopback and RFC 1918, decided by parsing the address.
19
+ *
20
+ * Prefix-matching the hostname does not work: `10.attacker.com` starts with
21
+ * `10.` and is an ordinary domain that resolves wherever its owner points it,
22
+ * so a string test hands the allowlist to the attacker.
23
+ */
24
+ function isPrivateHost(hostname) {
25
+ if (hostname === "localhost" || hostname === "[::1]") return true;
26
+ const parts = hostname.split(".");
27
+ if (parts.length !== 4 || parts.some((p) => !/^\d{1,3}$/.test(p))) return false;
28
+ const [a = -1, b = -1] = parts.map(Number);
29
+ if (a > 255 || b > 255) return false;
30
+ return a === 127 || a === 10 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31;
31
+ }
32
+ /**
33
+ * Whether `host` — the raw `x-byok-ollama` value — may be fetched.
34
+ *
35
+ * `allow` is the deployer's own list (`ollamaHosts`), matched exactly against
36
+ * `host` or `hostname`. Without one, only loopback and the private ranges pass.
37
+ */
38
+ function ollamaHostAllowed(host, allow) {
39
+ let url;
40
+ try {
41
+ url = new URL(host);
42
+ } catch {
43
+ return false;
44
+ }
45
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
46
+ return allow ? allow.some((a) => url.host === a || url.hostname === a) : isPrivateHost(url.hostname);
47
+ }
48
+ //#endregion
49
+ //#region src/server/catalog-handler.ts
50
+ function hasEnv(...names) {
51
+ return names.some((n) => {
52
+ const v = process.env[n];
53
+ return v !== void 0 && v !== "";
54
+ });
55
+ }
56
+ /** Whether the server can run this provider without a browser-supplied key. */
57
+ function covered(p) {
58
+ if (!p.keyRequired) {
59
+ if (p.id !== "vertex") return true;
60
+ return hasEnv("GOOGLE_VERTEX_API_KEY") || hasEnv("GOOGLE_CLOUD_PROJECT", "GOOGLE_VERTEX_PROJECT") && hasEnv("GOOGLE_CLOUD_LOCATION", "GOOGLE_VERTEX_LOCATION");
61
+ }
62
+ return hasEnv(...p.byok?.env ?? []);
63
+ }
64
+ function json(status, body) {
65
+ return new Response(JSON.stringify(body), {
66
+ status,
67
+ headers: { "content-type": "application/json" }
68
+ });
69
+ }
70
+ function createCatalogHandler(presets, options = {}) {
71
+ return { GET: async (request) => {
72
+ const url = new URL(request.url);
73
+ if (url.searchParams.get("refresh") === "1") {
74
+ const id = url.searchParams.get("provider");
75
+ const preset = presets.find((p) => p.id === id);
76
+ if (!preset) return json(400, { error: { type: "unknown_provider" } });
77
+ if (!preset.discoverModels) return json(400, { error: {
78
+ type: "discover_unsupported",
79
+ provider: preset.id
80
+ } });
81
+ const key = preset.byok ? getByokKey$1(request, preset.byok) : null;
82
+ if (preset.id === "ollama" && key !== null && !ollamaHostAllowed(key, options.ollamaHosts)) return json(400, { error: {
83
+ type: "host_not_allowed",
84
+ provider: preset.id
85
+ } });
86
+ try {
87
+ return Response.json({
88
+ provider: preset.id,
89
+ models: await preset.discoverModels(key)
90
+ });
91
+ } catch {
92
+ return json(502, { error: {
93
+ type: "discover_failed",
94
+ provider: preset.id
95
+ } });
96
+ }
97
+ }
98
+ const providers = presets.map(({ create: _c, thinking: _t, discoverModels: _d, ...rest }) => rest);
99
+ const coverage = Object.fromEntries(presets.map((p) => [p.id, covered(p)]));
100
+ return Response.json({
101
+ providers,
102
+ coverage,
103
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
104
+ });
105
+ } };
106
+ }
107
+ //#endregion
108
+ //#region src/server/selection.ts
109
+ const MODEL_ID$1 = /^[\w.\-:/~]{1,200}$/;
110
+ function bad$1(type) {
111
+ return new Response(JSON.stringify({ error: { type } }), {
112
+ status: 400,
113
+ headers: { "content-type": "application/json" }
114
+ });
115
+ }
116
+ /**
117
+ * Read `provider` / `model` / `thinking` off `forwardedProps` — nothing else on
118
+ * that object is trusted — and resolve them against the server-side presets.
119
+ */
120
+ function pickSelection(fp, presets, options) {
121
+ let provider = typeof fp.provider === "string" ? fp.provider : void 0;
122
+ let modelId = typeof fp.model === "string" ? fp.model : void 0;
123
+ if ((provider === void 0 || modelId === void 0) && options.defaultModel !== void 0) {
124
+ const d = parseModelRef(options.defaultModel);
125
+ provider ??= d.provider;
126
+ modelId ??= d.id;
127
+ }
128
+ if (provider === void 0 || modelId === void 0 || modelId === "") return bad$1("unknown_model");
129
+ const preset = presets.find((p) => p.id === provider);
130
+ if (!preset) return bad$1("unknown_provider");
131
+ if (!MODEL_ID$1.test(modelId)) return bad$1("unknown_model");
132
+ let model = preset.models.find((m) => m.id === modelId);
133
+ if (!model) {
134
+ if (!preset.discoverModels) return bad$1("unknown_model");
135
+ model = {
136
+ id: modelId,
137
+ name: modelId,
138
+ provider: preset.id,
139
+ input: ["text"],
140
+ reasoning: false
141
+ };
142
+ }
143
+ const raw = typeof fp.thinking === "string" && THINKING_LEVELS.includes(fp.thinking) ? fp.thinking : "off";
144
+ return {
145
+ preset,
146
+ model,
147
+ thinking: clampThinkingLevel(model, raw),
148
+ search: fp.search === true && model.search === true
149
+ };
150
+ }
151
+ //#endregion
152
+ //#region src/server/chat-handler.ts
153
+ /**
154
+ * Route-handler pair for `/api/ai/chat`. The request lifecycle (spec §服务端):
155
+ * size guard → parse → pick selection → onSelect → BYOK key → adapter →
156
+ * thinking → chat() → SSE. `GET` serves persistence reconstruction and
157
+ * durable-stream resumption when those options are present.
158
+ */
159
+ function createChatHandler(options) {
160
+ const onVercel = process.env.VERCEL === "1";
161
+ const presets = options.providers.filter((p) => !(onVercel && p.runtime === "local"));
162
+ const maxBody = options.maxBodyBytes ?? 4194304;
163
+ async function POST(request) {
164
+ const declared = request.headers.get("content-length");
165
+ if (declared !== null && !(Number(declared) <= maxBody)) return new Response("Payload too large", { status: 413 });
166
+ let params;
167
+ try {
168
+ params = await chatParamsFromRequest$1(request);
169
+ } catch (error) {
170
+ if (error instanceof Response) return error;
171
+ throw error;
172
+ }
173
+ const picked = pickSelection(params.forwardedProps ?? {}, presets, { defaultModel: options.defaultModel });
174
+ if (picked instanceof Response) return picked;
175
+ const selection = options.onSelect ? options.onSelect(picked, request) : picked;
176
+ if (selection instanceof Response) return selection;
177
+ const { preset, model, thinking } = selection;
178
+ const key = preset.byok ? getByokKey$1(request, preset.byok) : null;
179
+ if (preset.keyRequired && key === null && preset.byok) return byokMissing$1(preset.byok);
180
+ if (preset.id === "ollama" && key !== null && !ollamaHostAllowed(key, options.ollamaHosts)) return new Response("Ollama host not allowed", { status: 400 });
181
+ const adapter = preset.create(model.id, key);
182
+ const modelOptions = resolveThinking(preset, model, thinking);
183
+ const abortController = new AbortController();
184
+ const middleware = [...options.middleware ?? []];
185
+ if (options.persistence !== void 0) {
186
+ const { withPersistence } = await import("@tanstack/ai-persistence");
187
+ middleware.push(withPersistence(options.persistence));
188
+ }
189
+ const context = options.context ? await options.context(request) : void 0;
190
+ const stream = chat$1({
191
+ adapter,
192
+ messages: params.messages,
193
+ threadId: params.threadId,
194
+ runId: params.runId,
195
+ parentRunId: params.parentRunId,
196
+ resume: params.resume,
197
+ tools: mergeAgentTools$1(typeof options.tools === "function" ? options.tools(selection) : options.tools ?? [], params.tools),
198
+ systemPrompts: typeof options.systemPrompts === "function" ? options.systemPrompts(selection) : options.systemPrompts,
199
+ modelOptions,
200
+ middleware,
201
+ agentLoopStrategy: options.agentLoopStrategy,
202
+ context,
203
+ abortController,
204
+ debug: options.debug
205
+ });
206
+ return toServerSentEventsResponse$1(stream, {
207
+ abortController,
208
+ ...options.durability ? { durability: { adapter: options.durability(request) } } : {},
209
+ debug: options.debug
210
+ });
211
+ }
212
+ async function GET(request) {
213
+ const url = new URL(request.url);
214
+ if (options.persistence !== void 0 && url.searchParams.has("threadId")) {
215
+ const { reconstructChat } = await import("@tanstack/ai-persistence");
216
+ return reconstructChat(options.persistence, request, { authorize: options.authorize });
217
+ }
218
+ if (options.durability && (url.searchParams.has("runId") || request.headers.has("last-event-id"))) {
219
+ const { resumeServerSentEventsResponse } = await import("@tanstack/ai");
220
+ return resumeServerSentEventsResponse({ adapter: options.durability(request) });
221
+ }
222
+ return new Response("Not found", { status: 404 });
223
+ }
224
+ return {
225
+ POST,
226
+ GET
227
+ };
228
+ }
229
+ //#endregion
230
+ //#region src/server/envelope.ts
231
+ function isRecord(value) {
232
+ return typeof value === "object" && value !== null && !Array.isArray(value);
233
+ }
234
+ const SUMMARY_STYLES = [
235
+ "bullet-points",
236
+ "paragraph",
237
+ "concise"
238
+ ];
239
+ async function summarizeParamsFromRequest(request) {
240
+ let body;
241
+ try {
242
+ body = await request.json();
243
+ } catch {
244
+ throw new Error("Invalid JSON request body.");
245
+ }
246
+ if (!isRecord(body)) throw new Error("Summarize request body must be a JSON object.");
247
+ const input = isRecord(body.data) ? body.data : body;
248
+ if (typeof input.text !== "string") throw new Error("Summarize input must include text.");
249
+ const style = SUMMARY_STYLES.find((s) => s === input.style);
250
+ const focus = Array.isArray(input.focus) ? input.focus.filter((f) => typeof f === "string") : void 0;
251
+ return {
252
+ input: {
253
+ text: input.text,
254
+ maxLength: typeof input.maxLength === "number" ? input.maxLength : void 0,
255
+ style,
256
+ focus
257
+ },
258
+ forwardedProps: isRecord(body.forwardedProps) ? body.forwardedProps : {},
259
+ threadId: typeof body.threadId === "string" ? body.threadId : void 0,
260
+ runId: typeof body.runId === "string" ? body.runId : void 0
261
+ };
262
+ }
263
+ //#endregion
264
+ //#region src/server/generation-handlers.ts
265
+ const MODEL_ID = /^[\w.\-:/~]{1,200}$/;
266
+ function bad(message, type) {
267
+ return type === void 0 ? new Response(message, { status: 400 }) : new Response(JSON.stringify({ error: { type } }), {
268
+ status: 400,
269
+ headers: { "content-type": "application/json" }
270
+ });
271
+ }
272
+ /**
273
+ * The part of the lifecycle both handlers share (spec §服务端, mirrored from
274
+ * `createChatHandler`): size guard → parse → model → BYOK key → adapter.
275
+ */
276
+ async function resolve(request, options, parse) {
277
+ if (Number(request.headers.get("content-length") ?? 0) > (options.maxBodyBytes ?? 8388608)) return new Response("Payload too large", { status: 413 });
278
+ let params;
279
+ try {
280
+ params = await parse(request);
281
+ } catch (error) {
282
+ if (error instanceof Response) return error;
283
+ return bad(error instanceof Error ? error.message : "Invalid request body");
284
+ }
285
+ const requested = params.forwardedProps.model;
286
+ let model = options.defaultModel;
287
+ if (requested !== void 0) {
288
+ if (typeof requested !== "string" || !MODEL_ID.test(requested)) return bad("unknown model", "unknown_model");
289
+ model = requested;
290
+ }
291
+ const key = options.byok ? getByokKey$1(request, options.byok) : null;
292
+ if (options.byok && key === null) return byokMissing$1(options.byok);
293
+ return {
294
+ params,
295
+ adapter: options.adapter(model, key)
296
+ };
297
+ }
298
+ /** `POST /api/ai/transcription` for `useTranscription({ connection })`: the body is the generation envelope, the reply an SSE run ending in `generation:result`. */
299
+ function createTranscriptionHandler(options) {
300
+ return { POST: async (request) => {
301
+ const resolved = await resolve(request, options, (req) => generationParamsFromRequest("transcription", req));
302
+ if (resolved instanceof Response) return resolved;
303
+ const { input, threadId, runId } = resolved.params;
304
+ if (typeof input.audio !== "string") return bad("Transcription audio must be a base64 or data-URL string.");
305
+ const abortController = new AbortController();
306
+ const stream = generateTranscription({
307
+ adapter: resolved.adapter,
308
+ audio: input.audio,
309
+ language: input.language,
310
+ prompt: input.prompt,
311
+ responseFormat: input.responseFormat,
312
+ threadId,
313
+ runId,
314
+ stream: true,
315
+ abortSignal: abortController.signal,
316
+ debug: options.debug
317
+ });
318
+ return toServerSentEventsResponse$1(stream, {
319
+ abortController,
320
+ debug: options.debug
321
+ });
322
+ } };
323
+ }
324
+ /** `POST /api/ai/summarize` for `useSummarize({ connection })`; same lifecycle as the transcription handler. */
325
+ function createSummarizeHandler(options) {
326
+ return { POST: async (request) => {
327
+ const resolved = await resolve(request, options, summarizeParamsFromRequest);
328
+ if (resolved instanceof Response) return resolved;
329
+ const { input, threadId, runId } = resolved.params;
330
+ const abortController = new AbortController();
331
+ const stream = summarize({
332
+ adapter: resolved.adapter,
333
+ ...input,
334
+ threadId,
335
+ runId,
336
+ stream: true,
337
+ abortSignal: abortController.signal,
338
+ debug: options.debug
339
+ });
340
+ return toServerSentEventsResponse$1(stream, {
341
+ abortController,
342
+ debug: options.debug
343
+ });
344
+ } };
345
+ }
346
+ //#endregion
347
+ //#region src/server/title-handler.ts
348
+ /**
349
+ * The ChatGPT rule: name the conversation from its first exchange, in the
350
+ * user's language, a handful of words, nothing around it.
351
+ */
352
+ function defaultTitlePrompt(maxWords) {
353
+ return [
354
+ "You name conversations.",
355
+ `Given the first exchange of a chat, reply with a title of at most ${maxWords} words, in the language the user wrote in.`,
356
+ "Output only the title: no quotes, no trailing punctuation, no markdown, no explanation."
357
+ ].join(" ");
358
+ }
359
+ const WRAPPING = /^["'“”‘’«»「」『』`*_\s]+|["'“”‘’«»「」『』`*_\s]+$/g;
360
+ const TRAILING = /[.。!!??::;;,,、]+$/;
361
+ /** Strip what models add anyway — fences, a `Title:` label, quotes, trailing punctuation — and keep one line. */
362
+ function cleanTitle(raw, maxLength = 80) {
363
+ const title = (raw.replace(/```[a-z]*/gi, " ").split("\n").map((l) => l.trim()).find((l) => l !== "") ?? "").replace(/^title\s*[::]\s*/i, "").replace(WRAPPING, "").replace(TRAILING, "").replace(/\s+/g, " ").trim();
364
+ return title.length > maxLength ? `${title.slice(0, maxLength)}…` : title;
365
+ }
366
+ /** A summarize adapter over a text adapter: the summary is the title, so `useSummarize` on the client needs no new protocol. */
367
+ function titleAdapter(adapter, systemPrompt, modelOptions) {
368
+ const model = adapter.model;
369
+ return {
370
+ "kind": "summarize",
371
+ "name": adapter.name,
372
+ "model": model,
373
+ "~types": { providerOptions: {} },
374
+ "summarize": async ({ text, abortSignal }) => {
375
+ const abortController = new AbortController();
376
+ abortSignal?.addEventListener("abort", () => abortController.abort(), { once: true });
377
+ const raw = await chat$1({
378
+ adapter,
379
+ messages: [{
380
+ role: "user",
381
+ content: text
382
+ }],
383
+ systemPrompts: [systemPrompt],
384
+ modelOptions,
385
+ stream: false,
386
+ abortController
387
+ });
388
+ return {
389
+ id: crypto.randomUUID(),
390
+ model,
391
+ summary: cleanTitle(raw),
392
+ usage: {
393
+ promptTokens: 0,
394
+ completionTokens: 0,
395
+ totalTokens: 0
396
+ }
397
+ };
398
+ }
399
+ };
400
+ }
401
+ /**
402
+ * `POST /api/ai/title` for `useSummarize({ connection })`: the same envelope and
403
+ * SSE reply as the summarize handler, but the model is whichever the
404
+ * conversation selected (`forwardedProps.provider` / `model`, thinking off), and
405
+ * the reply is a title, not a summary.
406
+ */
407
+ function createTitleHandler(options) {
408
+ const systemPrompt = (options.prompt ?? defaultTitlePrompt)(options.maxWords ?? 6);
409
+ return { POST: async (request) => {
410
+ if (Number(request.headers.get("content-length") ?? 0) > (options.maxBodyBytes ?? 1048576)) return new Response("Payload too large", { status: 413 });
411
+ let params;
412
+ try {
413
+ params = await summarizeParamsFromRequest(request);
414
+ } catch (error) {
415
+ return new Response(error instanceof Error ? error.message : "Invalid request body", { status: 400 });
416
+ }
417
+ const picked = pickSelection(params.forwardedProps, options.providers, { defaultModel: options.defaultModel });
418
+ if (picked instanceof Response) return picked;
419
+ const { preset, model } = picked;
420
+ const key = preset.byok ? getByokKey$1(request, preset.byok) : null;
421
+ if (preset.keyRequired && key === null && preset.byok) return byokMissing$1(preset.byok);
422
+ const adapter = titleAdapter(preset.create(model.id, key), systemPrompt, resolveThinking(preset, model, "off"));
423
+ const abortController = new AbortController();
424
+ const stream = summarize({
425
+ adapter,
426
+ text: params.input.text,
427
+ threadId: params.threadId,
428
+ runId: params.runId,
429
+ stream: true,
430
+ abortSignal: abortController.signal,
431
+ debug: options.debug
432
+ });
433
+ return toServerSentEventsResponse$1(stream, {
434
+ abortController,
435
+ debug: options.debug
436
+ });
437
+ } };
438
+ }
439
+ //#endregion
440
+ export { EFFORT_3, anthropicThinking, byokMissing, chat, chatParamsFromRequest, cleanTitle, createCatalogHandler, createChatHandler, createSummarizeHandler, createTitleHandler, createTranscriptionHandler, deepseekResponsesThinking, deepseekThinking, defaultTitlePrompt, defineByokProvider, definePreset, geminiThinking, getByokKey, grokThinking, groqThinking, llmgatewayThinking, maxIterations, memoryStream, mergeAgentTools, noThinking, ollamaHostAllowed, ollamaThinking, openaiCompatibleThinking, openaiThinking, openrouterThinking, pickSelection, resolveThinking, toServerSentEventsResponse, toolDefinition, vercelGatewayThinking };
@@ -0,0 +1,26 @@
1
+ //#region src/catalog/thinking.ts
2
+ const THINKING_LEVELS = [
3
+ "off",
4
+ "minimal",
5
+ "low",
6
+ "medium",
7
+ "high",
8
+ "xhigh",
9
+ "max"
10
+ ];
11
+ function supportedThinkingLevels(model) {
12
+ if (!model || !model.reasoning) return ["off"];
13
+ return model.thinkingLevels ?? THINKING_LEVELS;
14
+ }
15
+ /**
16
+ * 向下取最近支持档;目标低于模型下限(Fable 5 这类不可关的模型)时取下限。
17
+ */
18
+ function clampThinkingLevel(model, level) {
19
+ const supported = supportedThinkingLevels(model);
20
+ if (supported.includes(level)) return level;
21
+ const rank = (l) => THINKING_LEVELS.indexOf(l);
22
+ const below = supported.filter((l) => rank(l) < rank(level));
23
+ return below.length > 0 ? below[below.length - 1] : supported[0];
24
+ }
25
+ //#endregion
26
+ export { clampThinkingLevel as n, supportedThinkingLevels as r, THINKING_LEVELS as t };