mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:21:10 +02:00
fix(llm): user-friendly error messages when no LLM tier available
Track skip reasons per tier in the orchestrator (no-consent,
no-backend, not-available, not-ready, runtime-error) and expose
them via NoTierAvailableError.getUserMessage() with actionable
German text pointing the user to the right settings page.
Before: "No tier could run task 'companion.chat' (attempted: cloud)"
After: "Cloud (Gemini): Cloud-Einwilligung fehlt. Aktiviere sie
unter Einstellungen → KI."
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c642e1be78
commit
2b96953ad1
3 changed files with 72 additions and 4 deletions
|
|
@ -11,7 +11,7 @@
|
|||
* routes through text-completion).
|
||||
*/
|
||||
|
||||
import { llmOrchestrator } from '@mana/shared-llm';
|
||||
import { llmOrchestrator, NoTierAvailableError } from '@mana/shared-llm';
|
||||
import { isLocalLlmSupported, getLocalLlmStatus, loadLocalLlm } from '@mana/local-llm';
|
||||
import { companionChatTask } from '$lib/llm-tasks/companion-chat';
|
||||
import { generateContextDocument } from '$lib/data/projections/context-document';
|
||||
|
|
@ -54,8 +54,11 @@ async function callLlm(messages: LlmMessage[], onToken?: (token: string) => void
|
|||
});
|
||||
return result.value.content;
|
||||
} catch (err) {
|
||||
if (err instanceof NoTierAvailableError) {
|
||||
return err.getUserMessage();
|
||||
}
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return `LLM nicht verfuegbar: ${msg}`;
|
||||
return `LLM nicht verfügbar: ${msg}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,15 +13,70 @@ export class LlmError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
/** Why a specific tier was skipped. */
|
||||
export type TierSkipReason =
|
||||
| 'no-consent'
|
||||
| 'no-backend'
|
||||
| 'not-available'
|
||||
| 'not-ready'
|
||||
| 'no-tiers-configured'
|
||||
| 'runtime-error';
|
||||
|
||||
export interface SkippedTier {
|
||||
tier: LlmTier;
|
||||
reason: TierSkipReason;
|
||||
}
|
||||
|
||||
/** No tier from the user's preference list was able to run the task. */
|
||||
export class NoTierAvailableError extends LlmError {
|
||||
constructor(
|
||||
public readonly taskName: string,
|
||||
public readonly attempted: LlmTier[]
|
||||
public readonly attempted: LlmTier[],
|
||||
public readonly skipped: SkippedTier[] = []
|
||||
) {
|
||||
super(`No tier could run task '${taskName}' (attempted: ${attempted.join(', ') || 'none'})`);
|
||||
this.name = 'NoTierAvailableError';
|
||||
}
|
||||
|
||||
/** User-friendly German explanation of what went wrong. */
|
||||
getUserMessage(): string {
|
||||
if (this.skipped.length === 0 && this.attempted.length === 0) {
|
||||
return 'Kein KI-Modell konfiguriert. Aktiviere ein Modell unter Einstellungen → KI.';
|
||||
}
|
||||
|
||||
const reasons = this.skipped.map((s) => {
|
||||
switch (s.reason) {
|
||||
case 'no-consent':
|
||||
return `**${tierLabel(s.tier)}**: Cloud-Einwilligung fehlt. Aktiviere sie unter Einstellungen → KI.`;
|
||||
case 'no-backend':
|
||||
return `**${tierLabel(s.tier)}**: Backend nicht registriert.`;
|
||||
case 'not-available':
|
||||
return `**${tierLabel(s.tier)}**: Nicht verfügbar (Service läuft nicht oder WebGPU nicht unterstützt).`;
|
||||
case 'not-ready':
|
||||
return `**${tierLabel(s.tier)}**: Modell noch nicht geladen.`;
|
||||
case 'runtime-error':
|
||||
return `**${tierLabel(s.tier)}**: Fehler bei der Ausführung.`;
|
||||
case 'no-tiers-configured':
|
||||
return 'Kein KI-Modell konfiguriert.';
|
||||
}
|
||||
});
|
||||
return reasons.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
function tierLabel(tier: LlmTier): string {
|
||||
switch (tier) {
|
||||
case 'browser':
|
||||
return 'Browser (lokal)';
|
||||
case 'mana-server':
|
||||
return 'Mana Server';
|
||||
case 'cloud':
|
||||
return 'Cloud (Gemini)';
|
||||
case 'byok':
|
||||
return 'Eigener API-Key';
|
||||
default:
|
||||
return String(tier);
|
||||
}
|
||||
}
|
||||
|
||||
/** The user's chosen tier is below the task's declared minimum tier. */
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
NoTierAvailableError,
|
||||
ProviderBlockedError,
|
||||
TierTooLowError,
|
||||
type SkippedTier,
|
||||
} from './errors';
|
||||
import type { LlmTask } from './task';
|
||||
import type { LlmTier } from './tiers';
|
||||
|
|
@ -146,6 +147,7 @@ export class LlmOrchestrator {
|
|||
const orderedTiers = override ? [override].filter((t) => candidates.includes(t)) : candidates;
|
||||
|
||||
// Rule 4-5: try the first runnable tier
|
||||
const skipped: SkippedTier[] = [];
|
||||
for (const tier of orderedTiers) {
|
||||
if (tier === 'none') {
|
||||
if (task.runRules) {
|
||||
|
|
@ -164,21 +166,25 @@ export class LlmOrchestrator {
|
|||
// Cloud-consent gate
|
||||
if (tier === 'cloud' && !this.settings.cloudConsentGiven) {
|
||||
attempted.push('cloud');
|
||||
skipped.push({ tier: 'cloud', reason: 'no-consent' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const backend = this.backendsByTier.get(tier);
|
||||
if (!backend) {
|
||||
attempted.push(tier);
|
||||
skipped.push({ tier, reason: 'no-backend' });
|
||||
continue;
|
||||
}
|
||||
if (!backend.isAvailable()) {
|
||||
attempted.push(tier);
|
||||
skipped.push({ tier, reason: 'not-available' });
|
||||
continue;
|
||||
}
|
||||
const ready = await backend.isReady();
|
||||
if (!ready) {
|
||||
attempted.push(tier);
|
||||
skipped.push({ tier, reason: 'not-ready' });
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -226,11 +232,15 @@ export class LlmOrchestrator {
|
|||
throw err;
|
||||
}
|
||||
// Unknown error — try the next tier in the list
|
||||
skipped.push({ tier, reason: 'runtime-error' });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new NoTierAvailableError(task.name, attempted);
|
||||
if (attempted.length === 0) {
|
||||
skipped.push({ tier: 'none' as LlmTier, reason: 'no-tiers-configured' });
|
||||
}
|
||||
throw new NoTierAvailableError(task.name, attempted, skipped);
|
||||
}
|
||||
|
||||
/** Highest tier in the user's allowedTiers list (by rank). */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue