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

+16 -3
View File
@@ -18,7 +18,12 @@ import {
integrationUserPermissions,
items,
} from "@homarr/db/schema";
import { assistantProviderIds, assistantProviderPresets, assistantProviderRequiresApiKey } from "@homarr/definitions";
import {
assistantProviderIds,
assistantProviderPresets,
assistantProviderRequiresApiKey,
resolveAssistantModelId,
} from "@homarr/definitions";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../trpc";
import type { createTRPCContext } from "../trpc";
@@ -256,7 +261,9 @@ export const getSelectedModelDetailsAsync = async (configuration: AssistantConfi
const cached = selectedModelCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) return cached.value;
const value = (await fetchModelsAsync(configuration)).find((model) => model.id === configuration.modelId) ?? null;
const models = await fetchModelsAsync(configuration);
const resolvedModelId = resolveAssistantModelId(models, configuration.modelId);
const value = models.find((model) => model.id === resolvedModelId) ?? null;
selectedModelCache.clear();
selectedModelCache.set(cacheKey, { expiresAt: Date.now() + 10 * 60_000, value });
return value;
@@ -547,9 +554,15 @@ export const assistantRouter = createTRPCRouter({
if (input.enabled && requiresApiKey && !configuration.encryptedApiKey) {
throw new TRPCError({ code: "PRECONDITION_FAILED", message: "This provider requires an API key." });
}
const discoveredModels = configuration.modelDiscoveryPath
? await fetchModelsAsync(configuration).catch(() => null)
: null;
const modelId = discoveredModels
? (resolveAssistantModelId(discoveredModels, input.modelId) ?? input.modelId)
: input.modelId;
await ctx.db
.update(assistantConfigurations)
.set({ enabled: input.enabled, modelId: input.modelId, updatedAt: new Date() })
.set({ enabled: input.enabled, modelId, updatedAt: new Date() })
.where(eq(assistantConfigurations.id, configurationId));
}),
@@ -5,6 +5,7 @@ import { parse } from "superjson";
import type { Session } from "@homarr/auth";
import { createId } from "@homarr/common";
import { eq } from "@homarr/db";
import {
apps,
assistantConfigurations,
@@ -101,6 +102,44 @@ describe("assistantRouter.updateConnection", () => {
});
});
describe("assistantRouter.updateConfiguration", () => {
test("normalizes legacy model labels to the discovered provider ID", async () => {
const db = await createConfiguredAssistantAsync();
await db
.update(assistantConfigurations)
.set({ encryptedHeaders: null })
.where(eq(assistantConfigurations.id, "default"));
const caller = assistantRouter.createCaller({ db, deviceType: undefined, session: adminSession });
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(
Response.json({
data: [
{
id: "deepseek/deepseek-v4-pro",
name: "DeepSeek: DeepSeek V4 Pro",
supported_parameters: ["tools"],
architecture: { output_modalities: ["text"] },
},
],
}),
),
);
try {
await caller.updateConfiguration({
enabled: true,
modelId: "DeepSeek: DeepSeek V4 Pro (deepseek/deepseek-v4-pro)",
});
} finally {
vi.unstubAllGlobals();
}
const [configuration] = await db.select().from(assistantConfigurations);
expect(configuration?.modelId).toBe("deepseek/deepseek-v4-pro");
});
});
describe("assistant conversation features", () => {
test("returns permission-checked entities for composer mentions", async () => {
const db = await createConfiguredAssistantAsync();
@@ -1,6 +1,12 @@
import { describe, expect, test } from "vitest";
import { assistantProviderIds, assistantProviderPresets, assistantProviderRequiresApiKey } from "@homarr/definitions";
import {
assistantProviderIds,
assistantProviderPresets,
assistantProviderRequiresApiKey,
getAssistantModelOptionLabel,
resolveAssistantModelId,
} from "@homarr/definitions";
describe("assistant provider presets", () => {
test("defines one complete preset for every provider", () => {
@@ -30,4 +36,15 @@ describe("assistant provider presets", () => {
expect(() => new URL(iconUrl ?? "")).not.toThrow();
},
);
test("keeps the display label separate from the provider model ID", () => {
const model = {
id: "deepseek/deepseek-v4-pro",
name: "DeepSeek: DeepSeek V4 Pro",
};
expect(getAssistantModelOptionLabel(model)).toBe("DeepSeek: DeepSeek V4 Pro (deepseek/deepseek-v4-pro)");
expect(resolveAssistantModelId([model], model.id)).toBe(model.id);
expect(resolveAssistantModelId([model], getAssistantModelOptionLabel(model))).toBe(model.id);
});
});