| 111 | // ─── prompts ─────────────────────────────────────────────────────────────── |
| 112 | |
| 113 | async function searchModel(options: { |
| 114 | message: string; |
| 115 | models: ModelOption[]; |
| 116 | }): Promise<string> { |
| 117 | const choices = options.models.map((m) => ({ |
| 118 | name: m.name === m.id ? m.id : `${m.id} · ${m.name}`, |
| 119 | value: m.id, |
| 120 | })); |
| 121 | |
| 122 | const result = await searchSelect<string, false>({ |
| 123 | message: options.message, |
| 124 | multiple: false, |
| 125 | loop: false, |
| 126 | clearInputWhenSelected: false, |
| 127 | placeholder: 'Type to search models, or enter a custom name', |
| 128 | options: async (search) => { |
| 129 | const trimmed = (search ?? '').trim(); |
| 130 | if (!trimmed) { |
| 131 | return choices; |
| 132 | } |
| 133 | const lowered = trimmed.toLowerCase(); |
| 134 | const filtered = choices.filter((c) => |
| 135 | c.value.toLowerCase().includes(lowered) || c.name.toLowerCase().includes(lowered), |
| 136 | ); |
| 137 | const hasExact = choices.some((c) => c.value === trimmed); |
| 138 | if (!hasExact) { |
| 139 | filtered.unshift({ name: `${trimmed} (custom)`, value: trimmed }); |
| 140 | } |
| 141 | return filtered; |
| 142 | }, |
| 143 | }); |
| 144 | if (result === null) { |
| 145 | throw new Error('Model name is required'); |
| 146 | } |
| 147 | return result; |
| 148 | } |
| 149 | |
| 150 | async function ensureApiKey(provider: Provider, env: EnvVars): Promise<string> { |
| 151 | const envKey = PROVIDER_ENV_KEYS[provider] ?? `${provider.toUpperCase().replace(/-/g, '_')}_API_KEY`; |