@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
@@ -0,0 +1,209 @@
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 { abortError, abortableSleep, isAbortError, readEvaluations } from "../engine.js";
24
+ /**
25
+ * Fal model ids verified live against fal.ai's own docs/catalog
26
+ * 2026-09-24, not invented. No default for "audio": CHAI has no audio
27
+ * `GenerationUseCase` yet, so guessing one
28
+ * here would be ahead of any real use case that needs it.
29
+ */
30
+ const DEFAULT_MODEL_BY_KIND = {
31
+ image: "fal-ai/flux/schnell",
32
+ video: "minimax/h3-max/text-to-video",
33
+ };
34
+ const DEFAULT_POLL_INTERVAL_MS = 1500;
35
+ const DEFAULT_TIMEOUT_MS = 120_000;
36
+ /** 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. */
37
+ export function describeFalError(json, fallback) {
38
+ const raw = json.error ?? json.detail;
39
+ if (!raw)
40
+ return fallback;
41
+ if (typeof raw === "string")
42
+ return raw;
43
+ try {
44
+ return JSON.stringify(raw);
45
+ }
46
+ catch {
47
+ return fallback;
48
+ }
49
+ }
50
+ /** 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). */
51
+ export function buildFalRequestBody(args) {
52
+ const body = { prompt: args.prompt };
53
+ if (args.media?.kind === "image")
54
+ body.image_url = args.media.src;
55
+ return body;
56
+ }
57
+ /** Pulls the generated media URL out of Fal's result JSON for the given output kind — the response shape differs by modality. */
58
+ export function extractFalResultSrc(json, kind) {
59
+ if (kind === "image")
60
+ return json.images?.[0]?.url;
61
+ if (kind === "video")
62
+ return json.video?.url;
63
+ return json.audio?.url ?? json.audio_url;
64
+ }
65
+ async function submitFalJob(args) {
66
+ const res = await args.fetchImpl(`${args.endpoint}/${args.model}`, {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify(args.body),
70
+ });
71
+ const json = (await res.json().catch(() => ({})));
72
+ if (!res.ok || json.error)
73
+ throw new Error(describeFalError(json, `${res.status} ${res.statusText}`));
74
+ if (!json.request_id)
75
+ throw new Error("submit response had no request_id");
76
+ return { requestId: json.request_id, status: json.status ?? "IN_QUEUE" };
77
+ }
78
+ /** Polls a submitted job's status until `COMPLETED`, then fetches and returns its result. */
79
+ async function awaitFalJob(args) {
80
+ let status = args.status;
81
+ const deadline = Date.now() + args.timeoutMs;
82
+ while (status !== "COMPLETED") {
83
+ if (Date.now() > deadline) {
84
+ throw new Error(`timed out after ${args.timeoutMs}ms waiting for the job to complete`);
85
+ }
86
+ await abortableSleep(args.pollIntervalMs, args.signal);
87
+ const res = await args.fetchImpl(`${args.endpoint}/${args.model}/requests/${args.requestId}/status`);
88
+ const json = (await res.json().catch(() => ({})));
89
+ if (!res.ok || json.error)
90
+ throw new Error(describeFalError(json, `${res.status} ${res.statusText}`));
91
+ status = json.status ?? status;
92
+ }
93
+ const res = await args.fetchImpl(`${args.endpoint}/${args.model}/requests/${args.requestId}`);
94
+ const json = (await res.json().catch(() => ({})));
95
+ if (!res.ok || json.error)
96
+ throw new Error(describeFalError(json, `${res.status} ${res.statusText}`));
97
+ return json;
98
+ }
99
+ /**
100
+ * Asks Fal to drop a job (`PUT …/requests/<id>/cancel`). A queued job is
101
+ * removed and never runs; a running one gets a cancel signal and may still
102
+ * finish, depending on the model. Best effort: a failed cancel call is
103
+ * ignored, since the person has already stopped waiting.
104
+ */
105
+ async function cancelFalJob(args) {
106
+ await args.fetchImpl(`${args.endpoint}/${args.model}/requests/${args.requestId}/cancel`, { method: "PUT" }).catch(() => { });
107
+ }
108
+ /**
109
+ * A media-generation `GenerationEngine` — submits the prompt (plus the
110
+ * first attached image, if any) to Fal's queue API and polls until the job
111
+ * completes, returning the real generated `src` URL. Calls the caller's
112
+ * `modelId` when given, otherwise `modelByKind`/`model`/the built-in default.
113
+ */
114
+ export function createFalEngine(options = {}) {
115
+ const endpoint = options.endpoint ?? "/api/chai/fal";
116
+ // Called through a wrapper, never as `args.fetchImpl(...)`: browsers throw
117
+ // "Illegal invocation" when fetch runs with an object as `this`.
118
+ const fetchImpl = (input, init) => (options.fetchImpl ?? fetch)(input, init);
119
+ const outputKind = options.outputKind ?? "image";
120
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
121
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
122
+ return {
123
+ async generate({ modelId, prompt, attachments, signal }) {
124
+ const model = modelId ?? options.modelByKind?.[outputKind] ?? options.model ?? DEFAULT_MODEL_BY_KIND[outputKind];
125
+ if (!model) {
126
+ throw new Error(`falEngine: no model configured for outputKind "${outputKind}" — pass \`model\` or \`modelByKind.${outputKind}\`.`);
127
+ }
128
+ const body = buildFalRequestBody({ prompt, media: attachments[0] });
129
+ if (signal?.aborted)
130
+ throw abortError();
131
+ try {
132
+ // The submit itself isn't aborted mid-flight: without its request id
133
+ // there'd be nothing to cancel at Fal.
134
+ const { requestId, status } = await submitFalJob({ endpoint, model, body, fetchImpl });
135
+ if (signal?.aborted) {
136
+ await cancelFalJob({ endpoint, model, requestId, fetchImpl });
137
+ throw abortError();
138
+ }
139
+ const result = await awaitFalJob({
140
+ endpoint,
141
+ model,
142
+ requestId,
143
+ status,
144
+ fetchImpl,
145
+ pollIntervalMs,
146
+ timeoutMs,
147
+ signal,
148
+ }).catch(async (err) => {
149
+ if (isAbortError(err))
150
+ await cancelFalJob({ endpoint, model, requestId, fetchImpl });
151
+ throw err;
152
+ });
153
+ const src = extractFalResultSrc(result, outputKind);
154
+ if (!src)
155
+ throw new Error(`no ${outputKind} output in Fal's response`);
156
+ // No `usage`: Fal bills per generation, not per token — `Usage`
157
+ // is token-shaped (promptTokens/completionTokens), and fabricating
158
+ // token counts to fit it would be a worse guess than reporting
159
+ // nothing, per the same rule `openrouter.ts`'s
160
+ // `pricePerMillionTokens` comment documents for cost.
161
+ return { src, kind: outputKind, evaluations: readEvaluations(result) };
162
+ }
163
+ catch (err) {
164
+ if (isAbortError(err))
165
+ throw err;
166
+ throw new Error(`falEngine: ${err instanceof Error ? err.message : String(err)}`);
167
+ }
168
+ },
169
+ };
170
+ }
171
+ /**
172
+ * A minimal, cheap connectivity check against Fal's fastest image model —
173
+ * the "use real inference sparingly" path, same role as `pingOpenRouter`.
174
+ * Deliberately not a `GenerationEngine` and not exercised by any automated
175
+ * test: meant to be triggered once, by a person, from the Develop page's
176
+ * "Test connection" button.
177
+ */
178
+ export async function pingFal(options = {}) {
179
+ const endpoint = options.endpoint ?? "/api/chai/fal";
180
+ // Called through a wrapper, never as `args.fetchImpl(...)`: browsers throw
181
+ // "Illegal invocation" when fetch runs with an object as `this`.
182
+ const fetchImpl = (input, init) => (options.fetchImpl ?? fetch)(input, init);
183
+ const model = options.model ?? DEFAULT_MODEL_BY_KIND.image;
184
+ try {
185
+ const { requestId, status } = await submitFalJob({
186
+ endpoint,
187
+ model,
188
+ body: buildFalRequestBody({ prompt: "a single red circle on a white background" }),
189
+ fetchImpl,
190
+ });
191
+ const result = await awaitFalJob({
192
+ endpoint,
193
+ model,
194
+ requestId,
195
+ status,
196
+ fetchImpl,
197
+ pollIntervalMs: 1000,
198
+ timeoutMs: 30_000,
199
+ });
200
+ const src = extractFalResultSrc(result, "image");
201
+ if (!src)
202
+ return { ok: false, error: "no image output in Fal's response" };
203
+ return { ok: true, src };
204
+ }
205
+ catch (err) {
206
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
207
+ }
208
+ }
209
+ //# sourceMappingURL=fal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fal.js","sourceRoot":"","sources":["../../src/engines/fal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * A real text/reasoning `GenerationEngine` backed by OpenRouter
3
+ * (openrouter.ai) — a fully-supported alternative to the default Fal.ai
4
+ * (see the Develop page's "Model providers" section). Returns text: prompt
5
+ * enhancement, media analysis (vision/video/audio understanding), and any
6
+ * other text-out call.
7
+ *
8
+ * Security: this engine never sees an API key and never calls
9
+ * `openrouter.ai` directly. It POSTs to a same-origin `endpoint` (default
10
+ * `/api/chai/openrouter/chat`, served by `createChaiHandler`) that a server-side proxy owns — the proxy is the
11
+ * only thing that reads `OPENROUTER_API_KEY` and attaches the
12
+ * `Authorization` header. Passing a key into this file would put it in the
13
+ * browser bundle; don't add one.
14
+ */
15
+ import { type GenerationEngine } from "../engine.js";
16
+ import type { DroppedMedia, Evaluation, MediaKind } from "../types.js";
17
+ export interface OpenRouterEngineOptions {
18
+ /** Same-origin proxy path that attaches the real key server-side. */
19
+ endpoint?: string;
20
+ /** OpenRouter model id (e.g. "google/gemini-2.5-flash"), used when the caller passes no `modelId` and no `modelByKind` entry matches. */
21
+ model?: string;
22
+ /** Per-attached-media-kind model override, e.g. a vision model for images and a different one for audio. */
23
+ modelByKind?: Partial<Record<MediaKind, string>>;
24
+ /**
25
+ * Cap on the model's response length. Required in practice, not just
26
+ * cost hygiene: some providers/models reject a request with no cap at
27
+ * all (defaulting to their max, e.g. 65535) once it exceeds the
28
+ * account's available credits, rather than just truncating. Defaults to
29
+ * a one-paragraph-ish budget — enough for a "detailed" seed-prompt
30
+ * description.
31
+ */
32
+ maxTokens?: number;
33
+ /**
34
+ * USD per million tokens for whichever model this engine calls, if you
35
+ * want a real cost figure on the returned `usage` instead of just token
36
+ * counts. Deliberately not defaulted to anything: OpenRouter's real
37
+ * per-model pricing isn't tracked anywhere in this repo, and a guessed
38
+ * number would be worse than an honestly absent cost. Pull the real rate
39
+ * from OpenRouter's `/models` endpoint or your own account dashboard and
40
+ * pass it here if you want `usage.costUsd` populated.
41
+ */
42
+ pricePerMillionTokens?: {
43
+ input: number;
44
+ output: number;
45
+ };
46
+ /** Injectable for tests; defaults to the global `fetch`. */
47
+ fetchImpl?: typeof fetch;
48
+ }
49
+ type ChatContentPart = {
50
+ type: "text";
51
+ text: string;
52
+ } | {
53
+ type: "image_url";
54
+ image_url: {
55
+ url: string;
56
+ };
57
+ } | {
58
+ type: "video_url";
59
+ video_url: {
60
+ url: string;
61
+ };
62
+ } | {
63
+ type: "input_audio";
64
+ input_audio: {
65
+ data: string;
66
+ format: string;
67
+ };
68
+ };
69
+ interface ChatCompletionResponse {
70
+ choices?: {
71
+ message?: {
72
+ content?: string;
73
+ };
74
+ }[];
75
+ usage?: {
76
+ prompt_tokens?: number;
77
+ completion_tokens?: number;
78
+ total_tokens?: number;
79
+ };
80
+ error?: {
81
+ message?: string;
82
+ metadata?: {
83
+ raw?: string;
84
+ };
85
+ };
86
+ }
87
+ /** Builds the one non-text content part for an attached image/video/audio file. */
88
+ export declare function contentPartForMedia(media: DroppedMedia): ChatContentPart;
89
+ /** Builds the OpenRouter chat-completions request body for a prompt plus any attached media. */
90
+ export declare function buildChatRequest(args: {
91
+ model: string;
92
+ prompt: string;
93
+ media?: DroppedMedia[];
94
+ maxTokens?: number;
95
+ }): {
96
+ model: string;
97
+ messages: {
98
+ role: "user";
99
+ content: ChatContentPart[];
100
+ }[];
101
+ max_tokens: number;
102
+ };
103
+ /**
104
+ * Reads an OpenRouter server-sent-events stream (`stream: true`), calling
105
+ * `onText` with the accumulated reply as each chunk lands. Handles the
106
+ * `: OPENROUTER PROCESSING` keep-alive comments and the `[DONE]` sentinel,
107
+ * and still works if a proxy buffered the whole stream into one body.
108
+ */
109
+ export declare function readChatStream(res: Response, onText: (textSoFar: string) => void): Promise<{
110
+ text: string;
111
+ usage?: ChatCompletionResponse["usage"];
112
+ evaluations?: Evaluation[];
113
+ }>;
114
+ /**
115
+ * Sends the prompt plus every attached file to OpenRouter via the local
116
+ * proxy and returns the model's reply as `"text"`. Calls the caller's
117
+ * `modelId` when given, otherwise `modelByKind` (keyed on the first
118
+ * attachment's kind), `model`, or the built-in default. Streams (OpenRouter's
119
+ * `stream: true`) when the caller passes `onText`; the proxy must pass the
120
+ * response body through rather than buffer it for that to be incremental.
121
+ */
122
+ export declare function createOpenRouterEngine(options?: OpenRouterEngineOptions): GenerationEngine;
123
+ /**
124
+ * A minimal, cheap connectivity check — the "use real inference sparingly"
125
+ * path. Deliberately not a `GenerationEngine` and not exercised by any
126
+ * automated test: it's meant to be triggered once, by a person, from the
127
+ * Develop page's "Test connection" button.
128
+ */
129
+ export declare function pingOpenRouter(options?: {
130
+ endpoint?: string;
131
+ model?: string;
132
+ fetchImpl?: typeof fetch;
133
+ }): Promise<{
134
+ ok: true;
135
+ reply: string;
136
+ } | {
137
+ ok: false;
138
+ error: string;
139
+ }>;
140
+ export {};
141
+ //# sourceMappingURL=openrouter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openrouter.d.ts","sourceRoot":"","sources":["../../src/engines/openrouter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,220 @@
1
+ /**
2
+ * A real text/reasoning `GenerationEngine` backed by OpenRouter
3
+ * (openrouter.ai) — a fully-supported alternative to the default Fal.ai
4
+ * (see the Develop page's "Model providers" section). Returns text: prompt
5
+ * enhancement, media analysis (vision/video/audio understanding), and any
6
+ * other text-out call.
7
+ *
8
+ * Security: this engine never sees an API key and never calls
9
+ * `openrouter.ai` directly. It POSTs to a same-origin `endpoint` (default
10
+ * `/api/chai/openrouter/chat`, served by `createChaiHandler`) that a server-side proxy owns — the proxy is the
11
+ * only thing that reads `OPENROUTER_API_KEY` and attaches the
12
+ * `Authorization` header. Passing a key into this file would put it in the
13
+ * browser bundle; don't add one.
14
+ */
15
+ import { readEvaluations } from "../engine.js";
16
+ const DEFAULT_MODEL = "google/gemini-2.5-flash";
17
+ const DEFAULT_MAX_TOKENS = 800;
18
+ /** Converts OpenRouter's raw `usage` into `Usage`, computing `costUsd` only when a real rate was supplied. */
19
+ function toUsage(usage, pricePerMillionTokens) {
20
+ if (!usage)
21
+ return undefined;
22
+ const promptTokens = usage.prompt_tokens ?? 0;
23
+ const completionTokens = usage.completion_tokens ?? 0;
24
+ const costUsd = pricePerMillionTokens
25
+ ? (promptTokens * pricePerMillionTokens.input +
26
+ completionTokens * pricePerMillionTokens.output) /
27
+ 1_000_000
28
+ : undefined;
29
+ return {
30
+ promptTokens,
31
+ completionTokens,
32
+ totalTokens: usage.total_tokens ?? promptTokens + completionTokens,
33
+ costUsd,
34
+ };
35
+ }
36
+ /** Prefers the upstream provider's own error message over OpenRouter's generic wrapper, when both are present. */
37
+ function describeError(json, fallback) {
38
+ const base = json.error?.message ?? fallback;
39
+ const raw = json.error?.metadata?.raw;
40
+ if (!raw)
41
+ return base;
42
+ try {
43
+ const parsed = JSON.parse(raw);
44
+ if (parsed.error?.message && parsed.error.message !== base) {
45
+ return `${base} — ${parsed.error.message}`;
46
+ }
47
+ }
48
+ catch {
49
+ // Not JSON; the generic message is all we have.
50
+ }
51
+ return base;
52
+ }
53
+ /** Builds the one non-text content part for an attached image/video/audio file. */
54
+ export function contentPartForMedia(media) {
55
+ if (media.kind === "video") {
56
+ return { type: "video_url", video_url: { url: media.src } };
57
+ }
58
+ if (media.kind === "audio") {
59
+ // OpenRouter's input_audio wants raw base64 + a format string, not a
60
+ // data: URL — audio can't be passed by URL at all per OpenRouter docs.
61
+ const match = /^data:audio\/(\w+);base64,(.+)$/.exec(media.src);
62
+ if (!match) {
63
+ throw new Error("openRouterEngine: audio media must be a base64 data URL (data:audio/<format>;base64,...)");
64
+ }
65
+ return { type: "input_audio", input_audio: { data: match[2], format: match[1] } };
66
+ }
67
+ return { type: "image_url", image_url: { url: media.src } };
68
+ }
69
+ /** Builds the OpenRouter chat-completions request body for a prompt plus any attached media. */
70
+ export function buildChatRequest(args) {
71
+ const content = [{ type: "text", text: args.prompt }];
72
+ for (const media of args.media ?? [])
73
+ content.push(contentPartForMedia(media));
74
+ return {
75
+ model: args.model,
76
+ messages: [{ role: "user", content }],
77
+ max_tokens: args.maxTokens ?? DEFAULT_MAX_TOKENS,
78
+ };
79
+ }
80
+ /**
81
+ * Reads an OpenRouter server-sent-events stream (`stream: true`), calling
82
+ * `onText` with the accumulated reply as each chunk lands. Handles the
83
+ * `: OPENROUTER PROCESSING` keep-alive comments and the `[DONE]` sentinel,
84
+ * and still works if a proxy buffered the whole stream into one body.
85
+ */
86
+ export async function readChatStream(res, onText) {
87
+ let text = "";
88
+ let usage;
89
+ let evaluations;
90
+ let buffer = "";
91
+ const handleLine = (line) => {
92
+ if (!line.startsWith("data:"))
93
+ return; // blank separators and ": keep-alive" comments
94
+ const data = line.slice(5).trim();
95
+ if (!data || data === "[DONE]")
96
+ return;
97
+ let chunk;
98
+ try {
99
+ chunk = JSON.parse(data);
100
+ }
101
+ catch {
102
+ return;
103
+ }
104
+ if (chunk.error) {
105
+ throw new Error(`openRouterEngine: ${describeError({ error: chunk.error }, "stream error")}`);
106
+ }
107
+ if (chunk.usage)
108
+ usage = chunk.usage;
109
+ // A backend that checks the output appends its verdicts as their own chunk.
110
+ evaluations = readEvaluations(chunk) ?? evaluations;
111
+ const delta = chunk.choices?.[0]?.delta?.content;
112
+ if (delta) {
113
+ text += delta;
114
+ onText(text);
115
+ }
116
+ };
117
+ const drain = (final) => {
118
+ const lines = buffer.split("\n");
119
+ buffer = final ? "" : lines.pop();
120
+ for (const line of lines)
121
+ handleLine(line.trimEnd());
122
+ };
123
+ if (res.body) {
124
+ const reader = res.body.getReader();
125
+ const decoder = new TextDecoder();
126
+ for (;;) {
127
+ const { done, value } = await reader.read();
128
+ if (done)
129
+ break;
130
+ buffer += decoder.decode(value, { stream: true });
131
+ drain(false);
132
+ }
133
+ buffer += decoder.decode();
134
+ }
135
+ else {
136
+ buffer = await res.text();
137
+ }
138
+ drain(true);
139
+ return { text, usage, evaluations };
140
+ }
141
+ /**
142
+ * Sends the prompt plus every attached file to OpenRouter via the local
143
+ * proxy and returns the model's reply as `"text"`. Calls the caller's
144
+ * `modelId` when given, otherwise `modelByKind` (keyed on the first
145
+ * attachment's kind), `model`, or the built-in default. Streams (OpenRouter's
146
+ * `stream: true`) when the caller passes `onText`; the proxy must pass the
147
+ * response body through rather than buffer it for that to be incremental.
148
+ */
149
+ export function createOpenRouterEngine(options = {}) {
150
+ const endpoint = options.endpoint ?? "/api/chai/openrouter/chat";
151
+ const fetchImpl = options.fetchImpl ?? fetch;
152
+ return {
153
+ async generate({ modelId, prompt, attachments, onText, signal }) {
154
+ const firstKind = attachments[0]?.kind;
155
+ const model = modelId || (firstKind && options.modelByKind?.[firstKind]) || options.model || DEFAULT_MODEL;
156
+ const body = buildChatRequest({ model, prompt, media: attachments, maxTokens: options.maxTokens });
157
+ // Aborting closes the connection. On a stream, OpenRouter then stops
158
+ // the model and billing for providers that support it; a
159
+ // non-streaming request keeps running at the provider.
160
+ const res = await fetchImpl(endpoint, {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/json" },
163
+ body: JSON.stringify(onText ? { ...body, stream: true } : body),
164
+ signal,
165
+ });
166
+ // A rejected request comes back as plain JSON even when streaming was
167
+ // asked for, so only a successful response is read as a stream.
168
+ if (onText && res.ok) {
169
+ const { text, usage, evaluations } = await readChatStream(res, onText);
170
+ if (!text)
171
+ throw new Error("openRouterEngine: empty response from OpenRouter");
172
+ return { src: text, kind: "text", usage: toUsage(usage, options.pricePerMillionTokens), evaluations };
173
+ }
174
+ const json = (await res.json().catch(() => ({})));
175
+ if (!res.ok || json.error) {
176
+ throw new Error(`openRouterEngine: ${describeError(json, `${res.status} ${res.statusText}`)}`);
177
+ }
178
+ const text = json.choices?.[0]?.message?.content;
179
+ if (!text)
180
+ throw new Error("openRouterEngine: empty response from OpenRouter");
181
+ return {
182
+ src: text,
183
+ kind: "text",
184
+ usage: toUsage(json.usage, options.pricePerMillionTokens),
185
+ evaluations: readEvaluations(json),
186
+ };
187
+ },
188
+ };
189
+ }
190
+ /**
191
+ * A minimal, cheap connectivity check — the "use real inference sparingly"
192
+ * path. Deliberately not a `GenerationEngine` and not exercised by any
193
+ * automated test: it's meant to be triggered once, by a person, from the
194
+ * Develop page's "Test connection" button.
195
+ */
196
+ export async function pingOpenRouter(options = {}) {
197
+ const endpoint = options.endpoint ?? "/api/chai/openrouter/chat";
198
+ const fetchImpl = options.fetchImpl ?? fetch;
199
+ const model = options.model ?? DEFAULT_MODEL;
200
+ try {
201
+ const res = await fetchImpl(endpoint, {
202
+ method: "POST",
203
+ headers: { "Content-Type": "application/json" },
204
+ body: JSON.stringify({
205
+ model,
206
+ messages: [{ role: "user", content: "Reply with exactly one word: OK." }],
207
+ max_tokens: 5,
208
+ }),
209
+ });
210
+ const json = (await res.json().catch(() => ({})));
211
+ if (!res.ok || json.error) {
212
+ return { ok: false, error: describeError(json, `${res.status} ${res.statusText}`) };
213
+ }
214
+ return { ok: true, reply: json.choices?.[0]?.message?.content?.trim() ?? "" };
215
+ }
216
+ catch (err) {
217
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
218
+ }
219
+ }
220
+ //# sourceMappingURL=openrouter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openrouter.js","sourceRoot":"","sources":["../../src/engines/openrouter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./engine.js";
3
+ export * from "./run.js";
4
+ export * from "./media-analysis-prompts.js";
5
+ export * from "./engines/openrouter.js";
6
+ export * from "./engines/fal.js";
7
+ export * from "./model-routing.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./engine.js";
3
+ export * from "./run.js";
4
+ export * from "./media-analysis-prompts.js";
5
+ export * from "./engines/openrouter.js";
6
+ export * from "./engines/fal.js";
7
+ export * from "./model-routing.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC"}
@@ -0,0 +1,18 @@
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
+ export type MediaAnalysisKind = "image" | "video" | "audio";
11
+ export type MediaAnalysisPromptLength = "terse" | "concise" | "detailed";
12
+ export declare const MEDIA_ANALYSIS_PROMPTS: Record<MediaAnalysisKind, Record<MediaAnalysisPromptLength, string>>;
13
+ export declare const DEFAULT_MEDIA_ANALYSIS_PROMPT_LENGTH: MediaAnalysisPromptLength;
14
+ export declare const MEDIA_ANALYSIS_PROMPT_LENGTHS: {
15
+ value: MediaAnalysisPromptLength;
16
+ label: string;
17
+ }[];
18
+ //# sourceMappingURL=media-analysis-prompts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-analysis-prompts.d.ts","sourceRoot":"","sources":["../src/media-analysis-prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAC5D,MAAM,MAAM,yBAAyB,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,CAAC;AAMzE,eAAO,MAAM,sBAAsB,EAAE,MAAM,CACzC,iBAAiB,EACjB,MAAM,CAAC,yBAAyB,EAAE,MAAM,CAAC,CAiB1C,CAAC;AAEF,eAAO,MAAM,oCAAoC,EAAE,yBAAqC,CAAC;AAEzF,eAAO,MAAM,6BAA6B,EAAE;IAAE,KAAK,EAAE,yBAAyB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAI9F,CAAC"}