add OpenRouter web search option
This commit is contained in:
1 parent
35673f113e
commit
f7b291b465
25 files changed
+5753
-18
No files matched your search
@@ -19,6 +19,7 @@ import {
|
||||
items,
|
||||
} from "@homarr/db/schema";
|
||||
import {
|
||||
assistantProviderCanUseOpenRouterServerTools,
|
||||
assistantProviderIds,
|
||||
assistantProviderPresets,
|
||||
assistantProviderRequiresApiKey,
|
||||
@@ -517,6 +518,7 @@ export const assistantRouter = createTRPCRouter({
|
||||
return {
|
||||
connectionConfigured: Boolean(configuration),
|
||||
enabled: configuration?.enabled ?? false,
|
||||
webSearchEnabled: configuration?.webSearchEnabled ?? false,
|
||||
provider: configuration?.provider ?? "openrouter",
|
||||
baseUrl: configuration?.baseUrl ?? "https://openrouter.ai/api/v1",
|
||||
modelDiscoveryPath: configuration?.modelDiscoveryPath ?? "/models",
|
||||
@@ -569,6 +571,10 @@ export const assistantRouter = createTRPCRouter({
|
||||
encryptedHeaders,
|
||||
modelId: connectionChanged ? null : existing.modelId,
|
||||
enabled: connectionChanged ? false : existing.enabled,
|
||||
webSearchEnabled:
|
||||
destinationChanged || !assistantProviderCanUseOpenRouterServerTools(input.provider)
|
||||
? false
|
||||
: existing.webSearchEnabled,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(assistantConfigurations.id, configurationId));
|
||||
@@ -614,7 +620,13 @@ export const assistantRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
updateConfiguration: adminProcedure
|
||||
.input(z.object({ enabled: z.boolean(), modelId: z.string().trim().min(1).max(256) }))
|
||||
.input(
|
||||
z.object({
|
||||
enabled: z.boolean(),
|
||||
modelId: z.string().trim().min(1).max(256),
|
||||
webSearchEnabled: z.boolean().default(false),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const configuration = await getConfigurationAsync(ctx.db);
|
||||
if (!configuration) {
|
||||
@@ -624,6 +636,12 @@ export const assistantRouter = createTRPCRouter({
|
||||
if (input.enabled && requiresApiKey && !configuration.encryptedApiKey) {
|
||||
throw new TRPCError({ code: "PRECONDITION_FAILED", message: "This provider requires an API key." });
|
||||
}
|
||||
if (input.webSearchEnabled && !assistantProviderCanUseOpenRouterServerTools(configuration.provider)) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Web search requires OpenRouter or an OpenRouter-compatible proxy endpoint.",
|
||||
});
|
||||
}
|
||||
const discoveredModels = configuration.modelDiscoveryPath
|
||||
? await fetchModelsAsync(configuration).catch(() => null)
|
||||
: null;
|
||||
@@ -632,7 +650,7 @@ export const assistantRouter = createTRPCRouter({
|
||||
: input.modelId;
|
||||
await ctx.db
|
||||
.update(assistantConfigurations)
|
||||
.set({ enabled: input.enabled, modelId, updatedAt: new Date() })
|
||||
.set({ enabled: input.enabled, modelId, webSearchEnabled: input.webSearchEnabled, updatedAt: new Date() })
|
||||
.where(eq(assistantConfigurations.id, configurationId));
|
||||
}),
|
||||
|
||||
|
||||
@@ -82,6 +82,26 @@ describe("assistantRouter.updateConnection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("disables OpenRouter server tools when the provider destination changes", async () => {
|
||||
const db = await createConfiguredAssistantAsync();
|
||||
await db
|
||||
.update(assistantConfigurations)
|
||||
.set({ webSearchEnabled: true })
|
||||
.where(eq(assistantConfigurations.id, "default"));
|
||||
const caller = assistantRouter.createCaller({ db, deviceType: undefined, session: adminSession });
|
||||
|
||||
await caller.updateConnection({
|
||||
provider: "openai",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
modelDiscoveryPath: "/models",
|
||||
clearApiKey: false,
|
||||
clearCustomHeaders: false,
|
||||
});
|
||||
|
||||
const [configuration] = await db.select().from(assistantConfigurations);
|
||||
expect(configuration?.webSearchEnabled).toBe(false);
|
||||
});
|
||||
|
||||
test("preserves the persisted model when the saved connection is unchanged", async () => {
|
||||
const db = await createConfiguredAssistantAsync();
|
||||
const caller = assistantRouter.createCaller({ db, deviceType: undefined, session: adminSession });
|
||||
@@ -139,6 +159,29 @@ describe("assistantRouter.updateConfiguration", () => {
|
||||
const [configuration] = await db.select().from(assistantConfigurations);
|
||||
expect(configuration?.modelId).toBe("deepseek/deepseek-v4-pro");
|
||||
});
|
||||
|
||||
test("persists the OpenRouter web-search server tool option", async () => {
|
||||
const db = await createConfiguredAssistantAsync();
|
||||
const caller = assistantRouter.createCaller({ db, deviceType: undefined, session: adminSession });
|
||||
|
||||
await caller.updateConfiguration({ enabled: true, modelId: "example/model", webSearchEnabled: true });
|
||||
|
||||
const [configuration] = await db.select().from(assistantConfigurations);
|
||||
expect(configuration?.webSearchEnabled).toBe(true);
|
||||
});
|
||||
|
||||
test("rejects OpenRouter server tools for an incompatible provider", async () => {
|
||||
const db = await createConfiguredAssistantAsync();
|
||||
await db
|
||||
.update(assistantConfigurations)
|
||||
.set({ provider: "openai", baseUrl: "https://api.openai.com/v1" })
|
||||
.where(eq(assistantConfigurations.id, "default"));
|
||||
const caller = assistantRouter.createCaller({ db, deviceType: undefined, session: adminSession });
|
||||
|
||||
await expect(
|
||||
caller.updateConfiguration({ enabled: true, modelId: "gpt-5", webSearchEnabled: true }),
|
||||
).rejects.toThrow("Web search requires OpenRouter or an OpenRouter-compatible proxy endpoint.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("assistant conversation features", () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
assistantProviderCanUseOpenRouterServerTools,
|
||||
assistantProviderIds,
|
||||
assistantProviderPresets,
|
||||
assistantProviderRequiresApiKey,
|
||||
@@ -47,4 +48,12 @@ describe("assistant provider presets", () => {
|
||||
expect(resolveAssistantModelId([model], model.id)).toBe(model.id);
|
||||
expect(resolveAssistantModelId([model], getAssistantModelOptionLabel(model))).toBe(model.id);
|
||||
});
|
||||
|
||||
test("allows OpenRouter server tools for OpenRouter, its Homarr proxy, and explicit custom proxies", () => {
|
||||
expect(assistantProviderCanUseOpenRouterServerTools("openrouter")).toBe(true);
|
||||
expect(assistantProviderCanUseOpenRouterServerTools("homarr")).toBe(true);
|
||||
expect(assistantProviderCanUseOpenRouterServerTools("custom")).toBe(true);
|
||||
expect(assistantProviderCanUseOpenRouterServerTools("openai")).toBe(false);
|
||||
expect(assistantProviderCanUseOpenRouterServerTools("anthropic")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user