feat: add the community-funded Homarr assistant provider (#6563)
* Add Homarr provider proxy foundation * Complete Homarr provider integration * Harden Homarr provider integration * Address Homarr provider review feedback * Address CodeRabbit provider feedback * Localize provider errors * Handle provider activity load failures * docs: improve workshop and homepage * Harden Homarr provider limits * fix: harden the Homarr provider * fix: address provider review feedback * fix: clarify provider request accounting * feat: complete the Homarr provider endpoint * fix: terminate invalid provider streams
This commit is contained in:
1 parent
b8f360c04e
commit
45d8bc9443
77 files changed
+4035
-763
No files matched your search
@@ -0,0 +1,52 @@
|
||||
const enabled = process.env.HOMARR_AI_LIVE_SMOKE === "1";
|
||||
if (!enabled)
|
||||
throw new Error("Set HOMARR_AI_LIVE_SMOKE=1 to acknowledge that this check consumes a real provider request");
|
||||
|
||||
const baseUrl = process.env.WORKSHOP_LIVE_TEST_URL?.replace(/\/+$/u, "");
|
||||
const token = process.env.WORKSHOP_LIVE_TEST_TOKEN;
|
||||
if (!baseUrl || !token) {
|
||||
throw new Error("WORKSHOP_LIVE_TEST_URL and WORKSHOP_LIVE_TEST_TOKEN are required");
|
||||
}
|
||||
|
||||
const requestJson = async (path, init = {}) => {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
...init,
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}`, ...init.headers },
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
});
|
||||
const body = await response.json().catch(() => null);
|
||||
if (!response.ok) throw new Error(`${response.status} ${path}: ${JSON.stringify(body)}`);
|
||||
return { body, headers: response.headers };
|
||||
};
|
||||
|
||||
const { body: models } = await requestJson("/api/ai/v1/models");
|
||||
if (models.data?.length !== 1 || models.data[0]?.id !== "homarr/model") {
|
||||
throw new Error("The live Workshop endpoint did not advertise the stable Homarr model");
|
||||
}
|
||||
|
||||
const { body: before } = await requestJson("/api/ai/usage");
|
||||
if (before.remaining < 1) throw new Error("The live test user has no remaining Homarr provider allowance");
|
||||
|
||||
const { body: completion, headers } = await requestJson("/api/ai/v1/chat/completions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [{ role: "user", content: "Reply with exactly OK." }],
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
if (
|
||||
completion.model !== "homarr/model" ||
|
||||
typeof completion.choices?.[0]?.message?.content !== "string" ||
|
||||
completion.choices[0].message.content.length === 0 ||
|
||||
headers.get("x-homarr-quota-remaining") === null
|
||||
) {
|
||||
throw new Error("The live Homarr provider did not return an OpenAI-compatible completion with quota headers");
|
||||
}
|
||||
|
||||
const { body: after } = await requestJson("/api/ai/usage");
|
||||
if (after.used < before.used + 1 || after.remaining >= before.remaining) {
|
||||
throw new Error("The live completion was not charged to the authenticated Workshop user");
|
||||
}
|
||||
|
||||
process.stdout.write("Live Homarr provider smoke test passed\n");
|
||||
@@ -0,0 +1,203 @@
|
||||
import { createServer } from "node:http";
|
||||
|
||||
const port = Number(process.env.PORT ?? 18091);
|
||||
const expectedModel = process.env.EXPECTED_MODEL ?? "mock/team-selected-model";
|
||||
const expectedApiKey = process.env.EXPECTED_API_KEY ?? "workshop-test-openrouter-key";
|
||||
|
||||
const json = (response, status, body) => {
|
||||
response.writeHead(status, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify(body));
|
||||
};
|
||||
|
||||
const server = createServer(async (request, response) => {
|
||||
if (request.method === "GET" && request.url === "/health") {
|
||||
json(response, 200, { ok: true });
|
||||
return;
|
||||
}
|
||||
if (request.method !== "POST" || request.url !== "/api/v1/chat/completions") {
|
||||
json(response, 404, { error: { message: "Not found" } });
|
||||
return;
|
||||
}
|
||||
if (request.headers.authorization !== `Bearer ${expectedApiKey}`) {
|
||||
json(response, 401, { error: { message: "Invalid upstream credentials" } });
|
||||
return;
|
||||
}
|
||||
if (
|
||||
request.headers["http-referer"] !== "https://homarr.dev" ||
|
||||
request.headers["x-openrouter-title"] !== "Homarr AI Assistant" ||
|
||||
request.headers["x-openrouter-metadata"] !== "enabled"
|
||||
) {
|
||||
json(response, 400, { error: { message: "Homarr upstream attribution headers are missing or spoofed" } });
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
for await (const chunk of request) chunks.push(chunk);
|
||||
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||||
if (body.model !== expectedModel) {
|
||||
json(response, 400, { error: { message: "Unexpected upstream model" } });
|
||||
return;
|
||||
}
|
||||
if (body.max_completion_tokens !== 32_768 || body.n !== 1 || body.parallel_tool_calls !== false) {
|
||||
json(response, 400, { error: { message: "Unsafe generation controls reached the upstream" } });
|
||||
return;
|
||||
}
|
||||
if (
|
||||
"max_tokens" in body ||
|
||||
"user" in body ||
|
||||
"metadata" in body ||
|
||||
"extra_body" in body ||
|
||||
"extra_headers" in body ||
|
||||
"max_tokens" in (body.reasoning ?? {})
|
||||
) {
|
||||
json(response, 400, { error: { message: "Private or unbounded request fields reached the upstream" } });
|
||||
return;
|
||||
}
|
||||
const forbiddenCostField = ["audio", "modalities", "logprobs", "top_logprobs", "prediction", "service_tier"].find(
|
||||
(field) => field in body,
|
||||
);
|
||||
if (forbiddenCostField) {
|
||||
json(response, 400, { error: { message: `Client cost field was forwarded: ${forbiddenCostField}` } });
|
||||
return;
|
||||
}
|
||||
const forbiddenRoutingField = ["models", "route", "plugins", "transforms"].find((field) => field in body);
|
||||
if (forbiddenRoutingField) {
|
||||
json(response, 400, { error: { message: `Client routing field was forwarded: ${forbiddenRoutingField}` } });
|
||||
return;
|
||||
}
|
||||
if (body.provider?.zdr !== true || body.provider?.data_collection !== "deny") {
|
||||
json(response, 400, { error: { message: "Required upstream privacy controls are missing" } });
|
||||
return;
|
||||
}
|
||||
const webSearchTools = body.tools?.filter((tool) => tool.type === "openrouter:web_search") ?? [];
|
||||
if (
|
||||
webSearchTools.length > 1 ||
|
||||
webSearchTools.some((tool) => tool.parameters?.max_results !== 5 || tool.parameters?.max_uses !== 3)
|
||||
) {
|
||||
json(response, 400, { error: { message: "Unbounded web search controls reached the upstream" } });
|
||||
return;
|
||||
}
|
||||
if (body.messages?.some((message) => message.content === "upstream failure")) {
|
||||
json(response, 503, { error: { message: "Simulated upstream failure" } });
|
||||
return;
|
||||
}
|
||||
if (body.messages?.some((message) => message.content === "malformed stream")) {
|
||||
response.writeHead(200, { "content-type": "text/event-stream" });
|
||||
response.end('data: {"model":\n\n');
|
||||
return;
|
||||
}
|
||||
if (body.messages?.some((message) => message.content === "in-flight probe")) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
const wantsToolCall = body.messages?.some((message) => message.content === "inspect my dashboard");
|
||||
if (wantsToolCall && !body.tools?.some((tool) => tool.type === "function" && tool.function?.name === "board_list")) {
|
||||
json(response, 400, { error: { message: "The authenticated function tool was not forwarded" } });
|
||||
return;
|
||||
}
|
||||
|
||||
const completion = {
|
||||
id: "gen-workshop-test",
|
||||
object: "chat.completion",
|
||||
model: body.model,
|
||||
choices: [
|
||||
wantsToolCall
|
||||
? {
|
||||
index: 0,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call-board-list",
|
||||
type: "function",
|
||||
function: { name: "board_list", arguments: "{}" },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: "tool_calls",
|
||||
}
|
||||
: {
|
||||
index: 0,
|
||||
message: { role: "assistant", content: "Hello from the Homarr provider" },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 12, completion_tokens: 7, total_tokens: 19, cost: 0.00041 },
|
||||
};
|
||||
if (!body.stream) {
|
||||
json(response, 200, completion);
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(200, {
|
||||
"content-type": "text/event-stream",
|
||||
"cache-control": "no-cache",
|
||||
"x-request-id": "mock-openrouter-request",
|
||||
});
|
||||
if (wantsToolCall) {
|
||||
response.write(
|
||||
`data: ${JSON.stringify({
|
||||
id: completion.id,
|
||||
object: "chat.completion.chunk",
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
role: "assistant",
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: "call-board-list",
|
||||
type: "function",
|
||||
function: { name: "board_list", arguments: "{" },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
})}\n\n`,
|
||||
);
|
||||
response.write(
|
||||
`data: ${JSON.stringify({
|
||||
id: completion.id,
|
||||
object: "chat.completion.chunk",
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { tool_calls: [{ index: 0, function: { arguments: "}" } }] },
|
||||
finish_reason: "tool_calls",
|
||||
},
|
||||
],
|
||||
usage: completion.usage,
|
||||
})}\n\n`,
|
||||
);
|
||||
response.end("data: [DONE]\n\n");
|
||||
return;
|
||||
}
|
||||
response.write(
|
||||
`data: ${JSON.stringify({
|
||||
id: completion.id,
|
||||
object: "chat.completion.chunk",
|
||||
model: body.model,
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: "Hello from " }, finish_reason: null }],
|
||||
})}\n\n`,
|
||||
);
|
||||
setTimeout(() => {
|
||||
response.write(
|
||||
`data: ${JSON.stringify({
|
||||
id: completion.id,
|
||||
object: "chat.completion.chunk",
|
||||
model: body.model,
|
||||
choices: [{ index: 0, delta: { content: "the Homarr provider" }, finish_reason: "stop" }],
|
||||
usage: completion.usage,
|
||||
})}\n\n`,
|
||||
);
|
||||
response.end("data: [DONE]\n\n");
|
||||
}, 150);
|
||||
});
|
||||
|
||||
server.listen(port, "0.0.0.0");
|
||||
@@ -73,6 +73,32 @@ const migration = await read("apps/workshop/pb_migrations/1784240000_workshop_wi
|
||||
for (const removedField of ["displayName", "avatarUrl", "githubProfileUrl", "githubUsername"]) {
|
||||
if (migration.includes(removedField)) throw new Error(`Redundant Workshop user field remains: ${removedField}`);
|
||||
}
|
||||
|
||||
const providerMigration = await read("apps/workshop/pb_migrations/1786500000_homarr_provider.js");
|
||||
for (const required of ['name: "assistant_quotas"', 'name: "assistant_global_quota"', "idx_assistant_quotas_user"]) {
|
||||
if (!providerMigration.includes(required)) throw new Error(`Homarr provider migration is missing: ${required}`);
|
||||
}
|
||||
for (const forbidden of ['name: "assistant_requests"', 'name: "assistant_activity"', 'name: "dailyLimit"']) {
|
||||
if (providerMigration.includes(forbidden)) throw new Error(`Homarr provider must not persist ${forbidden}`);
|
||||
}
|
||||
|
||||
const providerBackend = await read("apps/workshop/homarr_provider.go");
|
||||
for (const required of [
|
||||
'"homarr/model"',
|
||||
'apis.RequireAuth("users")',
|
||||
"DefaultActivityLoggerMiddlewareId",
|
||||
'os.Getenv("OPENROUTER_API_KEY")',
|
||||
'os.Getenv("HOMARR_AI_DAILY_REQUEST_LIMIT")',
|
||||
'os.Getenv("HOMARR_AI_GLOBAL_DAILY_REQUEST_LIMIT")',
|
||||
'os.Getenv("HOMARR_AI_OPENROUTER_MODEL")',
|
||||
'"data_collection": "deny"',
|
||||
'"zdr": true',
|
||||
]) {
|
||||
if (!providerBackend.includes(required)) throw new Error(`Homarr provider backend is missing: ${required}`);
|
||||
}
|
||||
if (/logger\(\)|\.Logger\(|log\.Print/u.test(providerBackend)) {
|
||||
throw new Error("The Homarr provider must not log request content or user tokens");
|
||||
}
|
||||
for (const required of [
|
||||
"workshop_migration_state",
|
||||
"addedUserFields",
|
||||
|
||||
@@ -35,10 +35,12 @@ for (const required of [
|
||||
"reports",
|
||||
"workshop_listings",
|
||||
"workshop_report_summaries",
|
||||
"assistant_quotas",
|
||||
"assistant_global_quota",
|
||||
]) {
|
||||
if (!collectionNames.has(required)) throw new Error(`Missing Workshop collection: ${required}`);
|
||||
}
|
||||
for (const removed of ["workshop_admin_actions", "workshop_admins"]) {
|
||||
for (const removed of ["workshop_admin_actions", "workshop_admins", "assistant_requests", "assistant_activity"]) {
|
||||
if (collectionNames.has(removed)) throw new Error(`Removed Workshop collection still exists: ${removed}`);
|
||||
}
|
||||
|
||||
@@ -99,6 +101,393 @@ if (unchangedAuthor.name !== "widget-author") {
|
||||
const visitorPassword = "WorkshopVisitor123!";
|
||||
const visitor = await createUser("widget-visitor@example.invalid", "widget-visitor", visitorPassword);
|
||||
const visitorSession = await signIn(visitor.email, visitorPassword);
|
||||
const concurrencyPassword = "WorkshopConcurrency123!";
|
||||
const concurrencyUser = await createUser(
|
||||
"provider-concurrency@example.invalid",
|
||||
"provider-concurrency",
|
||||
concurrencyPassword,
|
||||
);
|
||||
const concurrencySession = await signIn(concurrencyUser.email, concurrencyPassword);
|
||||
|
||||
const modelList = await request("/api/ai/v1/models");
|
||||
if (
|
||||
modelList.object !== "list" ||
|
||||
modelList.data.length !== 1 ||
|
||||
modelList.data[0].id !== "homarr/model" ||
|
||||
modelList.data[0].context_length !== 256 * 1024
|
||||
) {
|
||||
throw new Error("The Homarr provider must advertise exactly one model");
|
||||
}
|
||||
await expectStatus("/api/ai/usage", {}, 401);
|
||||
await expectStatus(
|
||||
"/api/ai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [{ role: "user", content: "unauthenticated" }],
|
||||
}),
|
||||
},
|
||||
401,
|
||||
);
|
||||
await expectStatus(
|
||||
"/api/ai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { authorization: "Bearer forged-workshop-user-token" },
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [{ role: "user", content: "forged identity" }],
|
||||
}),
|
||||
},
|
||||
401,
|
||||
);
|
||||
|
||||
const inFlightProbe = () =>
|
||||
fetch(`${baseUrl}/api/ai/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", ...concurrencySession.headers },
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [{ role: "user", content: "in-flight probe" }],
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
const inFlightResponses = await Promise.all([inFlightProbe(), inFlightProbe(), inFlightProbe()]);
|
||||
if (
|
||||
inFlightResponses
|
||||
.map((response) => response.status)
|
||||
.toSorted()
|
||||
.join(",") !== "200,200,429"
|
||||
) {
|
||||
throw new Error("The per-user in-flight request limit was not enforced");
|
||||
}
|
||||
const concurrencyUsage = await request("/api/ai/usage", { headers: concurrencySession.headers });
|
||||
if (concurrencyUsage.used !== 2) {
|
||||
throw new Error(`Rejected in-flight requests must not consume allowance: ${JSON.stringify(concurrencyUsage)}`);
|
||||
}
|
||||
|
||||
const initialUsage = await request("/api/ai/usage", { headers: authorSession.headers });
|
||||
const initialReset = new Date(initialUsage.resetsAt);
|
||||
if (
|
||||
initialUsage.limit !== 50 ||
|
||||
initialUsage.used !== 0 ||
|
||||
initialUsage.remaining !== 50 ||
|
||||
initialReset.getUTCHours() !== 0 ||
|
||||
initialReset <= new Date()
|
||||
) {
|
||||
throw new Error(`Unexpected initial Homarr provider allowance: ${JSON.stringify(initialUsage)}`);
|
||||
}
|
||||
|
||||
await expectStatus(
|
||||
"/api/ai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: authorSession.headers,
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [
|
||||
{ role: "user", content: "too many tools" },
|
||||
...Array.from({ length: 1000 }, (_, index) => ({
|
||||
role: "tool",
|
||||
tool_call_id: `tool-${index}`,
|
||||
content: "{}",
|
||||
})),
|
||||
],
|
||||
}),
|
||||
},
|
||||
400,
|
||||
);
|
||||
|
||||
await expectStatus(
|
||||
"/api/ai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: authorSession.headers,
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [{ role: "user", content: "x".repeat(256 * 1024 + 1) }],
|
||||
}),
|
||||
},
|
||||
413,
|
||||
);
|
||||
await expectStatus(
|
||||
"/api/ai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: authorSession.headers,
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "image_url", image_url: { url: "https://attacker.example/tracker.png" } }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
413,
|
||||
);
|
||||
const usageAfterRejectedRequest = await request("/api/ai/usage", { headers: authorSession.headers });
|
||||
if (usageAfterRejectedRequest.used !== 0) {
|
||||
throw new Error("A rejected oversized request-unit batch consumed allowance");
|
||||
}
|
||||
|
||||
await expectStatus(
|
||||
"/api/ai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: authorSession.headers,
|
||||
body: JSON.stringify({ model: "another-model", messages: [{ role: "user", content: "hello" }] }),
|
||||
},
|
||||
400,
|
||||
);
|
||||
|
||||
const streamStartedAt = performance.now();
|
||||
const streamResponse = await fetch(`${baseUrl}/api/ai/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", ...authorSession.headers },
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
if (!streamResponse.ok || streamResponse.headers.get("x-homarr-quota-remaining") !== "49") {
|
||||
throw new Error(`Streaming provider request failed: ${streamResponse.status}`);
|
||||
}
|
||||
const streamReader = streamResponse.body.getReader();
|
||||
const firstChunk = await streamReader.read();
|
||||
const firstChunkAt = performance.now();
|
||||
let streamText = new TextDecoder().decode(firstChunk.value ?? new Uint8Array());
|
||||
while (true) {
|
||||
const chunk = await streamReader.read();
|
||||
if (chunk.done) break;
|
||||
streamText += new TextDecoder().decode(chunk.value);
|
||||
}
|
||||
const dataFrames = streamText.split(/\r?\n/u).filter((line) => line.startsWith("data:"));
|
||||
if (
|
||||
firstChunk.done ||
|
||||
!streamText.includes("Hello from ") ||
|
||||
!streamText.includes("the Homarr provider") ||
|
||||
streamText.includes("mock/team-selected-model") ||
|
||||
!streamText.includes('"model":"homarr/model"') ||
|
||||
dataFrames.length < 3 ||
|
||||
dataFrames.at(-1)?.trim() !== "data: [DONE]" ||
|
||||
firstChunkAt - streamStartedAt > 5_000
|
||||
) {
|
||||
throw new Error("The Homarr provider did not preserve the upstream SSE stream");
|
||||
}
|
||||
const malformedStreamResponse = await fetch(`${baseUrl}/api/ai/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", ...concurrencySession.headers },
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [{ role: "user", content: "malformed stream" }],
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
const malformedStreamText = await malformedStreamResponse.text();
|
||||
if (
|
||||
!malformedStreamResponse.ok ||
|
||||
!malformedStreamText.includes('"type":"homarr_provider_error"') ||
|
||||
malformedStreamText.includes('data: {"model":') ||
|
||||
!malformedStreamText.endsWith("data: [DONE]\n\n")
|
||||
) {
|
||||
throw new Error(`Malformed upstream streams did not terminate safely: ${malformedStreamText}`);
|
||||
}
|
||||
const toolResponse = await fetch(`${baseUrl}/api/ai/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...authorSession.headers,
|
||||
"http-referer": "https://attacker.example",
|
||||
"x-openrouter-title": "Attacker",
|
||||
"x-openrouter-metadata": "attacker",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [
|
||||
{ role: "user", content: "inspect my dashboard" },
|
||||
{ role: "assistant", content: null, tool_calls: [{ id: "one" }, { id: "two" }] },
|
||||
{ role: "tool", tool_call_id: "one", content: "{}" },
|
||||
{ role: "tool", tool_call_id: "two", content: "{}" },
|
||||
],
|
||||
stream: false,
|
||||
models: ["attacker/model"],
|
||||
provider: { order: ["attacker"] },
|
||||
route: "fallback",
|
||||
plugins: ["web"],
|
||||
transforms: ["middle-out"],
|
||||
max_tokens: 1_000_000,
|
||||
max_completion_tokens: 1_000_000,
|
||||
n: 100,
|
||||
parallel_tool_calls: true,
|
||||
extra_body: { provider: { order: ["attacker"] } },
|
||||
extra_headers: { Authorization: "Bearer attacker" },
|
||||
metadata: { user: "must-not-reach-openrouter" },
|
||||
audio: { format: "wav", voice: "alloy" },
|
||||
modalities: ["text", "audio"],
|
||||
logprobs: true,
|
||||
top_logprobs: 20,
|
||||
prediction: { content: "attacker-controlled prediction" },
|
||||
service_tier: "priority",
|
||||
reasoning: { effort: "high", max_tokens: 1_000_000 },
|
||||
user: "must-not-reach-openrouter",
|
||||
tools: [
|
||||
{ type: "function", function: { name: "board_list" } },
|
||||
{ type: "openrouter:web_search", parameters: { max_results: 1000, max_uses: 1000 } },
|
||||
{ type: "openrouter:web_search" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (!toolResponse.ok || toolResponse.headers.get("x-homarr-quota-remaining") !== "48") {
|
||||
throw new Error("Every upstream inference must consume one Homarr provider request unit");
|
||||
}
|
||||
const toolCompletion = await toolResponse.json();
|
||||
if (
|
||||
toolCompletion.model !== "homarr/model" ||
|
||||
toolCompletion.choices?.[0]?.finish_reason !== "tool_calls" ||
|
||||
toolCompletion.choices?.[0]?.message?.tool_calls?.[0]?.function?.name !== "board_list"
|
||||
) {
|
||||
throw new Error(
|
||||
`The public model alias or function tool response was not forwarded: ${JSON.stringify(toolCompletion)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const streamedToolResponse = await fetch(`${baseUrl}/api/ai/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", ...concurrencySession.headers },
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [{ role: "user", content: "inspect my dashboard" }],
|
||||
tools: [{ type: "function", function: { name: "board_list", parameters: { type: "object" } } }],
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
const streamedToolText = await streamedToolResponse.text();
|
||||
if (
|
||||
!streamedToolResponse.ok ||
|
||||
streamedToolText.includes("mock/team-selected-model") ||
|
||||
!streamedToolText.includes('"model":"homarr/model"') ||
|
||||
!streamedToolText.includes('"name":"board_list"') ||
|
||||
!streamedToolText.includes('"arguments":"{"') ||
|
||||
!streamedToolText.includes('"arguments":"}"') ||
|
||||
!streamedToolText.includes('"finish_reason":"tool_calls"') ||
|
||||
!streamedToolText.endsWith("data: [DONE]\n\n")
|
||||
) {
|
||||
throw new Error(`Streamed function tool calls were not preserved safely: ${streamedToolText}`);
|
||||
}
|
||||
|
||||
const afterToolsUsage = await request("/api/ai/usage", { headers: authorSession.headers });
|
||||
if (afterToolsUsage.used !== 2 || afterToolsUsage.remaining !== 48) {
|
||||
throw new Error(`Tool request accounting is incorrect: ${JSON.stringify(afterToolsUsage)}`);
|
||||
}
|
||||
|
||||
const upstreamFailure = await fetch(`${baseUrl}/api/ai/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", ...authorSession.headers },
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [{ role: "user", content: "upstream failure" }],
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
if (upstreamFailure.status !== 503 || upstreamFailure.headers.get("x-homarr-quota-remaining") !== "47") {
|
||||
throw new Error("Every request forwarded upstream must consume Homarr provider allowance");
|
||||
}
|
||||
const upstreamFailureBody = await upstreamFailure.text();
|
||||
if (upstreamFailureBody.includes("Simulated upstream failure")) {
|
||||
throw new Error("Upstream error details must not be exposed to clients");
|
||||
}
|
||||
const afterFailureUsage = await request("/api/ai/usage", { headers: authorSession.headers });
|
||||
if (afterFailureUsage.used !== 3 || afterFailureUsage.remaining !== 47) {
|
||||
throw new Error(`Forwarded upstream failure escaped quota accounting: ${JSON.stringify(afterFailureUsage)}`);
|
||||
}
|
||||
const visitorUsage = await request("/api/ai/usage", { headers: visitorSession.headers });
|
||||
if (visitorUsage.used !== 0 || visitorUsage.remaining !== 50) {
|
||||
throw new Error(`Provider allowance leaked across users: ${JSON.stringify(visitorUsage)}`);
|
||||
}
|
||||
|
||||
await expectStatus("/api/collections/assistant_quotas/records", { headers: authorSession.headers }, 403);
|
||||
await expectStatus("/api/collections/assistant_global_quota/records", { headers: authorSession.headers }, 403);
|
||||
const quotas = await request("/api/collections/assistant_quotas/records?perPage=20", { headers: rootHeaders });
|
||||
const authorQuota = quotas.items.find((item) => item.user === author.id);
|
||||
if (
|
||||
!authorQuota ||
|
||||
authorQuota.used !== 3 ||
|
||||
["dailyLimit", "inputTokens", "outputTokens", "totalTokens", "model", "cost"].some((field) => field in authorQuota)
|
||||
) {
|
||||
throw new Error(`Private provider quota is incorrect: ${JSON.stringify(authorQuota)}`);
|
||||
}
|
||||
|
||||
await request(`/api/collections/assistant_quotas/records/${authorQuota.id}`, {
|
||||
method: "PATCH",
|
||||
headers: rootHeaders,
|
||||
body: JSON.stringify({ day: "2000-01-01", used: 49 }),
|
||||
});
|
||||
const resetUsage = await request("/api/ai/usage", { headers: authorSession.headers });
|
||||
if (resetUsage.limit !== 50 || resetUsage.used !== 0 || resetUsage.remaining !== 50) {
|
||||
throw new Error(`Quota did not reset at the UTC day boundary: ${JSON.stringify(resetUsage)}`);
|
||||
}
|
||||
const resetQuota = (
|
||||
await request("/api/collections/assistant_quotas/records?perPage=20", { headers: rootHeaders })
|
||||
).items.find((item) => item.user === author.id);
|
||||
await request(`/api/collections/assistant_quotas/records/${resetQuota.id}`, {
|
||||
method: "PATCH",
|
||||
headers: rootHeaders,
|
||||
body: JSON.stringify({ used: 49 }),
|
||||
});
|
||||
|
||||
const allowanceProbe = () =>
|
||||
fetch(`${baseUrl}/api/ai/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", ...authorSession.headers },
|
||||
body: JSON.stringify({
|
||||
model: "homarr/model",
|
||||
messages: [{ role: "user", content: "allowance probe" }],
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
const concurrentAllowanceResponses = await Promise.all([allowanceProbe(), allowanceProbe()]);
|
||||
const concurrentStatuses = concurrentAllowanceResponses.map((response) => response.status).toSorted();
|
||||
const exhaustedResponse = concurrentAllowanceResponses.find((response) => response.status === 429);
|
||||
if (
|
||||
concurrentStatuses.join(",") !== "200,429" ||
|
||||
exhaustedResponse?.headers.get("x-homarr-quota-remaining") !== "0" ||
|
||||
!exhaustedResponse.headers.get("x-homarr-quota-reset")
|
||||
) {
|
||||
throw new Error("The per-user allowance was not enforced atomically");
|
||||
}
|
||||
|
||||
const globalQuota = (await request("/api/collections/assistant_global_quota/records", { headers: rootHeaders }))
|
||||
.items[0];
|
||||
await request(`/api/collections/assistant_global_quota/records/${globalQuota.id}`, {
|
||||
method: "PATCH",
|
||||
headers: rootHeaders,
|
||||
body: JSON.stringify({ day: new Date().toISOString().slice(0, 10), used: 9_999 }),
|
||||
});
|
||||
await request(`/api/collections/assistant_quotas/records/${resetQuota.id}`, {
|
||||
method: "PATCH",
|
||||
headers: rootHeaders,
|
||||
body: JSON.stringify({ used: 0 }),
|
||||
});
|
||||
const globalResponses = await Promise.all([
|
||||
allowanceProbe(),
|
||||
fetch(`${baseUrl}/api/ai/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", ...visitorSession.headers },
|
||||
body: JSON.stringify({ model: "homarr/model", messages: [{ role: "user", content: "global allowance probe" }] }),
|
||||
}),
|
||||
]);
|
||||
if (
|
||||
globalResponses
|
||||
.map((response) => response.status)
|
||||
.toSorted()
|
||||
.join(",") !== "200,429"
|
||||
) {
|
||||
throw new Error("The shared upstream allowance was not enforced atomically across users");
|
||||
}
|
||||
|
||||
const widget = {
|
||||
$schema: "homarr-custom-widget-v2",
|
||||
|
||||
Reference in new issue
Block a user