File size: 2,437 Bytes
e249c6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import type { FailoverSignal } from "./signal.js";

export type ProviderRequestFacet =
  | "quota-429"
  | "conversation-state"
  | "provider-internal"
  | "provider-internal-503";

/** Classify copy-sensitive provider-request facts that are finer than FailoverReason. */
export function classifyProviderRequestFacets(signal: FailoverSignal): ProviderRequestFacet | null {
  const message = signal.message ?? "";
  const lower = normalizeLowercaseStringOrEmpty(message);
  const genericProviderError =
    lower.includes("an error occurred while processing your request") ||
    lower.includes("something went wrong while processing your request");
  const providerInternal503 =
    signal.status === 503 ||
    /\b(?:(?:unexpected\s+status|http)\s*503|503\s+service unavailable)\b|["'](?:status|code)["']\s*:\s*503\b/iu.test(
      message,
    );
  // Preserves provider quota/billing guidance for generic HTTP 429 failures.
  if (
    genericProviderError &&
    (signal.status === 429 ||
      /\b(?:http\s*)?429\b|["'](?:status|code)["']\s*:\s*429\b/iu.test(message))
  ) {
    return "quota-429";
  }
  // Preserves /new guidance for provider-rejected conversation state.
  if (isProviderConversationStateError(lower)) {
    return "conversation-state";
  }
  // Preserves the one safe HTTP retry and provider-internal copy for raw 503 failures.
  if (providerInternal503) {
    return "provider-internal-503";
  }
  return lower.includes("the ai service returned an internal error") ||
    lower.includes("provider returned an internal error") ||
    (genericProviderError && (lower.includes("server_error") || lower.includes("internal error")))
    ? "provider-internal"
    : null;
}

function isProviderConversationStateError(lower: string): boolean {
  return (
    (lower.includes("custom tool call output is missing") && lower.includes("call id")) ||
    (lower.includes("toolresult") &&
      lower.includes("tooluse") &&
      lower.includes("exceeds the number") &&
      lower.includes("previous turn")) ||
    (lower.includes("tool_use") && lower.includes("tool_result") && lower.includes("without")) ||
    lower.includes("function call turn comes immediately after") ||
    lower.includes("incorrect role information") ||
    lower.includes("roles must alternate") ||
    lower.includes("invalid_replay_transcript")
  );
}