Providers as data; add Ollama and Custom endpoints
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>
This commit is contained in:
@@ -42,6 +42,38 @@ BarWidget {
|
||||
}
|
||||
readonly property bool known: mode !== ""
|
||||
|
||||
// ---- Providers
|
||||
//
|
||||
// claude-mode publishes its providers in health.json - how to draw each one
|
||||
// and which panel features it has - so one added to providers.json appears
|
||||
// here without this file or Modes.js changing (and Modes.js cannot change
|
||||
// without a shell restart). Modes.js stays the fallback: for anthropic,
|
||||
// which is the native login and never in the list, and for a health.json
|
||||
// written before the list existed. The panel asks through these, too.
|
||||
readonly property var providerList: health && health.providers ? health.providers : []
|
||||
readonly property var modeOrder: {
|
||||
if (providerList.length === 0) return Modes.ORDER
|
||||
var out = ["anthropic"]
|
||||
for (var i = 0; i < providerList.length; i++) out.push(String(providerList[i].id))
|
||||
return out
|
||||
}
|
||||
|
||||
function providerInfo(m) {
|
||||
for (var i = 0; i < providerList.length; i++) {
|
||||
if (String(providerList[i].id) === m) return providerList[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
function modeTitle(m) { var p = providerInfo(m); return p && p.title ? String(p.title) : Modes.title(m) }
|
||||
function modeBlurb(m) { var p = providerInfo(m); return p ? String(p.blurb || "") : Modes.blurb(m) }
|
||||
function modeLogo(m) { var p = providerInfo(m); return p ? String(p.logo || "") : Modes.logo(m) }
|
||||
function modeLogoScale(m) { var p = providerInfo(m); return p && p.logoScale ? Number(p.logoScale) : Modes.logoScale(m) }
|
||||
function modeGlyph(m) {
|
||||
var p = providerInfo(m)
|
||||
if (p && p.glyph) return String.fromCodePoint(parseInt(String(p.glyph), 16))
|
||||
return Modes.glyph(m)
|
||||
}
|
||||
|
||||
// Measured against the neighbours: every stock bar glyph in this shell paints
|
||||
// 11px of ink from a 13px font. These marks fill their box rather than
|
||||
// carrying a font's internal padding, so the box itself has to be the smaller
|
||||
@@ -54,8 +86,8 @@ BarWidget {
|
||||
var n = Math.round(Number(root.setting("iconSize", Style.bar.iconFont)))
|
||||
return n % 2 === 0 ? n + 1 : n
|
||||
}
|
||||
readonly property string logo: Modes.logo(mode)
|
||||
readonly property string glyph: Modes.glyph(mode)
|
||||
readonly property string logo: modeLogo(mode)
|
||||
readonly property string glyph: modeGlyph(mode)
|
||||
readonly property string label: Modes.shortLabel(mode, preset)
|
||||
|
||||
// A gateway mode is spending money or leaning on a local server; native
|
||||
@@ -226,7 +258,7 @@ BarWidget {
|
||||
id: icon
|
||||
anchors.fill: parent
|
||||
pathData: root.logo
|
||||
opticalScale: Modes.logoScale(root.mode)
|
||||
opticalScale: root.modeLogoScale(root.mode)
|
||||
color: root.activeColor
|
||||
iconSize: root.iconPx
|
||||
}
|
||||
@@ -282,7 +314,7 @@ BarWidget {
|
||||
|
||||
function tooltipLabel() {
|
||||
if (!root.known) return "claude-mode: not installed"
|
||||
var lines = [Modes.title(root.mode)]
|
||||
var lines = [root.modeTitle(root.mode)]
|
||||
if (root.preset !== "") lines.push("preset " + root.preset)
|
||||
var models = root.health && root.health.models ? root.health.models : null
|
||||
if (models && models.opus) lines.push("opus " + models.opus)
|
||||
|
||||
Reference in New Issue
Block a user