fix(tui): surface model errors
This commit is contained in:
@@ -73,6 +73,7 @@
|
|||||||
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
|
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
|
||||||
- TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.
|
- TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.
|
||||||
- TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.
|
- TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.
|
||||||
|
- TUI: show LLM error messages (rate limits, auth, etc.) instead of `(no output)`.
|
||||||
- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`).
|
- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`).
|
||||||
- macOS: fix cron preview/testing payload to use `channel` key. (#867) — thanks @wes-davis.
|
- macOS: fix cron preview/testing payload to use `channel` key. (#867) — thanks @wes-davis.
|
||||||
- Telegram: honor `channels.telegram.timeoutSeconds` for grammY API requests. (#863) — thanks @Snaver.
|
- Telegram: honor `channels.telegram.timeoutSeconds` for grammY API requests. (#863) — thanks @Snaver.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { formatRawAssistantErrorForUi } from "./pi-embedded-helpers.js";
|
||||||
|
|
||||||
|
describe("formatRawAssistantErrorForUi", () => {
|
||||||
|
it("renders HTTP code + type + message from Anthropic payloads", () => {
|
||||||
|
const text = formatRawAssistantErrorForUi(
|
||||||
|
'429 {"type":"error","error":{"type":"rate_limit_error","message":"Rate limited."},"request_id":"req_123"}',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(text).toContain("HTTP 429");
|
||||||
|
expect(text).toContain("rate_limit_error");
|
||||||
|
expect(text).toContain("Rate limited.");
|
||||||
|
expect(text).toContain("req_123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a generic unknown error message when raw is empty", () => {
|
||||||
|
expect(formatRawAssistantErrorForUi("")).toContain("unknown error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -7,11 +7,13 @@ export {
|
|||||||
} from "./pi-embedded-helpers/bootstrap.js";
|
} from "./pi-embedded-helpers/bootstrap.js";
|
||||||
export {
|
export {
|
||||||
classifyFailoverReason,
|
classifyFailoverReason,
|
||||||
|
formatRawAssistantErrorForUi,
|
||||||
formatAssistantErrorText,
|
formatAssistantErrorText,
|
||||||
getApiErrorPayloadFingerprint,
|
getApiErrorPayloadFingerprint,
|
||||||
isAuthAssistantError,
|
isAuthAssistantError,
|
||||||
isAuthErrorMessage,
|
isAuthErrorMessage,
|
||||||
isBillingAssistantError,
|
isBillingAssistantError,
|
||||||
|
parseApiErrorInfo,
|
||||||
isBillingErrorMessage,
|
isBillingErrorMessage,
|
||||||
isCloudCodeAssistFormatError,
|
isCloudCodeAssistFormatError,
|
||||||
isCompactionFailureError,
|
isCompactionFailureError,
|
||||||
|
|||||||
@@ -100,6 +100,72 @@ export function isRawApiErrorPayload(raw?: string): boolean {
|
|||||||
return getApiErrorPayloadFingerprint(raw) !== null;
|
return getApiErrorPayloadFingerprint(raw) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApiErrorInfo = {
|
||||||
|
httpCode?: string;
|
||||||
|
type?: string;
|
||||||
|
message?: string;
|
||||||
|
requestId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseApiErrorInfo(raw?: string): ApiErrorInfo | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
let httpCode: string | undefined;
|
||||||
|
let candidate = trimmed;
|
||||||
|
|
||||||
|
const httpPrefixMatch = candidate.match(/^(\d{3})\s+(.+)$/s);
|
||||||
|
if (httpPrefixMatch) {
|
||||||
|
httpCode = httpPrefixMatch[1];
|
||||||
|
candidate = httpPrefixMatch[2].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = parseApiErrorPayload(candidate);
|
||||||
|
if (!payload) return null;
|
||||||
|
|
||||||
|
const requestId =
|
||||||
|
typeof payload.request_id === "string"
|
||||||
|
? payload.request_id
|
||||||
|
: typeof payload.requestId === "string"
|
||||||
|
? payload.requestId
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const topType = typeof payload.type === "string" ? payload.type : undefined;
|
||||||
|
const topMessage = typeof payload.message === "string" ? payload.message : undefined;
|
||||||
|
|
||||||
|
let errType: string | undefined;
|
||||||
|
let errMessage: string | undefined;
|
||||||
|
if (payload.error && typeof payload.error === "object" && !Array.isArray(payload.error)) {
|
||||||
|
const err = payload.error as Record<string, unknown>;
|
||||||
|
if (typeof err.type === "string") errType = err.type;
|
||||||
|
if (typeof err.code === "string" && !errType) errType = err.code;
|
||||||
|
if (typeof err.message === "string") errMessage = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpCode,
|
||||||
|
type: errType ?? topType,
|
||||||
|
message: errMessage ?? topMessage,
|
||||||
|
requestId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatRawAssistantErrorForUi(raw?: string): string {
|
||||||
|
const trimmed = (raw ?? "").trim();
|
||||||
|
if (!trimmed) return "LLM request failed with an unknown error.";
|
||||||
|
|
||||||
|
const info = parseApiErrorInfo(trimmed);
|
||||||
|
if (info?.message) {
|
||||||
|
const prefix = info.httpCode ? `HTTP ${info.httpCode}` : "LLM error";
|
||||||
|
const type = info.type ? ` ${info.type}` : "";
|
||||||
|
const requestId = info.requestId ? ` (request_id: ${info.requestId})` : "";
|
||||||
|
return `${prefix}${type}: ${info.message}${requestId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed.length > 600 ? `${trimmed.slice(0, 600)}…` : trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
export function formatAssistantErrorText(
|
export function formatAssistantErrorText(
|
||||||
msg: AssistantMessage,
|
msg: AssistantMessage,
|
||||||
opts?: { cfg?: ClawdbotConfig; sessionKey?: string },
|
opts?: { cfg?: ClawdbotConfig; sessionKey?: string },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NormalizedUsage } from "../../agents/usage.js";
|
import type { NormalizedUsage } from "../../agents/usage.js";
|
||||||
import { getChannelDock } from "../../channels/dock.js";
|
import { getChannelDock } from "../../channels/dock.js";
|
||||||
import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
|
import type { ChannelThreadingToolContext } from "../../channels/plugins/types.js";
|
||||||
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
import { normalizeChannelId } from "../../channels/registry.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
||||||
import { estimateUsageCost, formatTokenCount, formatUsd } from "../../utils/usage-format.js";
|
import { estimateUsageCost, formatTokenCount, formatUsd } from "../../utils/usage-format.js";
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ export function createEventHandlers(context: EventHandlerContext) {
|
|||||||
setActivityStatus("streaming");
|
setActivityStatus("streaming");
|
||||||
}
|
}
|
||||||
if (evt.state === "final") {
|
if (evt.state === "final") {
|
||||||
|
const stopReason =
|
||||||
|
evt.message && typeof evt.message === "object" && !Array.isArray(evt.message)
|
||||||
|
? typeof (evt.message as Record<string, unknown>).stopReason === "string"
|
||||||
|
? ((evt.message as Record<string, unknown>).stopReason as string)
|
||||||
|
: ""
|
||||||
|
: "";
|
||||||
const text = extractTextFromMessage(evt.message, {
|
const text = extractTextFromMessage(evt.message, {
|
||||||
includeThinking: state.showThinking,
|
includeThinking: state.showThinking,
|
||||||
});
|
});
|
||||||
@@ -57,7 +63,7 @@ export function createEventHandlers(context: EventHandlerContext) {
|
|||||||
chatLog.finalizeAssistant(finalText, evt.runId);
|
chatLog.finalizeAssistant(finalText, evt.runId);
|
||||||
noteFinalizedRun(evt.runId);
|
noteFinalizedRun(evt.runId);
|
||||||
state.activeChatRunId = null;
|
state.activeChatRunId = null;
|
||||||
setActivityStatus("idle");
|
setActivityStatus(stopReason === "error" ? "error" : "idle");
|
||||||
}
|
}
|
||||||
if (evt.state === "aborted") {
|
if (evt.state === "aborted") {
|
||||||
chatLog.addSystem("run aborted");
|
chatLog.addSystem("run aborted");
|
||||||
|
|||||||
31
src/tui/tui-formatters.test.ts
Normal file
31
src/tui/tui-formatters.test.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { extractTextFromMessage } from "./tui-formatters.js";
|
||||||
|
|
||||||
|
describe("extractTextFromMessage", () => {
|
||||||
|
it("renders errorMessage when assistant content is empty", () => {
|
||||||
|
const text = extractTextFromMessage({
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
stopReason: "error",
|
||||||
|
errorMessage:
|
||||||
|
'429 {"type":"error","error":{"type":"rate_limit_error","message":"This request would exceed your account\\u0027s rate limit. Please try again later."},"request_id":"req_123"}',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(text).toContain("HTTP 429");
|
||||||
|
expect(text).toContain("rate_limit_error");
|
||||||
|
expect(text).toContain("req_123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to a generic message when errorMessage is missing", () => {
|
||||||
|
const text = extractTextFromMessage({
|
||||||
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
|
stopReason: "error",
|
||||||
|
errorMessage: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(text).toContain("unknown error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { formatTokenCount } from "../utils/usage-format.js";
|
import { formatTokenCount } from "../utils/usage-format.js";
|
||||||
|
import { formatRawAssistantErrorForUi } from "../agents/pi-embedded-helpers.js";
|
||||||
|
|
||||||
export function resolveFinalAssistantText(params: {
|
export function resolveFinalAssistantText(params: {
|
||||||
finalText?: string | null;
|
finalText?: string | null;
|
||||||
@@ -38,7 +39,14 @@ export function extractTextFromMessage(
|
|||||||
): string {
|
): string {
|
||||||
if (!message || typeof message !== "object") return "";
|
if (!message || typeof message !== "object") return "";
|
||||||
const record = message as Record<string, unknown>;
|
const record = message as Record<string, unknown>;
|
||||||
return extractTextBlocks(record.content, opts);
|
const text = extractTextBlocks(record.content, opts);
|
||||||
|
if (text) return text;
|
||||||
|
|
||||||
|
const stopReason = typeof record.stopReason === "string" ? record.stopReason : "";
|
||||||
|
if (stopReason !== "error") return "";
|
||||||
|
|
||||||
|
const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : "";
|
||||||
|
return formatRawAssistantErrorForUi(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTokens(total?: number | null, context?: number | null) {
|
export function formatTokens(total?: number | null, context?: number | null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user