This commit is contained in:
lila 2026-07-06 15:21:52 +02:00
parent 1c4dcdd108
commit e89e3b7a70
6 changed files with 137 additions and 30 deletions

View file

@ -118,6 +118,7 @@ async function main() {
totalTokens: result.metrics.totalTokens, totalTokens: result.metrics.totalTokens,
promptTimeMs: result.metrics.promptTimeMs, promptTimeMs: result.metrics.promptTimeMs,
completionTimeMs: result.metrics.completionTimeMs, completionTimeMs: result.metrics.completionTimeMs,
totalTimeMs: result.metrics.totalTimeMs,
}); });
} }

View file

@ -9,8 +9,9 @@ interface LlmResponse {
promptTokens: number; promptTokens: number;
completionTokens: number; completionTokens: number;
totalTokens: number; totalTokens: number;
promptTimeMs: number; promptTimeMs: number | null;
completionTimeMs: number; completionTimeMs: number | null;
totalTimeMs: number;
} }
export interface EnrichmentResult { export interface EnrichmentResult {
@ -19,8 +20,9 @@ export interface EnrichmentResult {
promptTokens: number; promptTokens: number;
completionTokens: number; completionTokens: number;
totalTokens: number; totalTokens: number;
promptTimeMs: number; promptTimeMs: number | null;
completionTimeMs: number; completionTimeMs: number | null;
totalTimeMs: number;
}; };
} }
@ -54,6 +56,60 @@ function sanitizeLlmOutput(raw: string): string {
return cleaned.slice(start, end + 1); return cleaned.slice(start, end + 1);
} }
function validateSense(item: unknown, word: string, index: number): void {
if (typeof item !== "object" || item === null || Array.isArray(item)) {
throw new Error(`Sense ${index} for "${word}" is not an object`);
}
const sense = item as Record<string, unknown>;
if (typeof sense["sense"] !== "string" || !sense["sense"]) {
throw new Error(`Sense ${index} for "${word}": missing or invalid "sense"`);
}
if (typeof sense["example"] !== "string" || !sense["example"]) {
throw new Error(
`Sense ${index} for "${word}": missing or invalid "example"`,
);
}
if (
!["easy", "medium", "hard"].includes(sense["difficulty_level"] as string)
) {
throw new Error(`Sense ${index} for "${word}": invalid "difficulty_level"`);
}
if (
typeof sense["translations"] !== "object" ||
sense["translations"] === null
) {
throw new Error(`Sense ${index} for "${word}": missing "translations"`);
}
const trans = sense["translations"] as Record<string, unknown>;
for (const lang of ["de", "it", "es", "fr"]) {
if (!Array.isArray(trans[lang])) {
throw new Error(
`Sense ${index} for "${word}": missing or invalid "${lang}" translations`,
);
}
for (let j = 0; j < (trans[lang] as unknown[]).length; j++) {
const t = (trans[lang] as unknown[])[j] as Record<string, unknown>;
if (typeof t["word"] !== "string" || !t["word"]) {
throw new Error(
`Sense ${index} for "${word}": ${lang}[${j}] missing "word"`,
);
}
if (
!["masculine", "feminine", "neuter", null].includes(
t["gender"] as string | null,
)
) {
throw new Error(
`Sense ${index} for "${word}": ${lang}[${j}] invalid "gender"`,
);
}
}
}
}
/** /**
* Parses the LLM response string into a JavaScript object. * Parses the LLM response string into a JavaScript object.
* Throws if the response is not valid JSON or not an object with expected keys. * Throws if the response is not valid JSON or not an object with expected keys.
@ -86,6 +142,12 @@ export function parseLlmResponse(
if (!Array.isArray(obj[word]) || (obj[word] as unknown[]).length === 0) { if (!Array.isArray(obj[word]) || (obj[word] as unknown[]).length === 0) {
throw new Error(`LLM output for "${word}" is not a non-empty array`); throw new Error(`LLM output for "${word}" is not a non-empty array`);
} }
// Validate each sense in the array
const senses = obj[word] as unknown[];
for (let i = 0; i < senses.length; i++) {
validateSense(senses[i], word, i);
}
} }
return obj; return obj;
@ -145,6 +207,7 @@ export async function enrichWord(
totalTokens: llmResponse.totalTokens, totalTokens: llmResponse.totalTokens,
promptTimeMs: llmResponse.promptTimeMs, promptTimeMs: llmResponse.promptTimeMs,
completionTimeMs: llmResponse.completionTimeMs, completionTimeMs: llmResponse.completionTimeMs,
totalTimeMs: llmResponse.totalTimeMs,
}, },
}; };
} }
@ -207,10 +270,13 @@ export async function enrichWordWithRetry(
totalTokens: totalTokens:
leftResult.metrics.totalTokens + rightResult.metrics.totalTokens, leftResult.metrics.totalTokens + rightResult.metrics.totalTokens,
promptTimeMs: promptTimeMs:
leftResult.metrics.promptTimeMs + rightResult.metrics.promptTimeMs, (leftResult.metrics.promptTimeMs ?? 0) +
(rightResult.metrics.promptTimeMs ?? 0),
completionTimeMs: completionTimeMs:
leftResult.metrics.completionTimeMs + (leftResult.metrics.completionTimeMs ?? 0) +
rightResult.metrics.completionTimeMs, (rightResult.metrics.completionTimeMs ?? 0),
totalTimeMs:
leftResult.metrics.totalTimeMs + rightResult.metrics.totalTimeMs,
}; };
return { results: merged, metrics: mergedMetrics }; return { results: merged, metrics: mergedMetrics };

View file

@ -26,8 +26,9 @@ export class GeminiAdapter implements LlmAdapter {
promptTokens: number; promptTokens: number;
completionTokens: number; completionTokens: number;
totalTokens: number; totalTokens: number;
promptTimeMs: number; promptTimeMs: number | null;
completionTimeMs: number; completionTimeMs: number | null;
totalTimeMs: number;
}> { }> {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`; const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
@ -72,8 +73,9 @@ export class GeminiAdapter implements LlmAdapter {
promptTokens, promptTokens,
completionTokens, completionTokens,
totalTokens: json.usageMetadata.totalTokenCount, totalTokens: json.usageMetadata.totalTokenCount,
promptTimeMs: totalTimeMs * 0.3, promptTimeMs: null,
completionTimeMs: totalTimeMs * 0.7, completionTimeMs: null,
totalTimeMs,
}; };
} }
} }

View file

@ -29,8 +29,9 @@ export class OpenAiCompatibleAdapter implements LlmAdapter {
promptTokens: number; promptTokens: number;
completionTokens: number; completionTokens: number;
totalTokens: number; totalTokens: number;
promptTimeMs: number; promptTimeMs: number | null;
completionTimeMs: number; completionTimeMs: number | null;
totalTimeMs: number;
}> { }> {
const payload: Record<string, unknown> = { const payload: Record<string, unknown> = {
messages: [ messages: [
@ -78,16 +79,14 @@ export class OpenAiCompatibleAdapter implements LlmAdapter {
const promptTokens = json.usage.prompt_tokens; const promptTokens = json.usage.prompt_tokens;
const completionTokens = json.usage.completion_tokens; const completionTokens = json.usage.completion_tokens;
const promptTimeMs = json.timings?.prompt_ms ?? totalTimeMs * 0.3;
const completionTimeMs = json.timings?.predicted_ms ?? totalTimeMs * 0.7;
return { return {
content, content,
promptTokens, promptTokens,
completionTokens, completionTokens,
totalTokens: json.usage.total_tokens, totalTokens: json.usage.total_tokens,
promptTimeMs, promptTimeMs: json.timings?.prompt_ms ?? null,
completionTimeMs, completionTimeMs: json.timings?.predicted_ms ?? null,
totalTimeMs,
}; };
} }
} }

View file

@ -7,7 +7,8 @@ export interface LlmAdapter {
promptTokens: number; promptTokens: number;
completionTokens: number; completionTokens: number;
totalTokens: number; totalTokens: number;
promptTimeMs: number; promptTimeMs: number | null;
completionTimeMs: number; completionTimeMs: number | null;
totalTimeMs: number;
}>; }>;
} }

View file

@ -2,8 +2,9 @@ interface LlmMetrics {
promptTokens: number; promptTokens: number;
completionTokens: number; completionTokens: number;
totalTokens: number; totalTokens: number;
promptTimeMs: number; promptTimeMs: number | null;
completionTimeMs: number; completionTimeMs: number | null;
totalTimeMs: number;
} }
interface PipelineMetrics { interface PipelineMetrics {
@ -18,6 +19,7 @@ interface PipelineMetrics {
totalTokens: number; totalTokens: number;
totalPromptTimeMs: number; totalPromptTimeMs: number;
totalCompletionTimeMs: number; totalCompletionTimeMs: number;
totalTimeMs: number;
currentWordStartTime?: Date; currentWordStartTime?: Date;
} }
@ -40,6 +42,7 @@ export class PipelineTimer {
totalTokens: 0, totalTokens: 0,
totalPromptTimeMs: 0, totalPromptTimeMs: 0,
totalCompletionTimeMs: 0, totalCompletionTimeMs: 0,
totalTimeMs: 0,
}; };
} }
@ -59,9 +62,14 @@ export class PipelineTimer {
this.metrics.totalPromptTokens += llmMetrics.promptTokens; this.metrics.totalPromptTokens += llmMetrics.promptTokens;
this.metrics.totalCompletionTokens += llmMetrics.completionTokens; this.metrics.totalCompletionTokens += llmMetrics.completionTokens;
this.metrics.totalTokens += llmMetrics.totalTokens; this.metrics.totalTokens += llmMetrics.totalTokens;
if (llmMetrics.promptTimeMs !== null) {
this.metrics.totalPromptTimeMs += llmMetrics.promptTimeMs; this.metrics.totalPromptTimeMs += llmMetrics.promptTimeMs;
}
if (llmMetrics.completionTimeMs !== null) {
this.metrics.totalCompletionTimeMs += llmMetrics.completionTimeMs; this.metrics.totalCompletionTimeMs += llmMetrics.completionTimeMs;
} }
this.metrics.totalTimeMs += llmMetrics.totalTimeMs;
}
} }
recordSkipped(): void { recordSkipped(): void {
@ -125,22 +133,40 @@ export class PipelineTimer {
0, 0,
) )
: "0"; : "0";
const avgTotalTimeMs =
this.metrics.llmCalls > 0
? (this.metrics.totalTimeMs / this.metrics.llmCalls).toFixed(0)
: "0";
const unifiedThroughput =
this.metrics.totalTimeMs > 0
? (
this.metrics.totalTokens /
(this.metrics.totalTimeMs / 1000)
).toFixed(1)
: "N/A";
const hasDetailedTimings =
this.metrics.totalPromptTimeMs > 0 ||
this.metrics.totalCompletionTimeMs > 0;
const avgPromptSpeed = const avgPromptSpeed =
this.metrics.totalPromptTimeMs > 0 this.metrics.totalPromptTimeMs > 0
? ( ? (
this.metrics.totalPromptTokens / this.metrics.totalPromptTokens /
(this.metrics.totalPromptTimeMs / 1000) (this.metrics.totalPromptTimeMs / 1000)
).toFixed(1) ).toFixed(1)
: "0"; : "N/A";
const avgCompletionSpeed = const avgCompletionSpeed =
this.metrics.totalCompletionTimeMs > 0 this.metrics.totalCompletionTimeMs > 0
? ( ? (
this.metrics.totalCompletionTokens / this.metrics.totalCompletionTokens /
(this.metrics.totalCompletionTimeMs / 1000) (this.metrics.totalCompletionTimeMs / 1000)
).toFixed(1) ).toFixed(1)
: "0"; : "N/A";
return [ const lines = [
`⏱️ Pipeline Summary`, `⏱️ Pipeline Summary`,
` Duration: ${durationSec}s`, ` Duration: ${durationSec}s`,
` Processed: ${this.metrics.wordsProcessed}`, ` Processed: ${this.metrics.wordsProcessed}`,
@ -153,8 +179,20 @@ export class PipelineTimer {
` Calls: ${this.metrics.llmCalls}`, ` Calls: ${this.metrics.llmCalls}`,
` Avg prompt tokens: ${avgPromptTokens}`, ` Avg prompt tokens: ${avgPromptTokens}`,
` Avg completion tokens: ${avgCompletionTokens}`, ` Avg completion tokens: ${avgCompletionTokens}`,
` Avg total tokens: ${avgPromptTokens + avgCompletionTokens}`,
` Avg total request time: ${avgTotalTimeMs}ms`,
` Avg throughput: ${unifiedThroughput} tok/s`,
];
if (hasDetailedTimings) {
lines.push(
``,
` [Local breakdown]`,
` Avg prompt speed: ${avgPromptSpeed} tok/s`, ` Avg prompt speed: ${avgPromptSpeed} tok/s`,
` Avg completion speed: ${avgCompletionSpeed} tok/s`, ` Avg completion speed: ${avgCompletionSpeed} tok/s`,
].join("\n"); );
}
return lines.join("\n");
} }
} }