Every gateway provider is now an entry in providers.json - endpoint and auth template, how its model list is read, how it is probed before a switch, what setup asks, which doctor checks apply, and its title, colour and logo - installed next to the presets and read by all three consumers: the bash CLI (through cm-json.py), the Windows script, and the bar widget (through health.json). anthropic stays built in; it is the native login, not a gateway. Behaviour that differs in kind stays in code, chosen by name from the entry: catalogue parsers (openrouter, lmstudio, ollama, openai, static), probe rules (always, lenient, local), and named doctor checks. A provider that reuses them is an entry and a default preset, with no code. The widget draws providers from health.json, so a new one needs no QML change and no shell restart. The existing three are unchanged in behaviour: their blank presets come out byte-identical from the file, and setup, doctor, models and the picker run the same checks through the generic paths. Ollama: local server on :11434, placeholder token, one model for every tier, models from /api/tags. doctor reads the context each loaded model actually runs with (/api/ps) and its maximum (/api/show), because Ollama defaults to 4096 tokens unless OLLAMA_CONTEXT_LENGTH is set and silently truncates past it. A bare model name matches its :latest tag. Custom: any Anthropic-compatible endpoint. Ships with no address and is refused until it has one; key optional; models from /v1/models when the endpoint has a list, and a lenient probe so a proxy without one is not blocked. Also: preflight (and Set-ClaudeMode on Windows) refuses a preset with no server address; the server form, setup and set-auth use each provider's own default URL, key name and placeholder token instead of LM Studio's; the Windows build gains the no-models and no-address guards it never had. Tested on Linux against fake Ollama/Custom servers, and on Windows 5.1 in a USERPROFILE sandbox on winbox. 1.12.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
164 lines
8.8 KiB
JavaScript
164 lines
8.8 KiB
JavaScript
.pragma library
|
|
|
|
// Fallback presentation only. claude-mode publishes its providers - titles,
|
|
// blurbs, logos, scales, order - in health.json from providers.json, and the
|
|
// widget reads them from there (BarWidget.providerInfo). What remains here is
|
|
// anthropic, which is the native login and never in that list, and the
|
|
// original three gateways for a health.json written before the list existed.
|
|
// Nothing new belongs in this file: it is a .pragma library, cached until the
|
|
// shell restarts.
|
|
|
|
// Material Design icons from the Nerd Font patch set, written as codepoints
|
|
// rather than literals so they survive any editor or transport that is not
|
|
// UTF-8 clean. All verified present in JetBrainsMono Nerd Font, the shell's
|
|
// default family.
|
|
//
|
|
// Deliberately none of them a robot: omarchy.agents already paints one, and it
|
|
// commonly sits in the same stretch of bar, where two identical robots read as
|
|
// one widget drawn twice. Same reason lmstudio is a server rack rather than a
|
|
// screen - omarchy.monitor owns that silhouette.
|
|
var GLYPH = {
|
|
anthropic: String.fromCodePoint(0xF0674), // sparkles - the real thing
|
|
openrouter: String.fromCodePoint(0xF0469), // router - remote gateway
|
|
zai: String.fromCodePoint(0xF015F), // cloud - hosted GLM plan
|
|
lmstudio: String.fromCodePoint(0xF048B), // server - local LM Studio
|
|
unknown: String.fromCodePoint(0xF0625) // question - nothing readable
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Brand marks
|
|
//
|
|
// The real logo for each provider, as SVG path data in a 24x24 viewBox so the
|
|
// whole set scales from a single number. Drawn with QtQuick.Shapes rather than
|
|
// set as font glyphs: three of the four have no Nerd Font pictograph at all,
|
|
// and a stand-in pictograph is both less recognisable and impossible to keep
|
|
// visually consistent with the marks beside it.
|
|
//
|
|
// Sources: Claude, OpenRouter and LM Studio from simple-icons; Z.AI from
|
|
// lobe-icons. All four are monochrome single-path marks, which is what lets
|
|
// them take the bar's foreground colour and follow the theme. Trademarks
|
|
// belong to their respective owners.
|
|
//
|
|
// LM Studio's mark keeps its rounded-square container: the four bars alone
|
|
// read as a hamburger menu, and the container is what makes it that logo. It
|
|
// relies on nonzero winding to knock the bars out, which is QtQuick.Shapes'
|
|
// default, so no fill rule is set anywhere here.
|
|
var LOGO = {
|
|
anthropic: "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z",
|
|
openrouter: "M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z",
|
|
zai: "M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z",
|
|
lmstudio: "M14.025 0c3.492 0 5.237 0 6.571.68a6.24 6.24 0 0 1 2.725 2.724C24 4.738 24 6.484 24 9.975v4.05c0 3.492 0 5.237-.68 6.571a6.24 6.24 0 0 1-2.724 2.725c-1.334.679-3.08.679-6.571.679h-4.05c-3.492 0-5.237 0-6.571-.68A6.24 6.24 0 0 1 .68 20.597C0 19.262 0 17.516 0 14.025v-4.05c0-3.492 0-5.237.68-6.571A6.23 6.23 0 0 1 3.404.68C4.738 0 6.484 0 9.975 0zM7.688 16.313a1.313 1.313 0 0 0 0 2.625h11.625a1.313 1.313 0 0 0 0-2.625zm-3-3.75a1.313 1.313 0 0 0 0 2.624h11.625a1.313 1.313 0 0 0 0-2.624zm3-3.75a1.313 1.313 0 0 0 0 2.624h11.625a1.313 1.313 0 0 0 0-2.624zm-3-3.75a1.313 1.313 0 0 0 0 2.625h11.625a1.313 1.313 0 0 0 0-2.625z"
|
|
}
|
|
|
|
// Equal nominal size is not equal apparent size, so each mark is corrected by
|
|
// two measurements taken off a 200px render of it:
|
|
//
|
|
// bbox Z.AI and OpenRouter are wide-but-short marks - their ink spans only
|
|
// ~84% of the box height, so at equal nominal size they read smaller.
|
|
// ink LM Studio's filled container covers 69% of its box against ~38% for
|
|
// the other three, so at equal nominal size it reads much heavier.
|
|
//
|
|
// The correction is the geometric mean of both signals, which pulls the short
|
|
// marks up and the solid one down without letting either measure dominate.
|
|
var LOGO_SCALE = {
|
|
anthropic: 0.99,
|
|
openrouter: 1.12,
|
|
zai: 1.07,
|
|
lmstudio: 0.86
|
|
}
|
|
|
|
function logo(mode) {
|
|
return LOGO[mode] || ""
|
|
}
|
|
|
|
function logoScale(mode) {
|
|
return LOGO_SCALE[mode] || 1.0
|
|
}
|
|
|
|
var TITLE = {
|
|
anthropic: "Anthropic",
|
|
openrouter: "OpenRouter",
|
|
zai: "Z.AI",
|
|
lmstudio: "LM Studio"
|
|
}
|
|
|
|
var BLURB = {
|
|
anthropic: "Your subscription login. No gateway, no API key.",
|
|
openrouter: "Remote gateway, pay per token, any vendor.",
|
|
zai: "GLM coding plan on Z.AI's Anthropic endpoint.",
|
|
lmstudio: "Local LM Studio server. Offline and free."
|
|
}
|
|
|
|
var ORDER = ["anthropic", "openrouter", "zai", "lmstudio"]
|
|
|
|
// Everything except anthropic switches with a named preset.
|
|
function needsPreset(mode) {
|
|
return mode !== "" && mode !== "anthropic"
|
|
}
|
|
|
|
function glyph(mode) {
|
|
return GLYPH[mode] || GLYPH.unknown
|
|
}
|
|
|
|
function title(mode) {
|
|
return TITLE[mode] || "not installed"
|
|
}
|
|
|
|
function blurb(mode) {
|
|
return BLURB[mode] || ""
|
|
}
|
|
|
|
// Bar label. The preset matters more than the provider once you are on a
|
|
// gateway - "openrouter" tells you nothing you did not already know from the
|
|
// icon, where "cheap" is the thing you actually want to catch sight of.
|
|
function shortLabel(mode, preset) {
|
|
if (mode === "") return ""
|
|
if (mode === "anthropic") return "anthropic"
|
|
return preset !== "" ? preset : mode
|
|
}
|
|
|
|
function defaultRoot() {
|
|
return homeDir() + "/.claude-mode"
|
|
}
|
|
|
|
function homeDir() {
|
|
// Qt exposes no home path to a .pragma library, and the shell always runs as
|
|
// the owning user, so derive it the one way that holds in both places.
|
|
var url = Qt.resolvedUrl(".").toString()
|
|
var m = url.match(/^file:\/\/(\/home\/[^\/]+)\//)
|
|
if (m) return m[1]
|
|
m = url.match(/^file:\/\/(\/Users\/[^\/]+)\//)
|
|
return m ? m[1] : "/root"
|
|
}
|
|
|
|
// Presets from health.json that belong to one provider, default first so the
|
|
// list opens on the one `claude-mode <mode>` would have picked.
|
|
function presetsFor(health, mode, defaults) {
|
|
var all = (health && health.presets) ? health.presets : []
|
|
var out = []
|
|
for (var i = 0; i < all.length; i++) {
|
|
if (String(all[i].provider) === mode) out.push(all[i])
|
|
}
|
|
var preferred = defaults[mode]
|
|
out.sort(function (a, b) {
|
|
if (a.name === preferred) return -1
|
|
if (b.name === preferred) return 1
|
|
return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)
|
|
})
|
|
return out
|
|
}
|
|
|
|
// Mirrors default_preset_for() in the CLI.
|
|
var DEFAULT_PRESET = {
|
|
openrouter: "default",
|
|
zai: "zai",
|
|
lmstudio: "lmstudio"
|
|
}
|
|
|
|
function contextLabel(tokens) {
|
|
if (!tokens) return ""
|
|
if (tokens >= 1000000) return (Math.round(tokens / 100000) / 10) + "M context"
|
|
if (tokens >= 1000) return Math.round(tokens / 1024) + "k context"
|
|
return tokens + " context"
|
|
}
|