Show the serving provider in the model-info card (#2185)

* Show the serving provider in the model-info card

The model-info popup (click the model name on a message) shows the model
and pricing, with a logo inferred from the model NAME. But the same model
can be served by different endpoints — e.g. claude-haiku via OpenRouter
vs GitHub Copilot vs Anthropic direct — which the name-based logo can't
distinguish.

Add a 'Provider' line derived from the session's endpoint URL:
- new providerLabel(endpointUrl) in static/js/providers.js maps the host
  to a friendly name (GitHub Copilot, OpenRouter, Anthropic, OpenAI,
  Google, AWS Bedrock, DeepSeek, Mistral, Groq, Together, Fireworks,
  Perplexity, xAI), 'Local' for loopback/LAN, else the bare host.
- static/js/chatRenderer.js renders it under Model in the card, from
  window.sessionModule.getCurrentEndpointUrl().

* Anchor provider-label patterns to the hostname

providerLabel matched its patterns against the full endpoint URL with
unanchored substrings, so a host like max.airlines.com matched /x\.ai/ and was
mislabeled "xAI". Anchor each pattern to the end of the hostname ((^|.)domain$)
and test against the parsed host instead of the raw URL.
This commit is contained in:
Kenny Van de Maele
2026-06-04 18:22:31 +02:00
committed by GitHub
parent 8bfd79fe8e
commit 147d1fbde6
2 changed files with 55 additions and 2 deletions

View File

@@ -4,7 +4,7 @@
import uiModule from './ui.js';
import markdownModule from './markdown.js';
import { addAITTSButton } from './tts-ai.js';
import { providerLogo } from './providers.js';
import { providerLogo, providerLabel } from './providers.js';
import settingsModule from './settings.js';
import spinnerModule from './spinner.js';
import { bindMenuDismiss } from './escMenuStack.js';
@@ -577,6 +577,12 @@ export function applyModelColor(roleEl, modelName) {
if (logoHtml) html += '<span class="role-provider-logo" style="opacity:0.7">' + logoHtml + '</span>';
html += short + '</div>';
html += '<div><span class="ctx-label">Model</span> ' + modelName.split('/').pop() + '</div>';
// Provider = the serving endpoint, distinct from the model vendor/logo
// (e.g. the same model via OpenRouter vs Copilot vs Anthropic direct).
const _epUrl = (window.sessionModule && window.sessionModule.getCurrentEndpointUrl)
? window.sessionModule.getCurrentEndpointUrl() : null;
const _provLabel = providerLabel(_epUrl);
if (_provLabel) html += '<div><span class="ctx-label">Provider</span> ' + uiModule.esc(_provLabel) + '</div>';
// Show static context initially, then fetch real from server
const _realCtx = window._realContextLengths && window._realContextLengths[modelName];
if (_realCtx) {

View File

@@ -90,4 +90,51 @@ export function providerLogo(modelId) {
return null;
}
export default { providerLogo };
// Host suffix → friendly provider label. The model-info card shows this so the
// SAME model name served by DIFFERENT routes is distinguishable (e.g.
// `claude-haiku` via OpenRouter vs GitHub Copilot vs Anthropic direct); the logo
// only reflects the model vendor, not the actual endpoint. Patterns are anchored
// to the end of the hostname (^|.)domain$ so a host like `max.airlines.com`
// doesn't match `x.ai`.
const _ENDPOINT_LABELS = [
[/(^|\.)githubcopilot\.com$/i, "GitHub Copilot"],
[/(^|\.)openrouter\.ai$/i, "OpenRouter"],
[/(^|\.)anthropic\.com$/i, "Anthropic"],
[/(^|\.)openai\.com$/i, "OpenAI"],
[/(^|\.)(generativelanguage|aiplatform)\.googleapis\.com$/i, "Google"],
[/(^|\.)bedrock[\w.-]*\.amazonaws\.com$/i, "AWS Bedrock"],
[/(^|\.)deepseek\.com$/i, "DeepSeek"],
[/(^|\.)mistral\.ai$/i, "Mistral"],
[/(^|\.)groq\.com$/i, "Groq"],
[/(^|\.)together\.(ai|xyz)$/i, "Together"],
[/(^|\.)fireworks\.ai$/i, "Fireworks"],
[/(^|\.)perplexity\.ai$/i, "Perplexity"],
[/(^|\.)x\.ai$/i, "xAI"],
];
/**
* Friendly label for the endpoint that served a model, from its URL.
* Returns "Local" for loopback/LAN hosts, a known provider name when matched,
* else the bare host. Null when no URL is available.
*/
export function providerLabel(endpointUrl) {
if (!endpointUrl || typeof endpointUrl !== "string") return null;
let host;
try {
host = new URL(endpointUrl).hostname;
} catch (_) {
// Not a full URL (e.g. bare host[:port]) — strip scheme/path/port best-effort.
host = endpointUrl.replace(/^[a-z]+:\/\//i, "").split("/")[0].split(":")[0];
}
if (!host) return null;
if (/^(localhost|127\.|0\.0\.0\.0|::1|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/i.test(host)) {
return "Local";
}
for (const [re, label] of _ENDPOINT_LABELS) {
if (re.test(host)) return label;
}
// Unknown host → drop a leading "api." for a cleaner readout.
return host.replace(/^api\./i, "");
}
export default { providerLogo, providerLabel };