Address CodeRabbit provider feedback

This commit is contained in:
ajnart committed 2026-08-11 22:03:21 +02:00
1 parent ea9075bde5
commit abaf845e94
19 files changed
+293 -64

No files matched your search

@@ -21,7 +21,9 @@ describe("toProviderOptionsKey", () => {
test("leaves names without separators unchanged", () => {
expect(toProviderOptionsKey("homarr")).toBe("homarr");
});
});
describe("resolveHomarrProviderToken", () => {
test("accepts a Workshop token only for the matching Homarr provider endpoint", () => {
const headers = new Headers({ [assistantHomarrProviderTokenHeader]: "workshop-token" });
expect(
@@ -35,6 +37,24 @@ describe("toProviderOptionsKey", () => {
expect(getHomarrProviderBaseUrl("https://homarr.dev/")).toBe("https://homarr.dev/api/ai/v1");
});
test.each([
[undefined, null],
["", null],
[" ", null],
["a".repeat(4096), "a".repeat(4096)],
["a".repeat(4097), null],
])("validates the Workshop token header %s", (token, expected) => {
const headers = new Headers(token === undefined ? undefined : { [assistantHomarrProviderTokenHeader]: token });
expect(
resolveHomarrProviderToken({
provider: "homarr",
configuredBaseUrl: "https://homarr.dev/api/ai/v1",
workshopApiUrl: "https://homarr.dev",
headers,
}),
).toBe(expected);
});
test("never forwards a Workshop token to another endpoint", () => {
expect(() =>
resolveHomarrProviderToken({
@@ -432,11 +432,18 @@ export async function POST(request: Request) {
workshopApiUrl: appEnv.WORKSHOP_API_URL ?? appEnv.HOMARR_WEBSITE_URL,
headers: request.headers,
});
} catch {
} catch (error) {
logger.warn("Rejected an unsafe Homarr provider endpoint", {
provider: configuration.provider,
error,
});
return Response.json({ error: "The Homarr provider endpoint is not configured safely." }, { status: 503 });
}
if (homarrProviderToken === null) {
return Response.json({ error: "Sign in to the Homarr Community Workshop to use the Homarr provider." }, { status: 401 });
return Response.json(
{ error: "Sign in to the Homarr Community Workshop to use the Homarr provider." },
{ status: 401 },
);
}
const context = createTRPCContext({ headers: request.headers, session });
@@ -1232,7 +1232,7 @@
}
.providerQuotaTrigger[data-level="ok"] {
color: var(--mantine-color-green-7);
color: light-dark(var(--mantine-color-green-8), var(--mantine-color-green-4));
}
.providerQuotaTrigger[data-level="warning"] {
@@ -1244,7 +1244,7 @@
}
.providerQuotaTrigger[data-level="dead"] {
color: var(--mantine-color-red-7);
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-4));
}
.conversationContextPopover {
@@ -121,7 +121,7 @@ import { useTimeAgo } from "@homarr/common";
import { assistantProviderIds, assistantProviderPresets, assistantReasoningModes } from "@homarr/definitions";
import type { AssistantProvider } from "@homarr/definitions";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
import classes from "./assistant-panel.module.css";
import { getAssistantActivityState } from "./assistant-activity-state";
@@ -2558,8 +2558,10 @@ const providerQuotaColors = {
const HomarrProviderQuota = () => {
const t = useScopedI18n("common.assistant");
const locale = useCurrentIntlLocale();
const preferences = useAssistantPreferences();
const resetAt = useMemo(() => new Date(preferences.quota?.resetsAt ?? Date.now()), [preferences.quota?.resetsAt]);
const quota = preferences.quota;
const resetAt = useMemo(() => new Date(quota?.resetsAt ?? Date.now()), [quota?.resetsAt]);
const resetRelative = useTimeAgo(resetAt, 30_000);
if (preferences.provider !== "homarr") return null;
@@ -2570,13 +2572,22 @@ const HomarrProviderQuota = () => {
? Math.min(100, Math.max(0, (preferences.quota.remaining / Math.max(preferences.quota.limit, 1)) * 100))
: 0;
const label = preferences.providerUser
? preferences.quota
? quota
? t("providerQuota.remainingLabel", {
remaining: preferences.quota.remaining,
limit: preferences.quota.limit,
remaining: quota.remaining,
limit: quota.limit,
})
: t("providerQuota.loading")
: t("providerQuota.signInRequired");
const resetTime = quota ? (
<time dateTime={quota.resetsAt} title={new Date(quota.resetsAt).toLocaleString(locale)}>
{new Date(quota.resetsAt).toLocaleTimeString(locale, {
hour: "2-digit",
minute: "2-digit",
timeZoneName: "short",
})}
</time>
) : null;
return (
<Popover position="top" width={290} shadow="md" withinPortal>
@@ -2638,17 +2649,10 @@ const HomarrProviderQuota = () => {
<Progress value={percentage} color={color} size="sm" aria-label={label} />
</Box>
<Text size="xs" c="dimmed">
{t("providerQuota.reset", { relative: resetRelative })}{" "}
<time
dateTime={preferences.quota.resetsAt}
title={new Date(preferences.quota.resetsAt).toLocaleString()}
>
{new Date(preferences.quota.resetsAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
timeZoneName: "short",
})}
</time>
{t.rich("providerQuota.reset", {
relative: resetRelative,
time: () => resetTime,
})}
</Text>
<Text size="xs" c="dimmed">
{t("providerQuota.toolCalls")}
@@ -13,17 +13,22 @@ describe("getAssistantProviderQuotaLevel", () => {
[{ limit: 50, remaining: 25 }, "warning"],
[{ limit: 50, remaining: 10 }, "bad"],
[{ limit: 50, remaining: 0 }, "dead"],
[{ limit: 0, remaining: 0 }, "dead"],
] as const)("maps %o to %s", (usage, expected) => {
expect(getAssistantProviderQuotaLevel(usage)).toBe(expected);
});
});
describe("isAssistantProviderUnavailable", () => {
test("blocks only unavailable Homarr provider sessions", () => {
expect(isAssistantProviderUnavailable({ provider: "homarr", signedIn: false, remaining: undefined })).toBe(true);
expect(isAssistantProviderUnavailable({ provider: "homarr", signedIn: true, remaining: 0 })).toBe(true);
expect(isAssistantProviderUnavailable({ provider: "homarr", signedIn: true, remaining: undefined })).toBe(false);
expect(isAssistantProviderUnavailable({ provider: "openrouter", signedIn: false, remaining: 0 })).toBe(false);
});
});
describe("getAssistantProviderQuotaRefreshDelay", () => {
test("refreshes just after the UTC reset, or immediately when it has passed", () => {
const reset = "2026-08-12T00:00:00.000Z";
expect(getAssistantProviderQuotaRefreshDelay(reset, Date.parse("2026-08-11T23:59:00.000Z"))).toBe(61_000);
@@ -136,6 +136,7 @@ const AssistantPreferencesProvider = ({ children }: PropsWithChildren) => {
const [quota, setQuota] = useState<Awaited<ReturnType<typeof workshopClient.getAssistantUsage>> | null>(null);
const [quotaLoading, setQuotaLoading] = useState(false);
const [quotaError, setQuotaError] = useState<string | null>(null);
const quotaRequestRef = useRef<AbortController | null>(null);
const previousDefaultModelIdRef = useRef<string | null | undefined>(undefined);
const preferencesRef = useRef<{ modelId: string | null; reasoning: AssistantReasoningMode }>({
modelId: null,
@@ -176,19 +177,30 @@ const AssistantPreferencesProvider = ({ children }: PropsWithChildren) => {
return token ? { [assistantHomarrProviderTokenHeader]: token } : {};
}, [data?.provider, workshopClient]);
const refreshQuota = useCallback(async () => {
quotaRequestRef.current?.abort();
if (data?.provider !== "homarr" || !workshopClient.authToken) {
quotaRequestRef.current = null;
setQuota(null);
setQuotaError(null);
setQuotaLoading(false);
return;
}
const controller = new AbortController();
quotaRequestRef.current = controller;
setQuotaLoading(true);
setQuotaError(null);
try {
setQuota(await workshopClient.getAssistantUsage());
const nextQuota = await workshopClient.getAssistantUsage(controller.signal);
if (quotaRequestRef.current === controller && !controller.signal.aborted) setQuota(nextQuota);
} catch (error) {
setQuotaError(error instanceof Error ? error.message : "The Homarr provider allowance could not be loaded.");
if (quotaRequestRef.current === controller && !controller.signal.aborted) {
setQuotaError(error instanceof Error ? error.message : "The Homarr provider allowance could not be loaded.");
}
} finally {
setQuotaLoading(false);
if (quotaRequestRef.current === controller) {
quotaRequestRef.current = null;
setQuotaLoading(false);
}
}
}, [data?.provider, workshopClient]);
const signInToProvider = useCallback(async () => {
@@ -228,10 +240,20 @@ const AssistantPreferencesProvider = ({ children }: PropsWithChildren) => {
return;
}
const unsubscribe = workshopClient.subscribeToAuth(setProviderUser);
void workshopClient.refreshAuth().then(setProviderUser);
void workshopClient
.refreshAuth()
.then(setProviderUser)
.catch(() => undefined);
return unsubscribe;
}, [data?.provider, workshopClient]);
useEffect(
() => () => {
quotaRequestRef.current?.abort();
},
[],
);
useEffect(() => {
if (data?.provider !== "homarr" || !providerUser) return;
void refreshQuota();
@@ -419,7 +441,7 @@ const AssistantThreadRuntime = () => {
const localThreadId = useAuiState((state) => state.threadListItem.id);
const threadId = useAuiState((state) => state.threadListItem.remoteId);
const preferences = useAssistantPreferences();
const { getRequestBody, getRequestHeaders } = preferences;
const { getRequestBody, getRequestHeaders, refreshQuota } = preferences;
const transport = useMemo(
() =>
new AssistantChatTransport({
@@ -456,19 +478,20 @@ const AssistantThreadRuntime = () => {
[t, threadId],
);
const onError = useCallback(() => {
void preferences.refreshQuota();
void refreshQuota();
showErrorNotification({
title: t("responseError.title"),
message: t("responseError.description"),
autoClose: 10_000,
});
}, [preferences, t]);
}, [refreshQuota, t]);
const onFinish = useCallback(() => void refreshQuota(), [refreshQuota]);
const chat = useChat<AssistantUIMessage>({
id: localThreadId,
transport,
onError,
onFinish: () => void preferences.refreshQuota(),
onFinish,
sendAutomaticallyWhen: shouldAutomaticallyContinueAssistant,
});