fix(ui): bundle provider logos as static imports and unify fallback in Logo component (#34125)

* fix(ui): bundle provider logos as static imports and unify fallback in Logo component

providerLogoMap values are now content-hashed bundle URLs emitted by
static imports instead of /ui/assets/logos/ path strings, so any
deployment that serves the app JS also serves the logos: dev server,
proxy /ui mount, server_root_path sub-paths, and the split-chart nginx
image where the old route 404d in production. A missing file is now a
build error instead of a silent runtime 404.

resolveLogoSrc passes /_next/ URLs through untouched so bundled values
never get double-prefixed with the server root path. The new Logo
molecule owns resolution and the letter-avatar fallback and warns with
the failing URL on load error; ProviderLogo delegates to it. The three
bare img sites in the agents wizard render through Logo, fixing their
broken-image bug.

Dashscope now uses qwen.png, RunwayML the on-disk runway.png, and the
GradientAI entry is removed (no plausible asset exists). soniox.svg and
ai21.svg drop a single mismatched intrinsic dimension attribute that
Turbopack's import-time image parser rejects. Dead logoSrc lookup in
AddModelForm deleted. Vitest resolves image imports to Next's
StaticImageData shape via a config plugin so tests exercise the same
/_next/ URLs as production.

* fix(ui): retry logo load when src changes after an error

Track which src errored instead of a boolean so a Logo instance whose
source changes in place (agents modal title) attempts the new URL
rather than staying on the letter-avatar until remount.
This commit is contained in:
ryan-crabbe-berri 2026-07-21 14:27:36 -07:00 • committed by GitHub
parent 1cc70f84c0
commit 2e8403a073
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 400 additions and 140 deletions

View file

@ -1 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 103 24" xmlns="http://www.w3.org/2000/svg"><title>AI21</title><path d="M15.064 21.643l-.74-2.335H7.487l-.741 2.335H2L8.862 2.414h4.09l6.944 19.23h-4.832zM10.92 7.908L8.56 15.819h4.666l-2.305-7.911zm9.504-5.494h4.501v19.23h-4.501V2.413zm5.714 15.3a8.84 8.84 0 011.057-2.691 7.78 7.78 0 011.606-1.868 16.915 16.915 0 012.045-1.456c.567-.33 1.093-.646 1.578-.948.447-.275.874-.582 1.276-.92.348-.289.64-.638.865-1.03.214-.391.323-.832.315-1.278 0-.769-.21-1.323-.63-1.662-.447-.347-1-.526-1.565-.508A2.475 2.475 0 0030.943 6c-.467.43-.7 1.15-.7 2.156h-4.42a6.493 6.493 0 01.454-2.445c.294-.74.749-1.406 1.33-1.95a6.26 6.26 0 012.142-1.291A8.363 8.363 0 0132.657 2a9.048 9.048 0 012.512.344c.76.21 1.472.565 2.1 1.044a4.972 4.972 0 011.44 1.813c.374.823.557 1.72.536 2.623a4.64 4.64 0 01-.522 2.198 7.454 7.454 0 01-1.276 1.758c-.497.508-1.044.963-1.633 1.36-.586.394-1.117.728-1.592 1.003-.66.44-1.204.82-1.633 1.14a7.753 7.753 0 00-1.03.892 2.403 2.403 0 00-.535.852 3.128 3.128 0 00-.15 1.017h8.234v3.598h-13.34c-.023-1.32.102-2.637.371-3.928zM39.958 5.792c.769.016 1.538-.053 2.292-.206a3.307 3.307 0 001.386-.618 2.14 2.14 0 00.686-1.044c.137-.491.202-1 .192-1.51h3.926v19.23h-4.528V8.923h-3.954V5.792z"></path><path d="M53.534 2.414h4.199v19.23h-4.2V2.413zm19.704 5.978v13.241h-4.2v-1.868a3.434 3.434 0 01-.659.865 4.24 4.24 0 01-.946.686c-.371.198-.762.354-1.167.466-.41.118-.835.178-1.262.179a6.33 6.33 0 01-2.622-.536 6.207 6.207 0 01-2.044-1.456 6.455 6.455 0 01-1.318-2.197 8.402 8.402 0 010-5.522 6.455 6.455 0 011.318-2.198 6.207 6.207 0 012.044-1.455 6.329 6.329 0 012.622-.535c.427.001.852.061 1.263.179.406.113.798.274 1.166.48.347.194.666.434.947.715.257.252.48.539.659.851V8.392h4.199zm-7.356 10c.445.006.886-.088 1.29-.275a3.147 3.147 0 001.634-1.8c.156-.417.235-.859.233-1.304a3.546 3.546 0 00-.879-2.363 3.056 3.056 0 00-.989-.742 3.168 3.168 0 00-2.58 0 3.06 3.06 0 00-.989.742 3.549 3.549 0 00-.878 2.363c-.002.445.077.888.233 1.305a3.153 3.153 0 001.634 1.8c.404.187.845.28 1.29.273zm12.459 3.251h-4.2V2.414h4.2v7.883a4.218 4.218 0 011.619-1.566c.37-.202.76-.364 1.166-.48.415-.12.845-.18 1.276-.179a6.33 6.33 0 012.622.536c.773.34 1.47.836 2.044 1.456a6.47 6.47 0 011.318 2.197 8.413 8.413 0 010 5.522 6.47 6.47 0 01-1.318 2.198 6.203 6.203 0 01-2.044 1.456 6.33 6.33 0 01-2.622.535 4.577 4.577 0 01-1.276-.178 6.213 6.213 0 01-1.166-.466 4.12 4.12 0 01-.96-.687 3.435 3.435 0 01-.66-.865v1.867zm3.184-3.241c.436.004.868-.09 1.263-.275a3.148 3.148 0 001.634-1.8c.156-.416.235-.859.232-1.304a3.547 3.547 0 00-.878-2.363 3.056 3.056 0 00-.99-.741 2.901 2.901 0 00-1.261-.276 3.012 3.012 0 00-2.305 1.016 3.546 3.546 0 00-.879 2.362 3.66 3.66 0 00.233 1.305c.145.395.364.759.645 1.072a3.1 3.1 0 002.306 1.004zm15.219-4.56a20.56 20.56 0 011.646.535c.479.175.932.415 1.345.714.38.28.695.642.92 1.058.244.494.362 1.042.343 1.593a3.937 3.937 0 01-.467 1.992 3.71 3.71 0 01-1.29 1.319c-.587.353-1.234.6-1.907.727-.765.15-1.542.224-2.32.22-1.885 0-3.372-.412-4.46-1.236-1.09-.824-1.413-2.006-1.413-3.544h3.897c0 .696.197 1.195.59 1.497a2.43 2.43 0 001.524.453c.434.019.866-.08 1.248-.288a1.018 1.018 0 00.48-.948.898.898 0 00-.205-.618 1.982 1.982 0 00-.632-.44 7.884 7.884 0 00-1.097-.412c-.45-.137-.985-.306-1.606-.508a21.019 21.019 0 01-1.565-.535 5.688 5.688 0 01-1.304-.7 2.978 2.978 0 01-.891-1.045 3.427 3.427 0 01-.33-1.593c0-1.374.526-2.39 1.578-3.05 1.053-.659 2.475-.989 4.268-.99a8.335 8.335 0 012.512.344c.652.192 1.26.514 1.784.948.457.39.818.878 1.057 1.429.238.555.36 1.153.357 1.758h-4.145a1.798 1.798 0 00-.425-1.278 1.71 1.71 0 00-1.304-.453 2.04 2.04 0 00-1.098.289.972.972 0 00-.467.892.828.828 0 00.22.591c.185.182.405.327.645.426.344.15.697.279 1.057.384.42.13.905.285 1.455.468z" fill="#E91E63"></path></svg>
<svg fill="currentColor" fill-rule="evenodd" style="flex:none;line-height:1" viewBox="0 0 103 24" xmlns="http://www.w3.org/2000/svg"><title>AI21</title><path d="M15.064 21.643l-.74-2.335H7.487l-.741 2.335H2L8.862 2.414h4.09l6.944 19.23h-4.832zM10.92 7.908L8.56 15.819h4.666l-2.305-7.911zm9.504-5.494h4.501v19.23h-4.501V2.413zm5.714 15.3a8.84 8.84 0 011.057-2.691 7.78 7.78 0 011.606-1.868 16.915 16.915 0 012.045-1.456c.567-.33 1.093-.646 1.578-.948.447-.275.874-.582 1.276-.92.348-.289.64-.638.865-1.03.214-.391.323-.832.315-1.278 0-.769-.21-1.323-.63-1.662-.447-.347-1-.526-1.565-.508A2.475 2.475 0 0030.943 6c-.467.43-.7 1.15-.7 2.156h-4.42a6.493 6.493 0 01.454-2.445c.294-.74.749-1.406 1.33-1.95a6.26 6.26 0 012.142-1.291A8.363 8.363 0 0132.657 2a9.048 9.048 0 012.512.344c.76.21 1.472.565 2.1 1.044a4.972 4.972 0 011.44 1.813c.374.823.557 1.72.536 2.623a4.64 4.64 0 01-.522 2.198 7.454 7.454 0 01-1.276 1.758c-.497.508-1.044.963-1.633 1.36-.586.394-1.117.728-1.592 1.003-.66.44-1.204.82-1.633 1.14a7.753 7.753 0 00-1.03.892 2.403 2.403 0 00-.535.852 3.128 3.128 0 00-.15 1.017h8.234v3.598h-13.34c-.023-1.32.102-2.637.371-3.928zM39.958 5.792c.769.016 1.538-.053 2.292-.206a3.307 3.307 0 001.386-.618 2.14 2.14 0 00.686-1.044c.137-.491.202-1 .192-1.51h3.926v19.23h-4.528V8.923h-3.954V5.792z"></path><path d="M53.534 2.414h4.199v19.23h-4.2V2.413zm19.704 5.978v13.241h-4.2v-1.868a3.434 3.434 0 01-.659.865 4.24 4.24 0 01-.946.686c-.371.198-.762.354-1.167.466-.41.118-.835.178-1.262.179a6.33 6.33 0 01-2.622-.536 6.207 6.207 0 01-2.044-1.456 6.455 6.455 0 01-1.318-2.197 8.402 8.402 0 010-5.522 6.455 6.455 0 011.318-2.198 6.207 6.207 0 012.044-1.455 6.329 6.329 0 012.622-.535c.427.001.852.061 1.263.179.406.113.798.274 1.166.48.347.194.666.434.947.715.257.252.48.539.659.851V8.392h4.199zm-7.356 10c.445.006.886-.088 1.29-.275a3.147 3.147 0 001.634-1.8c.156-.417.235-.859.233-1.304a3.546 3.546 0 00-.879-2.363 3.056 3.056 0 00-.989-.742 3.168 3.168 0 00-2.58 0 3.06 3.06 0 00-.989.742 3.549 3.549 0 00-.878 2.363c-.002.445.077.888.233 1.305a3.153 3.153 0 001.634 1.8c.404.187.845.28 1.29.273zm12.459 3.251h-4.2V2.414h4.2v7.883a4.218 4.218 0 011.619-1.566c.37-.202.76-.364 1.166-.48.415-.12.845-.18 1.276-.179a6.33 6.33 0 012.622.536c.773.34 1.47.836 2.044 1.456a6.47 6.47 0 011.318 2.197 8.413 8.413 0 010 5.522 6.47 6.47 0 01-1.318 2.198 6.203 6.203 0 01-2.044 1.456 6.33 6.33 0 01-2.622.535 4.577 4.577 0 01-1.276-.178 6.213 6.213 0 01-1.166-.466 4.12 4.12 0 01-.96-.687 3.435 3.435 0 01-.66-.865v1.867zm3.184-3.241c.436.004.868-.09 1.263-.275a3.148 3.148 0 001.634-1.8c.156-.416.235-.859.232-1.304a3.547 3.547 0 00-.878-2.363 3.056 3.056 0 00-.99-.741 2.901 2.901 0 00-1.261-.276 3.012 3.012 0 00-2.305 1.016 3.546 3.546 0 00-.879 2.362 3.66 3.66 0 00.233 1.305c.145.395.364.759.645 1.072a3.1 3.1 0 002.306 1.004zm15.219-4.56a20.56 20.56 0 011.646.535c.479.175.932.415 1.345.714.38.28.695.642.92 1.058.244.494.362 1.042.343 1.593a3.937 3.937 0 01-.467 1.992 3.71 3.71 0 01-1.29 1.319c-.587.353-1.234.6-1.907.727-.765.15-1.542.224-2.32.22-1.885 0-3.372-.412-4.46-1.236-1.09-.824-1.413-2.006-1.413-3.544h3.897c0 .696.197 1.195.59 1.497a2.43 2.43 0 001.524.453c.434.019.866-.08 1.248-.288a1.018 1.018 0 00.48-.948.898.898 0 00-.205-.618 1.982 1.982 0 00-.632-.44 7.884 7.884 0 00-1.097-.412c-.45-.137-.985-.306-1.606-.508a21.019 21.019 0 01-1.565-.535 5.688 5.688 0 01-1.304-.7 2.978 2.978 0 01-.891-1.045 3.427 3.427 0 01-.33-1.593c0-1.374.526-2.39 1.578-3.05 1.053-.659 2.475-.989 4.268-.99a8.335 8.335 0 012.512.344c.652.192 1.26.514 1.784.948.457.39.818.878 1.057 1.429.238.555.36 1.153.357 1.758h-4.145a1.798 1.798 0 00-.425-1.278 1.71 1.71 0 00-1.304-.453 2.04 2.04 0 00-1.098.289.972.972 0 00-.467.892.828.828 0 00.22.591c.185.182.405.327.645.426.344.15.697.279 1.057.384.42.13.905.285 1.455.468z" fill="#E91E63"></path></svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -1 +1 @@
<svg viewBox="0 0 100 17.5" width="92" fill="white" xmlns="http://www.w3.org/2000/svg"><title>Soniox</title><path d="m0 14.866 2.1606-3.5214c1.8927 1.2576 3.9669 1.8995 5.6694 1.8995 1.0025 0 1.4606-0.3036 1.4606-0.8847v-0.0607c0-0.6419-0.9161-0.9194-2.6532-1.4138-3.2582-0.8587-5.8509-1.9602-5.8509-5.2995v-0.06938c0-3.5214 2.8088-5.4903 6.6114-5.4903 2.4112 0 4.9089 0.70255 6.8016 1.9342l-1.9791 3.6775c-1.7112-0.95408-3.5693-1.5352-4.8744-1.5352-0.88152 0-1.3396 0.33827-1.3396 0.79796v0.06071c0 0.64184 0.94202 0.95409 2.6792 1.4745 3.2582 0.91939 5.8509 2.0556 5.8509 5.2735v0.0607c0 3.6515-2.7137 5.551-6.741 5.551-2.7656-0.0087-5.5052-0.798-7.7955-2.4546z"></path><path d="m16.135 8.7342v-0.06071c0-4.7184 3.8372-8.6735 9.1436-8.6735 5.2719 0 9.0832 3.8944 9.0832 8.6127v0.06072c0 4.7184-3.8372 8.6735-9.1437 8.6735-5.2718 0-9.0831-3.8944-9.0831-8.6128zm12.583 0v-0.06071c0-2.0209-1.4606-3.7383-3.5088-3.7383-2.1001 0-3.4483 1.6826-3.4483 3.6775v0.06072c0 2.0209 1.4605 3.7383 3.5088 3.7383 2.1087 0 3.4483-1.6827 3.4483-3.6776z"></path><path d="m36.877 0.36428h5.7904v2.3332c1.063-1.3791 2.5927-2.6974 4.9348-2.6974 3.5089 0 5.609 2.3332 5.609 6.0974v10.85h-5.7905v-8.977c0-1.8041-0.942-2.7929-2.3161-2.7929-1.4001 0-2.4372 0.9801-2.4372 2.7929v8.977h-5.7904z"></path><path d="m55.951 0.36426h5.7904v16.584h-5.7904z"></path><path d="m64.29 8.7342v-0.06071c0-4.7184 3.8373-8.6735 9.1437-8.6735 5.2719 0 9.0832 3.8944 9.0832 8.6127v0.06072c0 4.7184-3.8372 8.6735-9.1437 8.6735-5.2719 0-9.0832-3.8944-9.0832-8.6128zm12.592 0v-0.06071c0-2.0209-1.4605-3.7383-3.5088-3.7383-2.1001 0-3.4483 1.6826-3.4483 3.6775v0.06072c0 2.0209 1.4606 3.7383 3.5088 3.7383 2.1088 0 3.4483-1.6827 3.4483-3.6776z"></path><path d="m88.082 8.578-5.4533-8.2138h6.2484l2.4372 4.0765 2.4371-4.0765h6.1275l-5.4274 8.1791 5.5484 8.3959h-6.2225l-2.5582-4.2587-2.5927 4.2587h-6.0929z"></path></svg>
<svg viewBox="0 0 100 17.5" fill="white" xmlns="http://www.w3.org/2000/svg"><title>Soniox</title><path d="m0 14.866 2.1606-3.5214c1.8927 1.2576 3.9669 1.8995 5.6694 1.8995 1.0025 0 1.4606-0.3036 1.4606-0.8847v-0.0607c0-0.6419-0.9161-0.9194-2.6532-1.4138-3.2582-0.8587-5.8509-1.9602-5.8509-5.2995v-0.06938c0-3.5214 2.8088-5.4903 6.6114-5.4903 2.4112 0 4.9089 0.70255 6.8016 1.9342l-1.9791 3.6775c-1.7112-0.95408-3.5693-1.5352-4.8744-1.5352-0.88152 0-1.3396 0.33827-1.3396 0.79796v0.06071c0 0.64184 0.94202 0.95409 2.6792 1.4745 3.2582 0.91939 5.8509 2.0556 5.8509 5.2735v0.0607c0 3.6515-2.7137 5.551-6.741 5.551-2.7656-0.0087-5.5052-0.798-7.7955-2.4546z"></path><path d="m16.135 8.7342v-0.06071c0-4.7184 3.8372-8.6735 9.1436-8.6735 5.2719 0 9.0832 3.8944 9.0832 8.6127v0.06072c0 4.7184-3.8372 8.6735-9.1437 8.6735-5.2718 0-9.0831-3.8944-9.0831-8.6128zm12.583 0v-0.06071c0-2.0209-1.4606-3.7383-3.5088-3.7383-2.1001 0-3.4483 1.6826-3.4483 3.6775v0.06072c0 2.0209 1.4605 3.7383 3.5088 3.7383 2.1087 0 3.4483-1.6827 3.4483-3.6776z"></path><path d="m36.877 0.36428h5.7904v2.3332c1.063-1.3791 2.5927-2.6974 4.9348-2.6974 3.5089 0 5.609 2.3332 5.609 6.0974v10.85h-5.7905v-8.977c0-1.8041-0.942-2.7929-2.3161-2.7929-1.4001 0-2.4372 0.9801-2.4372 2.7929v8.977h-5.7904z"></path><path d="m55.951 0.36426h5.7904v16.584h-5.7904z"></path><path d="m64.29 8.7342v-0.06071c0-4.7184 3.8373-8.6735 9.1437-8.6735 5.2719 0 9.0832 3.8944 9.0832 8.6127v0.06072c0 4.7184-3.8372 8.6735-9.1437 8.6735-5.2719 0-9.0832-3.8944-9.0832-8.6128zm12.592 0v-0.06071c0-2.0209-1.4605-3.7383-3.5088-3.7383-2.1001 0-3.4483 1.6826-3.4483 3.6775v0.06072c0 2.0209 1.4606 3.7383 3.5088 3.7383 2.1088 0 3.4483-1.6827 3.4483-3.6776z"></path><path d="m88.082 8.578-5.4533-8.2138h6.2484l2.4372 4.0765 2.4371-4.0765h6.1275l-5.4274 8.1791 5.5484 8.3959h-6.2225l-2.5582-4.2587-2.5927 4.2587h-6.0929z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,89 @@
import React from "react";
import { render, screen, fireEvent, within } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import AddAgentForm from "./add_agent_form";
import * as networking from "@/components/networking";
import type { AgentCreateInfo } from "@/components/networking";
vi.mock("@/components/networking", () => ({
createAgentCall: vi.fn(),
getAgentCreateMetadata: vi.fn(),
getAgentsList: vi.fn(),
keyCreateForAgentCall: vi.fn(),
keyListCall: vi.fn(),
keyUpdateCall: vi.fn(),
modelAvailableCall: vi.fn(),
}));
vi.mock("./agent_card_discovery", () => ({
default: () => <div data-testid="agent-card-discovery" />,
}));
vi.mock("./agent_form_fields", () => ({
default: () => <div data-testid="agent-form-fields" />,
}));
const a2aInfo: AgentCreateInfo = {
agent_type: "a2a",
agent_type_display_name: "A2A Agent",
description: "Agent-to-agent protocol",
logo_url: "/ui/assets/logos/a2a_agent.png",
credential_fields: [],
use_a2a_form_fields: true,
};
const renderForm = () =>
render(<AddAgentForm visible={true} onClose={vi.fn()} accessToken="test-token" onSuccess={vi.fn()} />);
describe("AddAgentForm logos", () => {
beforeEach(() => {
vi.mocked(networking.getAgentCreateMetadata).mockReset().mockResolvedValue([a2aInfo]);
vi.mocked(networking.getAgentsList).mockReset().mockResolvedValue({ agents: [] });
vi.mocked(networking.keyListCall).mockReset().mockResolvedValue({ keys: [] });
vi.mocked(networking.modelAvailableCall).mockReset().mockResolvedValue({ data: [] });
});
it("renders the modal title and agent type selection logos as images from logo_url", async () => {
renderForm();
const titleLogo = await screen.findByAltText("Agent logo");
expect(titleLogo).toBeInstanceOf(HTMLImageElement);
expect(titleLogo).toHaveAttribute("src", expect.stringContaining("assets/logos/a2a_agent.png"));
const selectionLogo = await screen.findByAltText("A2A Agent logo");
expect(selectionLogo).toBeInstanceOf(HTMLImageElement);
expect(selectionLogo).toHaveAttribute("src", expect.stringContaining("assets/logos/a2a_agent.png"));
});
it("renders the option logo when the agent type dropdown is opened", async () => {
renderForm();
await screen.findByAltText("A2A Agent logo");
fireEvent.mouseDown(screen.getByRole("combobox"));
const optionLogos = await screen.findAllByAltText("A2A Agent logo");
expect(optionLogos.length).toBeGreaterThanOrEqual(2);
optionLogos.forEach((img) => {
expect(img).toHaveAttribute("src", expect.stringContaining("assets/logos/a2a_agent.png"));
});
});
it("swaps a failing logo for a letter avatar and warns with the url", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
renderForm();
const titleLogo = await screen.findByAltText("Agent logo");
const header = screen.getByText("Add New Agent").parentElement!;
fireEvent.error(titleLogo);
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("assets/logos/a2a_agent.png"));
expect(screen.queryByAltText("Agent logo")).not.toBeInTheDocument();
expect(within(header).getByText("A")).toBeInTheDocument();
const selectionLogo = screen.getByAltText("A2A Agent logo");
fireEvent.error(selectionLogo);
expect(screen.queryByAltText("A2A Agent logo")).not.toBeInTheDocument();
expect(warnSpy).toHaveBeenCalledTimes(2);
warnSpy.mockRestore();
});
});

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Select, Input, Steps, Radio, Tag, Divider, Switch, InputNumber, Collapse } from "antd";
import MessageManager from "@/components/molecules/message_manager";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { Logo } from "@/components/molecules/logo/Logo";
import { Button } from "@tremor/react";
import { CheckCircleFilled, KeyOutlined, RobotOutlined, AppstoreOutlined, InfoCircleOutlined } from "@ant-design/icons";
import CreatedKeyDisplay from "@/components/shared/CreatedKeyDisplay";
@ -712,17 +712,13 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
value={info.agent_type}
label={
<div className="flex items-center gap-2">
<img src={resolveLogoSrc(info.logo_url) ?? ""} alt="" className="w-4 h-4 object-contain" />
<Logo src={info.logo_url} label={info.agent_type_display_name} className="w-4 h-4 object-contain" />
<span>{info.agent_type_display_name}</span>
</div>
}
>
<div className="flex items-center gap-3 py-1">
<img
src={resolveLogoSrc(info.logo_url) ?? ""}
alt={info.agent_type_display_name}
className="w-5 h-5 object-contain"
/>
<Logo src={info.logo_url} label={info.agent_type_display_name} className="w-5 h-5 object-contain" />
<div>
<div className="font-medium">{info.agent_type_display_name}</div>
{info.description && <div className="text-xs text-gray-500">{info.description}</div>}
@ -948,7 +944,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
title={
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
{selectedLogo && currentStep < 1 && (
<img src={resolveLogoSrc(selectedLogo)} alt="Agent" className="w-6 h-6 object-contain" />
<Logo src={selectedLogo} label="Agent" className="w-6 h-6 object-contain" />
)}
<h2 className="text-xl font-semibold text-gray-900">Add New Agent</h2>
</div>

View file

@ -10,7 +10,7 @@ import React, { useEffect, useMemo, useState } from "react";
import TeamDropdown from "../common_components/team_dropdown";
import type { Team } from "../key_team_helpers/key_list";
import { type CredentialItem, type ProviderCreateInfo, modelAvailableCall } from "../networking";
import { Providers, providerLogoMap } from "../provider_info_helpers";
import { Providers } from "../provider_info_helpers";
import { ProviderLogo } from "../molecules/models/ProviderLogo";
import AdvancedSettings from "./advanced_settings";
import ConditionalPublicModelName from "./conditional_public_model_name";
@ -181,7 +181,6 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
{sortedProviderMetadata.map((providerInfo) => {
const displayName = providerInfo.provider_display_name;
const providerKey = providerInfo.provider;
const logoSrc = providerLogoMap[displayName] ?? "";
return (
<AntdSelect.Option key={providerKey} value={providerKey} data-label={displayName}>

View file

@ -0,0 +1,73 @@
import React from "react";
import { describe, expect, it, vi } from "vitest";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { Logo } from "./Logo";
import { Providers, providerLogoMap } from "@/components/provider_info_helpers";
vi.mock("@/lib/serverRootPath", () => ({ serverRootPath: "/litellm" }));
describe("Logo", () => {
it("renders the bundled logo untouched by the server root path for a known provider", () => {
render(<Logo provider="openai" />);
const img = screen.getByRole("img", { name: "openai logo" });
expect(img.getAttribute("src")).toBe(providerLogoMap[Providers.OpenAI]);
expect(img.getAttribute("src")).toContain("openai_small");
});
it("renders a letter avatar and no img for an unknown provider", () => {
render(<Logo provider="unknown_provider_xyz" />);
expect(screen.getByText("u")).toBeInTheDocument();
expect(screen.queryByRole("img")).not.toBeInTheDocument();
});
it("renders a dash avatar when neither provider nor src is given", () => {
render(<Logo />);
expect(screen.getByText("-")).toBeInTheDocument();
expect(screen.queryByRole("img")).not.toBeInTheDocument();
});
it("resolves a backend asset path through the server root path in src mode", () => {
render(<Logo src="/ui/assets/logos/github.svg" label="GitHub" />);
const img = screen.getByRole("img", { name: "GitHub logo" });
expect(img.getAttribute("src")).toBe("/litellm/ui/assets/logos/github.svg");
});
it("passes an external https URL through untouched in src mode", () => {
render(<Logo src="https://cdn.example.com/logo.png" label="Ext" />);
expect(screen.getByRole("img").getAttribute("src")).toBe("https://cdn.example.com/logo.png");
});
it("swaps to the letter avatar and warns with the failing URL on image error", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
render(<Logo src="/ui/assets/logos/github.svg" label="GitHub" />);
const img = screen.getByRole("img", { name: "GitHub logo" });
act(() => {
fireEvent.error(img);
});
expect(screen.queryByRole("img")).not.toBeInTheDocument();
expect(screen.getByText("G")).toBeInTheDocument();
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("/litellm/ui/assets/logos/github.svg"));
warnSpy.mockRestore();
});
it("retries with a new src after a previous src errored", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { rerender } = render(<Logo src="/ui/assets/logos/broken.svg" label="Agent" />);
act(() => {
fireEvent.error(screen.getByRole("img", { name: "Agent logo" }));
});
expect(screen.queryByRole("img")).not.toBeInTheDocument();
rerender(<Logo src="/ui/assets/logos/github.svg" label="Agent" />);
const img = screen.getByRole("img", { name: "Agent logo" });
expect(img.getAttribute("src")).toBe("/litellm/ui/assets/logos/github.svg");
rerender(<Logo src="/ui/assets/logos/broken.svg" label="Agent" />);
expect(screen.queryByRole("img")).not.toBeInTheDocument();
expect(screen.getByText("A")).toBeInTheDocument();
warnSpy.mockRestore();
});
});

View file

@ -0,0 +1,36 @@
import React, { useState } from "react";
import { getProviderLogoAndName } from "@/components/provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
interface LogoProps {
provider?: string;
src?: string | null;
label?: string;
className?: string;
}
export const Logo: React.FC<LogoProps> = ({ provider, src, label, className = "w-4 h-4" }) => {
const [erroredSrc, setErroredSrc] = useState<string | null>(null);
const resolvedSrc = provider !== undefined ? getProviderLogoAndName(provider).logo : resolveLogoSrc(src) ?? "";
const name = label ?? provider ?? "";
if (erroredSrc === resolvedSrc || !resolvedSrc) {
return (
<div className={`${className} rounded-full bg-gray-200 flex items-center justify-center text-xs`}>
{name.charAt(0) || "-"}
</div>
);
}
return (
<img
src={resolvedSrc}
alt={`${name || "-"} logo`}
className={className}
onError={() => {
console.warn(`Logo failed to load: ${resolvedSrc}`);
setErroredSrc(resolvedSrc);
}}
/>
);
};

View file

@ -1,24 +1,11 @@
import React, { useState } from "react";
import { getProviderLogoAndName } from "../../provider_info_helpers";
import React from "react";
import { Logo } from "@/components/molecules/logo/Logo";
interface ProviderLogoProps {
provider: string;
className?: string;
}
export const ProviderLogo: React.FC<ProviderLogoProps> = ({ provider, className = "w-4 h-4" }) => {
const [hasError, setHasError] = useState(false);
const { logo } = getProviderLogoAndName(provider);
const showFallback = hasError || !logo;
if (showFallback) {
return (
<div className={`${className} rounded-full bg-gray-200 flex items-center justify-center text-xs`}>
{provider?.charAt(0) || "-"}
</div>
);
}
return <img src={logo} alt={`${provider} logo`} className={className} onError={() => setHasError(true)} />;
};
export const ProviderLogo: React.FC<ProviderLogoProps> = ({ provider, className = "w-4 h-4" }) => (
<Logo provider={provider} className={className} />
);

View file

@ -113,24 +113,20 @@ describe("provider_info_helpers", () => {
});
});
describe("provider logo asset paths", () => {
// Regression: a relative "../ui/assets/logos/" base resolved to
// "/ui/ui/assets/logos/..." (404) on the public model hub at
// /ui/model_hub_table/, which sits a level below the /ui/ SPA. Root-absolute
// paths resolve correctly at any route depth.
it("should expose every provider logo as a root-absolute /ui path", () => {
describe("provider logo bundled assets", () => {
it("should expose every provider logo as a truthy bundled URL, never a raw /ui/assets path", () => {
const logos = Object.values(providerLogoMap);
expect(logos.length).toBeGreaterThan(0);
logos.forEach((logo) => {
expect(logo.startsWith("/ui/assets/logos/")).toBe(true);
expect(logo).not.toContain("../");
expect(typeof logo).toBe("string");
expect(logo.length).toBeGreaterThan(0);
expect(logo.startsWith("/ui/assets/")).toBe(false);
});
});
it("should resolve a provider logo to a root-absolute path via getProviderLogoAndName", () => {
it("should resolve a provider to its own bundled logo via getProviderLogoAndName", () => {
const { logo } = getProviderLogoAndName("openai");
expect(logo.startsWith("/ui/assets/logos/")).toBe(true);
expect(logo).not.toContain("../");
expect(logo).toContain("openai_small");
});
});
@ -430,20 +426,19 @@ describe("getProviderLogoAndName under a custom server_root_path", () => {
vi.doUnmock("@/lib/serverRootPath");
});
// Regression: under SERVER_ROOT_PATH=/litellm the logo must be requested at
// /litellm/ui/assets/logos/... A bare /ui/... path is served off the root and
// 404s behind the reverse proxy.
it("prefixes the server root path onto the resolved logo", async () => {
it("returns the bundled logo URL untouched under a sub-path mount", async () => {
vi.resetModules();
vi.doMock("@/lib/serverRootPath", () => ({ serverRootPath: "/litellm" }));
const { getProviderLogoAndName } = await import("./provider_info_helpers");
expect(getProviderLogoAndName("openai").logo).toBe("/litellm/ui/assets/logos/openai_small.svg");
const helpers = await import("./provider_info_helpers");
const { logo } = helpers.getProviderLogoAndName("openai");
expect(logo).toBe(helpers.providerLogoMap[helpers.Providers.OpenAI]);
expect(logo.startsWith("/litellm")).toBe(false);
});
it("leaves the logo at /ui/... when mounted at the root", async () => {
it("returns the bundled logo URL untouched at the root mount", async () => {
vi.resetModules();
vi.doMock("@/lib/serverRootPath", () => ({ serverRootPath: "/" }));
const { getProviderLogoAndName } = await import("./provider_info_helpers");
expect(getProviderLogoAndName("openai").logo).toBe("/ui/assets/logos/openai_small.svg");
const helpers = await import("./provider_info_helpers");
expect(helpers.getProviderLogoAndName("openai").logo).toBe(helpers.providerLogoMap[helpers.Providers.OpenAI]);
});
});

View file

@ -1,4 +1,67 @@
import { resolveLogoSrc } from "@/lib/assetPaths";
import a2aAgentLogo from "../../public/assets/logos/a2a_agent.png";
import ai21Logo from "../../public/assets/logos/ai21.svg";
import aimlApiLogo from "../../public/assets/logos/aiml_api.svg";
import anthropicLogo from "../../public/assets/logos/anthropic.svg";
import assemblyaiSmallLogo from "../../public/assets/logos/assemblyai_small.png";
import basetenLogo from "../../public/assets/logos/baseten.svg";
import bedrockLogo from "../../public/assets/logos/bedrock.svg";
import cerebrasLogo from "../../public/assets/logos/cerebras.svg";
import cloudflareLogo from "../../public/assets/logos/cloudflare.svg";
import cohereLogo from "../../public/assets/logos/cohere.svg";
import cometapiLogo from "../../public/assets/logos/cometapi.svg";
import cursorLogo from "../../public/assets/logos/cursor.svg";
import databricksLogo from "../../public/assets/logos/databricks.svg";
import deepgramLogo from "../../public/assets/logos/deepgram.png";
import deepinfraLogo from "../../public/assets/logos/deepinfra.png";
import deepseekLogo from "../../public/assets/logos/deepseek.svg";
import elevenlabsLogo from "../../public/assets/logos/elevenlabs.png";
import falAiLogo from "../../public/assets/logos/fal_ai.jpg";
import featherlessLogo from "../../public/assets/logos/featherless.svg";
import fireworksLogo from "../../public/assets/logos/fireworks.svg";
import friendliLogo from "../../public/assets/logos/friendli.svg";
import githubCopilotLogo from "../../public/assets/logos/github_copilot.svg";
import googleLogo from "../../public/assets/logos/google.svg";
import groqLogo from "../../public/assets/logos/groq.svg";
import huggingfaceLogo from "../../public/assets/logos/huggingface.svg";
import hyperbolicLogo from "../../public/assets/logos/hyperbolic.svg";
import infinityLogo from "../../public/assets/logos/infinity.png";
import jinaLogo from "../../public/assets/logos/jina.png";
import lambdaLogo from "../../public/assets/logos/lambda.svg";
import lmstudioLogo from "../../public/assets/logos/lmstudio.svg";
import metaLlamaLogo from "../../public/assets/logos/meta_llama.svg";
import microsoftAzureLogo from "../../public/assets/logos/microsoft_azure.svg";
import minimaxLogo from "../../public/assets/logos/minimax.svg";
import mistralLogo from "../../public/assets/logos/mistral.svg";
import moonshotLogo from "../../public/assets/logos/moonshot.svg";
import morphLogo from "../../public/assets/logos/morph.svg";
import nebiusLogo from "../../public/assets/logos/nebius.svg";
import novitaLogo from "../../public/assets/logos/novita.svg";
import nvidiaNimLogo from "../../public/assets/logos/nvidia_nim.svg";
import nvidiaTritonLogo from "../../public/assets/logos/nvidia_triton.png";
import ollamaLogo from "../../public/assets/logos/ollama.svg";
import openaiSmallLogo from "../../public/assets/logos/openai_small.svg";
import openrouterLogo from "../../public/assets/logos/openrouter.svg";
import oracleLogo from "../../public/assets/logos/oracle.svg";
import perplexityAiLogo from "../../public/assets/logos/perplexity-ai.svg";
import qwenLogo from "../../public/assets/logos/qwen.png";
import recraftLogo from "../../public/assets/logos/recraft.svg";
import replicateLogo from "../../public/assets/logos/replicate.svg";
import runwayLogo from "../../public/assets/logos/runway.png";
import sambanovaLogo from "../../public/assets/logos/sambanova.svg";
import sapLogo from "../../public/assets/logos/sap.png";
import snowflakeLogo from "../../public/assets/logos/snowflake.svg";
import sonioxLogo from "../../public/assets/logos/soniox.svg";
import togetheraiLogo from "../../public/assets/logos/togetherai.svg";
import topazLogo from "../../public/assets/logos/topaz.svg";
import v0Logo from "../../public/assets/logos/v0.svg";
import vercelLogo from "../../public/assets/logos/vercel.svg";
import vllmLogo from "../../public/assets/logos/vllm.png";
import volcengineLogo from "../../public/assets/logos/volcengine.png";
import voyageLogo from "../../public/assets/logos/voyage.webp";
import watsonxLogo from "../../public/assets/logos/watsonx.svg";
import xaiLogo from "../../public/assets/logos/xai.svg";
import xinferenceLogo from "../../public/assets/logos/xinference.svg";
export enum Providers {
A2A_Agent = "A2A Agent",
@ -220,94 +283,91 @@ export const provider_map: Record<string, string> = {
const standaloneSubproviderSlugs = new Set<string>(["bedrock_mantle"]);
const asset_logos_folder = "/ui/assets/logos/";
export const providerLogoMap: Record<string, string> = {
[Providers.A2A_Agent]: `${asset_logos_folder}a2a_agent.png`,
[Providers.AI21]: `${asset_logos_folder}ai21.svg`,
[Providers.AI21_CHAT]: `${asset_logos_folder}ai21.svg`,
[Providers.AIML]: `${asset_logos_folder}aiml_api.svg`,
[Providers.AIOHTTP_OPENAI]: `${asset_logos_folder}openai_small.svg`,
[Providers.Anthropic]: `${asset_logos_folder}anthropic.svg`,
[Providers.ANTHROPIC_TEXT]: `${asset_logos_folder}anthropic.svg`,
[Providers.AssemblyAI]: `${asset_logos_folder}assemblyai_small.png`,
[Providers.Azure]: `${asset_logos_folder}microsoft_azure.svg`,
[Providers.Azure_AI_Studio]: `${asset_logos_folder}microsoft_azure.svg`,
[Providers.AZURE_TEXT]: `${asset_logos_folder}microsoft_azure.svg`,
[Providers.BASETEN]: `${asset_logos_folder}baseten.svg`,
[Providers.Bedrock]: `${asset_logos_folder}bedrock.svg`,
[Providers.BedrockMantle]: `${asset_logos_folder}bedrock.svg`,
[Providers.SageMaker]: `${asset_logos_folder}bedrock.svg`,
[Providers.Cerebras]: `${asset_logos_folder}cerebras.svg`,
[Providers.CLOUDFLARE]: `${asset_logos_folder}cloudflare.svg`,
[Providers.CODESTRAL]: `${asset_logos_folder}mistral.svg`,
[Providers.Cohere]: `${asset_logos_folder}cohere.svg`,
[Providers.COHERE_CHAT]: `${asset_logos_folder}cohere.svg`,
[Providers.COMETAPI]: `${asset_logos_folder}cometapi.svg`,
[Providers.Cursor]: `${asset_logos_folder}cursor.svg`,
[Providers.Databricks]: `${asset_logos_folder}databricks.svg`,
[Providers.Dashscope]: `${asset_logos_folder}dashscope.svg`,
[Providers.Deepseek]: `${asset_logos_folder}deepseek.svg`,
[Providers.Deepgram]: `${asset_logos_folder}deepgram.png`,
[Providers.DeepInfra]: `${asset_logos_folder}deepinfra.png`,
[Providers.ElevenLabs]: `${asset_logos_folder}elevenlabs.png`,
[Providers.FalAI]: `${asset_logos_folder}fal_ai.jpg`,
[Providers.FEATHERLESS_AI]: `${asset_logos_folder}featherless.svg`,
[Providers.FireworksAI]: `${asset_logos_folder}fireworks.svg`,
[Providers.FRIENDLIAI]: `${asset_logos_folder}friendli.svg`,
[Providers.GITHUB_COPILOT]: `${asset_logos_folder}github_copilot.svg`,
[Providers.Google_AI_Studio]: `${asset_logos_folder}google.svg`,
[Providers.GradientAI]: `${asset_logos_folder}gradientai.svg`,
[Providers.Groq]: `${asset_logos_folder}groq.svg`,
[Providers.Hosted_Vllm]: `${asset_logos_folder}vllm.png`,
[Providers.HUGGINGFACE]: `${asset_logos_folder}huggingface.svg`,
[Providers.HYPERBOLIC]: `${asset_logos_folder}hyperbolic.svg`,
[Providers.Infinity]: `${asset_logos_folder}infinity.png`,
[Providers.JinaAI]: `${asset_logos_folder}jina.png`,
[Providers.LAMBDA_AI]: `${asset_logos_folder}lambda.svg`,
[Providers.LM_STUDIO]: `${asset_logos_folder}lmstudio.svg`,
[Providers.LLAMA]: `${asset_logos_folder}meta_llama.svg`,
[Providers.MiniMax]: `${asset_logos_folder}minimax.svg`,
[Providers.MistralAI]: `${asset_logos_folder}mistral.svg`,
[Providers.MOONSHOT]: `${asset_logos_folder}moonshot.svg`,
[Providers.MORPH]: `${asset_logos_folder}morph.svg`,
[Providers.NEBIUS]: `${asset_logos_folder}nebius.svg`,
[Providers.NOVITA]: `${asset_logos_folder}novita.svg`,
[Providers.NVIDIA_NIM]: `${asset_logos_folder}nvidia_nim.svg`,
[Providers.Ollama]: `${asset_logos_folder}ollama.svg`,
[Providers.OLLAMA_CHAT]: `${asset_logos_folder}ollama.svg`,
[Providers.OOBABOOGA]: `${asset_logos_folder}openai_small.svg`,
[Providers.OpenAI]: `${asset_logos_folder}openai_small.svg`,
[Providers.OPENAI_LIKE]: `${asset_logos_folder}openai_small.svg`,
[Providers.OpenAI_Text]: `${asset_logos_folder}openai_small.svg`,
[Providers.OpenAI_Text_Compatible]: `${asset_logos_folder}openai_small.svg`,
[Providers.OpenAI_Compatible]: `${asset_logos_folder}openai_small.svg`,
[Providers.Openrouter]: `${asset_logos_folder}openrouter.svg`,
[Providers.Oracle]: `${asset_logos_folder}oracle.svg`,
[Providers.Perplexity]: `${asset_logos_folder}perplexity-ai.svg`,
[Providers.RECRAFT]: `${asset_logos_folder}recraft.svg`,
[Providers.REPLICATE]: `${asset_logos_folder}replicate.svg`,
[Providers.RunwayML]: `${asset_logos_folder}runwayml.png`,
[Providers.SAGEMAKER_LEGACY]: `${asset_logos_folder}bedrock.svg`,
[Providers.Sambanova]: `${asset_logos_folder}sambanova.svg`,
[Providers.SAP]: `${asset_logos_folder}sap.png`,
[Providers.Snowflake]: `${asset_logos_folder}snowflake.svg`,
[Providers.Soniox]: `${asset_logos_folder}soniox.svg`,
[Providers.TEXT_COMPLETION_CODESTRAL]: `${asset_logos_folder}mistral.svg`,
[Providers.TogetherAI]: `${asset_logos_folder}togetherai.svg`,
[Providers.TOPAZ]: `${asset_logos_folder}topaz.svg`,
[Providers.Triton]: `${asset_logos_folder}nvidia_triton.png`,
[Providers.V0]: `${asset_logos_folder}v0.svg`,
[Providers.VERCEL_AI_GATEWAY]: `${asset_logos_folder}vercel.svg`,
[Providers.Vertex_AI]: `${asset_logos_folder}google.svg`,
[Providers.VERTEX_AI_BETA]: `${asset_logos_folder}google.svg`,
[Providers.VLLM]: `${asset_logos_folder}vllm.png`,
[Providers.VolcEngine]: `${asset_logos_folder}volcengine.png`,
[Providers.Voyage]: `${asset_logos_folder}voyage.webp`,
[Providers.WATSONX]: `${asset_logos_folder}watsonx.svg`,
[Providers.WATSONX_TEXT]: `${asset_logos_folder}watsonx.svg`,
[Providers.xAI]: `${asset_logos_folder}xai.svg`,
[Providers.XINFERENCE]: `${asset_logos_folder}xinference.svg`,
[Providers.A2A_Agent]: a2aAgentLogo.src,
[Providers.AI21]: ai21Logo.src,
[Providers.AI21_CHAT]: ai21Logo.src,
[Providers.AIML]: aimlApiLogo.src,
[Providers.AIOHTTP_OPENAI]: openaiSmallLogo.src,
[Providers.Anthropic]: anthropicLogo.src,
[Providers.ANTHROPIC_TEXT]: anthropicLogo.src,
[Providers.AssemblyAI]: assemblyaiSmallLogo.src,
[Providers.Azure]: microsoftAzureLogo.src,
[Providers.Azure_AI_Studio]: microsoftAzureLogo.src,
[Providers.AZURE_TEXT]: microsoftAzureLogo.src,
[Providers.BASETEN]: basetenLogo.src,
[Providers.Bedrock]: bedrockLogo.src,
[Providers.BedrockMantle]: bedrockLogo.src,
[Providers.SageMaker]: bedrockLogo.src,
[Providers.Cerebras]: cerebrasLogo.src,
[Providers.CLOUDFLARE]: cloudflareLogo.src,
[Providers.CODESTRAL]: mistralLogo.src,
[Providers.Cohere]: cohereLogo.src,
[Providers.COHERE_CHAT]: cohereLogo.src,
[Providers.COMETAPI]: cometapiLogo.src,
[Providers.Cursor]: cursorLogo.src,
[Providers.Databricks]: databricksLogo.src,
[Providers.Dashscope]: qwenLogo.src,
[Providers.Deepseek]: deepseekLogo.src,
[Providers.Deepgram]: deepgramLogo.src,
[Providers.DeepInfra]: deepinfraLogo.src,
[Providers.ElevenLabs]: elevenlabsLogo.src,
[Providers.FalAI]: falAiLogo.src,
[Providers.FEATHERLESS_AI]: featherlessLogo.src,
[Providers.FireworksAI]: fireworksLogo.src,
[Providers.FRIENDLIAI]: friendliLogo.src,
[Providers.GITHUB_COPILOT]: githubCopilotLogo.src,
[Providers.Google_AI_Studio]: googleLogo.src,
[Providers.Groq]: groqLogo.src,
[Providers.Hosted_Vllm]: vllmLogo.src,
[Providers.HUGGINGFACE]: huggingfaceLogo.src,
[Providers.HYPERBOLIC]: hyperbolicLogo.src,
[Providers.Infinity]: infinityLogo.src,
[Providers.JinaAI]: jinaLogo.src,
[Providers.LAMBDA_AI]: lambdaLogo.src,
[Providers.LM_STUDIO]: lmstudioLogo.src,
[Providers.LLAMA]: metaLlamaLogo.src,
[Providers.MiniMax]: minimaxLogo.src,
[Providers.MistralAI]: mistralLogo.src,
[Providers.MOONSHOT]: moonshotLogo.src,
[Providers.MORPH]: morphLogo.src,
[Providers.NEBIUS]: nebiusLogo.src,
[Providers.NOVITA]: novitaLogo.src,
[Providers.NVIDIA_NIM]: nvidiaNimLogo.src,
[Providers.Ollama]: ollamaLogo.src,
[Providers.OLLAMA_CHAT]: ollamaLogo.src,
[Providers.OOBABOOGA]: openaiSmallLogo.src,
[Providers.OpenAI]: openaiSmallLogo.src,
[Providers.OPENAI_LIKE]: openaiSmallLogo.src,
[Providers.OpenAI_Text]: openaiSmallLogo.src,
[Providers.OpenAI_Text_Compatible]: openaiSmallLogo.src,
[Providers.OpenAI_Compatible]: openaiSmallLogo.src,
[Providers.Openrouter]: openrouterLogo.src,
[Providers.Oracle]: oracleLogo.src,
[Providers.Perplexity]: perplexityAiLogo.src,
[Providers.RECRAFT]: recraftLogo.src,
[Providers.REPLICATE]: replicateLogo.src,
[Providers.RunwayML]: runwayLogo.src,
[Providers.SAGEMAKER_LEGACY]: bedrockLogo.src,
[Providers.Sambanova]: sambanovaLogo.src,
[Providers.SAP]: sapLogo.src,
[Providers.Snowflake]: snowflakeLogo.src,
[Providers.Soniox]: sonioxLogo.src,
[Providers.TEXT_COMPLETION_CODESTRAL]: mistralLogo.src,
[Providers.TogetherAI]: togetheraiLogo.src,
[Providers.TOPAZ]: topazLogo.src,
[Providers.Triton]: nvidiaTritonLogo.src,
[Providers.V0]: v0Logo.src,
[Providers.VERCEL_AI_GATEWAY]: vercelLogo.src,
[Providers.Vertex_AI]: googleLogo.src,
[Providers.VERTEX_AI_BETA]: googleLogo.src,
[Providers.VLLM]: vllmLogo.src,
[Providers.VolcEngine]: volcengineLogo.src,
[Providers.Voyage]: voyageLogo.src,
[Providers.WATSONX]: watsonxLogo.src,
[Providers.WATSONX_TEXT]: watsonxLogo.src,
[Providers.xAI]: xaiLogo.src,
[Providers.XINFERENCE]: xinferenceLogo.src,
};
export const getProviderLogoAndName = (providerValue: string): { logo: string; displayName: string } => {

View file

@ -48,6 +48,16 @@ describe("resolveLogoSrc", () => {
expect(resolveLogoSrc("//cdn.example.com/x.svg")).toBe("//cdn.example.com/x.svg");
});
it("passes bundled /_next/ asset URLs through untouched even under a sub-path mount", async () => {
const { resolveLogoSrc } = await importWithRoot("/litellm");
expect(resolveLogoSrc("/_next/static/media/openai_small.abc123.svg")).toBe(
"/_next/static/media/openai_small.abc123.svg",
);
expect(resolveLogoSrc("/litellm-asset-prefix/_next/static/media/openai_small.abc123.svg")).toBe(
"/litellm-asset-prefix/_next/static/media/openai_small.abc123.svg",
);
});
it("roots a local asset path using the live server root path", async () => {
const { resolveLogoSrc } = await importWithRoot("/litellm");
expect(resolveLogoSrc("/ui/assets/logos/github.svg")).toBe("/litellm/ui/assets/logos/github.svg");

View file

@ -23,5 +23,6 @@ export const withServerRoot = (path: string, root: string): string => {
export const resolveLogoSrc = (value: string | null | undefined, root: string = serverRootPath): string | undefined => {
if (!value) return undefined;
if (EXTERNAL_SRC.test(value)) return value;
if (value.includes("/_next/")) return value;
return withServerRoot(value, root);
};

View file

@ -1,7 +1,19 @@
import { defineConfig } from "vitest/config";
import { defineConfig, type Plugin, type ViteUserConfig } from "vitest/config";
import { resolve } from "path";
export default defineConfig({
const staticImageData: Plugin = {
name: "next-static-image-data",
enforce: "pre",
load(id: string) {
const path = id.split("?")[0];
if (!/\.(svg|png|jpe?g|gif|webp|avif|ico|bmp)$/i.test(path)) return undefined;
const name = path.slice(path.lastIndexOf("/") + 1);
return `export default { src: ${JSON.stringify(`/_next/static/media/${name}`)}, height: 24, width: 24 };`;
},
};
const config: ViteUserConfig = {
plugins: [staticImageData],
test: {
environment: "jsdom",
setupFiles: ["tests/setupTests.ts"],
@ -47,4 +59,6 @@ export default defineConfig({
jsx: "automatic",
jsxImportSource: "react",
},
});
};
export default defineConfig(config);