From 147d1fbde6463ceb4e058ce506f067af207ce233 Mon Sep 17 00:00:00 2001 From: Kenny Van de Maele Date: Thu, 4 Jun 2026 18:22:31 +0200 Subject: [PATCH] Show the serving provider in the model-info card (#2185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- static/js/chatRenderer.js | 8 ++++++- static/js/providers.js | 49 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/static/js/chatRenderer.js b/static/js/chatRenderer.js index 9760665..93e6a7d 100644 --- a/static/js/chatRenderer.js +++ b/static/js/chatRenderer.js @@ -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 += ''; html += short + ''; html += '
Model ' + modelName.split('/').pop() + '
'; + // 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 += '
Provider ' + uiModule.esc(_provLabel) + '
'; // Show static context initially, then fetch real from server const _realCtx = window._realContextLengths && window._realContextLengths[modelName]; if (_realCtx) { diff --git a/static/js/providers.js b/static/js/providers.js index 1563e77..ee619ca 100644 --- a/static/js/providers.js +++ b/static/js/providers.js @@ -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 };