fix(RECIPE-0001): complete iteration 0 — automatic model loading and error display fix
This commit is contained in:
94
src/lib/server/llm.spec.ts
Normal file
94
src/lib/server/llm.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { checkModelAvailability } from './llm';
|
||||
|
||||
const { mockEnv } = vi.hoisted(() => {
|
||||
return {
|
||||
mockEnv: {
|
||||
OPENAI_BASE_URL: 'http://localhost:1234/v1',
|
||||
OPENAI_API_KEY: 'test-key',
|
||||
LLM_MODEL: 'test-model'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('$env/dynamic/private', () => ({
|
||||
env: mockEnv
|
||||
}));
|
||||
|
||||
const mockModelsList = vi.fn();
|
||||
|
||||
vi.mock('openai', () => ({
|
||||
default: vi.fn(function OpenAI() {
|
||||
return {
|
||||
models: {
|
||||
list: mockModelsList
|
||||
}
|
||||
};
|
||||
})
|
||||
}));
|
||||
|
||||
describe('checkModelAvailability', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return available: true when model is found', async () => {
|
||||
mockModelsList.mockResolvedValue({
|
||||
data: [{ id: 'test-model' }, { id: 'gpt-4o' }, { id: 'llama2' }]
|
||||
});
|
||||
|
||||
const result = await checkModelAvailability('test-model');
|
||||
|
||||
expect(result).toEqual({ available: true });
|
||||
expect(mockModelsList).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should return available: false with message when model not found', async () => {
|
||||
mockModelsList.mockResolvedValue({
|
||||
data: [{ id: 'gpt-4o' }, { id: 'llama2' }]
|
||||
});
|
||||
|
||||
const result = await checkModelAvailability('missing-model');
|
||||
|
||||
expect(result.available).toBe(false);
|
||||
expect(result.message).toContain('Model "missing-model" not found');
|
||||
expect(result.message).toContain('Available models: gpt-4o, llama2');
|
||||
});
|
||||
|
||||
it('should handle API errors gracefully', async () => {
|
||||
mockModelsList.mockRejectedValue(new Error('API connection failed'));
|
||||
|
||||
const result = await checkModelAvailability('test-model');
|
||||
|
||||
expect(result.available).toBe(false);
|
||||
expect(result.message).toContain('Failed to check model availability');
|
||||
expect(result.message).toContain('API connection failed');
|
||||
});
|
||||
|
||||
it('should match models by exact ID (case-sensitive)', async () => {
|
||||
mockModelsList.mockResolvedValue({
|
||||
data: [{ id: 'test-model' }, { id: 'Test-Model' }]
|
||||
});
|
||||
|
||||
const result1 = await checkModelAvailability('test-model');
|
||||
expect(result1.available).toBe(true);
|
||||
|
||||
const result2 = await checkModelAvailability('TEST-MODEL');
|
||||
expect(result2.available).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty model list', async () => {
|
||||
mockModelsList.mockResolvedValue({
|
||||
data: []
|
||||
});
|
||||
|
||||
const result = await checkModelAvailability('any-model');
|
||||
|
||||
expect(result.available).toBe(false);
|
||||
expect(result.message).toContain('Available models: ');
|
||||
});
|
||||
});
|
||||
@@ -40,4 +40,41 @@ export async function checkLLMHealth(): Promise<boolean> {
|
||||
console.error('[LLM] Health check failed:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific model is available in the OpenAI-compatible API
|
||||
* @param model - The model ID to check for availability
|
||||
* @returns Object with available status and optional message
|
||||
*/
|
||||
export async function checkModelAvailability(
|
||||
model: string
|
||||
): Promise<{ available: boolean; message?: string }> {
|
||||
try {
|
||||
console.log('[LLM] Checking model availability:', model);
|
||||
const { client } = createLLM();
|
||||
const response = await client.models.list();
|
||||
const models = response.data || [];
|
||||
|
||||
const foundModel = models.find((m) => m.id === model);
|
||||
|
||||
if (foundModel) {
|
||||
console.log('[LLM] Model available:', model);
|
||||
return { available: true };
|
||||
} else {
|
||||
const availableModels = models.map((m) => m.id).join(', ');
|
||||
console.warn('[LLM] Model not found:', model);
|
||||
console.warn('[LLM] Available models:', availableModels);
|
||||
return {
|
||||
available: false,
|
||||
message: `Model "${model}" not found. Available models: ${availableModels}`
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[LLM] Model availability check failed:', e);
|
||||
return {
|
||||
available: false,
|
||||
message: `Failed to check model availability: ${(e as Error).message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createLLM } from './llm';
|
||||
import { createLLM, checkModelAvailability } from './llm';
|
||||
import { zodResponseFormat } from 'openai/helpers/zod';
|
||||
import { z } from 'zod';
|
||||
import { RECIPE_DETECTION_PROMPT, RECIPE_EXTRACTION_PROMPT } from './prompts/recipe-extraction';
|
||||
@@ -56,6 +56,21 @@ export async function detectRecipe(text: string): Promise<boolean> {
|
||||
} catch (e) {
|
||||
console.error('[LLM] Recipe detection error:', e);
|
||||
console.error('[LLM] Stack trace:', (e as Error).stack);
|
||||
|
||||
// Check if this is a model-related error
|
||||
const errorMessage = (e as Error).message || '';
|
||||
const isModelError = errorMessage.includes('400') &&
|
||||
(errorMessage.toLowerCase().includes('model') ||
|
||||
errorMessage.toLowerCase().includes('load'));
|
||||
|
||||
if (isModelError) {
|
||||
const { model } = createLLM();
|
||||
const modelCheck = await checkModelAvailability(model);
|
||||
if (!modelCheck.available) {
|
||||
throw new Error(modelCheck.message || `Model "${model}" is not available`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to detect recipe: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
@@ -100,6 +115,20 @@ export async function parseRecipe(text: string): Promise<Recipe> {
|
||||
console.error('[LLM] Recipe parsing error:', e);
|
||||
console.error('[LLM] Stack trace:', (e as Error).stack);
|
||||
|
||||
// Check if this is a model-related error
|
||||
const errorMessage = (e as Error).message || '';
|
||||
const isModelError = errorMessage.includes('400') &&
|
||||
(errorMessage.toLowerCase().includes('model') ||
|
||||
errorMessage.toLowerCase().includes('load'));
|
||||
|
||||
if (isModelError) {
|
||||
const { model } = createLLM();
|
||||
const modelCheck = await checkModelAvailability(model);
|
||||
if (!modelCheck.available) {
|
||||
throw new Error(modelCheck.message || `Model "${model}" is not available`);
|
||||
}
|
||||
}
|
||||
|
||||
// If structured output fails, try standard completion
|
||||
if ((e as any).message?.includes('response_format') ||
|
||||
(e as any).message?.includes('structured output')) {
|
||||
|
||||
@@ -202,7 +202,18 @@
|
||||
</svg>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-red-800">Processing Error</div>
|
||||
<div class="text-sm text-red-700 mt-1">{item.error}</div>
|
||||
<div class="text-sm text-red-700 mt-1">
|
||||
{#if typeof item.error === 'object' && item.error?.message}
|
||||
{item.error.message}
|
||||
{:else}
|
||||
{item.error}
|
||||
{/if}
|
||||
</div>
|
||||
{#if typeof item.error === 'object' && item.error?.phase}
|
||||
<div class="text-xs text-red-600 mt-1">
|
||||
Failed during: {item.error.phase}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user