File size: 2,999 Bytes
ee2bffd
 
 
 
 
8132d56
ee2bffd
 
 
 
 
 
 
 
 
d6c1beb
b6e22cf
ee2bffd
 
 
 
 
 
 
 
 
 
 
 
b7bf6e2
 
 
 
 
 
3381f4e
 
 
b7bf6e2
ef103e2
8132d56
 
b7bf6e2
 
 
3381f4e
 
 
 
 
 
 
b7bf6e2
 
 
 
 
 
 
 
69bb4f2
 
 
 
 
 
3381f4e
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Maps raw digest / upstream errors to clear admin-lab messages (not shown to subscribers).
 */
export function classifyDigestAdminError(raw: string): string {
  const s = raw.toLowerCase();
  if (/429|rate limit|too many requests|search capacity|model chain exhausted/i.test(s)) {
    return "Groq rate limit hit — try later or check quota and DIGEST_SCORE_DELAY_MS.";
  }
  if (
    /resend/i.test(raw) ||
    /RESEND_API_KEY/i.test(raw) ||
    (s.includes("resend") && /missing|401|403|invalid/i.test(s))
  ) {
    return "Resend API key missing or invalid — check RESEND_API_KEY and sender domain.";
  }
  if (/HIREBASE|hirebase/i.test(raw) || s.includes("missing hirebase")) {
    return "A removed search provider was referenced. Discovery uses Fantastic Jobs, Firecrawl Search, and SearXNG.";
  }
  if (/groq|GROQ_API_KEY|openrouter|OPENROUTER/i.test(raw) && /key|401|403|missing/i.test(s)) {
    return "Scoring API key missing or rejected — check GROQ_API_KEY / OPENROUTER_API_KEY on the deployment.";
  }
  if (/insufficient credits/i.test(s)) {
    return "Insufficient credits for this user to run a paid digest.";
  }
  if (/no listings|no matches|passed threshold|passed today/i.test(s)) {
    return raw;
  }
  return raw;
}

export type DiscoveryFailureKind = "config" | "quota" | "timeout" | "scoring" | "unknown";

/** Classify discovery tool errors for digest handling and user messaging. */
export function classifyDiscoveryFailure(error: string): DiscoveryFailureKind {
  const s = error.toLowerCase();
  if (/monthly_limit_reached|monthly limit|insufficient credits/i.test(s)) {
    return "quota";
  }
  if (
    /temporarily busy|search inventory is limited|rate.?limit|too many requests|\b429\b|search capacity is temporarily limited|model chain exhausted/i.test(
      s,
    )
  ) {
    return "quota";
  }
  if (
    /temporarily unavailable|couldn't start|authentication failed|not permitted|not configured|missing hirebase|\bhirebase\b|job discovery is temporarily unavailable|contact support|backend (rejected|failed|authentication|is unavailable)|\b401\b|\b403\b|\b50[0-9]\b/i.test(
      s,
    )
  ) {
    return "config";
  }
  if (/timed out|timeout/i.test(s)) {
    return "timeout";
  }
  if (/scoring|score failed|couldn't score/i.test(s)) {
    return "scoring";
  }
  return "unknown";
}

/** True when chat should refund a discover deduct (same kinds digest already refunds). */
export function shouldRefundDiscoverCredits(error: string): boolean {
  const kind = classifyDiscoveryFailure(error);
  return kind === "config" || kind === "quota";
}

/** Timeout with nothing left to resume is a failed charge — refund. Running SearchRuns keep the charge. */
export function shouldRefundTimedOutDiscoverWithoutRun(opts: {
  timedOut?: boolean;
  searchRunId?: string;
  backgroundContinued?: boolean;
}): boolean {
  return (
    opts.timedOut === true &&
    !String(opts.searchRunId ?? "").trim() &&
    opts.backgroundContinued !== true
  );
}