Task 14: Port StructuredModel mit Anthropic-Adapter (@anthropic-ai/sdk 0.131.0 exakt, Lieferkette geprüft), fester Endpunkt https://api.anthropic.com (Umgebungs-Header werden entfernt), feste Systemregeln ohne Datum/Volk/Person, Transkript als Daten in Tags, strukturierte Ausgabe per JSON-Schema mit effort low, serverseitiger Fallback per VOICE_SERVER_FALLBACK abschaltbar. Jeder Vorschlag wird geprüft: bekanntes Feld, Wert in den Formulargrenzen, Zitat steht wörtlich im Gesagten (Dezimalzahl als ein Wort), Korrektur gewinnt. Netzwächter: nur die Worker-Verdrahtung erreicht den Adapter (transitiv geprüft). Task 15: Worker-Schleife voice-extractions (LISTEN + 15-s-Abfrage, ein Lauf zugleich, höchstens 20 Notizen je Lauf), Modellaufruf außerhalb jeder Transaktion mit 240-s-Obergrenze, Ergebnis versiegelt mit Ereignis nur bei gehaltenem Claim; ohne Schlüssel EXTRACT_DISABLED ohne Versuch, Tageslimit zählt jeden bezahlten Aufruf über das neue Journal VoiceExtractionCall (nur IDs, Modell, Tokens, Ergebnis; 90 Tage, nicht im Backup). Nur der Worker-Container erhält ANTHROPIC_API_KEY aus dem Secret apicore-anthropic (optional); API- und Job-Rollen warnen, wenn der Schlüssel bei ihnen gesetzt ist. Gates: Backend lint, typecheck, 4589 Unit-Tests, build; e2e 104 Suites (nur der Wetter-Test gegen die echte API schwankt); Manifest-Policy 55. Refs #27 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
122 lines
3.5 KiB
TypeScript
122 lines
3.5 KiB
TypeScript
/**
|
|
* Voice settings from the environment. Model ids live here and nowhere else
|
|
* (spec 7.1): a change of model is a change of this list and of VOICE_MODEL.
|
|
*/
|
|
export const VOICE_MODELS = [
|
|
'claude-sonnet-5-5',
|
|
'claude-haiku-5-5',
|
|
'claude-opus-5-5',
|
|
] as const;
|
|
export type VoiceModel = (typeof VOICE_MODELS)[number];
|
|
|
|
export const DEFAULT_VOICE_MODEL: VoiceModel = 'claude-sonnet-5-5';
|
|
|
|
/**
|
|
* Models with a server-side fallback when their safety check declines
|
|
* (`fallbacks: "default"`). Haiku has none: a declined request stays
|
|
* declined there.
|
|
*/
|
|
export const VOICE_SERVER_FALLBACK_MODELS: readonly string[] = [
|
|
'claude-sonnet-5-5',
|
|
'claude-opus-5-5',
|
|
];
|
|
|
|
/** VOICE_SERVER_FALLBACK: "default" uses the fallback above, "off" never. */
|
|
export const SERVER_FALLBACK_MODES = ['off', 'default'] as const;
|
|
export type ServerFallbackMode = (typeof SERVER_FALLBACK_MODES)[number];
|
|
|
|
export const WHISPER_MODELS = ['small', 'medium'] as const;
|
|
export type WhisperModel = (typeof WHISPER_MODELS)[number];
|
|
|
|
const DEFAULT_DAILY_EXTRACTIONS = 60;
|
|
const MIN_DAILY_EXTRACTIONS = 1;
|
|
const MAX_DAILY_EXTRACTIONS = 10000;
|
|
|
|
export interface VoiceConfig {
|
|
model: VoiceModel;
|
|
/** Operation-wide limit of Claude calls per UTC day. */
|
|
dailyExtractions: number;
|
|
/** Switch that shows or hides audio recording in the app (ConfigMap). */
|
|
recordingEnabled: boolean;
|
|
whisperModel: WhisperModel;
|
|
/** Whether a declined request may be re-run server-side on another model. */
|
|
serverFallback: ServerFallbackMode;
|
|
}
|
|
|
|
function trimmed(value: string | undefined): string | null {
|
|
const text = value?.trim();
|
|
return text ? text : null;
|
|
}
|
|
|
|
function oneOf<T extends string>(
|
|
name: string,
|
|
raw: string | undefined,
|
|
allowed: readonly T[],
|
|
fallback: T,
|
|
): T {
|
|
const value = trimmed(raw);
|
|
if (value === null) {
|
|
return fallback;
|
|
}
|
|
const match = allowed.find((each) => each === value);
|
|
if (match === undefined) {
|
|
throw new Error(`${name} must be one of ${allowed.join(', ')}`);
|
|
}
|
|
return match;
|
|
}
|
|
|
|
function dailyExtractions(raw: string | undefined): number {
|
|
const value = trimmed(raw);
|
|
if (value === null) {
|
|
return DEFAULT_DAILY_EXTRACTIONS;
|
|
}
|
|
const number = /^\d+$/.test(value) ? Number(value) : NaN;
|
|
if (
|
|
!Number.isInteger(number) ||
|
|
number < MIN_DAILY_EXTRACTIONS ||
|
|
number > MAX_DAILY_EXTRACTIONS
|
|
) {
|
|
throw new Error(
|
|
`VOICE_DAILY_EXTRACTIONS must be a whole number from ${MIN_DAILY_EXTRACTIONS} to ${MAX_DAILY_EXTRACTIONS}`,
|
|
);
|
|
}
|
|
return number;
|
|
}
|
|
|
|
function recordingEnabled(raw: string | undefined): boolean {
|
|
const value = trimmed(raw)?.toLowerCase() ?? null;
|
|
if (value === null || value === 'false') {
|
|
return false;
|
|
}
|
|
if (value === 'true') {
|
|
return true;
|
|
}
|
|
throw new Error('VOICE_RECORDING_ENABLED must be "true" or "false"');
|
|
}
|
|
|
|
/** Invalid values fail with the variable name (never a secret): startup refuses them. */
|
|
export function voiceConfig(env: NodeJS.ProcessEnv = process.env): VoiceConfig {
|
|
return {
|
|
model: oneOf(
|
|
'VOICE_MODEL',
|
|
env.VOICE_MODEL,
|
|
VOICE_MODELS,
|
|
DEFAULT_VOICE_MODEL,
|
|
),
|
|
dailyExtractions: dailyExtractions(env.VOICE_DAILY_EXTRACTIONS),
|
|
recordingEnabled: recordingEnabled(env.VOICE_RECORDING_ENABLED),
|
|
whisperModel: oneOf(
|
|
'WHISPER_MODEL',
|
|
env.WHISPER_MODEL,
|
|
WHISPER_MODELS,
|
|
'small',
|
|
),
|
|
serverFallback: oneOf(
|
|
'VOICE_SERVER_FALLBACK',
|
|
env.VOICE_SERVER_FALLBACK,
|
|
SERVER_FALLBACK_MODES,
|
|
'default',
|
|
),
|
|
};
|
|
}
|