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 };