@chai-ui/core 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +33 -3
  2. package/dist/check-suggested-models.d.ts +31 -0
  3. package/dist/check-suggested-models.d.ts.map +1 -0
  4. package/dist/check-suggested-models.js +123 -0
  5. package/dist/check-suggested-models.js.map +1 -0
  6. package/dist/composer.d.ts +188 -0
  7. package/dist/composer.d.ts.map +1 -0
  8. package/dist/composer.js +326 -0
  9. package/dist/composer.js.map +1 -0
  10. package/dist/engines/fal.d.ts +6 -4
  11. package/dist/engines/fal.d.ts.map +1 -1
  12. package/dist/engines/fal.js +20 -19
  13. package/dist/engines/fal.js.map +1 -1
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/media-analyzer.d.ts +55 -0
  19. package/dist/media-analyzer.d.ts.map +1 -0
  20. package/dist/media-analyzer.js +124 -0
  21. package/dist/media-analyzer.js.map +1 -0
  22. package/dist/reasoning.d.ts +59 -0
  23. package/dist/reasoning.d.ts.map +1 -0
  24. package/dist/reasoning.js +55 -0
  25. package/dist/reasoning.js.map +1 -0
  26. package/dist/suggested-models.d.ts +23 -0
  27. package/dist/suggested-models.d.ts.map +1 -0
  28. package/dist/suggested-models.js +110 -0
  29. package/dist/suggested-models.js.map +1 -0
  30. package/dist/types.d.ts +12 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/check-suggested-models.ts +147 -0
  34. package/src/composer.ts +525 -0
  35. package/src/engines/fal.ts +20 -19
  36. package/src/index.ts +5 -0
  37. package/src/media-analyzer.ts +182 -0
  38. package/src/reasoning.ts +90 -0
  39. package/src/suggested-models.ts +121 -0
  40. package/src/types.ts +13 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The Media Analyzer's behavior with no framework: sends the attached media
3
+ * and the instruction for its kind and prompt length
4
+ * (`MEDIA_ANALYSIS_PROMPTS`) to the reasoning engine, and keeps the
5
+ * generated prompt, which fills in as the text streams. A small store
6
+ * (`getState` + `subscribe`) that `useMediaAnalyzer` (React) and the Vue
7
+ * composable wrap.
8
+ */
9
+ import { warnNoReasoningOnce } from "./composer.js";
10
+ import { createMockEngine, isAbortError, normalizeGenerationError, type GenerationEngine } from "./engine.js";
11
+ import {
12
+ DEFAULT_MEDIA_ANALYSIS_PROMPT_LENGTH,
13
+ MEDIA_ANALYSIS_PROMPTS,
14
+ type MediaAnalysisPromptLength,
15
+ } from "./media-analysis-prompts.js";
16
+ import { routeModel, type ModelRoutingInput, type ModelRoutingResult } from "./model-routing.js";
17
+ import { reasoningModelFor, type Reasoning } from "./reasoning.js";
18
+ import type { DroppedMedia, ModelOption } from "./types.js";
19
+
20
+ export interface MediaAnalyzerAttachment extends DroppedMedia {
21
+ id: string;
22
+ }
23
+
24
+ export interface MediaAnalyzerSubmitPayload {
25
+ attachments: MediaAnalyzerAttachment[];
26
+ modelId: string | null;
27
+ /** When true, the analyzer routes among `models` at submit instead of using `modelId`. */
28
+ autoSelectModel: boolean;
29
+ /** The models offered for the attached media's kind. */
30
+ models: ModelOption[];
31
+ /** `null` until a person actually picks one — see `MediaAnalyzer`'s `promptLength` prop for why this doesn't default to "concise". */
32
+ promptLength: MediaAnalysisPromptLength | null;
33
+ }
34
+
35
+ export interface MediaAnalyzerOptions {
36
+ /**
37
+ * Overrides the nearest `ChaiProvider`'s reasoning engine. With neither,
38
+ * a mock text engine answers, so a first render works with no key.
39
+ */
40
+ engine?: GenerationEngine;
41
+ /** The model when the person hasn't picked one. Defaults to the provider's reasoning model for the media's kind. */
42
+ model?: string;
43
+ onResult?: (prompt: string) => void;
44
+ onError?: (message: string) => void;
45
+ /** Auto-select: fires with the routed model so the trigger shows it. Wire it to `MediaAnalyzer`'s `onModelChange` state. */
46
+ onModelChange?: (modelId: string) => void;
47
+ /** Replaces the routing instruction (`buildModelRoutingPrompt`). */
48
+ routingPrompt?: (input: ModelRoutingInput) => string;
49
+ /** Fires when auto-select fell back to the first listed model (no reasoning model, a failed call, or an unlisted reply). Analysis still runs. */
50
+ onRoutingFallback?: (result: Extract<ModelRoutingResult, { status: "fallback" }>) => void;
51
+ }
52
+
53
+ export interface MediaAnalyzerState {
54
+ analyzing: boolean;
55
+ error: string | null;
56
+ prompt: string | null;
57
+ }
58
+
59
+ const mockTextEngine = createMockEngine({ outputKind: "text" });
60
+
61
+ /**
62
+ * `getOptions` is read on every call, so options can change between
63
+ * renders; `getProvided` is the nearest provider's reasoning, if any.
64
+ */
65
+ export function createMediaAnalyzerStore(
66
+ getOptions: () => MediaAnalyzerOptions,
67
+ getProvided: () => Reasoning | undefined = () => undefined
68
+ ) {
69
+ let state: MediaAnalyzerState = { analyzing: false, error: null, prompt: null };
70
+ const listeners = new Set<() => void>();
71
+ const set = (next: Partial<MediaAnalyzerState>) => {
72
+ state = { ...state, ...next };
73
+ for (const l of listeners) l();
74
+ };
75
+
76
+ // Each submit gets an id; a late reply from an earlier submit is dropped,
77
+ // and its call aborted.
78
+ let callId = 0;
79
+ let controller: AbortController | null = null;
80
+
81
+ function submit(payload: MediaAnalyzerSubmitPayload) {
82
+ const opts = getOptions();
83
+ const reasoning = getProvided();
84
+ const id = ++callId;
85
+ controller?.abort();
86
+ const { signal } = (controller = new AbortController());
87
+ set({ error: null, prompt: null });
88
+
89
+ const kind = payload.attachments[0]?.kind;
90
+ if (kind !== "image" && kind !== "video" && kind !== "audio") {
91
+ const message = "Attach an image, video, or audio file to analyze.";
92
+ set({ error: message });
93
+ opts.onError?.(message);
94
+ return;
95
+ }
96
+
97
+ const engine = opts.engine ?? reasoning?.engine ?? mockTextEngine;
98
+ const defaultModel = opts.model ?? (reasoning ? reasoningModelFor(reasoning, payload.attachments) : undefined);
99
+ const instruction = MEDIA_ANALYSIS_PROMPTS[kind][payload.promptLength ?? DEFAULT_MEDIA_ANALYSIS_PROMPT_LENGTH];
100
+ const current = () => id === callId;
101
+ const autoSelect = payload.autoSelectModel && payload.models.length > 0;
102
+
103
+ const analyze = (modelId: string | undefined) =>
104
+ engine
105
+ .generate({
106
+ modelId,
107
+ prompt: instruction,
108
+ attachments: payload.attachments,
109
+ onText: (text) => current() && set({ prompt: text }),
110
+ signal,
111
+ })
112
+ .then(
113
+ ({ src }) => {
114
+ if (!current()) return;
115
+ set({ prompt: src });
116
+ getOptions().onResult?.(src);
117
+ },
118
+ (err) => {
119
+ if (!current() || isAbortError(err)) return;
120
+ const { message } = normalizeGenerationError(err);
121
+ set({ error: message });
122
+ getOptions().onError?.(message);
123
+ }
124
+ )
125
+ .finally(() => current() && set({ analyzing: false }));
126
+
127
+ set({ analyzing: true });
128
+ if (!autoSelect) {
129
+ void analyze(payload.modelId ?? defaultModel);
130
+ return;
131
+ }
132
+ // Auto-select: the reasoning model looks at the media and picks the
133
+ // analysis model first. A routing problem falls back, never blocks.
134
+ void routeModel({
135
+ prompt: "",
136
+ outputKind: "text",
137
+ attachmentKinds: payload.attachments.map((a) => a.kind),
138
+ attachments: payload.attachments,
139
+ models: payload.models,
140
+ engine: opts.engine ?? reasoning?.engine,
141
+ model: defaultModel,
142
+ routingPrompt: opts.routingPrompt,
143
+ signal,
144
+ }).then((routed) => {
145
+ if (!current() || signal.aborted) return;
146
+ const latest = getOptions();
147
+ if (routed.status === "fallback") {
148
+ if (routed.reason === "no-reasoning") warnNoReasoningOnce("analysis runs on the first listed model");
149
+ latest.onRoutingFallback?.(routed);
150
+ }
151
+ latest.onModelChange?.(routed.modelId);
152
+ return analyze(routed.modelId);
153
+ });
154
+ }
155
+
156
+ /** Stops the analysis in flight (cancelling at the provider where it can), keeping any text that already streamed in. */
157
+ function cancel() {
158
+ callId += 1;
159
+ controller?.abort();
160
+ controller = null;
161
+ set({ analyzing: false });
162
+ }
163
+
164
+ /** Stops anything in flight and clears the prompt and error, e.g. when the person changes their media. */
165
+ function reset() {
166
+ cancel();
167
+ set({ error: null, prompt: null });
168
+ }
169
+
170
+ return {
171
+ getState: () => state,
172
+ subscribe(listener: () => void) {
173
+ listeners.add(listener);
174
+ return () => void listeners.delete(listener);
175
+ },
176
+ submit,
177
+ cancel,
178
+ reset,
179
+ };
180
+ }
181
+
182
+ export type MediaAnalyzerStore = ReturnType<typeof createMediaAnalyzerStore>;
@@ -0,0 +1,90 @@
1
+ import { createOpenRouterEngine } from "./engines/openrouter.js";
2
+ import type { GenerationEngine } from "./engine.js";
3
+ import type { DroppedMedia, MediaKind } from "./types.js";
4
+
5
+ /**
6
+ * The suggested reasoning model for text and images: prompt enhancement,
7
+ * auto-select routing and image analysis. A default, not a requirement —
8
+ * pass any model id `reasoningEngine` can run.
9
+ */
10
+ export const DEFAULT_REASONING_MODEL = "anthropic/claude-opus-5";
11
+
12
+ /**
13
+ * Suggested per-kind overrides for media Claude can't read. Gemini takes
14
+ * video and audio directly. For a cheaper option, swap in
15
+ * `"qwen/qwen3.8-omni-flash"`, which takes both too.
16
+ */
17
+ export const DEFAULT_REASONING_MODEL_BY_KIND: Partial<Record<MediaKind, string>> = {
18
+ video: "google/gemini-3.8-flash",
19
+ audio: "google/gemini-3.8-flash",
20
+ };
21
+
22
+ /** The reasoning engine and models every CHAI component shares. */
23
+ export interface Reasoning {
24
+ engine: GenerationEngine;
25
+ /** The model for text and any attachment kind without its own entry in `modelByKind`. */
26
+ model: string;
27
+ modelByKind: Partial<Record<MediaKind, string>>;
28
+ }
29
+
30
+ /** What a `ChaiProvider` takes, in any framework. */
31
+ export interface ReasoningConfig {
32
+ /** Text, image and routing model. Defaults to `DEFAULT_REASONING_MODEL`. */
33
+ reasoningModel?: string;
34
+ /**
35
+ * Per-attachment-kind models, merged over `DEFAULT_REASONING_MODEL_BY_KIND`
36
+ * one kind at a time, e.g. `{ video: "qwen/qwen3.8-omni-flash" }` keeps
37
+ * the default for audio. Map a kind to `reasoningModel`'s value to use
38
+ * one model for everything.
39
+ */
40
+ reasoningModelByKind?: Partial<Record<MediaKind, string>>;
41
+ /**
42
+ * The engine the reasoning models run on. Defaults to OpenRouter,
43
+ * independent of the generation engine: Fal's own LLM access is an
44
+ * OpenRouter proxy, so defaulting to it would double-proxy.
45
+ */
46
+ reasoningEngine?: GenerationEngine;
47
+ }
48
+
49
+ /** A provider's `Reasoning`, from its config and the engine it holds (`config.reasoningEngine`, else one OpenRouter engine it created once). */
50
+ export function createReasoning(config: ReasoningConfig, engine: GenerationEngine): Reasoning {
51
+ const byKind = config.reasoningModelByKind ?? {};
52
+ const overrides = Object.fromEntries(Object.entries(byKind).filter(([, v]) => v !== undefined));
53
+ return {
54
+ engine,
55
+ model: config.reasoningModel ?? DEFAULT_REASONING_MODEL,
56
+ modelByKind: { ...DEFAULT_REASONING_MODEL_BY_KIND, ...overrides },
57
+ };
58
+ }
59
+
60
+ /** The reasoning engine a provider uses: the one passed, else OpenRouter's. */
61
+ export function reasoningEngineFor(config: ReasoningConfig): GenerationEngine {
62
+ return config.reasoningEngine ?? createOpenRouterEngine();
63
+ }
64
+
65
+ /**
66
+ * A hook's or composable's own pair wins, but only as a pair: half of one
67
+ * never mixes with half of the provider's (e.g. a hook engine with the
68
+ * provider's model). Half a pair means no reasoning.
69
+ */
70
+ export function pickReasoning(
71
+ own: { reasoningEngine?: GenerationEngine; reasoningModel?: string },
72
+ provided: Reasoning | undefined
73
+ ): Reasoning | undefined {
74
+ const { reasoningEngine, reasoningModel } = own;
75
+ if (!reasoningEngine && !reasoningModel) return provided;
76
+ return reasoningEngine && reasoningModel ? { engine: reasoningEngine, model: reasoningModel, modelByKind: {} } : undefined;
77
+ }
78
+
79
+ /**
80
+ * The reasoning model for a call with these attachments: the first
81
+ * attachment whose kind has its own model wins (a clip plus a still goes
82
+ * to the model that can read the clip), else `reasoning.model`.
83
+ */
84
+ export function reasoningModelFor(reasoning: Reasoning, attachments: DroppedMedia[] = []): string {
85
+ for (const { kind } of attachments) {
86
+ const model = reasoning.modelByKind[kind];
87
+ if (model) return model;
88
+ }
89
+ return reasoning.model;
90
+ }
@@ -0,0 +1,121 @@
1
+ import type { GenerationUseCase, MediaKind, ModelOption } from "./types.js";
2
+ import { DEFAULT_MAX_REGIONS } from "./region-edit.js";
3
+
4
+ /**
5
+ * Suggested models, so builders don't have to write model lists. Composer
6
+ * and MediaAnalyzer use them when no `models` is passed; passing `models`
7
+ * still replaces them. The first model in each list is the default pick.
8
+ *
9
+ * Generation runs on Fal (the default engine), analysis on OpenRouter (the
10
+ * reasoning engine). Every entry was checked against the provider's own
11
+ * catalog: Fal's model schemas (`api.fal.ai/v1/models?expand=openapi-3.0`)
12
+ * for the aspect ratios and the image field, OpenRouter's model list for
13
+ * the input kinds. They change only through package releases; run
14
+ * `checkSuggestedModels()` to see whether a provider has changed since.
15
+ *
16
+ * Checked 2026-10-06.
17
+ */
18
+
19
+ const NANO_BANANA_RATIOS = ["21:9", "16:9", "3:2", "4:3", "5:4", "1:1", "4:5", "3:4", "2:3", "9:16"];
20
+ const FLUX_3_RATIOS = ["21:9", "2:1", "16:9", "3:2", "7:5", "4:3", "5:4", "1:1", "4:5", "3:4", "5:7", "2:3", "9:16", "1:2"];
21
+ /** The ratios a named `image_size` covers (FLUX.1, GPT Image 2). */
22
+ const IMAGE_SIZE_RATIOS = ["1:1", "4:3", "16:9", "3:4", "9:16"];
23
+
24
+ /** Create image, from a prompt. */
25
+ export const SUGGESTED_IMAGE_MODELS: ModelOption[] = [
26
+ { id: "fal-ai/nano-banana-pro", label: "Nano Banana Pro", provider: "Google", speed: "standard", aspectRatios: NANO_BANANA_RATIOS, falInput: { aspectRatio: "aspect_ratio" } },
27
+ {
28
+ id: "fal-ai/nano-banana-2",
29
+ label: "Nano Banana 2",
30
+ provider: "Google",
31
+ speed: "fast",
32
+ aspectRatios: [...NANO_BANANA_RATIOS, "4:1", "1:4", "8:1", "1:8"],
33
+ falInput: { aspectRatio: "aspect_ratio" },
34
+ },
35
+ { id: "blackforestlabs/flux-3/text-to-image", label: "Flux 3 Image", provider: "Black Forest Labs", speed: "standard", aspectRatios: FLUX_3_RATIOS, falInput: { aspectRatio: "aspect_ratio" } },
36
+ { id: "openai/gpt-image-2", label: "GPT Image 2", provider: "OpenAI", speed: "slow", aspectRatios: IMAGE_SIZE_RATIOS, falInput: { aspectRatio: "image_size" } },
37
+ { id: "fal-ai/flux/schnell", label: "FLUX.1 schnell", provider: "Black Forest Labs", speed: "fast", aspectRatios: IMAGE_SIZE_RATIOS, falInput: { aspectRatio: "image_size" } },
38
+ ];
39
+
40
+ /** Create video, from a prompt alone. */
41
+ export const SUGGESTED_TEXT_TO_VIDEO_MODELS: ModelOption[] = [
42
+ { id: "minimax/h3-max/text-to-video", label: "MiniMax H3 Max", provider: "MiniMax", speed: "slow", aspectRatios: ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], falInput: { aspectRatio: "aspect_ratio" } },
43
+ { id: "bytedance/seedance-2.5/text-to-video", label: "Seedance 2.5", provider: "ByteDance", speed: "slow", aspectRatios: ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"], falInput: { aspectRatio: "aspect_ratio" } },
44
+ { id: "fal-ai/kling-video/v3/pro/text-to-video", label: "Kling v3 Pro", provider: "Kling", speed: "slow", aspectRatios: ["16:9", "9:16", "1:1"], falInput: { aspectRatio: "aspect_ratio" } },
45
+ { id: "fal-ai/veo3.1/fast", label: "Veo 3.1 Fast", provider: "Google", speed: "standard", aspectRatios: ["16:9", "9:16"], falInput: { aspectRatio: "aspect_ratio" } },
46
+ ];
47
+
48
+ /** Create video, starting from an attached image. The video follows the image's shape, so none lists aspect ratios. */
49
+ export const SUGGESTED_IMAGE_TO_VIDEO_MODELS: ModelOption[] = [
50
+ { id: "fal-ai/kling-video/v3/pro/image-to-video", label: "Kling v3 Pro", provider: "Kling", speed: "slow", falInput: { image: "start_image_url" } },
51
+ { id: "minimax/h3-max/image-to-video", label: "MiniMax H3 Max", provider: "MiniMax", speed: "slow", falInput: { image: "image_url" } },
52
+ { id: "bytedance/seedance-2.5/image-to-video", label: "Seedance 2.5", provider: "ByteDance", speed: "slow", falInput: { image: "image_url" } },
53
+ { id: "fal-ai/veo3.1/fast/image-to-video", label: "Veo 3.1 Fast", provider: "Google", speed: "standard", falInput: { image: "image_url" } },
54
+ ];
55
+
56
+ /**
57
+ * Edit an image. Flux 3 Image reads marked regions as boxes; the others get
58
+ * them described in words. An edit keeps the image's shape, so no aspect
59
+ * ratios.
60
+ */
61
+ export const SUGGESTED_EDIT_MODELS: ModelOption[] = [
62
+ {
63
+ id: "blackforestlabs/flux-3/edit-image",
64
+ label: "Flux 3 Image",
65
+ provider: "Black Forest Labs",
66
+ speed: "standard",
67
+ regionFormat: "flux-3-boxes",
68
+ maxRegions: DEFAULT_MAX_REGIONS,
69
+ falInput: { image: "image_urls" },
70
+ },
71
+ { id: "fal-ai/nano-banana-pro/edit", label: "Nano Banana Pro", provider: "Google", speed: "standard", falInput: { image: "image_urls" } },
72
+ { id: "openai/gpt-image-2/edit", label: "GPT Image 2", provider: "OpenAI", speed: "slow", falInput: { image: "image_urls" } },
73
+ ];
74
+
75
+ /**
76
+ * MediaAnalyzer's models per media kind, on OpenRouter. The first in each
77
+ * is `ChaiProvider`'s default reasoning model for that kind.
78
+ */
79
+ export const SUGGESTED_ANALYSIS_MODELS: Partial<Record<MediaKind, ModelOption[]>> = {
80
+ image: [
81
+ { id: "anthropic/claude-opus-5", label: "Claude Opus 5", provider: "Anthropic", speed: "standard" },
82
+ { id: "openai/gpt-5.2", label: "GPT-5.2", provider: "OpenAI", speed: "standard" },
83
+ { id: "google/gemini-3.5-flash", label: "Gemini 3.5 Flash", provider: "Google", speed: "fast" },
84
+ { id: "meta-llama/llama-4-maverick", label: "Llama 4 Maverick", provider: "Meta", speed: "standard" },
85
+ { id: "x-ai/grok-4.5", label: "Grok 4.5", provider: "xAI", speed: "standard" },
86
+ ],
87
+ video: [
88
+ { id: "google/gemini-3.8-flash", label: "Gemini 3.8 Flash", provider: "Google", speed: "fast" },
89
+ { id: "google/gemini-3.5-flash", label: "Gemini 3.5 Flash", provider: "Google", speed: "fast" },
90
+ { id: "amazon/nova-2-lite-v1", label: "Nova 2 Lite", provider: "Amazon", speed: "fast" },
91
+ { id: "bytedance-seed/seed-2.0-lite", label: "Seed 2.0 Lite", provider: "ByteDance", speed: "fast" },
92
+ { id: "moonshotai/kimi-k3", label: "Kimi K3", provider: "Moonshot", speed: "standard" },
93
+ ],
94
+ audio: [
95
+ { id: "google/gemini-3.8-flash", label: "Gemini 3.8 Flash", provider: "Google", speed: "fast" },
96
+ { id: "openai/gpt-audio", label: "GPT Audio", provider: "OpenAI", speed: "standard" },
97
+ { id: "mistralai/voxtral-small-24b-2507", label: "Voxtral Small 24B", provider: "Mistral", speed: "standard" },
98
+ { id: "xiaomi/mimo-v2.5", label: "MiMo V2.5", provider: "Xiaomi", speed: "standard" },
99
+ ],
100
+ };
101
+
102
+ /** The suggested Fal models for a generation use case, or none (text requests use the reasoning model). */
103
+ export function suggestedModels(useCase: GenerationUseCase | undefined): ModelOption[] {
104
+ switch (useCase) {
105
+ case "text-to-image":
106
+ return SUGGESTED_IMAGE_MODELS;
107
+ case "text-to-video":
108
+ return SUGGESTED_TEXT_TO_VIDEO_MODELS;
109
+ case "image-to-video":
110
+ return SUGGESTED_IMAGE_TO_VIDEO_MODELS;
111
+ case "image-edit":
112
+ return SUGGESTED_EDIT_MODELS;
113
+ default:
114
+ return [];
115
+ }
116
+ }
117
+
118
+ /** Every suggested Fal model, by id: how the Fal engine knows each one's inputs. */
119
+ export const SUGGESTED_FAL_MODELS: ReadonlyMap<string, ModelOption> = new Map(
120
+ [...SUGGESTED_IMAGE_MODELS, ...SUGGESTED_TEXT_TO_VIDEO_MODELS, ...SUGGESTED_IMAGE_TO_VIDEO_MODELS, ...SUGGESTED_EDIT_MODELS].map((m) => [m.id, m])
121
+ );
package/src/types.ts CHANGED
@@ -91,6 +91,13 @@ export interface ModelOption {
91
91
  regionFormat?: RegionFormat;
92
92
  /** For image-edit models: the most regions one edit takes. `EditCard` disables "New region" at this many. */
93
93
  maxRegions?: number;
94
+ /**
95
+ * How a Fal model takes its inputs, from its Fal schema: which field the
96
+ * attached image goes in, and whether the aspect ratio is sent as
97
+ * `aspect_ratio` or as a named `image_size`. The suggested lists carry it;
98
+ * without it the Fal engine guesses from the id.
99
+ */
100
+ falInput?: FalInputFormat;
94
101
  /**
95
102
  * The output aspect ratios the model takes, e.g. `["1:1", "16:9", "9:16"]`.
96
103
  * Composer's Aspect ratio menu offers only ratios the selected models
@@ -101,6 +108,12 @@ export interface ModelOption {
101
108
  aspectRatios?: string[];
102
109
  }
103
110
 
111
+ /** See `ModelOption.falInput`. */
112
+ export interface FalInputFormat {
113
+ image?: "image_url" | "image_urls" | "start_image_url";
114
+ aspectRatio?: "aspect_ratio" | "image_size";
115
+ }
116
+
104
117
  /** How an edit's regions reach the model — see `ModelOption.regionFormat`. */
105
118
  export type RegionFormat = "flux-3-boxes" | "text";
106
119