@chai-ui/core 0.1.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/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/engine.d.ts +84 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +173 -0
- package/dist/engine.js.map +1 -0
- package/dist/engines/fal.d.ts +95 -0
- package/dist/engines/fal.d.ts.map +1 -0
- package/dist/engines/fal.js +209 -0
- package/dist/engines/fal.js.map +1 -0
- package/dist/engines/openrouter.d.ts +141 -0
- package/dist/engines/openrouter.d.ts.map +1 -0
- package/dist/engines/openrouter.js +220 -0
- package/dist/engines/openrouter.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/media-analysis-prompts.d.ts +18 -0
- package/dist/media-analysis-prompts.d.ts.map +1 -0
- package/dist/media-analysis-prompts.js +35 -0
- package/dist/media-analysis-prompts.js.map +1 -0
- package/dist/model-routing.d.ts +49 -0
- package/dist/model-routing.d.ts.map +1 -0
- package/dist/model-routing.js +72 -0
- package/dist/model-routing.js.map +1 -0
- package/dist/run.d.ts +75 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +90 -0
- package/dist/run.js.map +1 -0
- package/dist/server/chai-handler.d.ts +43 -0
- package/dist/server/chai-handler.d.ts.map +1 -0
- package/dist/server/chai-handler.js +106 -0
- package/dist/server/chai-handler.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/node.d.ts +24 -0
- package/dist/server/node.d.ts.map +1 -0
- package/dist/server/node.js +60 -0
- package/dist/server/node.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/src/engine.ts +237 -0
- package/src/engines/fal.ts +271 -0
- package/src/engines/openrouter.ts +301 -0
- package/src/index.ts +7 -0
- package/src/media-analysis-prompts.ts +45 -0
- package/src/model-routing.ts +104 -0
- package/src/run.ts +145 -0
- package/src/server/chai-handler.ts +146 -0
- package/src/server/index.ts +3 -0
- package/src/server/node.ts +75 -0
- package/src/types.ts +96 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instruction text `MediaAnalyzer` sends to an analysis model — one
|
|
3
|
+
* complete, final string per (media kind × prompt length), not a template
|
|
4
|
+
* with placeholders, so tuning the wording is just editing English.
|
|
5
|
+
*
|
|
6
|
+
* Phrase optimization (rewriting for the target model's known prompt
|
|
7
|
+
* style) is folded directly into every prompt's wording below rather than
|
|
8
|
+
* modeled as a separate option.
|
|
9
|
+
*/
|
|
10
|
+
const OPTIMIZE_CLAUSE = "Rewrite the description for the selected model's known prompt style before finishing.";
|
|
11
|
+
const RESPOND_ONLY = "Respond only with the description, no preamble.";
|
|
12
|
+
export const MEDIA_ANALYSIS_PROMPTS = {
|
|
13
|
+
image: {
|
|
14
|
+
terse: `Analyze the attached image and produce a terse, single-sentence description suitable as a generation seed prompt — subject and composition only. ${OPTIMIZE_CLAUSE} ${RESPOND_ONLY}`,
|
|
15
|
+
concise: `Analyze the attached image and produce a concise description suitable as a generation seed prompt — subject, composition, and lighting in a few sentences. ${OPTIMIZE_CLAUSE} ${RESPOND_ONLY}`,
|
|
16
|
+
detailed: `Analyze the attached image and produce a detailed description suitable as a generation seed prompt — subject, composition, lighting, color palette, and style, thorough enough to reproduce it closely. ${OPTIMIZE_CLAUSE} ${RESPOND_ONLY}`,
|
|
17
|
+
},
|
|
18
|
+
video: {
|
|
19
|
+
terse: `Analyze the attached video's key frames and motion and produce a terse, single-sentence description suitable as a generation seed prompt — subject and camera movement only. ${OPTIMIZE_CLAUSE} ${RESPOND_ONLY}`,
|
|
20
|
+
concise: `Analyze the attached video's key frames and motion — subject, action, camera movement, pacing — and produce a concise description suitable as a generation seed prompt. ${OPTIMIZE_CLAUSE} ${RESPOND_ONLY}`,
|
|
21
|
+
detailed: `Analyze the attached video's key frames and motion — subject, action, camera movement, pacing, lighting, and style — and produce a detailed description suitable as a generation seed prompt, thorough enough to reproduce the shot closely. ${OPTIMIZE_CLAUSE} ${RESPOND_ONLY}`,
|
|
22
|
+
},
|
|
23
|
+
audio: {
|
|
24
|
+
terse: `Analyze the attached audio and produce a terse, single-sentence description suitable as a generation seed prompt — content and voice/instrumentation only. ${OPTIMIZE_CLAUSE} ${RESPOND_ONLY}`,
|
|
25
|
+
concise: `Analyze the attached audio — content, voice/instrumentation, tone, pacing — and produce a concise description suitable as a generation seed prompt. ${OPTIMIZE_CLAUSE} ${RESPOND_ONLY}`,
|
|
26
|
+
detailed: `Analyze the attached audio — content, voice/instrumentation, tone, pacing, and production style — and produce a detailed description suitable as a generation seed prompt, thorough enough to reproduce it closely. ${OPTIMIZE_CLAUSE} ${RESPOND_ONLY}`,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
export const DEFAULT_MEDIA_ANALYSIS_PROMPT_LENGTH = "concise";
|
|
30
|
+
export const MEDIA_ANALYSIS_PROMPT_LENGTHS = [
|
|
31
|
+
{ value: "terse", label: "Terse" },
|
|
32
|
+
{ value: "concise", label: "Concise" },
|
|
33
|
+
{ value: "detailed", label: "Detailed" },
|
|
34
|
+
];
|
|
35
|
+
//# sourceMappingURL=media-analysis-prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-analysis-prompts.js","sourceRoot":"","sources":["../src/media-analysis-prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,eAAe,GACnB,uFAAuF,CAAC;AAC1F,MAAM,YAAY,GAAG,iDAAiD,CAAC;AAEvE,MAAM,CAAC,MAAM,sBAAsB,GAG/B;IACF,KAAK,EAAE;QACL,KAAK,EAAE,oJAAoJ,eAAe,IAAI,YAAY,EAAE;QAC5L,OAAO,EAAE,8JAA8J,eAAe,IAAI,YAAY,EAAE;QACxM,QAAQ,EAAE,2MAA2M,eAAe,IAAI,YAAY,EAAE;KACvP;IACD,KAAK,EAAE;QACL,KAAK,EAAE,gLAAgL,eAAe,IAAI,YAAY,EAAE;QACxN,OAAO,EAAE,2KAA2K,eAAe,IAAI,YAAY,EAAE;QACrN,QAAQ,EAAE,gPAAgP,eAAe,IAAI,YAAY,EAAE;KAC5R;IACD,KAAK,EAAE;QACL,KAAK,EAAE,8JAA8J,eAAe,IAAI,YAAY,EAAE;QACtM,OAAO,EAAE,uJAAuJ,eAAe,IAAI,YAAY,EAAE;QACjM,QAAQ,EAAE,uNAAuN,eAAe,IAAI,YAAY,EAAE;KACnQ;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,oCAAoC,GAA8B,SAAS,CAAC;AAEzF,MAAM,CAAC,MAAM,6BAA6B,GAA0D;IAClG,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;IAClC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;IACtC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;CACzC,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-select model routing: a reasoning model reads the request and picks
|
|
3
|
+
* one model from the builder's own list. The instruction is plain English
|
|
4
|
+
* in `buildModelRoutingPrompt` below, so tuning it is just editing text;
|
|
5
|
+
* pass your own builder to the hooks' `routingPrompt` option to replace it.
|
|
6
|
+
*/
|
|
7
|
+
import { type GenerationEngine } from "./engine.js";
|
|
8
|
+
import type { DroppedMedia, MediaKind, ModelOption } from "./types.js";
|
|
9
|
+
export interface ModelRoutingInput {
|
|
10
|
+
/** What the person typed. Empty for media-only requests (e.g. Media Analyzer). */
|
|
11
|
+
prompt: string;
|
|
12
|
+
/** What the request produces, e.g. "image" for Create image, "text" for analysis. */
|
|
13
|
+
outputKind: MediaKind;
|
|
14
|
+
/** Kinds of media attached, if any. */
|
|
15
|
+
attachmentKinds: MediaKind[];
|
|
16
|
+
/** The candidates: only models the builder offers for this request. */
|
|
17
|
+
models: ModelOption[];
|
|
18
|
+
}
|
|
19
|
+
export declare function buildModelRoutingPrompt({ prompt, outputKind, attachmentKinds, models }: ModelRoutingInput): string;
|
|
20
|
+
/** Finds the model id in a reasoning model's reply, or `undefined` if it named none of `models`. */
|
|
21
|
+
export declare function parseModelRoutingReply(reply: string, models: ModelOption[]): string | undefined;
|
|
22
|
+
export type ModelRoutingResult = {
|
|
23
|
+
status: "routed";
|
|
24
|
+
modelId: string;
|
|
25
|
+
} | {
|
|
26
|
+
status: "fallback";
|
|
27
|
+
modelId: string;
|
|
28
|
+
reason: "no-reasoning" | "request-failed" | "invalid-reply";
|
|
29
|
+
/** The provider's own message when the reasoning call failed. */
|
|
30
|
+
error?: string;
|
|
31
|
+
};
|
|
32
|
+
export interface RouteModelArgs extends ModelRoutingInput {
|
|
33
|
+
/** The reasoning engine and model. Omit both and routing falls back without a call. */
|
|
34
|
+
engine?: GenerationEngine;
|
|
35
|
+
model?: string;
|
|
36
|
+
attachments?: DroppedMedia[];
|
|
37
|
+
/** Replaces `buildModelRoutingPrompt`. */
|
|
38
|
+
routingPrompt?: (input: ModelRoutingInput) => string;
|
|
39
|
+
/** Stops the reasoning call; the result is then a `"request-failed"` fallback. */
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Asks the reasoning model to pick from `models`. Never throws: when it
|
|
44
|
+
* can't route (no reasoning model, a failed call, an answer not in the
|
|
45
|
+
* list) it falls back to the first listed model and says why, so a
|
|
46
|
+
* routing problem never blocks the request itself.
|
|
47
|
+
*/
|
|
48
|
+
export declare function routeModel(args: RouteModelArgs): Promise<ModelRoutingResult>;
|
|
49
|
+
//# sourceMappingURL=model-routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-routing.d.ts","sourceRoot":"","sources":["../src/model-routing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAA4B,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEvE,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,UAAU,EAAE,SAAS,CAAC;IACtB,uCAAuC;IACvC,eAAe,EAAE,SAAS,EAAE,CAAC;IAC7B,uEAAuE;IACvE,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,wBAAgB,uBAAuB,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,EAAE,iBAAiB,GAAG,MAAM,CAgBlH;AAED,oGAAoG;AACpG,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,GAAG,SAAS,CAO/F;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrC;IACE,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,cAAc,GAAG,gBAAgB,GAAG,eAAe,CAAC;IAC5D,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACvD,uFAAuF;IACvF,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC;IAC7B,0CAA0C;IAC1C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,MAAM,CAAC;IACrD,kFAAkF;IAClF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA2BlF"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-select model routing: a reasoning model reads the request and picks
|
|
3
|
+
* one model from the builder's own list. The instruction is plain English
|
|
4
|
+
* in `buildModelRoutingPrompt` below, so tuning it is just editing text;
|
|
5
|
+
* pass your own builder to the hooks' `routingPrompt` option to replace it.
|
|
6
|
+
*/
|
|
7
|
+
import { normalizeGenerationError } from "./engine.js";
|
|
8
|
+
export function buildModelRoutingPrompt({ prompt, outputKind, attachmentKinds, models }) {
|
|
9
|
+
const roster = models.map((m) => `- ${m.id}: ${m.label} (${m.provider}, ${m.speed})`).join("\n");
|
|
10
|
+
const attached = attachmentKinds.length ? attachmentKinds.join(", ") : "none";
|
|
11
|
+
const request = prompt.trim() ? prompt.trim() : "(no text; the attached media is the request)";
|
|
12
|
+
return [
|
|
13
|
+
`Pick the one model best suited to this request. The output is ${outputKind}.`,
|
|
14
|
+
`Weigh what the request needs (subject, detail, style, length, attached media) against each model's strengths. Prefer a faster model when the request is simple.`,
|
|
15
|
+
``,
|
|
16
|
+
`Request: ${request}`,
|
|
17
|
+
`Attached media: ${attached}`,
|
|
18
|
+
``,
|
|
19
|
+
`Models:`,
|
|
20
|
+
roster,
|
|
21
|
+
``,
|
|
22
|
+
`Reply with only the chosen model id, exactly as written above, and nothing else.`,
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
/** Finds the model id in a reasoning model's reply, or `undefined` if it named none of `models`. */
|
|
26
|
+
export function parseModelRoutingReply(reply, models) {
|
|
27
|
+
const cleaned = reply.trim().replace(/^[`"'*\s]+|[`"'*.\s]+$/g, "");
|
|
28
|
+
const exact = models.find((m) => m.id === cleaned);
|
|
29
|
+
if (exact)
|
|
30
|
+
return exact.id;
|
|
31
|
+
// A chatty reply ("I'd pick openai/gpt-5.2 because…"): take the longest
|
|
32
|
+
// id it contains, so "x/model-pro" wins over its prefix "x/model".
|
|
33
|
+
return [...models].sort((a, b) => b.id.length - a.id.length).find((m) => reply.includes(m.id))?.id;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Asks the reasoning model to pick from `models`. Never throws: when it
|
|
37
|
+
* can't route (no reasoning model, a failed call, an answer not in the
|
|
38
|
+
* list) it falls back to the first listed model and says why, so a
|
|
39
|
+
* routing problem never blocks the request itself.
|
|
40
|
+
*/
|
|
41
|
+
export async function routeModel(args) {
|
|
42
|
+
const fallback = args.models[0]?.id;
|
|
43
|
+
if (!fallback)
|
|
44
|
+
throw new Error("routeModel: `models` is empty; there's nothing to pick from.");
|
|
45
|
+
if (args.models.length === 1)
|
|
46
|
+
return { status: "routed", modelId: fallback };
|
|
47
|
+
if (!args.engine || !args.model)
|
|
48
|
+
return { status: "fallback", modelId: fallback, reason: "no-reasoning" };
|
|
49
|
+
const input = {
|
|
50
|
+
prompt: args.prompt,
|
|
51
|
+
outputKind: args.outputKind,
|
|
52
|
+
attachmentKinds: args.attachmentKinds,
|
|
53
|
+
models: args.models,
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
const { src } = await args.engine.generate({
|
|
57
|
+
modelId: args.model,
|
|
58
|
+
prompt: (args.routingPrompt ?? buildModelRoutingPrompt)(input),
|
|
59
|
+
// The media itself helps (e.g. a photo needing an edit-capable model).
|
|
60
|
+
attachments: args.attachments ?? [],
|
|
61
|
+
signal: args.signal,
|
|
62
|
+
});
|
|
63
|
+
const picked = parseModelRoutingReply(src, args.models);
|
|
64
|
+
return picked
|
|
65
|
+
? { status: "routed", modelId: picked }
|
|
66
|
+
: { status: "fallback", modelId: fallback, reason: "invalid-reply" };
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
return { status: "fallback", modelId: fallback, reason: "request-failed", error: normalizeGenerationError(err).message };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=model-routing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-routing.js","sourceRoot":"","sources":["../src/model-routing.ts"],"names":[],"mappings":"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"}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The the design data model — Request/Run/Result — plus the pure state
|
|
3
|
+
* transitions over it: pure, framework-free logic here; `useComposer`
|
|
4
|
+
* (`@chai-ui/react`) is the thin React binding on top.
|
|
5
|
+
*/
|
|
6
|
+
import type { DroppedMedia, Evaluation, GenerationUseCase, MediaKind, Usage } from "./types.js";
|
|
7
|
+
export interface Request {
|
|
8
|
+
/** The specific generation use case, when one could be derived — see `deriveGenerationUseCase`. Undefined when the use case (or its kind) has no `GenerationUseCase` match yet, e.g. audio. */
|
|
9
|
+
mode?: GenerationUseCase;
|
|
10
|
+
prompt: string;
|
|
11
|
+
attachments: DroppedMedia[];
|
|
12
|
+
modelIds: string[];
|
|
13
|
+
params: Record<string, unknown>;
|
|
14
|
+
presetId?: string;
|
|
15
|
+
}
|
|
16
|
+
/** `"cancelled"`: the person stopped it. A streamed text result keeps the text that arrived. */
|
|
17
|
+
export type ResultStatus = "queued" | "running" | "done" | "error" | "cancelled";
|
|
18
|
+
export interface Result {
|
|
19
|
+
id: string;
|
|
20
|
+
runId: string;
|
|
21
|
+
modelId: string;
|
|
22
|
+
status: ResultStatus;
|
|
23
|
+
output?: {
|
|
24
|
+
src: string;
|
|
25
|
+
kind: MediaKind;
|
|
26
|
+
};
|
|
27
|
+
usage?: Usage;
|
|
28
|
+
/** Verdicts from the builder's own backend checks on this output — see `Evaluation`. */
|
|
29
|
+
evaluations?: Evaluation[];
|
|
30
|
+
error?: {
|
|
31
|
+
message: string;
|
|
32
|
+
reasons?: string[];
|
|
33
|
+
};
|
|
34
|
+
/** Set by `startResult` — internal bookkeeping for `durationMs`, not meant to be read directly by a consumer (it's a raw `Date.now()`, not itself a duration). */
|
|
35
|
+
startedAt?: number;
|
|
36
|
+
/** How long this result took from `startResult` to `resolveResult`/`failResult`, in ms — `undefined` if it resolved without ever going through `startResult` (e.g. resolved directly from `"queued"`). ResultCard's info-flip is what actually surfaces this (reference: the Figma mock's "3.03s" chip). */
|
|
37
|
+
durationMs?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface Run {
|
|
40
|
+
id: string;
|
|
41
|
+
request: Request;
|
|
42
|
+
createdAt: number;
|
|
43
|
+
results: Result[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Fans a `Request` out into one `"queued"` `Result` per model — a run's
|
|
47
|
+
* results all exist from the start, so a caller can render "3 of 4 done"
|
|
48
|
+
* immediately rather than watching the list grow.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createRun(request: Request): Run;
|
|
51
|
+
export declare function startResult(run: Run, resultId: string): Run;
|
|
52
|
+
/**
|
|
53
|
+
* Folds partial streamed text into a running result: `output` fills in
|
|
54
|
+
* while `status` stays `"running"`, so "running with output" is what a
|
|
55
|
+
* streaming result looks like. A no-op once the result has settled, so a
|
|
56
|
+
* late chunk can't overwrite a final `resolveResult`/`failResult`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function streamResult(run: Run, resultId: string, textSoFar: string): Run;
|
|
59
|
+
export declare function resolveResult(run: Run, resultId: string, output: Result["output"], usage?: Usage, evaluations?: Evaluation[]): Run;
|
|
60
|
+
export declare function failResult(run: Run, resultId: string, error: Result["error"]): Run;
|
|
61
|
+
/** Marks a queued or running result as stopped by the person. A settled result is left as it is. */
|
|
62
|
+
export declare function cancelResult(run: Run, resultId: string): Run;
|
|
63
|
+
/**
|
|
64
|
+
* Derives a `GenerationUseCase` from Composer's own `{ kind, label }` use
|
|
65
|
+
* case plus whether anything is attached — this is real derivation logic,
|
|
66
|
+
* not just plumbing: the same `kind: "video"` means `"text-to-video"` with
|
|
67
|
+
* nothing attached but `"image-to-video"` once an image is. Returns
|
|
68
|
+
* `undefined` for "audio" (no `GenerationUseCase` member exists yet;
|
|
69
|
+
* audio generation is planned). `"text"` is a text request: a reply to
|
|
70
|
+
* the prompt, or a description of the attached media ("vision").
|
|
71
|
+
*/
|
|
72
|
+
export declare function deriveGenerationUseCase(useCase: {
|
|
73
|
+
kind: MediaKind;
|
|
74
|
+
} | null | undefined, attachments: DroppedMedia[]): GenerationUseCase | undefined;
|
|
75
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEhG,MAAM,WAAW,OAAO;IACtB,+LAA+L;IAC/L,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,gGAAgG;AAChG,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;AAEjF,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,CAAC;IAC1C,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,wFAAwF;IACxF,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,kKAAkK;IAClK,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4SAA4S;IAC5S,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAQD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,GAAG,CAa/C;AAMD,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,GAAG,CAI/E;AAED,wBAAgB,aAAa,CAC3B,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,EACxB,KAAK,CAAC,EAAE,KAAK,EACb,WAAW,CAAC,EAAE,UAAU,EAAE,GACzB,GAAG,CASL;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,GAAG,CAOlF;AAED,oGAAoG;AACpG,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,CAM5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EAC/C,WAAW,EAAE,YAAY,EAAE,GAC1B,iBAAiB,GAAG,SAAS,CAO/B"}
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The the design data model — Request/Run/Result — plus the pure state
|
|
3
|
+
* transitions over it: pure, framework-free logic here; `useComposer`
|
|
4
|
+
* (`@chai-ui/react`) is the thin React binding on top.
|
|
5
|
+
*/
|
|
6
|
+
let idCounter = 0;
|
|
7
|
+
function nextId(prefix) {
|
|
8
|
+
idCounter += 1;
|
|
9
|
+
return `${prefix}_${idCounter}_${Math.random().toString(36).slice(2, 7)}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Fans a `Request` out into one `"queued"` `Result` per model — a run's
|
|
13
|
+
* results all exist from the start, so a caller can render "3 of 4 done"
|
|
14
|
+
* immediately rather than watching the list grow.
|
|
15
|
+
*/
|
|
16
|
+
export function createRun(request) {
|
|
17
|
+
const id = nextId("run");
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
request,
|
|
21
|
+
createdAt: Date.now(),
|
|
22
|
+
results: request.modelIds.map((modelId) => ({
|
|
23
|
+
id: nextId("result"),
|
|
24
|
+
runId: id,
|
|
25
|
+
modelId,
|
|
26
|
+
status: "queued",
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function updateResult(run, resultId, fn) {
|
|
31
|
+
return { ...run, results: run.results.map((r) => (r.id === resultId ? fn(r) : r)) };
|
|
32
|
+
}
|
|
33
|
+
export function startResult(run, resultId) {
|
|
34
|
+
return updateResult(run, resultId, (r) => ({ ...r, status: "running", startedAt: Date.now() }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Folds partial streamed text into a running result: `output` fills in
|
|
38
|
+
* while `status` stays `"running"`, so "running with output" is what a
|
|
39
|
+
* streaming result looks like. A no-op once the result has settled, so a
|
|
40
|
+
* late chunk can't overwrite a final `resolveResult`/`failResult`.
|
|
41
|
+
*/
|
|
42
|
+
export function streamResult(run, resultId, textSoFar) {
|
|
43
|
+
return updateResult(run, resultId, (r) => r.status === "running" ? { ...r, output: { src: textSoFar, kind: "text" } } : r);
|
|
44
|
+
}
|
|
45
|
+
export function resolveResult(run, resultId, output, usage, evaluations) {
|
|
46
|
+
return updateResult(run, resultId, (r) => ({
|
|
47
|
+
...r,
|
|
48
|
+
status: "done",
|
|
49
|
+
output,
|
|
50
|
+
usage,
|
|
51
|
+
evaluations,
|
|
52
|
+
durationMs: r.startedAt != null ? Date.now() - r.startedAt : undefined,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
export function failResult(run, resultId, error) {
|
|
56
|
+
return updateResult(run, resultId, (r) => ({
|
|
57
|
+
...r,
|
|
58
|
+
status: "error",
|
|
59
|
+
error,
|
|
60
|
+
durationMs: r.startedAt != null ? Date.now() - r.startedAt : undefined,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
/** Marks a queued or running result as stopped by the person. A settled result is left as it is. */
|
|
64
|
+
export function cancelResult(run, resultId) {
|
|
65
|
+
return updateResult(run, resultId, (r) => r.status === "queued" || r.status === "running"
|
|
66
|
+
? { ...r, status: "cancelled", durationMs: r.startedAt != null ? Date.now() - r.startedAt : undefined }
|
|
67
|
+
: r);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Derives a `GenerationUseCase` from Composer's own `{ kind, label }` use
|
|
71
|
+
* case plus whether anything is attached — this is real derivation logic,
|
|
72
|
+
* not just plumbing: the same `kind: "video"` means `"text-to-video"` with
|
|
73
|
+
* nothing attached but `"image-to-video"` once an image is. Returns
|
|
74
|
+
* `undefined` for "audio" (no `GenerationUseCase` member exists yet;
|
|
75
|
+
* audio generation is planned). `"text"` is a text request: a reply to
|
|
76
|
+
* the prompt, or a description of the attached media ("vision").
|
|
77
|
+
*/
|
|
78
|
+
export function deriveGenerationUseCase(useCase, attachments) {
|
|
79
|
+
if (!useCase)
|
|
80
|
+
return undefined;
|
|
81
|
+
const hasMedia = attachments.length > 0;
|
|
82
|
+
if (useCase.kind === "video")
|
|
83
|
+
return hasMedia ? "image-to-video" : "text-to-video";
|
|
84
|
+
if (useCase.kind === "image")
|
|
85
|
+
return hasMedia ? "image-edit" : "text-to-image";
|
|
86
|
+
if (useCase.kind === "text")
|
|
87
|
+
return hasMedia ? "vision" : "text-to-text";
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=run.js.map
|
package/dist/run.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createChaiHandler`: the one server route the built-in engines call.
|
|
3
|
+
* The browser never sees an API key. `createFalEngine()` and
|
|
4
|
+
* `createOpenRouterEngine()` post to this route (default `/api/chai/...`),
|
|
5
|
+
* and it adds the key and forwards the request to the provider.
|
|
6
|
+
*
|
|
7
|
+
* Built on the web-standard `Request` → `Response`, so the same handler
|
|
8
|
+
* runs in Next.js, React Router (Remix), Hono, Cloudflare Workers, Bun and
|
|
9
|
+
* Deno. `toNodeHandler` (./node.ts) adapts it for Express and plain Node.
|
|
10
|
+
*/
|
|
11
|
+
export type ChaiProviderId = "fal" | "openrouter";
|
|
12
|
+
export interface ChaiHandlerOptions {
|
|
13
|
+
/** Defaults to the `FAL_KEY` environment variable. */
|
|
14
|
+
falKey?: string;
|
|
15
|
+
/** Defaults to the `OPENROUTER_API_KEY` environment variable. */
|
|
16
|
+
openRouterKey?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Who may use this route. Return false to reject with 403. Without it,
|
|
19
|
+
* anyone who can reach your site can spend your API credit: check your
|
|
20
|
+
* own login or session here before going live.
|
|
21
|
+
*/
|
|
22
|
+
authorize?: (request: Request) => boolean | Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* The only model ids this route will run, across both providers (e.g.
|
|
25
|
+
* `"fal-ai/flux/schnell"`, `"anthropic/claude-opus-5"`). Anything else is
|
|
26
|
+
* rejected with 403, so nobody can run an expensive model through you.
|
|
27
|
+
* Omit to allow every model.
|
|
28
|
+
*/
|
|
29
|
+
allowedModels?: string[];
|
|
30
|
+
/** Injectable for tests; defaults to the global `fetch`. */
|
|
31
|
+
fetch?: typeof fetch;
|
|
32
|
+
}
|
|
33
|
+
export interface ChaiHandler {
|
|
34
|
+
/** Handles any request under the route. */
|
|
35
|
+
handle: (request: Request) => Promise<Response>;
|
|
36
|
+
/** The same function, named for frameworks that export one per method (Next.js). */
|
|
37
|
+
GET: (request: Request) => Promise<Response>;
|
|
38
|
+
POST: (request: Request) => Promise<Response>;
|
|
39
|
+
/** Fal's cancel call. */
|
|
40
|
+
PUT: (request: Request) => Promise<Response>;
|
|
41
|
+
}
|
|
42
|
+
export declare function createChaiHandler(options?: ChaiHandlerOptions): ChaiHandler;
|
|
43
|
+
//# sourceMappingURL=chai-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chai-handler.d.ts","sourceRoot":"","sources":["../../src/server/chai-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,YAAY,CAAC;AAElD,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7D;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,2CAA2C;IAC3C,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChD,oFAAoF;IACpF,GAAG,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,yBAAyB;IACzB,GAAG,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC9C;AAED,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAmD/E"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createChaiHandler`: the one server route the built-in engines call.
|
|
3
|
+
* The browser never sees an API key. `createFalEngine()` and
|
|
4
|
+
* `createOpenRouterEngine()` post to this route (default `/api/chai/...`),
|
|
5
|
+
* and it adds the key and forwards the request to the provider.
|
|
6
|
+
*
|
|
7
|
+
* Built on the web-standard `Request` → `Response`, so the same handler
|
|
8
|
+
* runs in Next.js, React Router (Remix), Hono, Cloudflare Workers, Bun and
|
|
9
|
+
* Deno. `toNodeHandler` (./node.ts) adapts it for Express and plain Node.
|
|
10
|
+
*/
|
|
11
|
+
const FAL_QUEUE_BASE = "https://queue.fal.run";
|
|
12
|
+
const OPENROUTER_CHAT_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
13
|
+
export function createChaiHandler(options = {}) {
|
|
14
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
15
|
+
async function handle(request) {
|
|
16
|
+
const route = parseRoute(new URL(request.url).pathname);
|
|
17
|
+
if (!route) {
|
|
18
|
+
return Response.json({ error: { message: "chai-handler: expected a path ending in /fal/<model…> or /openrouter/chat." } }, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
const fail = (status, message) => errorResponse(route.provider, status, message);
|
|
21
|
+
if (options.authorize && !(await options.authorize(request)))
|
|
22
|
+
return fail(403, "Not authorized.");
|
|
23
|
+
if (route.provider === "openrouter") {
|
|
24
|
+
if (route.rest !== "chat")
|
|
25
|
+
return fail(404, `Unknown OpenRouter path "${route.rest}".`);
|
|
26
|
+
if (request.method !== "POST")
|
|
27
|
+
return fail(405, "Use POST.");
|
|
28
|
+
const key = options.openRouterKey ?? readEnv("OPENROUTER_API_KEY");
|
|
29
|
+
if (!key)
|
|
30
|
+
return fail(500, "OPENROUTER_API_KEY isn't set on the server.");
|
|
31
|
+
const body = await request.text();
|
|
32
|
+
const model = readModel(body);
|
|
33
|
+
if (options.allowedModels && !(model && options.allowedModels.includes(model))) {
|
|
34
|
+
return fail(403, `Model "${model ?? "(none)"}" isn't allowed on this server.`);
|
|
35
|
+
}
|
|
36
|
+
return forward(fetchImpl, route.provider, OPENROUTER_CHAT_URL, {
|
|
37
|
+
signal: request.signal,
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
40
|
+
body,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Fal's queue API: POST /<model> submits, GET /<model>/requests/<id>[/status]
|
|
44
|
+
// polls, PUT /<model>/requests/<id>/cancel cancels.
|
|
45
|
+
const isCancel = request.method === "PUT" && route.rest.endsWith("/cancel");
|
|
46
|
+
if (request.method !== "GET" && request.method !== "POST" && !isCancel)
|
|
47
|
+
return fail(405, "Use GET or POST.");
|
|
48
|
+
const key = options.falKey ?? readEnv("FAL_KEY");
|
|
49
|
+
if (!key)
|
|
50
|
+
return fail(500, "FAL_KEY isn't set on the server.");
|
|
51
|
+
if (options.allowedModels && !options.allowedModels.some((m) => route.rest === m || route.rest.startsWith(`${m}/`))) {
|
|
52
|
+
return fail(403, `Model "${falModel(route.rest)}" isn't allowed on this server.`);
|
|
53
|
+
}
|
|
54
|
+
return forward(fetchImpl, route.provider, `${FAL_QUEUE_BASE}/${route.rest}${new URL(request.url).search}`, {
|
|
55
|
+
signal: request.signal,
|
|
56
|
+
method: request.method,
|
|
57
|
+
headers: { Authorization: `Key ${key}`, "Content-Type": "application/json" },
|
|
58
|
+
body: request.method === "POST" ? await request.text() : undefined,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return { handle, GET: handle, POST: handle, PUT: handle };
|
|
62
|
+
}
|
|
63
|
+
/** Finds the provider segment anywhere in the path, so the route can be mounted at any prefix. */
|
|
64
|
+
function parseRoute(pathname) {
|
|
65
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
66
|
+
const i = parts.findIndex((p) => p === "fal" || p === "openrouter");
|
|
67
|
+
if (i === -1 || i === parts.length - 1)
|
|
68
|
+
return null;
|
|
69
|
+
return { provider: parts[i], rest: parts.slice(i + 1).join("/") };
|
|
70
|
+
}
|
|
71
|
+
/** Passes the provider's response straight through, streaming included. */
|
|
72
|
+
async function forward(fetchImpl, provider, url, init) {
|
|
73
|
+
try {
|
|
74
|
+
const upstream = await fetchImpl(url, init);
|
|
75
|
+
return new Response(upstream.body, {
|
|
76
|
+
status: upstream.status,
|
|
77
|
+
headers: { "Content-Type": upstream.headers.get("content-type") ?? "application/json" },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return errorResponse(provider, 502, `Couldn't reach ${provider === "fal" ? "Fal.ai" : "OpenRouter"}: ${err instanceof Error ? err.message : String(err)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Errors in the shape each engine already reads: Fal's `{ error }`, OpenRouter's `{ error: { message } }`. */
|
|
85
|
+
function errorResponse(provider, status, message) {
|
|
86
|
+
const body = provider === "fal" ? { error: message } : { error: { message } };
|
|
87
|
+
return Response.json(body, { status });
|
|
88
|
+
}
|
|
89
|
+
function readModel(body) {
|
|
90
|
+
try {
|
|
91
|
+
const model = JSON.parse(body).model;
|
|
92
|
+
return typeof model === "string" ? model : undefined;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** The model part of a Fal queue path, for error messages: everything before `/requests/`. */
|
|
99
|
+
function falModel(rest) {
|
|
100
|
+
return rest.split("/requests/")[0];
|
|
101
|
+
}
|
|
102
|
+
function readEnv(name) {
|
|
103
|
+
const env = globalThis.process?.env;
|
|
104
|
+
return env?.[name] || undefined;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=chai-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chai-handler.js","sourceRoot":"","sources":["../../src/server/chai-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAC/C,MAAM,mBAAmB,GAAG,+CAA+C,CAAC;AAoC5E,MAAM,UAAU,iBAAiB,CAAC,UAA8B,EAAE;IAChE,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC;IAEzC,KAAK,UAAU,MAAM,CAAC,OAAgB;QACpC,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;QACxD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,QAAQ,CAAC,IAAI,CAClB,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,4EAA4E,EAAE,EAAE,EACpG,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,OAAe,EAAE,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAEjG,IAAI,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;QAElG,IAAI,KAAK,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;YACpC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;gBAAE,OAAO,IAAI,CAAC,GAAG,EAAE,4BAA4B,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;YACxF,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM;gBAAE,OAAO,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;YAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAAC;YACnE,IAAI,CAAC,GAAG;gBAAE,OAAO,IAAI,CAAC,GAAG,EAAE,6CAA6C,CAAC,CAAC;YAC1E,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,OAAO,CAAC,aAAa,IAAI,CAAC,CAAC,KAAK,IAAI,OAAO,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBAC/E,OAAO,IAAI,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,QAAQ,iCAAiC,CAAC,CAAC;YACjF,CAAC;YACD,OAAO,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,QAAQ,EAAE,mBAAmB,EAAE;gBAC7D,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/E,IAAI;aACL,CAAC,CAAC;QACL,CAAC;QAED,8EAA8E;QAC9E,oDAAoD;QACpD,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAC5E,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QAC7G,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,kCAAkC,CAAC,CAAC;QAC/D,IAAI,OAAO,CAAC,aAAa,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACpH,OAAO,IAAI,CAAC,GAAG,EAAE,UAAU,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;QACpF,CAAC;QACD,OAAO,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,QAAQ,EAAE,GAAG,cAAc,IAAI,KAAK,CAAC,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;YACzG,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,EAAE,aAAa,EAAE,OAAO,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC5E,IAAI,EAAE,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS;SACnE,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;AAC5D,CAAC;AAED,kGAAkG;AAClG,SAAS,UAAU,CAAC,QAAgB;IAClC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,YAAY,CAAC,CAAC;IACpE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACpD,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAmB,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AACtF,CAAC;AAED,2EAA2E;AAC3E,KAAK,UAAU,OAAO,CAAC,SAAuB,EAAE,QAAwB,EAAE,GAAW,EAAE,IAAiB;IACtG,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC5C,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE;YACjC,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,OAAO,EAAE,EAAE,cAAc,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,kBAAkB,EAAE;SACxF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,aAAa,CAAC,QAAQ,EAAE,GAAG,EAAE,kBAAkB,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC7J,CAAC;AACH,CAAC;AAED,+GAA+G;AAC/G,SAAS,aAAa,CAAC,QAAwB,EAAE,MAAc,EAAE,OAAe;IAC9E,MAAM,IAAI,GAAG,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC;IAC9E,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,CAAC;QACH,MAAM,KAAK,GAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAyB,CAAC,KAAK,CAAC;QAC9D,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,8FAA8F;AAC9F,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAE,CAAC;AACtC,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,MAAM,GAAG,GAAI,UAAyE,CAAC,OAAO,EAAE,GAAG,CAAC;IACpG,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AACA,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a `ChaiHandler` on Node's own request/response objects, for Express
|
|
3
|
+
* and plain `http.createServer` (and dev servers built on them, like
|
|
4
|
+
* Vite's). Typed structurally, so this package needs no Node types.
|
|
5
|
+
*/
|
|
6
|
+
import type { ChaiHandler } from "./chai-handler.js";
|
|
7
|
+
interface NodeRequest extends AsyncIterable<Uint8Array | string> {
|
|
8
|
+
method?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
/** Express keeps the full path here when the route is mounted at a prefix. */
|
|
11
|
+
originalUrl?: string;
|
|
12
|
+
headers: Record<string, string | string[] | undefined>;
|
|
13
|
+
}
|
|
14
|
+
interface NodeResponse {
|
|
15
|
+
statusCode: number;
|
|
16
|
+
writableEnded?: boolean;
|
|
17
|
+
on?(event: "close", listener: () => void): unknown;
|
|
18
|
+
setHeader(name: string, value: string): unknown;
|
|
19
|
+
write(chunk: Uint8Array): unknown;
|
|
20
|
+
end(chunk?: string | Uint8Array): unknown;
|
|
21
|
+
}
|
|
22
|
+
export declare function toNodeHandler(handler: ChaiHandler): (req: NodeRequest, res: NodeResponse) => Promise<void>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/server/node.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,UAAU,WAAY,SAAQ,aAAa,CAAC,UAAU,GAAG,MAAM,CAAC;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CACxD;AAED,UAAU,YAAY;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;IACnD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAChD,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC;IAClC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC;CAC3C;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,IAClC,KAAK,WAAW,EAAE,KAAK,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAsClE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a `ChaiHandler` on Node's own request/response objects, for Express
|
|
3
|
+
* and plain `http.createServer` (and dev servers built on them, like
|
|
4
|
+
* Vite's). Typed structurally, so this package needs no Node types.
|
|
5
|
+
*/
|
|
6
|
+
export function toNodeHandler(handler) {
|
|
7
|
+
return async (req, res) => {
|
|
8
|
+
const host = String(req.headers.host ?? "localhost");
|
|
9
|
+
const url = new URL(req.originalUrl ?? req.url ?? "/", `http://${host}`);
|
|
10
|
+
const method = req.method ?? "GET";
|
|
11
|
+
const headers = new Headers();
|
|
12
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
13
|
+
if (value !== undefined)
|
|
14
|
+
headers.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
15
|
+
}
|
|
16
|
+
let body;
|
|
17
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
18
|
+
const chunks = [];
|
|
19
|
+
for await (const chunk of req)
|
|
20
|
+
chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
21
|
+
body = concat(chunks);
|
|
22
|
+
}
|
|
23
|
+
// When the browser disconnects (e.g. the person pressed stop), abort the
|
|
24
|
+
// provider call too, so a stream stops at the provider.
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
res.on?.("close", () => {
|
|
27
|
+
if (!res.writableEnded)
|
|
28
|
+
controller.abort();
|
|
29
|
+
});
|
|
30
|
+
const response = await handler.handle(new Request(url, { method, headers, body, signal: controller.signal }));
|
|
31
|
+
res.statusCode = response.status;
|
|
32
|
+
response.headers.forEach((value, name) => res.setHeader(name, value));
|
|
33
|
+
if (!response.body)
|
|
34
|
+
return void res.end();
|
|
35
|
+
// Streamed through as it arrives, so streaming text renders live.
|
|
36
|
+
const reader = response.body.getReader();
|
|
37
|
+
try {
|
|
38
|
+
for (;;) {
|
|
39
|
+
const { done, value } = await reader.read();
|
|
40
|
+
if (done)
|
|
41
|
+
break;
|
|
42
|
+
res.write(value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// The provider stream was aborted because the browser left; nothing to send.
|
|
47
|
+
}
|
|
48
|
+
res.end();
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function concat(chunks) {
|
|
52
|
+
const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
|
|
53
|
+
let offset = 0;
|
|
54
|
+
for (const c of chunks) {
|
|
55
|
+
out.set(c, offset);
|
|
56
|
+
offset += c.length;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../../src/server/node.ts"],"names":[],"mappings":"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"}
|