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 += '' + logoHtml + '';
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 };