@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.
- package/README.md +71 -0
- package/dist/anthropic-DdrANeO1.mjs +236 -0
- package/dist/bedrock-534H9yfk.mjs +75 -0
- package/dist/catalog-CvvS2JaW.mjs +32 -0
- package/dist/deepseek-C1b74T2F.mjs +55 -0
- package/dist/gemini-DcJ5Kr3h.mjs +229 -0
- package/dist/grok-CffQuBd6.mjs +77 -0
- package/dist/groq-Ba6TTYLp.mjs +98 -0
- package/dist/index.d.mts +1056 -0
- package/dist/index.mjs +2809 -0
- package/dist/llmgateway-Pt5CS6aQ.mjs +211 -0
- package/dist/mistral-BGMwwbU9.mjs +107 -0
- package/dist/mock/index.d.mts +168 -0
- package/dist/mock/index.mjs +467 -0
- package/dist/ollama-BBJs-3mK.mjs +80 -0
- package/dist/openai--FIeahIH.mjs +153 -0
- package/dist/openrouter-q-lx-2Cb.mjs +167 -0
- package/dist/preset-CEyIPxfS.mjs +157 -0
- package/dist/preset-DRT0T14q.d.mts +19 -0
- package/dist/providers/anthropic.d.mts +6 -0
- package/dist/providers/anthropic.mjs +11 -0
- package/dist/providers/bedrock.d.mts +6 -0
- package/dist/providers/bedrock.mjs +11 -0
- package/dist/providers/byteplus.d.mts +6 -0
- package/dist/providers/byteplus.mjs +21 -0
- package/dist/providers/deepseek.d.mts +35 -0
- package/dist/providers/deepseek.mjs +283 -0
- package/dist/providers/gemini.d.mts +6 -0
- package/dist/providers/gemini.mjs +11 -0
- package/dist/providers/grok.d.mts +6 -0
- package/dist/providers/grok.mjs +11 -0
- package/dist/providers/groq.d.mts +6 -0
- package/dist/providers/groq.mjs +11 -0
- package/dist/providers/llmgateway.d.mts +6 -0
- package/dist/providers/llmgateway.mjs +11 -0
- package/dist/providers/mistral.d.mts +6 -0
- package/dist/providers/mistral.mjs +11 -0
- package/dist/providers/ollama.d.mts +9 -0
- package/dist/providers/ollama.mjs +28 -0
- package/dist/providers/openai-compatible.d.mts +32 -0
- package/dist/providers/openai-compatible.mjs +45 -0
- package/dist/providers/openai.d.mts +6 -0
- package/dist/providers/openai.mjs +11 -0
- package/dist/providers/openrouter.d.mts +6 -0
- package/dist/providers/openrouter.mjs +11 -0
- package/dist/providers/vercel-gateway.d.mts +6 -0
- package/dist/providers/vercel-gateway.mjs +11 -0
- package/dist/providers/vertex.d.mts +6 -0
- package/dist/providers/vertex.mjs +11 -0
- package/dist/server/index.d.mts +173 -0
- package/dist/server/index.mjs +440 -0
- package/dist/thinking-CH2uL1dr.mjs +26 -0
- package/dist/types-56Hn7Qq2.d.mts +46 -0
- package/dist/vercel-gateway-DW3Z0Os1.mjs +109 -0
- package/dist/vertex-hg4Dt54O.mjs +20 -0
- package/package.json +114 -0
- 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 };
|