ai-cli 0.0.13 → 0.1.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.
- package/README.md +67 -246
- package/package.json +33 -34
- package/src/cli.test.ts +95 -0
- package/src/commands/completions.ts +296 -0
- package/src/commands/image.ts +136 -0
- package/src/commands/models.ts +117 -0
- package/src/commands/text.ts +117 -0
- package/src/commands/video.ts +113 -0
- package/src/index.ts +30 -0
- package/src/lib/color.ts +5 -0
- package/src/lib/h264-wasm.ts +164 -0
- package/src/lib/h264.test.ts +48 -0
- package/src/lib/jobs.ts +192 -0
- package/src/lib/kitty.ts +55 -0
- package/src/lib/models.test.ts +197 -0
- package/src/lib/models.ts +163 -0
- package/src/lib/mp4.test.ts +231 -0
- package/src/lib/mp4.ts +560 -0
- package/src/lib/openh264.d.mts +28 -0
- package/src/lib/openh264.mjs +423 -0
- package/src/lib/openh264.wasm +0 -0
- package/src/lib/openh264.wasm.d.ts +2 -0
- package/src/lib/output.ts +97 -0
- package/src/lib/p-map.test.ts +63 -0
- package/src/lib/p-map.ts +30 -0
- package/src/lib/parse.test.ts +114 -0
- package/src/lib/parse.ts +44 -0
- package/src/lib/png.test.ts +104 -0
- package/src/lib/png.ts +90 -0
- package/src/lib/progress.ts +214 -0
- package/src/lib/shimmer.test.ts +39 -0
- package/src/lib/shimmer.ts +42 -0
- package/src/lib/stdin.ts +31 -0
- package/dist/ai.mjs +0 -630
|
@@ -0,0 +1,296 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { generateImage, gateway } from "ai";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
import { buildJobs, runJobs } from "../lib/jobs.js";
|
|
5
|
+
import { resolveModels } from "../lib/models.js";
|
|
6
|
+
import { parsePositiveInt, parseSize, parseAspectRatio } from "../lib/parse.js";
|
|
7
|
+
import { readStdin } from "../lib/stdin.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
11
|
+
|
|
12
|
+
interface ImageOptions {
|
|
13
|
+
model?: string;
|
|
14
|
+
output?: string;
|
|
15
|
+
count?: string;
|
|
16
|
+
size?: string;
|
|
17
|
+
aspectRatio?: string;
|
|
18
|
+
quality?: string;
|
|
19
|
+
style?: string;
|
|
20
|
+
quiet?: boolean;
|
|
21
|
+
json?: boolean;
|
|
22
|
+
concurrency?: string;
|
|
23
|
+
preview?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerImageCommand(program: Command) {
|
|
27
|
+
program
|
|
28
|
+
.command("image")
|
|
29
|
+
.description("Generate an image from a prompt")
|
|
30
|
+
.argument("[prompt]", "The prompt to generate an image from")
|
|
31
|
+
.option(
|
|
32
|
+
"-m, --model <model>",
|
|
33
|
+
"Model ID (creator/model-name), comma-separated for multi-model"
|
|
34
|
+
)
|
|
35
|
+
.option("-o, --output <path>", "Output file path or directory")
|
|
36
|
+
.option("-n, --count <n>", "Number of images per model (default: 1)")
|
|
37
|
+
.option("--size <WxH>", "Image size (e.g. 1024x1024)")
|
|
38
|
+
.option("--aspect-ratio <W:H>", "Aspect ratio (e.g. 16:9)")
|
|
39
|
+
.option("--quality <level>", "Quality (standard, hd)")
|
|
40
|
+
.option("--style <style>", "Style (e.g. vivid, natural)")
|
|
41
|
+
.option("-q, --quiet", "Suppress progress output")
|
|
42
|
+
.option("--json", "Output metadata as JSON")
|
|
43
|
+
.option(
|
|
44
|
+
"--no-preview",
|
|
45
|
+
"Disable inline image preview in supported terminals"
|
|
46
|
+
)
|
|
47
|
+
.option(
|
|
48
|
+
"-p, --concurrency <n>",
|
|
49
|
+
`Max parallel generations (default: ${DEFAULT_CONCURRENCY})`
|
|
50
|
+
)
|
|
51
|
+
.action(async (rawPrompt: string | undefined, opts: ImageOptions) => {
|
|
52
|
+
const prompt = rawPrompt?.trim() || undefined;
|
|
53
|
+
const stdin = await readStdin();
|
|
54
|
+
if (!prompt && !stdin) {
|
|
55
|
+
process.stderr.write(
|
|
56
|
+
"Error: prompt is required (provide as argument or pipe via stdin)\n"
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
let imagePrompt: string | { images: Uint8Array[]; text?: string } =
|
|
61
|
+
prompt!;
|
|
62
|
+
if (stdin) {
|
|
63
|
+
imagePrompt = prompt
|
|
64
|
+
? { images: [new Uint8Array(stdin)], text: prompt }
|
|
65
|
+
: { images: [new Uint8Array(stdin)] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const models = resolveModels("image", opts.model);
|
|
69
|
+
const countPerModel = opts.count
|
|
70
|
+
? parsePositiveInt(opts.count, "count")
|
|
71
|
+
: 1;
|
|
72
|
+
const size = opts.size ? parseSize(opts.size) : undefined;
|
|
73
|
+
const aspectRatio = opts.aspectRatio
|
|
74
|
+
? parseAspectRatio(opts.aspectRatio)
|
|
75
|
+
: undefined;
|
|
76
|
+
const provOpts = buildProviderOptions(opts);
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
(opts.quality || opts.style) &&
|
|
80
|
+
models.every((m) => !m.startsWith("openai/"))
|
|
81
|
+
) {
|
|
82
|
+
process.stderr.write(
|
|
83
|
+
"Warning: --quality and --style only apply to OpenAI models\n"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const jobs = buildJobs(models, countPerModel);
|
|
88
|
+
|
|
89
|
+
const { total, failed } = await runJobs(
|
|
90
|
+
jobs,
|
|
91
|
+
async (modelId) => {
|
|
92
|
+
const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
|
|
93
|
+
const result = await generateImage({
|
|
94
|
+
headers: {
|
|
95
|
+
"http-referer": "https://github.com/vercel-labs/ai-cli",
|
|
96
|
+
"x-title": "ai-cli",
|
|
97
|
+
},
|
|
98
|
+
model: gateway.image(modelId),
|
|
99
|
+
prompt: imagePrompt,
|
|
100
|
+
abortSignal: abort,
|
|
101
|
+
n: 1,
|
|
102
|
+
size,
|
|
103
|
+
aspectRatio,
|
|
104
|
+
providerOptions:
|
|
105
|
+
Object.keys(provOpts).length > 0 ? provOpts : undefined,
|
|
106
|
+
});
|
|
107
|
+
return Buffer.from(result.image.uint8Array);
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
noun: "image",
|
|
111
|
+
format: "image",
|
|
112
|
+
outputPath: opts.output,
|
|
113
|
+
quiet: opts.quiet,
|
|
114
|
+
json: opts.json,
|
|
115
|
+
display: opts.preview,
|
|
116
|
+
concurrency: opts.concurrency
|
|
117
|
+
? parsePositiveInt(opts.concurrency, "concurrency")
|
|
118
|
+
: DEFAULT_CONCURRENCY,
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
if (failed === total) process.exit(1);
|
|
122
|
+
if (failed > 0) process.exit(2);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildProviderOptions(
|
|
127
|
+
opts: ImageOptions
|
|
128
|
+
): Record<string, Record<string, string>> {
|
|
129
|
+
const providerOptions: Record<string, Record<string, string>> = {};
|
|
130
|
+
if (opts.quality || opts.style) {
|
|
131
|
+
providerOptions.openai = {};
|
|
132
|
+
if (opts.quality) providerOptions.openai.quality = opts.quality;
|
|
133
|
+
if (opts.style) providerOptions.openai.style = opts.style;
|
|
134
|
+
}
|
|
135
|
+
return providerOptions;
|
|
136
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
import { fetchGatewayModels, type ModelEntry } from "../lib/models.js";
|
|
4
|
+
|
|
5
|
+
function groupByProvider(models: ModelEntry[]): Map<string, ModelEntry[]> {
|
|
6
|
+
const groups = new Map<string, ModelEntry[]>();
|
|
7
|
+
for (const m of models) {
|
|
8
|
+
const slash = m.id.indexOf("/");
|
|
9
|
+
const provider = slash !== -1 ? m.id.slice(0, slash) : "other";
|
|
10
|
+
if (!groups.has(provider)) groups.set(provider, []);
|
|
11
|
+
groups.get(provider)!.push(m);
|
|
12
|
+
}
|
|
13
|
+
return new Map(
|
|
14
|
+
[...groups.entries()].sort((a, b) => a[0].localeCompare(b[0]))
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function modelName(id: string): string {
|
|
19
|
+
const slash = id.indexOf("/");
|
|
20
|
+
return slash !== -1 ? id.slice(slash + 1) : id;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerModelsCommand(program: Command) {
|
|
24
|
+
program
|
|
25
|
+
.command("models")
|
|
26
|
+
.description("List available models from AI Gateway")
|
|
27
|
+
.option("--type <type>", "Filter by type: text, image, video")
|
|
28
|
+
.option("--provider <name>", "Filter by provider (e.g. openai, google)")
|
|
29
|
+
.option("--json", "Output as JSON (includes descriptions)")
|
|
30
|
+
.action(
|
|
31
|
+
async (opts: { type?: string; provider?: string; json?: boolean }) => {
|
|
32
|
+
const validTypes = ["text", "image", "video"];
|
|
33
|
+
const filterType = opts.type?.toLowerCase();
|
|
34
|
+
if (filterType && !validTypes.includes(filterType)) {
|
|
35
|
+
process.stderr.write(
|
|
36
|
+
`Error: --type must be one of: ${validTypes.join(", ")} (got "${opts.type}")\n`
|
|
37
|
+
);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const filterProvider = opts.provider?.toLowerCase();
|
|
41
|
+
|
|
42
|
+
const gatewayModels = await fetchGatewayModels();
|
|
43
|
+
|
|
44
|
+
const filterGrouped = (grouped: Map<string, ModelEntry[]>) => {
|
|
45
|
+
if (!filterProvider) return grouped;
|
|
46
|
+
const filtered = new Map<string, ModelEntry[]>();
|
|
47
|
+
for (const [provider, models] of grouped) {
|
|
48
|
+
if (provider.toLowerCase() === filterProvider) {
|
|
49
|
+
filtered.set(provider, models);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return filtered;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (opts.json) {
|
|
56
|
+
const output: Record<string, unknown> = {};
|
|
57
|
+
const jsonMapper = (m: ModelEntry) => ({
|
|
58
|
+
id: m.id,
|
|
59
|
+
...(m.name ? { name: m.name } : {}),
|
|
60
|
+
...(m.description ? { description: m.description } : {}),
|
|
61
|
+
});
|
|
62
|
+
if (!filterType || filterType === "text") {
|
|
63
|
+
output.text = Object.fromEntries(
|
|
64
|
+
[...filterGrouped(groupByProvider(gatewayModels.text))].map(
|
|
65
|
+
([provider, models]) => [provider, models.map(jsonMapper)]
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (!filterType || filterType === "image") {
|
|
70
|
+
output.image = Object.fromEntries(
|
|
71
|
+
[...filterGrouped(groupByProvider(gatewayModels.image))].map(
|
|
72
|
+
([provider, models]) => [provider, models.map(jsonMapper)]
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!filterType || filterType === "video") {
|
|
77
|
+
output.video = Object.fromEntries(
|
|
78
|
+
[...filterGrouped(groupByProvider(gatewayModels.video))].map(
|
|
79
|
+
([provider, models]) => [provider, models.map(jsonMapper)]
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sections: { title: string; entries: ModelEntry[] }[] = [];
|
|
88
|
+
if (!filterType || filterType === "text")
|
|
89
|
+
sections.push({ title: "Text", entries: gatewayModels.text });
|
|
90
|
+
if (!filterType || filterType === "image")
|
|
91
|
+
sections.push({ title: "Image", entries: gatewayModels.image });
|
|
92
|
+
if (!filterType || filterType === "video")
|
|
93
|
+
sections.push({ title: "Video", entries: gatewayModels.video });
|
|
94
|
+
|
|
95
|
+
let totalCount = 0;
|
|
96
|
+
for (const section of sections) {
|
|
97
|
+
const grouped = filterGrouped(groupByProvider(section.entries));
|
|
98
|
+
const count = [...grouped.values()].reduce((s, m) => s + m.length, 0);
|
|
99
|
+
if (count === 0) continue;
|
|
100
|
+
totalCount += count;
|
|
101
|
+
process.stdout.write(`\n${section.title} models (${count}):\n`);
|
|
102
|
+
for (const [provider, models] of grouped) {
|
|
103
|
+
process.stdout.write(`\n ${provider}\n`);
|
|
104
|
+
for (const m of models) {
|
|
105
|
+
process.stdout.write(` ${modelName(m.id)}\n`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (totalCount === 0) {
|
|
111
|
+
process.stderr.write("No models found matching filters\n");
|
|
112
|
+
} else {
|
|
113
|
+
process.stdout.write("\n");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { generateText, gateway } from "ai";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
import { buildJobs, runJobs } from "../lib/jobs.js";
|
|
5
|
+
import { resolveModels } from "../lib/models.js";
|
|
6
|
+
import type { OutputFormat } from "../lib/output.js";
|
|
7
|
+
import { parsePositiveInt, parseTemperature } from "../lib/parse.js";
|
|
8
|
+
import { readStdin, stdinAsText } from "../lib/stdin.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
12
|
+
|
|
13
|
+
interface TextOptions {
|
|
14
|
+
model?: string;
|
|
15
|
+
output?: string;
|
|
16
|
+
format?: string;
|
|
17
|
+
system?: string;
|
|
18
|
+
maxTokens?: string;
|
|
19
|
+
temperature?: string;
|
|
20
|
+
count?: string;
|
|
21
|
+
concurrency?: string;
|
|
22
|
+
quiet?: boolean;
|
|
23
|
+
json?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveFormat(fmt?: string): OutputFormat {
|
|
27
|
+
if (!fmt || fmt === "md") return "md";
|
|
28
|
+
if (fmt === "txt") return "txt";
|
|
29
|
+
throw new Error(`--format must be one of: md, txt (got "${fmt}")`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function registerTextCommand(program: Command) {
|
|
33
|
+
program
|
|
34
|
+
.command("text")
|
|
35
|
+
.description("Generate text from a prompt")
|
|
36
|
+
.argument("[prompt]", "The prompt to generate text from")
|
|
37
|
+
.option(
|
|
38
|
+
"-m, --model <model>",
|
|
39
|
+
"Model ID (creator/model-name), comma-separated for multi-model"
|
|
40
|
+
)
|
|
41
|
+
.option("-o, --output <path>", "Output file path or directory")
|
|
42
|
+
.option("-f, --format <fmt>", "Output format: md, txt (default: md)")
|
|
43
|
+
.option("-n, --count <n>", "Number of generations (default: 1)")
|
|
44
|
+
.option(
|
|
45
|
+
"-p, --concurrency <n>",
|
|
46
|
+
`Max parallel generations (default: ${DEFAULT_CONCURRENCY})`
|
|
47
|
+
)
|
|
48
|
+
.option("-s, --system <prompt>", "System prompt")
|
|
49
|
+
.option("--max-tokens <n>", "Maximum tokens to generate")
|
|
50
|
+
.option("-t, --temperature <n>", "Temperature (0-2)")
|
|
51
|
+
.option("-q, --quiet", "Suppress progress output")
|
|
52
|
+
.option("--json", "Output metadata as JSON")
|
|
53
|
+
.action(async (rawPrompt: string | undefined, opts: TextOptions) => {
|
|
54
|
+
const prompt = rawPrompt?.trim() || undefined;
|
|
55
|
+
const stdin = await readStdin();
|
|
56
|
+
if (!prompt && !stdin) {
|
|
57
|
+
process.stderr.write(
|
|
58
|
+
"Error: prompt is required (provide as argument or pipe via stdin)\n"
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
let fullPrompt: string;
|
|
63
|
+
if (stdin && prompt) {
|
|
64
|
+
fullPrompt = `${stdinAsText(stdin)}\n\n---\n\n${prompt}`;
|
|
65
|
+
} else if (stdin) {
|
|
66
|
+
fullPrompt = stdinAsText(stdin);
|
|
67
|
+
} else {
|
|
68
|
+
fullPrompt = prompt!;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const format = resolveFormat(opts.format);
|
|
72
|
+
const models = resolveModels("text", opts.model);
|
|
73
|
+
const countPerModel = opts.count
|
|
74
|
+
? parsePositiveInt(opts.count, "count")
|
|
75
|
+
: 1;
|
|
76
|
+
const maxTokens = opts.maxTokens
|
|
77
|
+
? parsePositiveInt(opts.maxTokens, "max-tokens")
|
|
78
|
+
: undefined;
|
|
79
|
+
const temperature = opts.temperature
|
|
80
|
+
? parseTemperature(opts.temperature)
|
|
81
|
+
: undefined;
|
|
82
|
+
|
|
83
|
+
const jobs = buildJobs(models, countPerModel);
|
|
84
|
+
|
|
85
|
+
const { total, failed } = await runJobs(
|
|
86
|
+
jobs,
|
|
87
|
+
async (modelId) => {
|
|
88
|
+
const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
|
|
89
|
+
const result = await generateText({
|
|
90
|
+
headers: {
|
|
91
|
+
"http-referer": "https://github.com/vercel-labs/ai-cli",
|
|
92
|
+
"x-title": "ai-cli",
|
|
93
|
+
},
|
|
94
|
+
model: gateway(modelId),
|
|
95
|
+
prompt: fullPrompt,
|
|
96
|
+
system: opts.system,
|
|
97
|
+
maxOutputTokens: maxTokens,
|
|
98
|
+
temperature,
|
|
99
|
+
abortSignal: abort,
|
|
100
|
+
});
|
|
101
|
+
return result.text;
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
noun: "text",
|
|
105
|
+
format,
|
|
106
|
+
outputPath: opts.output,
|
|
107
|
+
quiet: opts.quiet,
|
|
108
|
+
json: opts.json,
|
|
109
|
+
concurrency: opts.concurrency
|
|
110
|
+
? parsePositiveInt(opts.concurrency, "concurrency")
|
|
111
|
+
: DEFAULT_CONCURRENCY,
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
if (failed === total) process.exit(1);
|
|
115
|
+
if (failed > 0) process.exit(2);
|
|
116
|
+
});
|
|
117
|
+
}
|