|
1 | 1 | import { callAI, type Message, type CallAIOptions } from 'call-ai'; |
2 | | -import { CALLAI_ENDPOINT } from './config/env'; |
| 2 | +import { CALLAI_ENDPOINT, APP_MODE } from './config/env'; |
3 | 3 | // Import all LLM text files statically |
4 | 4 | import callaiTxt from './llms/callai.txt?raw'; |
5 | 5 | import fireproofTxt from './llms/fireproof.txt?raw'; |
@@ -31,16 +31,26 @@ export function isValidModelId(id: unknown): id is string { |
31 | 31 | } |
32 | 32 |
|
33 | 33 | // Relaxed validator for any reasonable model ID format (for custom models) |
34 | | -function isReasonableModelId(id: unknown): id is string { |
35 | | - return typeof id === 'string' && id.trim().length > 0 && /^[a-zA-Z0-9\/_.-]+$/.test(id.trim()); |
| 34 | +function normalizeModelIdInternal(id: unknown): string | undefined { |
| 35 | + if (typeof id !== 'string') return undefined; |
| 36 | + const trimmed = id.trim(); |
| 37 | + return trimmed.length > 0 ? trimmed : undefined; |
| 38 | +} |
| 39 | + |
| 40 | +export function normalizeModelId(id: unknown): string | undefined { |
| 41 | + return normalizeModelIdInternal(id); |
| 42 | +} |
| 43 | + |
| 44 | +export function isPermittedModelId(id: unknown): id is string { |
| 45 | + return typeof normalizeModelIdInternal(id) === 'string'; |
36 | 46 | } |
37 | 47 |
|
38 | 48 | // Resolve the effective model id given optional session and global settings |
39 | 49 | export function resolveEffectiveModel(settingsDoc?: UserSettings, vibeDoc?: VibeDocument): string { |
40 | | - const sessionChoice = vibeDoc?.selectedModel; |
41 | | - if (isReasonableModelId(sessionChoice)) return sessionChoice; |
42 | | - const globalChoice = settingsDoc?.model; |
43 | | - if (isReasonableModelId(globalChoice)) return globalChoice; |
| 50 | + const sessionChoice = normalizeModelIdInternal(vibeDoc?.selectedModel); |
| 51 | + if (sessionChoice) return sessionChoice; |
| 52 | + const globalChoice = normalizeModelIdInternal(settingsDoc?.model); |
| 53 | + if (globalChoice) return globalChoice; |
44 | 54 | return DEFAULT_CODING_MODEL; |
45 | 55 | } |
46 | 56 |
|
@@ -123,6 +133,10 @@ export async function selectLlmsAndOptions( |
123 | 133 | userPrompt: string, |
124 | 134 | history: HistoryMessage[] |
125 | 135 | ): Promise<LlmSelectionDecisions> { |
| 136 | + // In test mode, avoid network and return all modules to keep deterministic coverage |
| 137 | + if (APP_MODE === 'test' && !/localhost|127\.0\.0\.1/i.test(String(CALLAI_ENDPOINT))) { |
| 138 | + return { selected: llmsCatalog.map((l) => l.name), instructionalText: true, demoData: true }; |
| 139 | + } |
126 | 140 | const catalog = llmsCatalog.map((l) => ({ name: l.name, description: l.description || '' })); |
127 | 141 | const payload = { catalog, userPrompt: userPrompt || '', history: history || [] }; |
128 | 142 |
|
@@ -156,7 +170,20 @@ export async function selectLlmsAndOptions( |
156 | 170 | }; |
157 | 171 |
|
158 | 172 | try { |
159 | | - const raw = (await callAI(messages, options)) as string; |
| 173 | + // Add a soft timeout to prevent hanging if the model service is unreachable |
| 174 | + const withTimeout = <T>(p: Promise<T>, ms = 4000): Promise<T> => |
| 175 | + new Promise<T>((resolve, reject) => { |
| 176 | + const t = setTimeout(() => reject(new Error('callAI timeout')), ms); |
| 177 | + p.then((v) => { |
| 178 | + clearTimeout(t); |
| 179 | + resolve(v); |
| 180 | + }).catch((e) => { |
| 181 | + clearTimeout(t); |
| 182 | + reject(e); |
| 183 | + }); |
| 184 | + }); |
| 185 | + |
| 186 | + const raw = (await withTimeout(callAI(messages, options))) as string; |
160 | 187 | const parsed = JSON.parse(raw) ?? {}; |
161 | 188 | const selected = Array.isArray(parsed?.selected) |
162 | 189 | ? parsed.selected.filter((v: unknown) => typeof v === 'string') |
|
0 commit comments