@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,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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"}
|
package/dist/index.d.ts
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.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"}
|