@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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/engine.d.ts +84 -0
  4. package/dist/engine.d.ts.map +1 -0
  5. package/dist/engine.js +173 -0
  6. package/dist/engine.js.map +1 -0
  7. package/dist/engines/fal.d.ts +95 -0
  8. package/dist/engines/fal.d.ts.map +1 -0
  9. package/dist/engines/fal.js +209 -0
  10. package/dist/engines/fal.js.map +1 -0
  11. package/dist/engines/openrouter.d.ts +141 -0
  12. package/dist/engines/openrouter.d.ts.map +1 -0
  13. package/dist/engines/openrouter.js +220 -0
  14. package/dist/engines/openrouter.js.map +1 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +8 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/media-analysis-prompts.d.ts +18 -0
  20. package/dist/media-analysis-prompts.d.ts.map +1 -0
  21. package/dist/media-analysis-prompts.js +35 -0
  22. package/dist/media-analysis-prompts.js.map +1 -0
  23. package/dist/model-routing.d.ts +49 -0
  24. package/dist/model-routing.d.ts.map +1 -0
  25. package/dist/model-routing.js +72 -0
  26. package/dist/model-routing.js.map +1 -0
  27. package/dist/run.d.ts +75 -0
  28. package/dist/run.d.ts.map +1 -0
  29. package/dist/run.js +90 -0
  30. package/dist/run.js.map +1 -0
  31. package/dist/server/chai-handler.d.ts +43 -0
  32. package/dist/server/chai-handler.d.ts.map +1 -0
  33. package/dist/server/chai-handler.js +106 -0
  34. package/dist/server/chai-handler.js.map +1 -0
  35. package/dist/server/index.d.ts +3 -0
  36. package/dist/server/index.d.ts.map +1 -0
  37. package/dist/server/index.js +4 -0
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/node.d.ts +24 -0
  40. package/dist/server/node.d.ts.map +1 -0
  41. package/dist/server/node.js +60 -0
  42. package/dist/server/node.js.map +1 -0
  43. package/dist/types.d.ts +85 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +5 -0
  46. package/dist/types.js.map +1 -0
  47. package/package.json +63 -0
  48. package/src/engine.ts +237 -0
  49. package/src/engines/fal.ts +271 -0
  50. package/src/engines/openrouter.ts +301 -0
  51. package/src/index.ts +7 -0
  52. package/src/media-analysis-prompts.ts +45 -0
  53. package/src/model-routing.ts +104 -0
  54. package/src/run.ts +145 -0
  55. package/src/server/chai-handler.ts +146 -0
  56. package/src/server/index.ts +3 -0
  57. package/src/server/node.ts +75 -0
  58. package/src/types.ts +96 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kazarazat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @chai-ui/core
2
+
3
+ The headless core behind CHAI's components: the data types, the
4
+ Request → Run → Result model a submitted prompt flows through, and the
5
+ pluggable generation engines that actually call models. No framework, no
6
+ rendering — `@chai-ui/react` is one binding of this; a future non-React
7
+ binding would depend on this package the same way.
8
+
9
+ ## Layout
10
+
11
+ - `types.ts` — shared types: `MediaKind`, `DroppedMedia`, `ModelOption`, `Usage`, `GenerationUseCase`.
12
+ - `run.ts` — `Request`/`Run`/`Result` plus pure transitions over them (`createRun`, `startResult`, `resolveResult`, `failResult`, `deriveGenerationUseCase`).
13
+ - `engine.ts` — the `GenerationEngine` interface every provider implements, `GenerationError`, and `mockEngine`/`createMockEngine` (placeholder output, no network call, no API key).
14
+ - `engines/fal.ts` — `createFalEngine`, media generation through Fal.ai (the default).
15
+ - `engines/openrouter.ts` — `createOpenRouterEngine`, text/reasoning through OpenRouter (prompt enhancement, media analysis).
16
+ - `media-analysis-prompts.ts` — the instruction text `MediaAnalyzer` sends, per media kind × prompt length.
17
+ - `model-routing.ts` — auto-select: `buildModelRoutingPrompt`, `parseModelRoutingReply`, and `routeModel`, which falls back to the first listed model instead of throwing.
18
+ - `server/` — `createChaiHandler` and `toNodeHandler`, exported from `@chai-ui/core/server` (server-only).
19
+
20
+ ## The server route: `@chai-ui/core/server`
21
+
22
+ The built-in engines never hold an API key. They call one route on your
23
+ server, `/api/chai/*`, and `createChaiHandler` serves it: it adds the key
24
+ and forwards the request to Fal.ai or OpenRouter, streaming replies
25
+ through.
26
+
27
+ ```ts
28
+ // Next.js: app/api/chai/[...path]/route.ts
29
+ import { createChaiHandler } from "@chai-ui/core/server";
30
+
31
+ export const { GET, POST, PUT } = createChaiHandler();
32
+ ```
33
+
34
+ Set `FAL_KEY` and `OPENROUTER_API_KEY` in the server's environment (never
35
+ with a `VITE_` or `NEXT_PUBLIC_` prefix). A missing key comes back as a
36
+ clear error, e.g. "FAL_KEY isn't set on the server."
37
+
38
+ **Where it runs.** Chai's components are React-only, so the route lives in
39
+ your React app's framework or on the API server behind it. Mount it at
40
+ `/api/chai`, where the components send their requests. `handle(request)`
41
+ takes a standard web `Request` and returns a `Response`; `GET`, `POST` and
42
+ `PUT` (Fal's cancel call) are the same function. When the browser
43
+ disconnects, the provider call is aborted too.
44
+
45
+ | Your setup | Add this |
46
+ |---|---|
47
+ | Next.js (App Router) | `export const { GET, POST, PUT } = createChaiHandler();` in `app/api/chai/[...path]/route.ts` |
48
+ | Remix / React Router | `export const loader = ({ request }) => chai.handle(request); export const action = loader;` in `app/routes/api.chai.$.ts` |
49
+ | Hono | `app.all("/api/chai/*", (c) => chai.handle(c.req.raw))` |
50
+ | Bun, Deno | route `/api/chai/*` to `chai.handle` in `Bun.serve` / `Deno.serve` |
51
+ | Cloudflare Workers | `createChaiHandler({ falKey: env.FAL_KEY, openRouterKey: env.OPENROUTER_API_KEY }).handle(request)` |
52
+ | Express, plain Node | `app.use("/api/chai", toNodeHandler(createChaiHandler()))` |
53
+
54
+ **Before going live**, limit who can use the route and what it runs.
55
+ Without these, anyone who reaches your site can spend your API credit:
56
+
57
+ ```ts
58
+ createChaiHandler({
59
+ authorize: async (request) => Boolean(await getSession(request)), // false → 403
60
+ allowedModels: ["fal-ai/flux/schnell", "anthropic/claude-opus-5"], // anything else → 403
61
+ });
62
+ ```
63
+
64
+ Never import `@chai-ui/core/server` from browser code.
65
+
66
+ ## Writing an engine
67
+
68
+ One method: a prompt plus any attached media in, one piece of output back.
69
+
70
+ ```ts
71
+ import type { GenerationEngine } from "@chai-ui/core";
72
+
73
+ const myEngine: GenerationEngine = {
74
+ async generate({ modelId, prompt, attachments }) {
75
+ // call your provider…
76
+ return { src: "https://…/output.png", kind: "image" };
77
+ },
78
+ };
79
+ ```
80
+
81
+ Throw a `GenerationError(message, reasons?)` to fail a call with
82
+ user-facing content rather than a bare network error. API keys never
83
+ belong in an engine: call a server route that adds the key, like
84
+ `createChaiHandler` above.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * A pluggable generation engine — the one interface every model provider
3
+ * (`createFalEngine`, `createOpenRouterEngine`, `mockEngine`, or your own)
4
+ * implements, and the one `useComposer` calls. One call = one model run:
5
+ * a prompt plus any attached media in, one piece of output back.
6
+ *
7
+ * `mockEngine` stands in for a real model API during prototyping: it
8
+ * "generates" a small deterministic placeholder image without any network
9
+ * call or API key. Swap in a real engine without touching any component —
10
+ * that's the whole point of keeping this behind an interface.
11
+ */
12
+ import type { DroppedMedia, Evaluation, MediaKind, Usage } from "./types.js";
13
+ export interface GenerationEngine {
14
+ generate(args: {
15
+ /**
16
+ * The model the person picked. An engine uses it when set and falls
17
+ * back to its own configured default when it isn't.
18
+ */
19
+ modelId?: string;
20
+ prompt: string;
21
+ attachments: DroppedMedia[];
22
+ /**
23
+ * Optional streaming hook for text output. An engine that can stream
24
+ * calls this with the full text received so far (not a delta) each time
25
+ * more arrives; one that can't simply never calls it. The returned
26
+ * promise's value is still the authoritative final output.
27
+ */
28
+ onText?: (textSoFar: string) => void;
29
+ /**
30
+ * Aborted when the person stops the request. An engine that can cancel
31
+ * at the provider does so, then rejects with an `AbortError` (see
32
+ * `abortError`). Cancellation is best effort: some providers keep
33
+ * running, and billing, once a request has started.
34
+ */
35
+ signal?: AbortSignal;
36
+ }): Promise<{
37
+ src: string;
38
+ kind: MediaKind;
39
+ usage?: Usage;
40
+ evaluations?: Evaluation[];
41
+ }>;
42
+ }
43
+ /** The error an engine rejects with when its `signal` was aborted. */
44
+ export declare function abortError(): Error;
45
+ /** True for an `AbortError`, the way an engine reports a cancelled call. */
46
+ export declare function isAbortError(err: unknown): boolean;
47
+ /** Resolves after `ms`, or rejects with an `AbortError` as soon as `signal` aborts. */
48
+ export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
49
+ /**
50
+ * Thrown by a `GenerationEngine` to fail a call with structured,
51
+ * user-facing content — `reasons` can render as a bullet list, `message`
52
+ * alone as a single sentence. A real engine should unwrap its provider's
53
+ * specific error into this shape rather than let a bare network/HTTP error
54
+ * reach the UI.
55
+ */
56
+ export declare class GenerationError extends Error {
57
+ reasons?: string[];
58
+ constructor(message: string, reasons?: string[]);
59
+ }
60
+ /**
61
+ * Reads the top-level `evaluations` field a builder's backend proxy
62
+ * appended to a provider response, the convention both built-in engines
63
+ * follow. Malformed entries are dropped rather than rendered as verdicts
64
+ * nobody actually returned.
65
+ */
66
+ export declare function readEvaluations(json: unknown): Evaluation[] | undefined;
67
+ /** Turns anything an engine threw into the `{ message, reasons }` shape a `Result`'s `error` holds. */
68
+ export declare function normalizeGenerationError(err: unknown): {
69
+ message: string;
70
+ reasons?: string[];
71
+ };
72
+ export interface MockEngineOptions {
73
+ /** What the mock "generates". Defaults to "image" (a placeholder gradient); "text" returns a plausible text reply instead. */
74
+ outputKind?: "image" | "text";
75
+ /**
76
+ * Fraction of calls that fail with `GenerationError`, 0-1. Default 0 —
77
+ * pass 1 to force every call to fail (a UI-testing convenience for
78
+ * previewing error states on demand).
79
+ */
80
+ failureRate?: number;
81
+ }
82
+ export declare function createMockEngine(options?: MockEngineOptions): GenerationEngine;
83
+ export declare const mockEngine: GenerationEngine;
84
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE7E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE;QACb;;;WAGG;QACH,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,YAAY,EAAE,CAAC;QAC5B;;;;;WAKG;QACH,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;QACrC;;;;;WAKG;QACH,MAAM,CAAC,EAAE,WAAW,CAAC;KACtB,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAC;QAAC,WAAW,CAAC,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC,CAAC;CAC1F;AAED,sEAAsE;AACtE,wBAAgB,UAAU,IAAI,KAAK,CAElC;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAElD;AAED,uFAAuF;AACvF,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAa9E;AAED;;;;;;GAMG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;gBACP,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE;CAKhD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU,EAAE,GAAG,SAAS,CAQvE;AAED,uGAAuG;AACvG,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAM9F;AAoFD,MAAM,WAAW,iBAAiB;IAChC,8HAA8H;IAC9H,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAgClF;AAED,eAAO,MAAM,UAAU,EAAE,gBAAqC,CAAC"}
package/dist/engine.js ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * A pluggable generation engine — the one interface every model provider
3
+ * (`createFalEngine`, `createOpenRouterEngine`, `mockEngine`, or your own)
4
+ * implements, and the one `useComposer` calls. One call = one model run:
5
+ * a prompt plus any attached media in, one piece of output back.
6
+ *
7
+ * `mockEngine` stands in for a real model API during prototyping: it
8
+ * "generates" a small deterministic placeholder image without any network
9
+ * call or API key. Swap in a real engine without touching any component —
10
+ * that's the whole point of keeping this behind an interface.
11
+ */
12
+ /** The error an engine rejects with when its `signal` was aborted. */
13
+ export function abortError() {
14
+ return new DOMException("The request was cancelled.", "AbortError");
15
+ }
16
+ /** True for an `AbortError`, the way an engine reports a cancelled call. */
17
+ export function isAbortError(err) {
18
+ return typeof err === "object" && err !== null && err.name === "AbortError";
19
+ }
20
+ /** Resolves after `ms`, or rejects with an `AbortError` as soon as `signal` aborts. */
21
+ export function abortableSleep(ms, signal) {
22
+ return new Promise((resolve, reject) => {
23
+ if (signal?.aborted)
24
+ return reject(abortError());
25
+ const timer = setTimeout(() => {
26
+ signal?.removeEventListener("abort", onAbort);
27
+ resolve();
28
+ }, ms);
29
+ const onAbort = () => {
30
+ clearTimeout(timer);
31
+ reject(abortError());
32
+ };
33
+ signal?.addEventListener("abort", onAbort, { once: true });
34
+ });
35
+ }
36
+ /**
37
+ * Thrown by a `GenerationEngine` to fail a call with structured,
38
+ * user-facing content — `reasons` can render as a bullet list, `message`
39
+ * alone as a single sentence. A real engine should unwrap its provider's
40
+ * specific error into this shape rather than let a bare network/HTTP error
41
+ * reach the UI.
42
+ */
43
+ export class GenerationError extends Error {
44
+ constructor(message, reasons) {
45
+ super(message);
46
+ this.name = "GenerationError";
47
+ this.reasons = reasons;
48
+ }
49
+ }
50
+ /**
51
+ * Reads the top-level `evaluations` field a builder's backend proxy
52
+ * appended to a provider response, the convention both built-in engines
53
+ * follow. Malformed entries are dropped rather than rendered as verdicts
54
+ * nobody actually returned.
55
+ */
56
+ export function readEvaluations(json) {
57
+ const raw = json?.evaluations;
58
+ if (!Array.isArray(raw))
59
+ return undefined;
60
+ const valid = raw.filter((e) => !!e && typeof e.id === "string" && typeof e.label === "string" && typeof e.passed === "boolean");
61
+ return valid.length ? valid.map(({ id, label, passed }) => ({ id, label, passed })) : undefined;
62
+ }
63
+ /** Turns anything an engine threw into the `{ message, reasons }` shape a `Result`'s `error` holds. */
64
+ export function normalizeGenerationError(err) {
65
+ if (err instanceof GenerationError) {
66
+ return { message: err.message, reasons: err.reasons };
67
+ }
68
+ if (err instanceof Error)
69
+ return { message: err.message };
70
+ return { message: "Something went wrong. Please try again." };
71
+ }
72
+ function hash(input) {
73
+ let h = 0;
74
+ for (let i = 0; i < input.length; i++) {
75
+ h = (h << 5) - h + input.charCodeAt(i);
76
+ h |= 0;
77
+ }
78
+ return Math.abs(h);
79
+ }
80
+ function placeholderSvg(seed, caption) {
81
+ const h = hash(seed);
82
+ const hue = h % 360;
83
+ const hue2 = (hue + 40 + (h % 60)) % 360;
84
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="480" height="360">
85
+ <defs>
86
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
87
+ <stop offset="0%" stop-color="hsl(${hue} 55% 38%)" />
88
+ <stop offset="100%" stop-color="hsl(${hue2} 60% 24%)" />
89
+ </linearGradient>
90
+ </defs>
91
+ <rect width="480" height="360" fill="url(#g)" />
92
+ <text x="24" y="332" font-family="ui-monospace, monospace" font-size="13" fill="rgba(255,255,255,0.85)">${escapeXml(caption)}</text>
93
+ </svg>`;
94
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
95
+ }
96
+ function escapeXml(s) {
97
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c] ?? c);
98
+ }
99
+ /**
100
+ * A plausible text reply for a text-output mock (e.g. standing in for a
101
+ * reasoning/vision model) — still not a real model call, just a more
102
+ * honest stand-in than a gradient image would be.
103
+ */
104
+ function mockText(prompt, modelId) {
105
+ const stylePhrases = [
106
+ "warm, diffused daylight",
107
+ "a shallow depth of field",
108
+ "muted, desaturated tones",
109
+ "a centered, symmetrical composition",
110
+ "soft shadows and high contrast",
111
+ ];
112
+ const subjectPhrases = [
113
+ "a single object resting on a plain surface",
114
+ "a figure positioned slightly off-center",
115
+ "an outdoor scene with layered depth",
116
+ "a close-up study of texture and material",
117
+ ];
118
+ const style = stylePhrases[hash(modelId + "style") % stylePhrases.length];
119
+ const subject = subjectPhrases[hash(modelId + "subject") % subjectPhrases.length];
120
+ const detail = prompt.includes("detailed")
121
+ ? " Rendered here with extra material and lighting detail, per the requested length."
122
+ : "";
123
+ return `${subject}, shot with ${style}.${detail} (mock — ${modelId})`;
124
+ }
125
+ /**
126
+ * A plausible, deterministic-per-call token/cost fabrication, so a demo can
127
+ * show usage without a real tokenizer or pricing API. `costUsd` uses an
128
+ * illustrative, not verified, rate — a real engine should only ever report
129
+ * a cost it actually derived from real pricing.
130
+ */
131
+ function mockUsage(seed) {
132
+ const promptTokens = 180 + (hash(seed + "prompt") % 420);
133
+ const completionTokens = 40 + (hash(seed + "completion") % 260);
134
+ const illustrativeRatePerMillion = { input: 0.15, output: 0.6 };
135
+ const costUsd = (promptTokens * illustrativeRatePerMillion.input +
136
+ completionTokens * illustrativeRatePerMillion.output) /
137
+ 1_000_000;
138
+ return { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens, costUsd };
139
+ }
140
+ let mockCallCount = 0;
141
+ export function createMockEngine(options = {}) {
142
+ const outputKind = options.outputKind ?? "image";
143
+ const failureRate = options.failureRate ?? 0;
144
+ return {
145
+ async generate({ modelId = "mock-model", prompt, onText, signal }) {
146
+ mockCallCount += 1;
147
+ const seed = `${modelId}:${prompt}:${mockCallCount}`;
148
+ await abortableSleep(500 + (hash(seed) % 1200), signal);
149
+ if (failureRate > 0 && (hash(seed) % 100) / 100 < failureRate) {
150
+ throw new GenerationError("Generation failed", [
151
+ "The model could not complete this request.",
152
+ "Try again, or select a different model.",
153
+ ]);
154
+ }
155
+ if (outputKind === "text") {
156
+ const text = mockText(prompt, modelId);
157
+ if (onText) {
158
+ // Word by word, so a streaming UI has something real to render.
159
+ const words = text.split(" ");
160
+ for (let i = 1; i <= words.length; i++) {
161
+ onText(words.slice(0, i).join(" "));
162
+ await abortableSleep(60, signal);
163
+ }
164
+ }
165
+ return { src: text, kind: "text", usage: mockUsage(seed) };
166
+ }
167
+ const caption = `${modelId} · ${prompt.slice(0, 48)}${prompt.length > 48 ? "…" : ""}`;
168
+ return { src: placeholderSvg(`${modelId}:${prompt}`, caption), kind: "image", usage: mockUsage(seed) };
169
+ },
170
+ };
171
+ }
172
+ export const mockEngine = createMockEngine();
173
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * A real `GenerationEngine` backed by Fal.ai (fal.ai) — CHAI's default
3
+ * model marketplace as of 2026-09-24, chosen for the
4
+ * text/image-to-video and audio-generation coverage OpenRouter lacks.
5
+ * OpenRouter remains a fully-supported alternative, not deprecated by
6
+ * this — see the Develop page's "Model providers" section.
7
+ *
8
+ * Handles media generation only: produces real image/video media via Fal's
9
+ * queue API (submit → poll → fetch result). Fal's own vision/LLM access
10
+ * (`any-llm`, `any-llm/vision`) is itself a proxy in front of OpenRouter,
11
+ * not an independent catalog — for text/reasoning calls
12
+ * (e.g. prompt enhancement, media analysis) use `createOpenRouterEngine`
13
+ * instead of routing through Fal to OpenRouter.
14
+ *
15
+ * Security: this engine never sees an API key and never calls
16
+ * `queue.fal.run` directly. It calls a same-origin `endpoint` (default
17
+ * `/api/chai/fal`, served by `createChaiHandler`) that a server-side proxy owns and mirrors Fal's own queue
18
+ * URL shape (`/{model}`, `/{model}/requests/{id}/status`,
19
+ * `/{model}/requests/{id}`) — the proxy is the only thing that reads
20
+ * `FAL_KEY` and attaches the `Authorization: Key ...` header. Passing a
21
+ * key into this file would put it in the browser bundle; don't add one.
22
+ */
23
+ import { type GenerationEngine } from "../engine.js";
24
+ import type { DroppedMedia, MediaKind } from "../types.js";
25
+ /** Output kinds this engine can produce — text goes through a reasoning engine instead (see file doc comment). */
26
+ type FalOutputKind = Exclude<MediaKind, "text">;
27
+ export interface FalEngineOptions {
28
+ /** Same-origin proxy path that attaches the real key server-side and mirrors Fal's queue URL shape. */
29
+ endpoint?: string;
30
+ /** Fal model id to call when the caller passes no `modelId` and no `modelByKind` entry matches `outputKind`. */
31
+ model?: string;
32
+ /** Per-output-kind Fal model id override — e.g. `{ image: "fal-ai/flux/schnell", video: "minimax/h3-max/text-to-video" }`. */
33
+ modelByKind?: Partial<Record<FalOutputKind, string>>;
34
+ /** Which kind of media this engine instance produces. Defaults to "image". */
35
+ outputKind?: FalOutputKind;
36
+ /** Poll interval while a queued job is in progress. Defaults to 1500ms. */
37
+ pollIntervalMs?: number;
38
+ /** Give up waiting after this long. Defaults to 120000ms (2 min) — video/audio jobs run longer than image ones. */
39
+ timeoutMs?: number;
40
+ /** Injectable for tests; defaults to the global `fetch`. */
41
+ fetchImpl?: typeof fetch;
42
+ }
43
+ interface FalResultResponse {
44
+ images?: {
45
+ url: string;
46
+ }[];
47
+ video?: {
48
+ url: string;
49
+ };
50
+ audio?: {
51
+ url: string;
52
+ };
53
+ audio_url?: string;
54
+ error?: unknown;
55
+ detail?: unknown;
56
+ }
57
+ /** Fal's error shape varies by failure type — validation errors carry `detail`, job failures carry `error`. Prefer whichever is present and stringify a non-string one. */
58
+ export declare function describeFalError(json: {
59
+ error?: unknown;
60
+ detail?: unknown;
61
+ }, fallback: string): string;
62
+ /** Builds the one Fal request body CHAI sends — a prompt, plus an `image_url` when an attached image is available (for image-to-video / edit-shaped models). */
63
+ export declare function buildFalRequestBody(args: {
64
+ prompt: string;
65
+ media?: DroppedMedia;
66
+ }): Record<string, unknown>;
67
+ /** Pulls the generated media URL out of Fal's result JSON for the given output kind — the response shape differs by modality. */
68
+ export declare function extractFalResultSrc(json: FalResultResponse, kind: FalOutputKind): string | undefined;
69
+ /**
70
+ * A media-generation `GenerationEngine` — submits the prompt (plus the
71
+ * first attached image, if any) to Fal's queue API and polls until the job
72
+ * completes, returning the real generated `src` URL. Calls the caller's
73
+ * `modelId` when given, otherwise `modelByKind`/`model`/the built-in default.
74
+ */
75
+ export declare function createFalEngine(options?: FalEngineOptions): GenerationEngine;
76
+ /**
77
+ * A minimal, cheap connectivity check against Fal's fastest image model —
78
+ * the "use real inference sparingly" path, same role as `pingOpenRouter`.
79
+ * Deliberately not a `GenerationEngine` and not exercised by any automated
80
+ * test: meant to be triggered once, by a person, from the Develop page's
81
+ * "Test connection" button.
82
+ */
83
+ export declare function pingFal(options?: {
84
+ endpoint?: string;
85
+ model?: string;
86
+ fetchImpl?: typeof fetch;
87
+ }): Promise<{
88
+ ok: true;
89
+ src: string;
90
+ } | {
91
+ ok: false;
92
+ error: string;
93
+ }>;
94
+ export {};
95
+ //# sourceMappingURL=fal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fal.d.ts","sourceRoot":"","sources":["../../src/engines/fal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAA6D,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChH,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE3D,kHAAkH;AAClH,KAAK,aAAa,GAAG,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAEhD,MAAM,WAAW,gBAAgB;IAC/B,uGAAuG;IACvG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gHAAgH;IAChH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8HAA8H;IAC9H,WAAW,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,8EAA8E;IAC9E,UAAU,CAAC,EAAE,aAAa,CAAC;IAC3B,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mHAAmH;IACnH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AA6BD,UAAU,iBAAiB;IACzB,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACxB,KAAK,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAGD,2KAA2K;AAC3K,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAStG;AAED,gKAAgK;AAChK,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI1B;AAED,iIAAiI;AACjI,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAIpG;AA0DD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,gBAAgB,CAyDhF;AAED;;;;;;GAMG;AACH,wBAAsB,OAAO,CAC3B,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CAAO,GAC5E,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CA4BnE"}