jeo-code 0.8.15 → 0.8.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { isUsageLimitError, isRefusalError, isConnectionError, ConnectionContextError } from "./retry";
8
8
  import { companyLabel } from "../ai/model-catalog";
9
+ import { ProviderHttpError, ProviderStreamError } from "../ai/providers/errors";
9
10
  // Re-export: engine.ts and callers import the refusal predicate from here; the
10
11
  // definition lives in retry.ts so defaultRetryable can fail fast without a cycle.
11
12
  export { isRefusalError } from "./retry";
@@ -15,8 +16,8 @@ export { isRefusalError } from "./retry";
15
16
  * mismatch) — when the PROVIDER says the prompt doesn't fit, the loop can react
16
17
  * (reactive trim + one retry) instead of dying on an opaque 400 (round-6 #4). */
17
18
  export function isContextOverflowError(err: unknown): boolean {
18
- const msg = (err as Error)?.message ?? String(err);
19
- const status = (err as { status?: number })?.status;
19
+ const msg = err instanceof Error ? err.message : String(err);
20
+ const status = statusOf(err);
20
21
  const pattern = /context[ _-]?length|context window|prompt is too long|input is too long|too many tokens|maximum (input|context)|exceeds.{0,30}(context|token)/i;
21
22
  if (pattern.test(msg)) return true;
22
23
  return status === 413; // payload-too-large is always an overflow signal
@@ -32,6 +33,43 @@ function formatDuration(ms: number): string {
32
33
  return rem ? `~${hours}h ${rem}m` : `~${hours}h`;
33
34
  }
34
35
 
36
+ /** Narrow an unknown error to one carrying a numeric `.status` field — covers the
37
+ * known structured classes AND ad-hoc errors a caller/test attaches `.status` to
38
+ * directly (`Object.assign(new Error(...), { status: 413 })`), matching the prior
39
+ * permissive cast-based read without an unchecked `as`. */
40
+ function hasNumericStatus(err: unknown): err is { status: number } {
41
+ return (
42
+ typeof err === "object" &&
43
+ err !== null &&
44
+ "status" in err &&
45
+ typeof err.status === "number"
46
+ );
47
+ }
48
+
49
+ /** True provider label for an error, when it's known: `ProviderHttpError`/
50
+ * `ProviderStreamError` always carry the ACTUAL backend's DISPLAY label (already
51
+ * `companyLabel`'d at every throw site — see `relabelProviderError`'s callers), so
52
+ * use it as-is rather than guessing from message text. `undefined` for anything else
53
+ * (bare `Error`, a re-wrapped doneReason string, …), letting the caller fall back to
54
+ * the regex-based guess below. */
55
+ function knownErrorProvider(err: unknown): string | undefined {
56
+ if (err instanceof ProviderHttpError || err instanceof ProviderStreamError) {
57
+ return err.provider;
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ function statusOf(err: unknown): number | undefined {
63
+ if (err instanceof ProviderHttpError || err instanceof ProviderStreamError) {
64
+ return err.status;
65
+ }
66
+ return hasNumericStatus(err) ? err.status : undefined;
67
+ }
68
+
69
+ function retryAfterMsOf(err: unknown): number | undefined {
70
+ return err instanceof ProviderHttpError ? err.retryAfterMs : undefined;
71
+ }
72
+
35
73
  export function friendlyProviderError(err: unknown): string {
36
74
  if (err instanceof ConnectionContextError) {
37
75
  const provider = companyLabel(err.provider);
@@ -45,23 +83,32 @@ export function friendlyProviderError(err: unknown): string {
45
83
  if (isConnectionError(err)) {
46
84
  return `Could not connect to the provider. Check the configured base URL, your network connection, or switch model with /model.`;
47
85
  }
48
- const msg = (err as Error)?.message ?? String(err);
49
- const status = (err as { status?: number })?.status;
50
- const provider = /antigravity/i.test(msg)
51
- ? "Antigravity"
52
- : /anthropic/i.test(msg)
53
- ? "Anthropic"
54
- : /openai/i.test(msg)
55
- ? "OpenAI"
56
- : /gemini|google/i.test(msg)
57
- ? "Gemini"
58
- : "the provider";
86
+ const msg = err instanceof Error ? err.message : String(err);
87
+ const status = statusOf(err);
88
+ // Prefer the error's OWN carried provider (accurate even for compat backends like
89
+ // Tencent/groq that speak Anthropic's/OpenAI's wire protocol — see
90
+ // `relabelProviderError`); only guess from message text for errors that never
91
+ // carried a `.provider` field (bare Error, a re-wrapped doneReason string, …).
92
+ const provider = knownErrorProvider(err) ?? (
93
+ /antigravity/i.test(msg)
94
+ ? "Antigravity"
95
+ : /anthropic/i.test(msg)
96
+ ? "Anthropic"
97
+ : /openai/i.test(msg)
98
+ ? "OpenAI"
99
+ : /gemini|google/i.test(msg)
100
+ ? "Gemini"
101
+ : "the provider"
102
+ );
59
103
 
60
104
  if (isUsageLimitError(err)) {
61
105
  return `${provider} usage/quota limit reached — this window will not clear in seconds, so auto-retry was skipped. Switch model with /model (e.g. a local ollama model), use another provider, or wait for the limit window to reset.`;
62
106
  }
107
+ if (status === 402 || /\b402\b/.test(msg) || /payment required|insufficient credit|billing (?:is )?not enabled|free trial quota/i.test(msg)) {
108
+ return `${provider} requires billing/payment on this account (HTTP 402) — free trial quota exhausted or postpaid billing not enabled. Enable billing on the ${provider} account, or switch model with /model.`;
109
+ }
63
110
  if (status === 429 || /\b429\b/.test(msg) || /rate[ _]?limit/i.test(msg)) {
64
- const retryAfterMs = (err as { retryAfterMs?: number })?.retryAfterMs;
111
+ const retryAfterMs = retryAfterMsOf(err);
65
112
  const retry = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0
66
113
  ? ` Server requested retry after ${formatDuration(retryAfterMs)}.`
67
114
  : "";
@@ -70,7 +117,7 @@ export function friendlyProviderError(err: unknown): string {
70
117
  if (status === 401 || status === 403 || /\b40[13]\b/.test(msg)) {
71
118
  return `${provider} rejected the credential (HTTP ${status ?? "401/403"}). Run 'jeo auth status', re-login with /provider login <name>, and for Antigravity prefer '/provider login antigravity' (gemini login only works when the Cloud Code Assist backend authorizes that token).`;
72
119
  }
73
- if ((err as { name?: string })?.name === "TimeoutError" || /the operation timed out/i.test(msg)) {
120
+ if ((err instanceof Error && err.name === "TimeoutError") || /the operation timed out/i.test(msg)) {
74
121
  return `${provider} did not complete the request within the call timeout (default 30min). This is expected for a HIGH/XHIGH-reasoning-effort completion that legitimately runs long on a hard problem — raise JEO_CALL_TIMEOUT_MS (ms) if this recurs, or lower the thinking level with /model.`;
75
122
  }
76
123
  if (/exceeded the overall deadline \(JEO_STREAM_MAX_MS\)/i.test(msg)) {