fix: send provider model ids

This commit is contained in:
ajnart committed 2026-07-30 16:00:09 +02:00
1 parent 6bd331bc73
commit af75bcd741
9 files changed
+235 -33

No files matched your search

@@ -0,0 +1,34 @@
import { describe, expect, test } from "vitest";
import { getAssistantStreamErrorMessage } from "./assistant-stream-error";
describe("assistant stream errors", () => {
test("explains invalid provider model identifiers", () => {
expect(
getAssistantStreamErrorMessage({
statusCode: 400,
responseBody: JSON.stringify({
error: {
message: "DeepSeek: DeepSeek V4 Pro (deepseek/deepseek-v4-pro) is not a valid model ID",
},
}),
}),
).toBe("The provider rejected the selected model. Ask an administrator to select a valid model ID.");
});
test.each([
[401, "The provider rejected the configured credentials. Ask an administrator to update the API key."],
[402, "The provider account has insufficient credits for this request."],
[404, "The selected model or chat endpoint was not found. Ask an administrator to verify the model and API URL."],
[429, "The model endpoint is rate limited. Wait a moment and try again."],
[503, "The model provider is temporarily unavailable. Try again later."],
])("maps provider status %s to an actionable message", (statusCode, expected) => {
expect(getAssistantStreamErrorMessage({ statusCode })).toBe(expected);
});
test("recognizes nested timeout errors", () => {
expect(getAssistantStreamErrorMessage(new Error("request failed", { cause: new Error("timed out") }))).toBe(
"The model endpoint took too long to respond. Try again.",
);
});
});
@@ -0,0 +1,61 @@
const asRecord = (value: unknown): Record<string, unknown> | null =>
typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
const getStatusCode = (error: unknown): number | undefined => {
const record = asRecord(error);
if (!record) return undefined;
const statusCode =
typeof record.statusCode === "number"
? record.statusCode
: typeof record.status === "number"
? record.status
: undefined;
if (statusCode !== undefined) return statusCode;
return getStatusCode(record.cause) ?? getStatusCode(record.response);
};
const getErrorMessage = (error: unknown): string => {
if (error instanceof Error) {
return `${error.message} ${getErrorMessage(error.cause)}`.trim();
}
const record = asRecord(error);
if (!record) return typeof error === "string" ? error : "";
const responseBody = typeof record.responseBody === "string" ? record.responseBody : "";
const message = typeof record.message === "string" ? record.message : "";
return `${message} ${responseBody} ${getErrorMessage(record.cause)}`.trim();
};
export const getAssistantStreamErrorMessage = (error: unknown) => {
const statusCode = getStatusCode(error);
const message = getErrorMessage(error);
if (
/\bmodel(?:\s+id)?\b/iu.test(message) &&
/\b(invalid|unknown|unavailable|not found|not a valid)\b/iu.test(message)
) {
return "The provider rejected the selected model. Ask an administrator to select a valid model ID.";
}
if (statusCode === 400) {
return "The provider rejected the request. The selected model may not support the requested input or tools.";
}
if (statusCode === 401 || statusCode === 403) {
return "The provider rejected the configured credentials. Ask an administrator to update the API key.";
}
if (statusCode === 402) {
return "The provider account has insufficient credits for this request.";
}
if (statusCode === 404) {
return "The selected model or chat endpoint was not found. Ask an administrator to verify the model and API URL.";
}
if (statusCode === 408 || statusCode === 504 || /\b(timeout|timed out)\b/iu.test(message)) {
return "The model endpoint took too long to respond. Try again.";
}
if (statusCode === 429) {
return "The model endpoint is rate limited. Wait a moment and try again.";
}
if (statusCode !== undefined && statusCode >= 500) {
return "The model provider is temporarily unavailable. Try again later.";
}
return "The model endpoint stopped the response. Try again, or ask an administrator to verify its URL, model, and credentials.";
};
@@ -27,6 +27,7 @@ import type {
import { extractMcpTools } from "../../mcp/_extract-tools";
import { getRequestedMentionIds, sanitizeAttachmentFilename } from "./assistant-chat-input";
import { getAssistantStreamErrorMessage } from "./assistant-stream-error";
export const maxDuration = 60;
@@ -35,8 +36,6 @@ const getToolApprovalSecret = () =>
Buffer.from(
hkdfSync("sha256", Buffer.from(env.SECRET_ENCRYPTION_KEY, "hex"), "", "assistant-tool-approval", 32),
).toString("base64url");
const safeStreamError =
"The model endpoint stopped the response. Try again, or ask an administrator to verify its URL, model, and credentials.";
const allowedAttachmentMediaTypes = new Set([
"image/gif",
"image/jpeg",
@@ -314,8 +313,6 @@ export async function POST(request: Request) {
if (!configuration?.enabled || !configuration.modelId || (requiresApiKey && !configuration.encryptedApiKey)) {
return Response.json({ error: "Homarr Assistant is not configured." }, { status: 503 });
}
const modelId = configuration.modelId;
const thread = await db.query.assistantThreads.findFirst({
where: and(eq(assistantThreads.id, parsed.data.id), eq(assistantThreads.userId, session.user.id)),
});
@@ -330,6 +327,7 @@ export async function POST(request: Request) {
getSelectedModelDetailsAsync(configuration).catch(() => null),
getMentionContextAsync(context, parsed.data.messages),
]);
const modelId = selectedModel?.id ?? configuration.modelId;
if (
selectedModel &&
selectedModel.inputModalities.length > 0 &&
@@ -488,6 +486,9 @@ export async function POST(request: Request) {
},
onError: ({ error }) => {
logger.error("Assistant response stream failed", {
requestId,
provider: configuration.provider,
modelId,
error: error instanceof Error ? error.message : String(error),
});
},
@@ -555,7 +556,7 @@ export async function POST(request: Request) {
},
};
},
onError: () => safeStreamError,
onError: getAssistantStreamErrorMessage,
});
} catch (error) {
logger.error("Assistant response could not start", {