fix(web): address review follow-ups from the lib/languageModels cleanup (#15127)

This commit is contained in:
Raunak Bhagat authored and GitHub committed 2026-09-25 17:48:58 +00:00
1 parent 656c5ac77c
commit 0371b66696
21 files changed
+153 -103

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ import { Button } from "@opal/components";
import { Text } from "@opal/components";
import { ContentAction } from "@opal/layouts";
import { SvgChevronLeft, SvgChevronRight, SvgEyeOff, SvgX } from "@opal/icons";
import { getModelIcon } from "@/lib/languageModels";
import { getModelIcon } from "@/lib/languageModels/utils";
import AgentMessage, {
AgentMessageProps,
} from "@/app/app/message/messageComponents/AgentMessage";
@@ -35,7 +35,7 @@ import TTSButton from "@/app/app/message/messageComponents/TTSButton";
import { useVoiceMode } from "@/providers/VoiceModeProvider";
import { useVoiceStatus } from "@/hooks/useVoiceStatus";
import { findModelConfigId } from "@/lib/languageModels/options";
import { getModelIcon } from "@/lib/languageModels";
import { getModelIcon } from "@/lib/languageModels/utils";
interface SouurcesTagWrapperProps {
citations: StreamingCitation[];
@@ -20,7 +20,7 @@ import {
setStoredRecommendedModelsOnly,
} from "@/app/craft/utils/llmPreferences";
import { useUser } from "@/providers/UserProvider";
import { getModelIcon } from "@/lib/languageModels";
import { getModelIcon } from "@/lib/languageModels/utils";
import { AGGREGATOR_PROVIDERS } from "@/lib/languageModels/svc";
import { Section } from "@/layouts/general-layouts";
import {
@@ -5,7 +5,7 @@ import { SelectButton } from "@opal/components";
import { cn } from "@opal/utils";
import { BuildLLMPopover } from "@/app/craft/components/BuildLLMPopover";
import { useLanguageModels } from "@/lib/languageModels/hooks";
import { getModelIcon } from "@/lib/languageModels";
import { getModelIcon } from "@/lib/languageModels/utils";
import { BuildLlmSelection } from "@/app/craft/onboarding/constants";
import { getPreferredLlmSelection } from "@/app/craft/utils/llmPreferences";
import { useUser } from "@/providers/UserProvider";
@@ -6,7 +6,7 @@ import { SvgCpu } from "@opal/icons";
import { Divider } from "@opal/components";
import { ContentAction } from "@opal/layouts";
import LLMProviderCard from "@/sections/onboarding/components/LLMProviderCard";
import { getProvider } from "@/lib/languageModels";
import { getProvider } from "@/lib/languageModels/utils";
import { useLLMProviderOptions } from "@/lib/hooks/useLLMProviderOptions";
import {
CRAFT_PROVIDERS,
@@ -6,6 +6,7 @@ import useSWR from "swr";
import {
useLanguageModels,
useLanguageModelsForAgent,
useVisionLanguageModels,
} from "@/lib/languageModels/hooks";
import { errorHandlingFetcher } from "@/lib/fetcher";
@@ -75,6 +76,48 @@ describe("useLanguageModels", () => {
expect(mockMutate).toHaveBeenCalled();
});
test("useVisionLanguageModels keeps only visible image-input models", () => {
const model = (
name: string,
is_visible: boolean,
supports_image_input: boolean
) => ({ name, is_visible, supports_image_input });
const providers = [
{
name: "Mixed",
model_configurations: [
model("vision-visible", true, true),
model("vision-hidden", false, true),
model("text-only", true, false),
],
},
{
name: "Text Only",
model_configurations: [model("text-a", true, false)],
},
{
name: "Hidden Vision",
model_configurations: [model("vision-b", false, true)],
},
];
mockUseSWR.mockReturnValue({
data: { providers, default_text: null, default_vision: null },
error: undefined,
mutate: jest.fn(),
isValidating: false,
} as any);
const { result } = renderHook(() => useVisionLanguageModels());
expect(result.current.llmProviders).toHaveLength(1);
expect(result.current.llmProviders?.[0]?.name).toBe("Mixed");
expect(
result.current.llmProviders?.[0]?.model_configurations.map(
(mc) => mc.name
)
).toEqual(["vision-visible"]);
});
test("reports not loading when SWR returns an error", () => {
mockUseSWR.mockReturnValue({
data: undefined,
@@ -1,7 +1,11 @@
import { defaultTailwindCSS } from "@/components/icons/icons";
import { getModelIcon } from "@/lib/languageModels";
import { IconProps } from "@opal/types";
import type { ModelIconProps } from "@/lib/languageModels/types";
import { getModelIcon } from "@/lib/languageModels/utils";
import type { IconProps } from "@opal/types";
export interface ModelIconProps extends IconProps {
provider: string;
modelName?: string;
}
export function ModelIcon({
provider,
@@ -1 +1,4 @@
export { ModelIcon } from "@/lib/languageModels/components/ModelIcon";
export {
ModelIcon,
type ModelIconProps,
} from "@/lib/languageModels/components/ModelIcon";
@@ -21,11 +21,7 @@ import {
SvgVercel,
} from "@opal/logos";
import { ZAIIcon } from "@/components/icons/icons";
import {
LLMProviderFormProps,
LLMProviderName,
} from "@/lib/languageModels/types";
import type { LLMProviderView } from "@/lib/languageModels/types";
import { LLMProviderName } from "@/lib/languageModels/types";
import OpenAIModal from "@/sections/modals/languageModels/OpenAIModal";
import AnthropicModal from "@/sections/modals/languageModels/AnthropicModal";
import OllamaModal from "@/sections/modals/languageModels/OllamaModal";
@@ -45,7 +41,7 @@ import type { ProviderEntry } from "@/lib/languageModels/types";
// ─── Text (LLM) providers ────────────────────────────────────────────────────
const PROVIDERS: Record<string, ProviderEntry> = {
export const PROVIDERS: Record<string, ProviderEntry> = {
[LLMProviderName.OPENAI]: {
icon: SvgOpenai,
productName: "GPT",
@@ -144,7 +140,7 @@ const PROVIDERS: Record<string, ProviderEntry> = {
},
};
const DEFAULT_ENTRY: ProviderEntry = {
export const DEFAULT_ENTRY: ProviderEntry = {
icon: SvgCpu,
productName: "",
companyName: "",
@@ -153,37 +149,13 @@ const DEFAULT_ENTRY: ProviderEntry = {
// Providers that don't use custom_config themselves, so a non-empty
// custom_config means the provider was originally created via CustomModal.
const CUSTOM_CONFIG_OVERRIDES = new Set<string>([
export const CUSTOM_CONFIG_OVERRIDES = new Set<string>([
LLMProviderName.OPENAI,
LLMProviderName.ANTHROPIC,
LLMProviderName.AZURE,
LLMProviderName.OPENROUTER,
]);
export function getProvider(
providerName: string,
existingProvider?: LLMProviderView
): ProviderEntry {
const entry = PROVIDERS[providerName] ?? {
...DEFAULT_ENTRY,
productName: providerName,
companyName: providerName,
};
// An empty custom_config carries no signal of origin. Only a non-empty map
// marks a provider created via the custom form.
const customConfig = existingProvider?.custom_config;
if (
customConfig != null &&
Object.keys(customConfig).length > 0 &&
CUSTOM_CONFIG_OVERRIDES.has(providerName)
) {
return { ...entry, Modal: CustomModal };
}
return entry;
}
// ─── Aggregator providers ────────────────────────────────────────────────────
// Providers that host models from multiple vendors (e.g. Bedrock hosts Claude,
// Llama, etc.) Used by the model-icon resolver to prioritise vendor icons.
@@ -203,9 +175,9 @@ export const AGGREGATOR_PROVIDERS = new Set([
LLMProviderName.VERTEX_AI,
]);
// ─── Model-aware icon resolver ───────────────────────────────────────────────
// ─── Model icons ─────────────────────────────────────────────────────────────
const MODEL_ICON_MAP: Record<string, IconFunctionComponent> = {
export const MODEL_ICON_MAP: Record<string, IconFunctionComponent> = {
[LLMProviderName.OPENAI]: SvgOpenai,
[LLMProviderName.ANTHROPIC]: SvgClaude,
[LLMProviderName.OLLAMA_CHAT]: SvgOllama,
@@ -239,45 +211,3 @@ const MODEL_ICON_MAP: Record<string, IconFunctionComponent> = {
zai: ZAIIcon,
bedrock_converse: SvgAws,
};
/**
* Model-aware icon resolver that checks both provider name and model name
* to pick the most specific icon (e.g. Claude icon for a Bedrock Claude model).
*/
export function getModelIcon(
providerName: string,
modelName?: string
): IconFunctionComponent {
const lowerProviderName = providerName.toLowerCase();
// For aggregator providers, prioritise showing the vendor icon based on model name
if (AGGREGATOR_PROVIDERS.has(lowerProviderName) && modelName) {
const lowerModelName = modelName.toLowerCase();
for (const [key, icon] of Object.entries(MODEL_ICON_MAP)) {
if (lowerModelName.includes(key)) {
return icon;
}
}
}
// Check if provider name directly matches an icon
if (lowerProviderName in MODEL_ICON_MAP) {
const icon = MODEL_ICON_MAP[lowerProviderName];
if (icon) {
return icon;
}
}
// For non-aggregator providers, check if model name contains any of the keys
if (modelName) {
const lowerModelName = modelName.toLowerCase();
for (const [key, icon] of Object.entries(MODEL_ICON_MAP)) {
if (lowerModelName.includes(key)) {
return icon;
}
}
}
// Fallback to CPU icon if no matches
return SvgCpu;
}
+1 -3
View File
@@ -1,6 +1,4 @@
import type { FunctionComponent } from "react";
import type { IconProps } from "@opal/types";
import { getModelIcon, getProvider } from "@/lib/languageModels";
import { getModelIcon, getProvider } from "@/lib/languageModels/utils";
import { AGGREGATOR_PROVIDERS } from "@/lib/languageModels/svc";
import type {
LLMOption,
-5
View File
@@ -422,8 +422,3 @@ export interface ProviderEntry {
companyName: string;
Modal: React.ComponentType<LLMProviderFormProps>;
}
export interface ModelIconProps extends IconProps {
provider: string;
modelName?: string;
}
+78
View File
@@ -1,8 +1,20 @@
import type { IconFunctionComponent } from "@opal/types";
import { SvgCpu } from "@opal/icons";
import { MinimalAgent } from "@/lib/agents/types";
import {
AGGREGATOR_PROVIDERS,
CUSTOM_CONFIG_OVERRIDES,
DEFAULT_ENTRY,
MODEL_ICON_MAP,
PROVIDERS,
} from "@/lib/languageModels/constants";
import CustomModal from "@/sections/modals/languageModels/CustomModal";
import type {
DefaultModel,
LLMProviderDescriptor,
LLMProviderView,
ModelConfiguration,
ProviderEntry,
} from "@/lib/languageModels/types";
import { LlmDescriptor } from "@/lib/hooks";
@@ -207,3 +219,69 @@ export function getDisplayName(
}
return undefined;
}
export function getProvider(
providerName: string,
existingProvider?: LLMProviderView
): ProviderEntry {
const entry = PROVIDERS[providerName] ?? {
...DEFAULT_ENTRY,
productName: providerName,
companyName: providerName,
};
// An empty custom_config carries no signal of origin. Only a non-empty map
// marks a provider created via the custom form.
const customConfig = existingProvider?.custom_config;
if (
customConfig != null &&
Object.keys(customConfig).length > 0 &&
CUSTOM_CONFIG_OVERRIDES.has(providerName)
) {
return { ...entry, Modal: CustomModal };
}
return entry;
}
/**
* Model-aware icon resolver that checks both provider name and model name
* to pick the most specific icon (e.g. Claude icon for a Bedrock Claude model).
*/
export function getModelIcon(
providerName: string,
modelName?: string
): IconFunctionComponent {
const lowerProviderName = providerName.toLowerCase();
// For aggregator providers, prioritise showing the vendor icon based on model name
if (AGGREGATOR_PROVIDERS.has(lowerProviderName) && modelName) {
const lowerModelName = modelName.toLowerCase();
for (const [key, icon] of Object.entries(MODEL_ICON_MAP)) {
if (lowerModelName.includes(key)) {
return icon;
}
}
}
// Check if provider name directly matches an icon
if (lowerProviderName in MODEL_ICON_MAP) {
const icon = MODEL_ICON_MAP[lowerProviderName];
if (icon) {
return icon;
}
}
// For non-aggregator providers, check if model name contains any of the keys
if (modelName) {
const lowerModelName = modelName.toLowerCase();
for (const [key, icon] of Object.entries(MODEL_ICON_MAP)) {
if (lowerModelName.includes(key)) {
return icon;
}
}
}
// Fallback to CPU icon if no matches
return SvgCpu;
}
@@ -1,6 +1,6 @@
"use client";
import { getProvider } from "@/lib/languageModels";
import { getProvider } from "@/lib/languageModels/utils";
import { LLMProviderFormProps } from "@/lib/languageModels/types";
interface ProviderSetupModalProps extends Omit<
@@ -41,7 +41,7 @@ import {
type ModelSettingsPatch,
} from "@/sections/modals/languageModels/ModelSettingsPopover";
import { setDefaultLlmModelAndRefresh } from "@/lib/languageModels/cache";
import { modelDisplayName } from "@/lib/languageModels/utils";
import { getProvider, modelDisplayName } from "@/lib/languageModels/utils";
import { useAdminLanguageModels } from "@/lib/languageModels/hooks";
import { useSWRConfig } from "swr";
import {
@@ -65,7 +65,6 @@ import { SvgEdit } from "@opal/icons";
import AgentAvatar from "@/refresh-components/avatars/AgentAvatar";
import useUsers from "@/hooks/useUsers";
import { Modal } from "@opal/components";
import { getProvider } from "@/lib/languageModels";
import { useSettings } from "@/lib/settings/hooks";
// ─── DisplayNameField ────────────────────────────────────────────────────────
@@ -3,7 +3,7 @@
import React, { useState, useCallback, useMemo, useRef } from "react";
import { useTranslations } from "next-intl";
import { Popover, OpenButton } from "@opal/components";
import { getModelIcon } from "@/lib/languageModels";
import { getModelIcon } from "@/lib/languageModels/utils";
import { GLOBAL_DEFAULT_LLM_OPTION } from "@/lib/languageModels/options";
import {
useLanguageModels,
@@ -2,7 +2,7 @@
import { useState, useMemo, useRef } from "react";
import { useTranslations } from "next-intl";
import { getModelIcon } from "@/lib/languageModels";
import { getModelIcon } from "@/lib/languageModels/utils";
import {
Button,
SelectButton,
@@ -12,7 +12,7 @@ import {
OnboardingStep,
} from "@/interfaces/onboarding";
import { WellKnownLLMProviderDescriptor } from "@/lib/languageModels/types";
import { getProvider } from "@/lib/languageModels";
import { getProvider } from "@/lib/languageModels/utils";
import ProviderSetupModal from "@/sections/modals/languageModels/ProviderSetupModal";
import { Disabled } from "@opal/core";
import { ModelIcon } from "@/lib/languageModels/components";
@@ -6,7 +6,7 @@ import { Button, Modal, ProgressBar, Text, Tooltip } from "@opal/components";
import { Section } from "@opal/layouts";
import type { IconFunctionComponent } from "@opal/types";
import { formatCalendarDay } from "@/lib/dateUtils";
import { getModelIcon } from "@/lib/languageModels";
import { getModelIcon } from "@/lib/languageModels/utils";
import type { UsageExportUser } from "@/lib/usage/userUsage";
import { formatCost, formatTokens } from "@/lib/utils";
+1 -1
View File
@@ -16,7 +16,7 @@ import { Hoverable } from "@opal/core";
import { SvgCheck, SvgEdit, SvgPlus, SvgTrash, SvgX } from "@opal/icons";
import { markdown } from "@opal/utils";
import ModelSelector from "@/sections/model-selector/ModelSelector";
import { getProvider } from "@/lib/languageModels";
import { getProvider } from "@/lib/languageModels/utils";
import type { LLMOption } from "@/lib/languageModels/types";
import { useAdminLanguageModels } from "@/lib/languageModels/hooks";
import * as GeneralLayouts from "@/layouts/general-layouts";
@@ -29,7 +29,7 @@ import { SvgSlash, SvgUnplug } from "@opal/icons";
import { markdown } from "@opal/utils";
import { getImageGenForm } from "@/views/admin/ImageGenerationPage/forms";
import ProviderCard from "@/sections/admin/ProviderCard";
import { getModelIcon } from "@/lib/languageModels";
import { getModelIcon } from "@/lib/languageModels/utils";
const NO_DEFAULT_VALUE = "__none__";
+1 -1
View File
@@ -21,7 +21,7 @@ import { SvgArrowExchange, SvgSettings, SvgTrash } from "@opal/icons";
import { SettingsLayouts } from "@opal/layouts";
import { ADMIN_ROUTES } from "@/lib/admin-routes";
import * as GeneralLayouts from "@/layouts/general-layouts";
import { getProvider } from "@/lib/languageModels";
import { getProvider } from "@/lib/languageModels/utils";
import {
refreshLlmProviderCaches,
setDefaultLlmModelAndRefresh,