From a49d34bf49f99a2ccab6bb14835f8d89820bbf2e Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:08:44 -0300 Subject: [PATCH] feat(providers): custom icon URL for compatible provider nodes (#5815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.44 — custom icon URL for compatible provider nodes (DB migration 113 + nodes.ts + Zod schema + API routes + catalog + ProviderIcon UI). Re-cut onto the release tip (branch was fossilized ~13 real files); reconciled icon_url into the release's evolved nodes.ts/routes via 3-way. Validated: 14 backend + 5 frontend(vitest) + 24 page-utils tests green, typecheck:core 0, provider-consistency OK, file-size/env-doc-sync pass. UNSTABLE red is the inherited environmental setup-claude base-red. --- config/quality/file-size-baseline.json | 2 +- .../[id]/components/CompatibleNodeCard.tsx | 50 ++-- .../[id]/components/ProviderPageHeader.tsx | 8 + .../modals/EditCompatibleNodeModal.tsx | 11 + .../components/AddCompatibleProviderModal.tsx | 10 + .../providers/components/ProviderCard.tsx | 16 +- .../dashboard/providers/providerPageUtils.ts | 13 +- src/app/api/provider-nodes/[id]/route.ts | 6 +- src/app/api/provider-nodes/route.ts | 3 + .../migrations/113_provider_node_icon_url.sql | 5 + src/lib/db/providers/nodes.ts | 12 +- src/lib/providers/catalog.ts | 5 + src/shared/components/ProviderIcon.tsx | 63 ++++ src/shared/validation/schemas/provider.ts | 24 ++ tests/unit/provider-node-icon-url.test.ts | 280 ++++++++++++++++++ tests/unit/providers-page-utils.test.ts | 19 +- tests/unit/ui/ProviderIcon-icon-url.test.tsx | 110 +++++++ 17 files changed, 611 insertions(+), 26 deletions(-) create mode 100644 src/lib/db/migrations/113_provider_node_icon_url.sql create mode 100644 tests/unit/provider-node-icon-url.test.ts create mode 100644 tests/unit/ui/ProviderIcon-icon-url.test.tsx diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 8c9420bada..69e8fbf2bc 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -299,7 +299,7 @@ "tests/unit/provider-models-route.test.ts": 1752, "tests/unit/provider-validation-specialty.test.ts": 2874, "_rebaseline_pr4613_compatible_provider_groups": "Reconcile #4613 already-merged growth: providers-page-utils.test.ts 1004->1052 (+48, buildCompatibleProviderGroups partition unit test). Fast-gate PR->release does not run check:file-size, so this surfaced post-merge.", - "tests/unit/providers-page-utils.test.ts": 1092, + "tests/unit/providers-page-utils.test.ts": 1109, "tests/unit/reasoning-cache.test.ts": 980, "tests/unit/route-edge-coverage.test.ts": 1234, "tests/unit/search-handler-extended.test.ts": 1124, diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleNodeCard.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleNodeCard.tsx index 1b01f2eb7d..5347c075a8 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleNodeCard.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/CompatibleNodeCard.tsx @@ -3,6 +3,7 @@ // Phase 1t.2 extraction — Issue #3501 import { useRouter } from "next/navigation"; import { Card, Button } from "@/shared/components"; +import ProviderIcon from "@/shared/components/ProviderIcon"; import { getApiLabel, getApiPath } from "../providerPageHelpers"; import type { ProviderMessageTranslator } from "../providerPageHelpers"; @@ -11,6 +12,8 @@ interface ProviderNode { apiType?: string; chatPath?: string; prefix?: string; + /** Optional operator-supplied remote icon URL (#2166). */ + iconUrl?: string; [key: string]: unknown; } @@ -42,24 +45,35 @@ export default function CompatibleNodeCard({ return (
-
-

- {isCcCompatible - ? t("ccCompatibleDetailsTitle") - : isAnthropicCompatible - ? t("anthropicCompatibleDetails") - : t("openaiCompatibleDetails")} -

-

- {getApiLabel(t, isAnthropicProtocolCompatible, providerNode?.apiType)} ·{" "} - {(providerNode.baseUrl || "").replace(/\/$/, "")}/ - {getApiPath( - isCcCompatible, - isAnthropicCompatible, - providerNode?.apiType, - providerNode?.chatPath - )} -

+
+ {providerNode.iconUrl && ( + + )} +
+

+ {isCcCompatible + ? t("ccCompatibleDetailsTitle") + : isAnthropicCompatible + ? t("anthropicCompatibleDetails") + : t("openaiCompatibleDetails")} +

+

+ {getApiLabel(t, isAnthropicProtocolCompatible, providerNode?.apiType)} ·{" "} + {(providerNode.baseUrl || "").replace(/\/$/, "")}/ + {getApiPath( + isCcCompatible, + isAnthropicCompatible, + providerNode?.apiType, + providerNode?.chatPath + )} +

+
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx index 049797e4df..ec0f1fcdf7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditCompatibleNodeModal.tsx @@ -11,6 +11,7 @@ interface EditCompatibleNodeModalNode { baseUrl?: string; chatPath?: string; modelsPath?: string; + iconUrl?: string; } interface EditCompatibleNodeModalProps { @@ -38,6 +39,7 @@ export default function EditCompatibleNodeModal({ baseUrl: "https://api.openai.com/v1", chatPath: "", modelsPath: "", + iconUrl: "", }); const [saving, setSaving] = useState(false); const [checkKey, setCheckKey] = useState(""); @@ -63,6 +65,7 @@ export default function EditCompatibleNodeModal({ : "https://api.openai.com/v1"), chatPath: node.chatPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_CHAT_PATH : ""), modelsPath: isCcCompatible ? "" : node.modelsPath || "", + iconUrl: node.iconUrl || "", }); setShowAdvanced( !!( @@ -93,6 +96,7 @@ export default function EditCompatibleNodeModal({ baseUrl: formData.baseUrl, chatPath: formData.chatPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_CHAT_PATH : ""), modelsPath: isCcCompatible ? "" : formData.modelsPath, + iconUrl: formData.iconUrl.trim(), }; if (!isAnthropic) { payload.apiType = formData.apiType; @@ -208,6 +212,13 @@ export default function EditCompatibleNodeModal({ }) } /> + setFormData({ ...formData, iconUrl: e.target.value })} + placeholder="https://example.com/logo.png" + hint={t("iconUrlHint")} + />