Skip to content

Commit 4aa9eb2

Browse files
committed
fix: add model fallback chain to enhancer + dependency install enforcement in system prompt
1 parent 40e8ff3 commit 4aa9eb2

2 files changed

Lines changed: 132 additions & 26 deletions

File tree

app/lib/common/prompts/new-prompt.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ export const getFineTunedPrompt = (
101101
- If the request is complex, REDUCE feature scope but ALWAYS include: App.tsx with working UI, at least one feature component, all routing, and state management
102102
- SELF-CHECK before finishing: Count your component files (*.tsx with JSX). If the count is ZERO, you have failed — go back and write the components
103103
- File priority: App.tsx and feature components FIRST, config files SECOND. Never stop after config files
104+
105+
DEPENDENCY INSTALLATION (CRITICAL — SECOND MOST COMMON FAILURE):
106+
- For EVERY third-party package imported in your code, you MUST include a shell action: npm install <package>
107+
- SELF-CHECK before finishing: Scan every import statement. If a package is NOT in the starter template's package.json, it MUST have a corresponding npm install action
108+
- Common missed packages: @dnd-kit/core, @dnd-kit/sortable, class-variance-authority, clsx, tailwind-merge, zustand, @tanstack/react-query, framer-motion, react-icons, lucide-react, recharts, react-router-dom, @radix-ui/*
109+
- Combine all installs into ONE shell action when possible: npm install pkg1 pkg2 pkg3
110+
- Place npm install AFTER package.json is written but BEFORE npm run dev
111+
- If you import from a package and forget to install it, the app WILL crash with "Module not found" errors
104112
</completeness_requirements>
105113
106114
<response_requirements>

app/routes/api.enhancer.ts

Lines changed: 124 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { AppError, AppErrorType } from '~/lib/api/errors';
1212
import { AUTH_PRESETS } from '~/lib/security-config';
1313
import { DEFAULT_PROVIDER, PROVIDER_LIST } from '~/utils/constants';
1414
import { resolveModel } from '~/lib/.server/llm/resolve-model';
15+
import { LLMManager } from '~/lib/modules/llm/manager';
1516

1617
export const action = withSecurity(enhancerAction, {
1718
auth: AUTH_PRESETS.authenticated,
@@ -22,6 +23,26 @@ export const action = withSecurity(enhancerAction, {
2223

2324
const logger = createScopedLogger('api.enhancer');
2425

26+
const MAX_FALLBACK_ATTEMPTS = 3;
27+
28+
/**
29+
* Checks whether an error indicates the model is unavailable (deprecated, removed, not found).
30+
* Only these errors warrant a fallback attempt — auth errors, rate limits, and other
31+
* failures should propagate immediately.
32+
*/
33+
function isModelNotFoundError(error: unknown): boolean {
34+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
35+
const status = (error as { status?: number })?.status ?? (error as { statusCode?: number })?.statusCode;
36+
37+
return (
38+
status === 404 ||
39+
(message.includes('not found') && (message.includes('model') || message.includes('models/'))) ||
40+
message.includes('model_not_found') ||
41+
message.includes('does not exist') ||
42+
message.includes('deprecated')
43+
);
44+
}
45+
2546
// providerSchema imported from ~/lib/api/schemas
2647

2748
const enhancerRequestSchema = z.object({
@@ -73,6 +94,29 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
7394
logger,
7495
});
7596

97+
// Build the shared generateText parameters (reused across primary + fallback attempts)
98+
const systemPrompt =
99+
"You are a prompt engineer for an AI web app builder. The builder runs locally with Node.js and creates complete apps using React (default), Vue, Svelte, or Angular with Tailwind CSS. Apps use local state management and seed data — never external APIs with API keys. Your job: take the user's idea and produce a clear, specific, buildable prompt. Output ONLY the enhanced prompt text.";
100+
const userPrompt = stripIndents`
101+
Enhance the user's prompt so an AI coding assistant can build a complete, working app in one response.
102+
103+
<original_prompt>
104+
${message}
105+
</original_prompt>
106+
107+
Enhancement rules:
108+
1. PRESERVE the user's core intent — do NOT change what they want to build
109+
2. If the app has multiple pages/views, LIST each page and its purpose explicitly
110+
3. For data-driven apps, DEFINE the data model (entity names, key fields, relationships)
111+
4. Specify interactive features: CRUD operations, filters, search, sorting, modals, form validation
112+
5. Add a brief design direction ONLY if the user gave none (e.g., "clean minimal dark theme with blue accents")
113+
6. Mention responsive behavior: sidebar collapses on mobile, grid stacks to single column, etc.
114+
7. If the app needs sample data, say "populate with realistic seed data" — NEVER suggest external API calls
115+
8. Keep the enhanced prompt concise — add only details that prevent ambiguity
116+
9. NEVER add: external APIs, API keys, deployment, hosting, CI/CD, testing, or authentication unless the user asked for it
117+
10. Output ONLY the enhanced prompt — no explanations, headers, or wrapper tags
118+
`;
119+
76120
try {
77121
const result = await generateText({
78122
model: resolvedProvider.getModelInstance({
@@ -81,27 +125,8 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
81125
apiKeys,
82126
providerSettings,
83127
}),
84-
system:
85-
"You are a prompt engineer for an AI web app builder. The builder runs locally with Node.js and creates complete apps using React (default), Vue, Svelte, or Angular with Tailwind CSS. Apps use local state management and seed data — never external APIs with API keys. Your job: take the user's idea and produce a clear, specific, buildable prompt. Output ONLY the enhanced prompt text.",
86-
prompt: stripIndents`
87-
Enhance the user's prompt so an AI coding assistant can build a complete, working app in one response.
88-
89-
<original_prompt>
90-
${message}
91-
</original_prompt>
92-
93-
Enhancement rules:
94-
1. PRESERVE the user's core intent — do NOT change what they want to build
95-
2. If the app has multiple pages/views, LIST each page and its purpose explicitly
96-
3. For data-driven apps, DEFINE the data model (entity names, key fields, relationships)
97-
4. Specify interactive features: CRUD operations, filters, search, sorting, modals, form validation
98-
5. Add a brief design direction ONLY if the user gave none (e.g., "clean minimal dark theme with blue accents")
99-
6. Mention responsive behavior: sidebar collapses on mobile, grid stacks to single column, etc.
100-
7. If the app needs sample data, say "populate with realistic seed data" — NEVER suggest external API calls
101-
8. Keep the enhanced prompt concise — add only details that prevent ambiguity
102-
9. NEVER add: external APIs, API keys, deployment, hosting, CI/CD, testing, or authentication unless the user asked for it
103-
10. Output ONLY the enhanced prompt — no explanations, headers, or wrapper tags
104-
`,
128+
system: systemPrompt,
129+
prompt: userPrompt,
105130
});
106131

107132
return new Response(result.text, {
@@ -110,11 +135,84 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
110135
'Content-Type': 'text/plain; charset=utf-8',
111136
},
112137
});
113-
} catch (error: unknown) {
114-
logger.error(error);
138+
} catch (primaryError: unknown) {
139+
logger.error(
140+
`Primary model ${providerName}/${modelDetails.name} failed:`,
141+
primaryError instanceof Error ? primaryError.message : String(primaryError),
142+
);
143+
144+
// Only attempt fallback for model-not-found errors (deprecated, removed, 404).
145+
// Auth errors, rate limits, and other failures propagate immediately.
146+
if (isModelNotFoundError(primaryError)) {
147+
const llm = LLMManager.getInstance();
148+
let candidateModels = llm.getModelList().filter(
149+
(m) => m.provider === resolvedProvider.name && m.name !== modelDetails.name,
150+
);
151+
152+
// If the full model list has no alternatives, fall back to static list
153+
if (candidateModels.length === 0) {
154+
candidateModels = llm.getStaticModelListFromProvider(resolvedProvider).filter(
155+
(m) => m.name !== modelDetails.name,
156+
);
157+
}
158+
159+
// Limit fallback attempts to avoid excessive API calls
160+
const attemptsToTry = candidateModels.slice(0, MAX_FALLBACK_ATTEMPTS);
161+
162+
for (const candidate of attemptsToTry) {
163+
logger.info(`Trying fallback candidate: ${resolvedProvider.name}/${candidate.name}`);
164+
165+
try {
166+
const candidateModelDetails = await resolveModel({
167+
provider: resolvedProvider,
168+
currentModel: candidate.name,
169+
apiKeys,
170+
providerSettings,
171+
serverEnv: context.cloudflare?.env,
172+
logger,
173+
});
174+
175+
const fallbackResult = await generateText({
176+
model: resolvedProvider.getModelInstance({
177+
model: candidateModelDetails.name,
178+
serverEnv: context.cloudflare?.env,
179+
apiKeys,
180+
providerSettings,
181+
}),
182+
system: systemPrompt,
183+
prompt: userPrompt,
184+
});
185+
186+
logger.info(
187+
`Fallback succeeded: ${resolvedProvider.name}/${candidateModelDetails.name} ` +
188+
`(primary ${providerName}/${modelDetails.name} was unavailable)`,
189+
);
190+
191+
return new Response(fallbackResult.text, {
192+
status: 200,
193+
headers: {
194+
'Content-Type': 'text/plain; charset=utf-8',
195+
},
196+
});
197+
} catch (candidateError: unknown) {
198+
logger.warn(
199+
`Fallback candidate ${resolvedProvider.name}/${candidate.name} failed: ` +
200+
`${candidateError instanceof Error ? candidateError.message : String(candidateError)}`,
201+
);
202+
// Continue to next candidate
203+
}
204+
}
205+
206+
// All fallback candidates exhausted
207+
logger.error(
208+
`All ${attemptsToTry.length} fallback candidates for ${resolvedProvider.name} failed ` +
209+
`(primary model ${modelDetails.name} was unavailable)`,
210+
);
211+
}
115212

116-
if (error instanceof Error) {
117-
const msg = error.message.toLowerCase();
213+
// Auth error detection — surface a clear message for API key issues
214+
if (primaryError instanceof Error) {
215+
const msg = primaryError.message.toLowerCase();
118216

119217
if (
120218
msg.includes('api key') ||
@@ -130,6 +228,6 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
130228
}
131229
}
132230

133-
return errorResponse(error instanceof Error ? error : new AppError(AppErrorType.INTERNAL, 'Internal Server Error'));
231+
return errorResponse(primaryError instanceof Error ? primaryError : new AppError(AppErrorType.INTERNAL, 'Internal Server Error'));
134232
}
135233
}

0 commit comments

Comments
 (0)