ai-cli 0.1.1 → 0.2.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/src/lib/models.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { gateway } from "ai";
2
-
3
1
  export type Modality = "text" | "image" | "video";
4
2
 
5
3
  const DEFAULTS: Record<Modality, string> = {
@@ -8,156 +6,168 @@ const DEFAULTS: Record<Modality, string> = {
8
6
  video: process.env.AI_CLI_VIDEO_MODEL ?? "bytedance/seedance-2.0",
9
7
  };
10
8
 
11
- export const FALLBACK_TEXT_MODELS = [
12
- "anthropic/claude-sonnet-4",
13
- "google/gemini-2.5-pro",
14
- "meta/llama-4-maverick",
15
- "openai/gpt-4.1",
16
- "openai/gpt-4.1-mini",
17
- "openai/gpt-4.1-nano",
18
- "openai/gpt-5.5",
19
- "openai/o3",
20
- "openai/o4-mini",
21
- "xai/grok-3",
22
- ];
23
-
24
- export const FALLBACK_IMAGE_MODELS = [
25
- "bfl/flux-2-flex",
26
- "bfl/flux-2-klein-4b",
27
- "bfl/flux-2-klein-9b",
28
- "bfl/flux-2-max",
29
- "bfl/flux-2-pro",
30
- "bfl/flux-kontext-max",
31
- "bfl/flux-kontext-pro",
32
- "bfl/flux-pro-1.0-fill",
33
- "bfl/flux-pro-1.1",
34
- "bfl/flux-pro-1.1-ultra",
35
- "bytedance/seedream-4.0",
36
- "bytedance/seedream-4.5",
37
- "bytedance/seedream-5.0-lite",
38
- "google/imagen-4.0-fast-generate-001",
39
- "google/imagen-4.0-generate-001",
40
- "google/imagen-4.0-ultra-generate-001",
41
- "openai/gpt-image-1",
42
- "openai/gpt-image-1-mini",
43
- "openai/gpt-image-1.5",
44
- "openai/gpt-image-2",
45
- "prodia/flux-fast-schnell",
46
- "recraft/recraft-v2",
47
- "recraft/recraft-v3",
48
- "recraft/recraft-v4",
49
- "recraft/recraft-v4-pro",
50
- "xai/grok-imagine-image",
51
- "xai/grok-imagine-image-pro",
52
- ];
53
-
54
- export const FALLBACK_VIDEO_MODELS = [
55
- "alibaba/wan-v2.5-t2v-preview",
56
- "alibaba/wan-v2.6-i2v",
57
- "alibaba/wan-v2.6-i2v-flash",
58
- "alibaba/wan-v2.6-r2v",
59
- "alibaba/wan-v2.6-r2v-flash",
60
- "alibaba/wan-v2.6-t2v",
61
- "bytedance/seedance-2.0",
62
- "bytedance/seedance-2.0-fast",
63
- "bytedance/seedance-v1.0-lite-i2v",
64
- "bytedance/seedance-v1.0-lite-t2v",
65
- "bytedance/seedance-v1.0-pro",
66
- "bytedance/seedance-v1.0-pro-fast",
67
- "bytedance/seedance-v1.5-pro",
68
- "google/veo-3.0-fast-generate-001",
69
- "google/veo-3.0-generate-001",
70
- "google/veo-3.1-fast-generate-001",
71
- "google/veo-3.1-generate-001",
72
- "klingai/kling-v2.5-turbo-i2v",
73
- "klingai/kling-v2.5-turbo-t2v",
74
- "klingai/kling-v2.6-i2v",
75
- "klingai/kling-v2.6-motion-control",
76
- "klingai/kling-v2.6-t2v",
77
- "klingai/kling-v3.0-i2v",
78
- "klingai/kling-v3.0-t2v",
79
- "xai/grok-imagine-video",
80
- ];
9
+ const GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1/models";
10
+ const GATEWAY_TIMEOUT_MS = 5_000;
11
+
12
+ export interface ModelPricing {
13
+ input?: string;
14
+ output?: string;
15
+ image?: string;
16
+ }
81
17
 
82
18
  export interface ModelEntry {
83
19
  id: string;
84
20
  name?: string;
85
21
  description?: string;
22
+ creator: string;
23
+ capabilities: Modality[];
24
+ pricing?: ModelPricing;
86
25
  }
87
26
 
88
27
  export interface GatewayModels {
89
28
  text: ModelEntry[];
90
29
  image: ModelEntry[];
91
30
  video: ModelEntry[];
31
+ all: ModelEntry[];
32
+ languageImageModelIds: Set<string>;
92
33
  }
93
34
 
94
- const MODEL_TYPE_TO_MODALITY: Record<string, Modality> = {
95
- language: "text",
96
- image: "image",
97
- video: "video",
98
- };
35
+ interface RawGatewayModel {
36
+ id: string;
37
+ name?: string;
38
+ description?: string;
39
+ owned_by?: string;
40
+ type?: string;
41
+ tags?: string[];
42
+ pricing?: {
43
+ input?: string;
44
+ output?: string;
45
+ image?: string;
46
+ };
47
+ }
99
48
 
100
- export async function fetchGatewayModels(): Promise<GatewayModels> {
101
- const result: GatewayModels = { text: [], image: [], video: [] };
49
+ let cached: Promise<GatewayModels> | null = null;
50
+
51
+ export function fetchGatewayModels(): Promise<GatewayModels> {
52
+ if (!cached) {
53
+ cached = doFetch().catch((err) => {
54
+ cached = null;
55
+ throw err;
56
+ });
57
+ }
58
+ return cached;
59
+ }
60
+
61
+ export function resetGatewayCache(): void {
62
+ cached = null;
63
+ }
64
+
65
+ async function doFetch(): Promise<GatewayModels> {
66
+ const result: GatewayModels = {
67
+ text: [],
68
+ image: [],
69
+ video: [],
70
+ all: [],
71
+ languageImageModelIds: new Set(),
72
+ };
102
73
 
103
74
  try {
104
- const { models } = await gateway.getAvailableModels();
75
+ const res = await fetch(GATEWAY_MODELS_URL, {
76
+ signal: AbortSignal.timeout(GATEWAY_TIMEOUT_MS),
77
+ });
78
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
79
+ const json = (await res.json()) as { data?: RawGatewayModel[] };
80
+ const models = json.data ?? [];
81
+
82
+ const entryMap = new Map<string, ModelEntry>();
83
+
105
84
  for (const m of models) {
106
- const modality =
107
- MODEL_TYPE_TO_MODALITY[(m as { modelType?: string }).modelType ?? ""];
108
- if (!modality) continue;
109
- result[modality].push({
85
+ const tags = m.tags ?? [];
86
+ const isImageGen = tags.includes("image-generation");
87
+ const capabilities: Modality[] = [];
88
+
89
+ switch (m.type) {
90
+ case "language":
91
+ capabilities.push("text");
92
+ if (isImageGen) capabilities.push("image");
93
+ break;
94
+ case "image":
95
+ capabilities.push("image");
96
+ break;
97
+ case "video":
98
+ capabilities.push("video");
99
+ break;
100
+ default:
101
+ continue;
102
+ }
103
+
104
+ const creator =
105
+ m.owned_by ??
106
+ (m.id.slice(0, Math.max(0, m.id.indexOf("/"))) || "other");
107
+
108
+ const pricing: ModelPricing | undefined =
109
+ m.pricing?.input || m.pricing?.output || m.pricing?.image
110
+ ? {
111
+ ...(m.pricing.input ? { input: m.pricing.input } : {}),
112
+ ...(m.pricing.output ? { output: m.pricing.output } : {}),
113
+ ...(m.pricing.image ? { image: m.pricing.image } : {}),
114
+ }
115
+ : undefined;
116
+
117
+ const entry: ModelEntry = {
110
118
  id: m.id,
111
119
  name: m.name,
112
- description: m.description ?? undefined,
113
- });
114
- }
115
- } catch {
116
- result.text = FALLBACK_TEXT_MODELS.map((id) => ({ id }));
117
- result.image = FALLBACK_IMAGE_MODELS.map((id) => ({ id }));
118
- result.video = FALLBACK_VIDEO_MODELS.map((id) => ({ id }));
119
- }
120
+ description: m.description,
121
+ creator,
122
+ capabilities,
123
+ pricing,
124
+ };
120
125
 
121
- if (result.text.length === 0) {
122
- result.text = FALLBACK_TEXT_MODELS.map((id) => ({ id }));
123
- }
124
- if (result.image.length === 0) {
125
- result.image = FALLBACK_IMAGE_MODELS.map((id) => ({ id }));
126
- }
127
- if (result.video.length === 0) {
128
- result.video = FALLBACK_VIDEO_MODELS.map((id) => ({ id }));
129
- }
130
-
131
- return result;
132
- }
133
-
134
- function expandModelId(input: string, modality: Modality): string {
135
- if (input.includes("/")) return input;
126
+ entryMap.set(m.id, entry);
136
127
 
137
- const knownLists: string[][] = [];
138
- if (modality === "text") knownLists.push(FALLBACK_TEXT_MODELS);
139
- else if (modality === "image") knownLists.push(FALLBACK_IMAGE_MODELS);
140
- else if (modality === "video") knownLists.push(FALLBACK_VIDEO_MODELS);
128
+ if (capabilities.includes("text")) result.text.push(entry);
129
+ if (capabilities.includes("image")) result.image.push(entry);
130
+ if (capabilities.includes("video")) result.video.push(entry);
141
131
 
142
- for (const list of knownLists) {
143
- for (const fullId of list) {
144
- const name = fullId.slice(fullId.indexOf("/") + 1);
145
- if (name === input) return fullId;
132
+ if (m.type === "language" && isImageGen) {
133
+ result.languageImageModelIds.add(m.id);
134
+ }
146
135
  }
136
+
137
+ result.all = [...entryMap.values()];
138
+ } catch {
139
+ cached = null;
140
+ process.stderr.write("Warning: could not fetch models from AI Gateway\n");
147
141
  }
148
142
 
149
- return input;
143
+ return result;
150
144
  }
151
145
 
152
146
  export function resolveModels(
153
147
  modality: Modality,
154
- userModel?: string
148
+ userModel?: string,
149
+ knownModels?: Pick<ModelEntry, "id">[]
155
150
  ): string[] {
156
151
  if (!userModel) return [DEFAULTS[modality]];
157
152
  const models = userModel
158
153
  .split(",")
159
154
  .map((m) => m.trim())
160
155
  .filter(Boolean)
161
- .map((m) => expandModelId(m, modality));
156
+ .map((m) => expandModelId(m, knownModels));
162
157
  return models.length > 0 ? models : [DEFAULTS[modality]];
163
158
  }
159
+
160
+ function expandModelId(
161
+ input: string,
162
+ knownModels?: Pick<ModelEntry, "id">[]
163
+ ): string {
164
+ if (input.includes("/")) return input;
165
+ if (!knownModels) return input;
166
+
167
+ for (const m of knownModels) {
168
+ const name = m.id.slice(m.id.indexOf("/") + 1);
169
+ if (name === input) return m.id;
170
+ }
171
+
172
+ return input;
173
+ }
@@ -1,296 +0,0 @@
1
- import type { Command } from "commander";
2
-
3
- import {
4
- FALLBACK_TEXT_MODELS,
5
- FALLBACK_IMAGE_MODELS,
6
- FALLBACK_VIDEO_MODELS,
7
- } from "../lib/models.js";
8
-
9
- export function registerCompletionsCommand(program: Command) {
10
- program
11
- .command("completions")
12
- .description("Output shell completion script")
13
- .argument("<shell>", "Shell type: zsh, bash, fish")
14
- .action((shell: string) => {
15
- switch (shell.toLowerCase()) {
16
- case "zsh":
17
- process.stdout.write(generateZsh());
18
- break;
19
- case "bash":
20
- process.stdout.write(generateBash());
21
- break;
22
- case "fish":
23
- process.stdout.write(generateFish());
24
- break;
25
- default:
26
- process.stderr.write(
27
- `Unknown shell: ${shell}. Supported: zsh, bash, fish\n`
28
- );
29
- process.exit(1);
30
- }
31
- });
32
- }
33
-
34
- const SUBCOMMANDS = ["text", "image", "video", "models", "completions", "help"];
35
- const GLOBAL_FLAGS = [
36
- "--model",
37
- "--output",
38
- "--count",
39
- "--concurrency",
40
- "--quiet",
41
- "--json",
42
- "--help",
43
- "--version",
44
- ];
45
- const TEXT_FLAGS = ["--format", "--system", "--max-tokens", "--temperature"];
46
- const IMAGE_FLAGS = [
47
- "--size",
48
- "--aspect-ratio",
49
- "--quality",
50
- "--style",
51
- "--no-preview",
52
- ];
53
- const VIDEO_FLAGS = ["--aspect-ratio", "--duration", "--no-preview"];
54
- const MODEL_FLAGS = ["--type", "--provider", "--json", "--help"];
55
-
56
- const ALL_MODELS = [
57
- ...FALLBACK_TEXT_MODELS,
58
- ...FALLBACK_IMAGE_MODELS,
59
- ...FALLBACK_VIDEO_MODELS,
60
- ];
61
- const MODEL_NAMES = ALL_MODELS.map((m) => m.slice(m.indexOf("/") + 1));
62
-
63
- function generateZsh(): string {
64
- return `#compdef ai
65
-
66
- _ai() {
67
- local -a subcommands
68
- subcommands=(
69
- 'text:Generate text from a prompt'
70
- 'image:Generate an image from a prompt'
71
- 'video:Generate a video from a prompt'
72
- 'models:List available models'
73
- 'completions:Output shell completion script'
74
- 'help:Display help'
75
- )
76
-
77
- local -a models
78
- models=(${ALL_MODELS.join(" ")})
79
-
80
- local -a model_names
81
- model_names=(${MODEL_NAMES.join(" ")})
82
-
83
- _arguments -C \\
84
- '1:command:->cmd' \\
85
- '*::arg:->args'
86
-
87
- case $state in
88
- cmd)
89
- _describe 'command' subcommands
90
- ;;
91
- args)
92
- case $words[1] in
93
- text)
94
- _arguments \\
95
- '-m[Model ID]:model:($models $model_names)' \\
96
- '--model[Model ID]:model:($models $model_names)' \\
97
- '-o[Output path]:file:_files' \\
98
- '--output[Output path]:file:_files' \\
99
- '-f[Format]:format:(md txt)' \\
100
- '--format[Format]:format:(md txt)' \\
101
- '-n[Count]:count:' \\
102
- '--count[Count]:count:' \\
103
- '-p[Concurrency]:concurrency:' \\
104
- '--concurrency[Concurrency]:concurrency:' \\
105
- '-s[System prompt]:system:' \\
106
- '--system[System prompt]:system:' \\
107
- '--max-tokens[Max tokens]:tokens:' \\
108
- '-t[Temperature]:temp:' \\
109
- '--temperature[Temperature]:temp:' \\
110
- '-q[Quiet]' \\
111
- '--quiet[Quiet]' \\
112
- '--json[JSON output]' \\
113
- '*:prompt:'
114
- ;;
115
- image)
116
- _arguments \\
117
- '-m[Model ID]:model:($models $model_names)' \\
118
- '--model[Model ID]:model:($models $model_names)' \\
119
- '-o[Output path]:file:_files' \\
120
- '--output[Output path]:file:_files' \\
121
- '-n[Count]:count:' \\
122
- '--count[Count]:count:' \\
123
- '-p[Concurrency]:concurrency:' \\
124
- '--concurrency[Concurrency]:concurrency:' \\
125
- '--size[Size]:size:' \\
126
- '--aspect-ratio[Aspect ratio]:ratio:' \\
127
- '--quality[Quality]:quality:(standard hd)' \\
128
- '--style[Style]:style:(vivid natural)' \\
129
- '--no-preview[Disable inline image preview]' \\
130
- '-q[Quiet]' \\
131
- '--quiet[Quiet]' \\
132
- '--json[JSON output]' \\
133
- '*:prompt:'
134
- ;;
135
- video)
136
- _arguments \\
137
- '-m[Model ID]:model:($models $model_names)' \\
138
- '--model[Model ID]:model:($models $model_names)' \\
139
- '-o[Output path]:file:_files' \\
140
- '--output[Output path]:file:_files' \\
141
- '-n[Count]:count:' \\
142
- '--count[Count]:count:' \\
143
- '-p[Concurrency]:concurrency:' \\
144
- '--concurrency[Concurrency]:concurrency:' \\
145
- '--aspect-ratio[Aspect ratio]:ratio:' \\
146
- '--duration[Duration]:seconds:' \\
147
- '--no-preview[Disable inline video frame preview]' \\
148
- '-q[Quiet]' \\
149
- '--quiet[Quiet]' \\
150
- '--json[JSON output]' \\
151
- '*:prompt:'
152
- ;;
153
- models)
154
- _arguments \\
155
- '--type[Filter by type]:type:(text image video)' \\
156
- '--provider[Filter by provider]:provider:' \\
157
- '--json[JSON output]'
158
- ;;
159
- completions)
160
- _arguments '1:shell:(zsh bash fish)'
161
- ;;
162
- esac
163
- ;;
164
- esac
165
- }
166
-
167
- _ai "$@"
168
- `;
169
- }
170
-
171
- function generateBash(): string {
172
- return `_ai_completions() {
173
- local cur prev subcmd
174
- COMPREPLY=()
175
- cur="\${COMP_WORDS[COMP_CWORD]}"
176
- prev="\${COMP_WORDS[COMP_CWORD-1]}"
177
- subcmd="\${COMP_WORDS[1]}"
178
-
179
- if [[ \${COMP_CWORD} -eq 1 ]]; then
180
- COMPREPLY=($(compgen -W "${SUBCOMMANDS.join(" ")}" -- "$cur"))
181
- return
182
- fi
183
-
184
- case "$prev" in
185
- -m|--model)
186
- COMPREPLY=($(compgen -W "${ALL_MODELS.join(" ")} ${MODEL_NAMES.join(" ")}" -- "$cur"))
187
- return
188
- ;;
189
- -o|--output)
190
- COMPREPLY=($(compgen -f -- "$cur"))
191
- return
192
- ;;
193
- -f|--format)
194
- COMPREPLY=($(compgen -W "md txt" -- "$cur"))
195
- return
196
- ;;
197
- --quality)
198
- COMPREPLY=($(compgen -W "standard hd" -- "$cur"))
199
- return
200
- ;;
201
- --style)
202
- COMPREPLY=($(compgen -W "vivid natural" -- "$cur"))
203
- return
204
- ;;
205
- --type)
206
- COMPREPLY=($(compgen -W "text image video" -- "$cur"))
207
- return
208
- ;;
209
- esac
210
-
211
- case "$subcmd" in
212
- text)
213
- COMPREPLY=($(compgen -W "${[...GLOBAL_FLAGS, ...TEXT_FLAGS].join(" ")}" -- "$cur"))
214
- ;;
215
- image)
216
- COMPREPLY=($(compgen -W "${[...GLOBAL_FLAGS, ...IMAGE_FLAGS].join(" ")}" -- "$cur"))
217
- ;;
218
- video)
219
- COMPREPLY=($(compgen -W "${[...GLOBAL_FLAGS, ...VIDEO_FLAGS].join(" ")}" -- "$cur"))
220
- ;;
221
- models)
222
- COMPREPLY=($(compgen -W "${MODEL_FLAGS.join(" ")}" -- "$cur"))
223
- ;;
224
- completions)
225
- COMPREPLY=($(compgen -W "zsh bash fish" -- "$cur"))
226
- ;;
227
- esac
228
- }
229
-
230
- complete -F _ai_completions ai
231
- `;
232
- }
233
-
234
- function generateFish(): string {
235
- const lines: string[] = [];
236
- lines.push("# ai completions for fish");
237
- lines.push("");
238
-
239
- for (const sub of SUBCOMMANDS) {
240
- lines.push(`complete -c ai -n '__fish_use_subcommand' -a '${sub}'`);
241
- }
242
- lines.push("");
243
-
244
- const SHORT_FLAG_MAP: Record<string, string> = {
245
- "--model": "m",
246
- "--output": "o",
247
- "--count": "n",
248
- "--concurrency": "p",
249
- "--quiet": "q",
250
- "--format": "f",
251
- "--system": "s",
252
- "--temperature": "t",
253
- };
254
-
255
- const addFlags = (sub: string, flags: string[]) => {
256
- for (const flag of flags) {
257
- const name = flag.replace(/^--/, "");
258
- const short = SHORT_FLAG_MAP[flag];
259
- const shortPart = short ? ` -s ${short}` : "";
260
- lines.push(
261
- `complete -c ai -n '__fish_seen_subcommand_from ${sub}'${shortPart} -l '${name}'`
262
- );
263
- }
264
- };
265
-
266
- addFlags("text", [...GLOBAL_FLAGS, ...TEXT_FLAGS]);
267
- addFlags("image", [...GLOBAL_FLAGS, ...IMAGE_FLAGS]);
268
- addFlags("video", [...GLOBAL_FLAGS, ...VIDEO_FLAGS]);
269
- addFlags("models", MODEL_FLAGS);
270
-
271
- lines.push("");
272
- lines.push(
273
- `complete -c ai -n '__fish_seen_subcommand_from completions' -a 'zsh bash fish'`
274
- );
275
- lines.push("");
276
-
277
- const modelCompletions = ALL_MODELS.concat(MODEL_NAMES);
278
- lines.push(
279
- `complete -c ai -n '__fish_seen_subcommand_from text image video' -s m -l model -a '${modelCompletions.join(" ")}'`
280
- );
281
- lines.push(
282
- `complete -c ai -n '__fish_seen_subcommand_from text' -s f -l format -a 'md txt'`
283
- );
284
- lines.push(
285
- `complete -c ai -n '__fish_seen_subcommand_from image' -l quality -a 'standard hd'`
286
- );
287
- lines.push(
288
- `complete -c ai -n '__fish_seen_subcommand_from image' -l style -a 'vivid natural'`
289
- );
290
- lines.push(
291
- `complete -c ai -n '__fish_seen_subcommand_from models' -l type -a 'text image video'`
292
- );
293
- lines.push("");
294
-
295
- return lines.join("\n");
296
- }