File size: 4,857 Bytes
ef46e44
 
 
 
 
 
 
 
ff49634
ef46e44
 
 
 
 
 
 
 
ff49634
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b62b0c
ff49634
 
df94b74
 
 
 
 
 
 
ff49634
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef46e44
 
 
 
ff49634
ef46e44
 
 
 
 
 
 
 
 
 
 
ff49634
 
 
 
ef46e44
 
 
 
ff49634
 
ef46e44
 
 
ff49634
ef46e44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff49634
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
export function isNonRetryableProviderError(error: unknown): boolean {
  const message = error instanceof Error ? error.message : String(error);
  const lower = message.toLowerCase();

  if (/openai-compatible api error 5\d\d/i.test(message)) return true;
  if (/api error 5\d\d/i.test(message)) return true;
  if (lower.includes("midstreamfallbackerror")) return true;
  if (lower.includes("apiconnectionerror")) return true;
  if (lower.includes("upstream returned an html error page")) return true;
  if (lower.includes("provider returned html")) return true;
  if (/\berror 502\b/.test(lower) || /\berror 503\b/.test(lower) || /\berror 504\b/.test(lower)) {
    return true;
  }

  return false;
}

function truncate(text: string, maxLen: number): string {
  return text.length <= maxLen ? text : `${text.slice(0, maxLen)}…`;
}

function sectionHint(failedSections: string[]): string {
  return failedSections.length > 0 ? ` Section gagal: ${failedSections.join(", ")}.` : "";
}

/** Turn raw pipeline/client errors into user-facing messages that name the real cause. */
export function formatGenerationErrorMessage(input: {
  cause?: string;
  failedSections?: string[];
}): string {
  const failedSections = [...new Set(input.failedSections ?? [])];
  const hint = sectionHint(failedSections);
  const cause = input.cause?.trim();

  if (!cause) {
    return `Tidak ada soal yang berhasil dibuat.${hint}`;
  }

  const lower = cause.toLowerCase();

  if (
    lower.includes("redacted_thinking") ||
    lower.includes("<thinking>") ||
    /<\/?think>/.test(lower)
  ) {
    return `Model reasoning tidak kompatibel dengan parser Pram.${hint} Coba non-reasoning model, atau update server ke versi terbaru.`;
  }

  if (
    lower.includes("json response was truncated") ||
    lower.includes("unterminated string")
  ) {
    return `Respons model terpotong sebelum JSON selesai (biasanya max tokens terlalu kecil).${hint} Naikkan Max Tokens di Settings atau kurangi jumlah soal/format.`;
  }

  if (lower.includes("failed to parse ai response") || lower.includes("invalid json response")) {
    return `Model merespons, tapi output tidak valid JSON.${hint} ${truncate(cause, 280)}`;
  }

  if (lower.includes("empty response from ai") || lower.includes("empty response body")) {
    return `Model tidak mengembalikan konten.${hint} Coba model lain atau kurangi jumlah soal.`;
  }

  if (lower.includes("no valid questions")) {
    return `Model menghasilkan soal, tapi semuanya gagal validasi.${hint} ${truncate(cause, 280)}`;
  }

  if (
    lower.includes("upstream returned an html error page") ||
    lower.includes("provider returned html") ||
    lower.includes("openai-compatible api error") ||
    /api error 5\d\d/i.test(cause) ||
    lower.includes("apiconnectionerror") ||
    lower.includes("midstreamfallbackerror")
  ) {
    return `Gagal menghubungi API provider.${hint} ${truncate(cause, 280)}`;
  }

  if (lower.includes("metadata/private network") || lower.includes("invalid base url")) {
    return `Base URL provider tidak valid atau tidak dapat diakses dari server.${hint}`;
  }

  return `Generasi gagal.${hint} ${truncate(cause, 280)}`;
}

export function resolveGenerationJobOutcome(input: {
  requestedCount: number;
  generatedCount: number;
  failedSections?: string[];
  cause?: string;
}): {
  status: "completed" | "completed_partial" | "failed";
  progressMessage: string;
  errorMessage: string | null;
} {
  const failedSections = [...new Set(input.failedSections ?? [])];

  if (input.generatedCount <= 0) {
    return {
      status: "failed",
      progressMessage: "Generation failed",
      errorMessage: formatGenerationErrorMessage({
        cause: input.cause,
        failedSections,
      }),
    };
  }

  if (input.generatedCount < input.requestedCount) {
    const sectionHintText =
      failedSections.length > 0 ? ` Section gagal: ${failedSections.join(", ")}.` : "";
    return {
      status: "completed_partial",
      progressMessage: `Selesai sebagian: ${input.generatedCount}/${input.requestedCount} soal`,
      errorMessage: `Hanya ${input.generatedCount} dari ${input.requestedCount} soal berhasil dibuat.${sectionHintText}`,
    };
  }

  return {
    status: "completed",
    progressMessage: "Completed",
    errorMessage: null,
  };
}

export function sectionsWithNoQuestions(
  sectionSplits: Array<{ section: string; count: number }>,
  questions: Array<{ section: string }>,
): string[] {
  return sectionSplits
    .filter((split) => !questions.some((q) => q.section === split.section))
    .map((split) => split.section);
}

export function collectShardFailureCause(
  failures: Iterable<{ error: string } | undefined>,
): string | undefined {
  for (const failure of failures) {
    const message = failure?.error?.trim();
    if (message) return message;
  }
  return undefined;
}