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:
@@ -1,7 +1,8 @@
|
||||
# claude-mode
|
||||
|
||||
Switch Claude Code system-wide between **Anthropic**, **OpenRouter**, **Z.AI**, and a
|
||||
local **LM Studio** server — with named per-tier model presets.
|
||||
Switch Claude Code system-wide between **Anthropic**, **OpenRouter**, **Z.AI**, a
|
||||
local **LM Studio** or **Ollama** server, and any **custom** Anthropic-compatible
|
||||
endpoint — with named per-tier model presets.
|
||||
|
||||
One write to `~/.claude/settings.json` covers the CLI, the VS Code extension, and
|
||||
the desktop app. Restart Claude Code afterwards — nothing else. Windows
|
||||
@@ -14,12 +15,16 @@ claude-mode anthropic # subscription login
|
||||
claude-mode openrouter # remote gateway (preset: default)
|
||||
claude-mode zai # Z.AI GLM coding plan (preset: zai)
|
||||
claude-mode lmstudio # local server (preset: lmstudio)
|
||||
claude-mode ollama # local server (preset: ollama)
|
||||
claude-mode custom # your own endpoint (preset: custom)
|
||||
```
|
||||
|
||||
**Contents:** [Install](#install) · [First run](#first-run) · [The interactive
|
||||
menu](#the-interactive-menu) · [Commands](#commands) · [Presets](#presets-shipped) ·
|
||||
[Context windows](#context-windows-and-early-auto-compaction) · [Z.AI](#zai-mode) ·
|
||||
[LM Studio](#lm-studio-mode) · [Live sessions](#live-sessions) ·
|
||||
[LM Studio](#lm-studio-mode) · [Ollama](#ollama-mode) · [Custom
|
||||
endpoints](#custom-endpoints) · [Adding a provider](#adding-a-provider) ·
|
||||
[Live sessions](#live-sessions) ·
|
||||
[Troubleshooting](#troubleshooting) · [Design decisions](#design-decisions) ·
|
||||
[Layout](#layout) · [Linux / Omarchy](#linux--omarchy) · [The bar
|
||||
widget](#the-omarchy-bar-widget) · [Uninstall](#uninstall)
|
||||
@@ -177,7 +182,9 @@ claude-mode status active mode, preset, model map
|
||||
claude-mode anthropic native login (deletes all managed keys)
|
||||
claude-mode openrouter [preset] default preset: default
|
||||
claude-mode zai [preset] default preset: zai (alias: z.ai, z-ai)
|
||||
claude-mode lmstudio [preset] default preset: lmstudio
|
||||
claude-mode lmstudio [preset] default preset: lmstudio (alias: lm-studio)
|
||||
claude-mode ollama [preset] default preset: ollama
|
||||
claude-mode custom [preset] default preset: custom
|
||||
|
||||
claude-mode presets list presets (* = active)
|
||||
claude-mode preset show <name>
|
||||
@@ -401,6 +408,98 @@ like every other credential — a real key on a public address is a real key.
|
||||
Nothing here changes the shipped presets unless you ask it to; the local default
|
||||
stays exactly as it was.
|
||||
|
||||
## Ollama mode
|
||||
|
||||
Ollama serves Anthropic's Messages API itself, at `/v1/messages`, so nothing sits
|
||||
in between. `claude-mode setup ollama` asks where the server is (it ships on
|
||||
`http://127.0.0.1:11434`), whether it needs a key, and picks one model for every
|
||||
tier from what the server has pulled. One model for all four is the right shape
|
||||
for a local server: it holds one in memory at a time, and mapping tiers to
|
||||
different models just means paying the load cost on every tier change. The token
|
||||
is a placeholder, `ollama`, which Ollama requires but does not check.
|
||||
|
||||
### The context window is set on the server
|
||||
|
||||
This is the part that bites. Ollama sizes the context on the server, not per
|
||||
request: **4096 tokens** unless `ollama serve` runs with `OLLAMA_CONTEXT_LENGTH`,
|
||||
and anything past it is cut off without an error. Claude Code's system prompt
|
||||
alone is most of that. Ollama recommends 64k or more for Claude Code, and the
|
||||
shipped preset declares 65,536 — but `contextTokens` only tells Claude Code what
|
||||
to expect. It cannot change what the server does.
|
||||
|
||||
```bash
|
||||
OLLAMA_CONTEXT_LENGTH=65536 ollama serve
|
||||
# or, for the systemd service: systemctl edit ollama
|
||||
# [Service]
|
||||
# Environment=OLLAMA_CONTEXT_LENGTH=65536
|
||||
```
|
||||
|
||||
`claude-mode doctor` reads what each loaded model is actually running with
|
||||
(`/api/ps`) and the model's own maximum (`/api/show`), and says so when either is
|
||||
below the preset. When nothing is loaded it cannot see the server's setting, and
|
||||
says that instead of guessing.
|
||||
|
||||
Also worth knowing:
|
||||
|
||||
- A bare name like `qwen3-coder` is `qwen3-coder:latest` to Ollama; both work.
|
||||
- Ollama does not implement `count_tokens` or prompt caching, so long sessions
|
||||
redo more work than they would on a hosted provider. Nothing breaks.
|
||||
- To reach it from another machine, serve it with `OLLAMA_HOST=0.0.0.0` and
|
||||
point the preset there: `claude-mode preset url ollama http://192.168.1.40:11434`.
|
||||
|
||||
## Custom endpoints
|
||||
|
||||
For anything else that speaks Anthropic's Messages API: a LiteLLM or Vercel
|
||||
gateway, vLLM, llama.cpp's server, a company proxy. The `custom` preset ships
|
||||
with no address — there is no sensible one to guess — and a switch to it is
|
||||
refused until it has one.
|
||||
|
||||
```bash
|
||||
claude-mode setup custom # asks for all of the below
|
||||
# or by hand:
|
||||
claude-mode preset url custom https://llm.example.com
|
||||
claude-mode preset auth custom key custom # the key lives in the vault as 'custom'
|
||||
claude-mode set-key custom
|
||||
claude-mode preset set custom opus <model-id> # ...and each other tier
|
||||
```
|
||||
|
||||
Several endpoints are several presets, each with its own address and key name:
|
||||
`claude-mode preset new work --provider custom --blank`, then
|
||||
`claude-mode preset auth work key work`.
|
||||
|
||||
The model list comes from `/v1/models` when the endpoint has one (OpenAI's shape
|
||||
or Anthropic's). Plenty of proxies serve Messages and nothing else. That is fine:
|
||||
the check before a switch only wants something to answer, and model ids can be
|
||||
typed by hand. The cost guard still applies. A custom endpoint in front of
|
||||
Anthropic's own models needs `"allowAnthropicModels": true` in the preset, which
|
||||
keeps it deliberate.
|
||||
|
||||
## Adding a provider
|
||||
|
||||
Every gateway provider is an entry in [`providers.json`](providers.json), installed
|
||||
next to the presets and read by the CLI, the bar widget (through `health.json`)
|
||||
and the Windows build. Anthropic is not in it: it is the native login, not a
|
||||
gateway. An entry holds:
|
||||
|
||||
| field | what it says |
|
||||
|---|---|
|
||||
| `id`, `aliases`, `title`, `label`, `blurb`, `color` | names and how the CLI, menu and panel show it |
|
||||
| `defaultPreset`, `preset` | the preset `claude-mode <id>` picks, and the template for a blank one (URL, auth, context, extra env) |
|
||||
| `server` | whether the address is editable, how it is probed before a switch (`always`, `lenient` for proxies, `local` only when it is on this machine), setup hint |
|
||||
| `catalogue.kind` | how its model list is read: `openrouter`, `lmstudio`, `ollama`, `openai` (`/v1/models`) or `static` (a list kept in the entry) |
|
||||
| `setup` | whether a key is required or optional, where to get one, and one model for every tier or one per tier |
|
||||
| `doctor` | which checks apply: `catalogue-models`, `openrouter-key`, `guardrail`, `message-check`, `ollama-context`, `lmstudio-templates` (Windows) |
|
||||
| `logo`, `logoScale` | a single-path 24×24 SVG mark, and its optical size correction |
|
||||
|
||||
A provider that reuses those kinds is an entry plus a default preset in
|
||||
`presets/`, and no code. A hosted Anthropic-compatible coding plan — Kimi,
|
||||
MiniMax, DeepSeek — is an entry shaped like `zai`, with its own URL and a
|
||||
`static` or `openai` catalogue. Only a genuinely new kind of behaviour, such as
|
||||
a catalogue format none of the parsers read, needs code: a parser in
|
||||
`cm-json.py`, and a branch in `provider_catalogue` (bash) and
|
||||
`Get-ProviderCatalogue` (Windows). The bar widget picks new providers up from
|
||||
`health.json` — no QML change, and no shell restart.
|
||||
|
||||
## Live sessions
|
||||
|
||||
### Why a switch breaks a running session
|
||||
@@ -719,6 +818,7 @@ Windows:
|
||||
```
|
||||
~/.claude-mode/
|
||||
claude-mode.ps1 main script
|
||||
providers.json what each gateway provider is (replaced on every install)
|
||||
state.json mode, active preset, and the exact env keys last written
|
||||
presets/*.json provider + model maps
|
||||
vault/*.cred DPAPI-encrypted keys (openrouter, zai, ...)
|
||||
|
||||
+361
-188
@@ -92,22 +92,60 @@ $script:BaseManagedEnvKeys = @(
|
||||
$script:Tiers = @('opus', 'sonnet', 'haiku', 'fable')
|
||||
$script:Version = '0.0.0'
|
||||
try { $__v = Join-Path $PSScriptRoot 'VERSION'; if (Test-Path $__v) { $script:Version = (Get-Content $__v -Raw).Trim() } } catch { }
|
||||
$script:Modes = @('anthropic', 'openrouter', 'zai', 'lmstudio')
|
||||
$script:NodeExe = $null # resolved lazily by Format-JsonPretty
|
||||
|
||||
$script:ModeLabel = @{
|
||||
'anthropic' = 'Anthropic - your subscription login, no gateway'
|
||||
'openrouter' = 'OpenRouter - remote, pay-per-token, any vendor'
|
||||
'zai' = 'Z.AI - GLM coding plan'
|
||||
'lmstudio' = 'LM Studio - local server, offline, free'
|
||||
# ---------------------------------------------------------------------------
|
||||
# Providers
|
||||
#
|
||||
# Every gateway provider is an entry in providers.json, next to this script and
|
||||
# shared with the POSIX build: endpoint, auth, how its model list is fetched,
|
||||
# which doctor checks apply. This script only knows the *kinds* of behaviour
|
||||
# (Get-ProviderCatalogue, Test-PresetCatalogue) and picks one by name from the
|
||||
# entry, so a provider that reuses them needs no change here. anthropic is
|
||||
# built in: it is the native login, not a gateway.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$script:ProvidersPath = Join-Path $PSScriptRoot 'providers.json'
|
||||
$script:Providers = @()
|
||||
try {
|
||||
$__pj = Get-Content -LiteralPath $script:ProvidersPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$script:Providers = @($__pj.providers | Where-Object { $_.id })
|
||||
} catch { }
|
||||
if ($script:Providers.Count -eq 0) {
|
||||
Write-Host " FAIL providers.json missing or unreadable at $($script:ProvidersPath) - re-run install.ps1" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$script:Modes = @('anthropic') + @($script:Providers | ForEach-Object { [string]$_.id })
|
||||
$script:ModeLabel = @{ 'anthropic' = 'Anthropic - your subscription login, no gateway' }
|
||||
|
||||
# `claude-mode <provider>` with no preset named uses this one. Deliberately a
|
||||
# fixed choice rather than "most recently used", so the command is predictable.
|
||||
$script:ProviderDefaultPreset = @{
|
||||
'openrouter' = 'default'
|
||||
'zai' = 'zai'
|
||||
'lmstudio' = 'lmstudio'
|
||||
$script:ProviderDefaultPreset = @{}
|
||||
foreach ($__p in $script:Providers) {
|
||||
$script:ModeLabel[[string]$__p.id] = [string]$__p.label
|
||||
$script:ProviderDefaultPreset[[string]$__p.id] = $(if ($__p.defaultPreset) { [string]$__p.defaultPreset } else { [string]$__p.id })
|
||||
}
|
||||
|
||||
function Get-Provider {
|
||||
param([string] $Id)
|
||||
return ($script:Providers | Where-Object { $_.id -eq $Id } | Select-Object -First 1)
|
||||
}
|
||||
|
||||
# An id or an alias (z.ai, z-ai) to the provider id; $null if neither.
|
||||
function Resolve-ProviderId {
|
||||
param([string] $Word)
|
||||
$w = ([string]$Word).ToLower()
|
||||
foreach ($p in $script:Providers) {
|
||||
if ([string]$p.id -eq $w -or (@($p.aliases) -contains $w)) { return [string]$p.id }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Test-ProviderDoctor {
|
||||
param([string] $Id, [string] $Check)
|
||||
$p = Get-Provider $Id
|
||||
return [bool]($p -and (@($p.doctor) -contains $Check))
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -121,11 +159,12 @@ function Write-Head ($m) { Write-Host ""; Write-Host $m -ForegroundColor Cyan }
|
||||
|
||||
# Each mode gets an identity colour, reused for its menu row, its banner tagline
|
||||
# and its status line - so "which mode am I in" is answerable at a glance.
|
||||
$script:ModeColor = @{
|
||||
'anthropic' = 'Magenta'
|
||||
'openrouter' = 'Cyan'
|
||||
'zai' = 'Green'
|
||||
'lmstudio' = 'Yellow'
|
||||
$script:ModeColor = @{ 'anthropic' = 'Magenta' }
|
||||
$__colors = @{ cyan = 'Cyan'; green = 'Green'; yellow = 'Yellow'; magenta = 'Magenta'
|
||||
white = 'White'; gray = 'Gray'; dkcyan = 'DarkCyan'; red = 'Red' }
|
||||
foreach ($__p in $script:Providers) {
|
||||
$__c = $__colors[[string]$__p.color]
|
||||
$script:ModeColor[[string]$__p.id] = $(if ($__c) { $__c } else { 'Gray' })
|
||||
}
|
||||
|
||||
# Deliberately ASCII-only. This file is read by Windows PowerShell 5.1, which
|
||||
@@ -162,15 +201,19 @@ function Show-Banner {
|
||||
|
||||
function Show-Usage {
|
||||
@'
|
||||
claude-mode - switch Claude Code between Anthropic, OpenRouter, Z.AI, LM Studio
|
||||
claude-mode - switch Claude Code between Anthropic and gateway providers
|
||||
|
||||
claude-mode interactive menu
|
||||
claude-mode status active mode, preset, model map
|
||||
|
||||
claude-mode anthropic native login/subscription (clears all gateway config)
|
||||
claude-mode openrouter [preset] remote gateway (default preset: default)
|
||||
claude-mode zai [preset] Z.AI GLM coding plan (default preset: zai)
|
||||
claude-mode lmstudio [preset] local LM Studio (default preset: lmstudio)
|
||||
'@ | Write-Host
|
||||
# One line per provider in providers.json, so a new one documents itself.
|
||||
foreach ($p in $script:Providers) {
|
||||
$desc = ([string]$p.label) -replace '^[^-]*-\s*', ''
|
||||
Write-Host (" claude-mode {0,-26} {1} (default preset: {2})" -f "$($p.id) [preset]", $desc, $script:ProviderDefaultPreset[[string]$p.id])
|
||||
}
|
||||
@'
|
||||
|
||||
claude-mode presets list presets
|
||||
claude-mode preset show <name>
|
||||
@@ -474,7 +517,7 @@ function Clear-ManagedSettings {
|
||||
|
||||
function Set-ClaudeMode {
|
||||
param(
|
||||
[ValidateSet('anthropic', 'openrouter', 'zai', 'lmstudio')] [string] $Mode,
|
||||
[string] $Mode,
|
||||
[string] $PresetName
|
||||
)
|
||||
|
||||
@@ -483,6 +526,9 @@ function Set-ClaudeMode {
|
||||
New-Item -ItemType Directory -Path $script:SettingsDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Any provider in providers.json, rather than a fixed ValidateSet.
|
||||
if ($Mode -ne 'anthropic' -and -not (Get-Provider $Mode)) { throw "unknown mode '$Mode'" }
|
||||
|
||||
$settings = Read-JsonFile $script:Settings
|
||||
if ($null -eq $settings) { $settings = [ordered]@{} }
|
||||
|
||||
@@ -500,6 +546,21 @@ function Set-ClaudeMode {
|
||||
$models = $preset['models']
|
||||
if ($null -eq $models) { throw "preset '$PresetName' has no 'models' block" }
|
||||
|
||||
# A custom endpoint ships with no address, since there is no sensible
|
||||
# one to guess. Switching to it would point every session at nothing.
|
||||
if ([string]::IsNullOrWhiteSpace([string]$preset['baseUrl'])) {
|
||||
throw "preset '$PresetName' has no server address - set baseUrl in $(Get-PresetPath $PresetName)"
|
||||
}
|
||||
|
||||
# Every tier empty - a fresh blank preset - would switch cleanly and
|
||||
# leave Claude Code asking for its own default Anthropic models: billed
|
||||
# at full price through OpenRouter, refused by the other providers.
|
||||
$anyTier = $false
|
||||
foreach ($tier in $script:Tiers) { if (-not [string]::IsNullOrWhiteSpace([string]$models[$tier])) { $anyTier = $true } }
|
||||
if (-not $anyTier) {
|
||||
throw "preset '$PresetName' has no models set - set a tier first: claude-mode preset set $PresetName <tier> <model-id>"
|
||||
}
|
||||
|
||||
# Cost guard. Gateways resell Anthropic models at full list price, with no
|
||||
# subscription discount - routing a tier there is almost never intended
|
||||
# and is expensive enough to be worth blocking outright. Opt in per
|
||||
@@ -566,7 +627,9 @@ function Set-ClaudeMode {
|
||||
}
|
||||
$settings['apiKeyHelper'] = Get-HelperCommandLine
|
||||
} else {
|
||||
$tok = 'lmstudio'
|
||||
# The provider's own placeholder (lmstudio, ollama), not LM Studio's.
|
||||
$pv = Get-Provider ([string]$preset['provider'])
|
||||
$tok = $(if ($pv -and $pv.preset.auth.token) { [string]$pv.preset.auth.token } else { 'lmstudio' })
|
||||
if ($auth.Contains('token') -and $auth['token']) { $tok = [string]$auth['token'] }
|
||||
$envBlock['ANTHROPIC_AUTH_TOKEN'] = $tok
|
||||
}
|
||||
@@ -666,7 +729,9 @@ function Test-OpenRouterGuardrail {
|
||||
# nothing actionable to print for them.
|
||||
function Show-GuardrailStatus {
|
||||
param([string] $Mode, [string] $Key)
|
||||
if ($Mode -ne 'openrouter' -or -not $Key) { return }
|
||||
# An OpenRouter feature, flagged per provider rather than by name.
|
||||
$pv = Get-Provider $Mode
|
||||
if (-not ($pv -and $pv.guardrail) -or -not $Key) { return }
|
||||
|
||||
$label = ' guardrail '
|
||||
$state = Test-OpenRouterGuardrail -Key $Key
|
||||
@@ -1054,6 +1119,202 @@ function Test-LmStudioTemplate {
|
||||
return $rep
|
||||
}
|
||||
|
||||
# The credential a preset would send: its vault key, or its inline token.
|
||||
function Get-PresetToken {
|
||||
param($Preset)
|
||||
$auth = Get-PresetAuth $Preset
|
||||
if ([string]$auth['mode'] -eq 'vault') {
|
||||
$ref = $(if ($auth.Contains('keyRef') -and $auth['keyRef']) { [string]$auth['keyRef'] } else { 'openrouter' })
|
||||
return (Get-VaultKey $ref)
|
||||
}
|
||||
return [string]$auth['token']
|
||||
}
|
||||
|
||||
# One catalogue fetch for any provider, by its catalogue.kind in providers.json.
|
||||
# Every row comes back in one shape - Id, Ctx, State, InM, OutM, Note - so the
|
||||
# callers (models, the picker, doctor) never branch on the provider itself.
|
||||
function Get-ProviderCatalogue {
|
||||
param($Preset)
|
||||
$prov = Get-Provider ([string]$Preset['provider'])
|
||||
if (-not $prov) { return @() }
|
||||
$base = ([string]$Preset['baseUrl']).TrimEnd('/')
|
||||
switch ([string]$prov.catalogue.kind) {
|
||||
'openrouter' {
|
||||
return @((Invoke-RestMethod -Uri 'https://openrouter.ai/api/v1/models' -TimeoutSec 30).data | ForEach-Object {
|
||||
[pscustomobject]@{
|
||||
Id = $_.id; Ctx = $_.context_length; State = $null; Note = ''
|
||||
InM = $(if ($_.pricing -and $_.pricing.prompt) { [math]::Round([double]$_.pricing.prompt * 1e6, 3) } else { $null })
|
||||
OutM = $(if ($_.pricing -and $_.pricing.completion) { [math]::Round([double]$_.pricing.completion * 1e6, 3) } else { $null })
|
||||
}
|
||||
})
|
||||
}
|
||||
'lmstudio' {
|
||||
return @(Get-LmStudioModels $base | ForEach-Object {
|
||||
[pscustomobject]@{ Id = $_.Id; Ctx = $_.Ctx; State = $_.State; InM = $null; OutM = $null; Note = '' }
|
||||
})
|
||||
}
|
||||
'ollama' {
|
||||
$h = @{}
|
||||
$tok = Get-PresetToken $Preset
|
||||
if ($tok) { $h['Authorization'] = "Bearer $tok" }
|
||||
return @((Invoke-RestMethod -Uri "$base/api/tags" -Headers $h -TimeoutSec 10).models | ForEach-Object {
|
||||
$d = $_.details
|
||||
$note = $(if ($d) { (@($d.parameter_size, $d.quantization_level) | Where-Object { $_ }) -join ' ' } else { '' })
|
||||
[pscustomobject]@{ Id = $_.name; Ctx = $null; State = $null; InM = $null; OutM = $null; Note = $note }
|
||||
})
|
||||
}
|
||||
'openai' {
|
||||
# Both header styles: a proxy in front of Anthropic wants x-api-key,
|
||||
# one in front of anything else wants Bearer.
|
||||
$h = @{ 'anthropic-version' = '2023-06-01' }
|
||||
$tok = Get-PresetToken $Preset
|
||||
if ($tok) { $h['Authorization'] = "Bearer $tok"; $h['x-api-key'] = $tok }
|
||||
return @((Invoke-RestMethod -Uri "$base/v1/models" -Headers $h -TimeoutSec 15).data | ForEach-Object {
|
||||
[pscustomobject]@{ Id = $_.id; Ctx = $null; State = $null; InM = $null; OutM = $null; Note = '' }
|
||||
})
|
||||
}
|
||||
'static' {
|
||||
return @($prov.catalogue.static | ForEach-Object {
|
||||
[pscustomobject]@{ Id = $_.id; Ctx = $null; State = $null; InM = $null; OutM = $null; Note = [string]$_.note }
|
||||
})
|
||||
}
|
||||
}
|
||||
return @()
|
||||
}
|
||||
|
||||
# The preset's model ids against what the provider actually offers. A server
|
||||
# provider that does not answer is a failure; a hosted catalogue that cannot be
|
||||
# fetched is only a warning, since the endpoint may be fine regardless.
|
||||
function Test-PresetCatalogue {
|
||||
param([string] $Mode, $Preset)
|
||||
$prov = Get-Provider $Mode
|
||||
$kind = [string]$prov.catalogue.kind
|
||||
$title = [string]$prov.title
|
||||
$base = ([string]$Preset['baseUrl']).TrimEnd('/')
|
||||
$server = [bool]$prov.server.editable
|
||||
|
||||
$cat = @()
|
||||
try { $cat = @(Get-ProviderCatalogue $Preset) } catch { $cat = @() }
|
||||
if ($cat.Count -eq 0) {
|
||||
if ($server -and [string]$prov.server.probe -eq 'lenient') {
|
||||
Write-Warn2 "$title at $base lists no models - fine for a proxy, but the ids below cannot be checked"
|
||||
} elseif ($server) {
|
||||
$start = [string]$prov.server.start
|
||||
Write-Err2 ("$title not reachable at $base" + $(if ($start) { " - $start" } else { '' }))
|
||||
} else {
|
||||
Write-Warn2 "could not fetch the $title model list"
|
||||
}
|
||||
return
|
||||
}
|
||||
if ($server) { Write-Ok "$title reachable at $base ($($cat.Count) models)" }
|
||||
|
||||
$declared = $(if ($Preset.Contains('contextTokens') -and $Preset['contextTokens']) { [int]$Preset['contextTokens'] } else { 0 })
|
||||
$tpl = $(if (Test-ProviderDoctor $Mode 'lmstudio-templates') { Get-LmStudioTemplateReport } else { @() })
|
||||
$warned = @{} # a one-for-all preset names one model four times; say things about it once
|
||||
|
||||
foreach ($t in $script:Tiers) {
|
||||
if (-not ($Preset['models'].Contains($t) -and $Preset['models'][$t])) { continue }
|
||||
$id = [string]$Preset['models'][$t]
|
||||
$m = $cat | Where-Object { $_.Id -eq $id } | Select-Object -First 1
|
||||
# Ollama lists every model with its tag; a bare name means :latest.
|
||||
if (-not $m -and $kind -eq 'ollama' -and $id -notlike '*:*') {
|
||||
$m = $cat | Where-Object { $_.Id -eq "${id}:latest" } | Select-Object -First 1
|
||||
}
|
||||
if (-not $m) {
|
||||
# A fixed list is documentation, not the provider's word.
|
||||
if ($kind -eq 'static') { Write-Ok ("{0,-6} {1} (not in the documented list)" -f $t, $id) }
|
||||
else { Write-Err2 "$t model NOT available from ${title}: $id" }
|
||||
continue
|
||||
}
|
||||
switch ($kind) {
|
||||
'lmstudio' {
|
||||
if ($m.State -eq 'loaded') { Write-Ok ("{0,-6} {1} [loaded, ctx {2}]" -f $t, $id, $m.Ctx) }
|
||||
else { Write-Ok ("{0,-6} {1} [{2} - LM Studio will JIT-load it on first request, ctx {3}]" -f $t, $id, $m.State, $m.Ctx) }
|
||||
if (-not $warned.ContainsKey($id)) {
|
||||
$warned[$id] = $true
|
||||
if ($m.Ctx -and [int]$m.Ctx -lt 25000) { Write-Warn2 "$id context is $($m.Ctx); LM Studio recommends >25k for Claude Code." }
|
||||
if ($declared -and $m.Ctx -and [int]$m.Ctx -lt $declared) {
|
||||
Write-Warn2 ("declared contextTokens {0:N0} exceeds {1}'s {2:N0} - lower it." -f $declared, $id, [int]$m.Ctx)
|
||||
}
|
||||
$short = ($id -split '/')[-1].ToLower()
|
||||
$risk = $tpl | Where-Object { $_.Key -eq $short } | Select-Object -First 1
|
||||
if ($risk -and $risk.Assertions.Count -gt 0) {
|
||||
Write-Warn2 "$id chat template hard-asserts message order ($($risk.Assertions -join '; '))."
|
||||
Write-Warn2 " This can surface as: [Server Error] 'Unable to generate parser for this template'."
|
||||
Write-Warn2 " If you hit that, switch to a model without this flag - see 'claude-mode models'."
|
||||
}
|
||||
}
|
||||
}
|
||||
'openrouter' { Write-Ok ("{0,-6} {1} [ctx {2:N0}]" -f $t, $id, [int]$m.Ctx) }
|
||||
'ollama' { Write-Ok ("{0,-6} {1} [{2}]" -f $t, $id, $m.Note) }
|
||||
default { Write-Ok ("{0,-6} {1}" -f $t, $id) }
|
||||
}
|
||||
}
|
||||
|
||||
if ($kind -eq 'openrouter') { Test-ContextWindow -Preset $Preset -Catalogue $cat }
|
||||
elseif ($declared) { Write-Ok ("declared context window: {0:N0} tokens" -f $declared) }
|
||||
else { Write-Warn2 'preset has no contextTokens - Claude Code will guess a small window and auto-compact early.' }
|
||||
}
|
||||
|
||||
# Ollama sets the context window on the server, not per request from Claude
|
||||
# Code: 4096 tokens unless `ollama serve` runs with OLLAMA_CONTEXT_LENGTH, and
|
||||
# anything past it is cut off without an error. contextTokens only tells Claude
|
||||
# Code what to expect, so say what the server is actually running where that
|
||||
# can be seen, and what to set where it cannot.
|
||||
function Test-OllamaContext {
|
||||
param($Preset)
|
||||
if (-not ($Preset.Contains('contextTokens') -and $Preset['contextTokens'])) { return }
|
||||
$declared = [int]$Preset['contextTokens']
|
||||
$base = ([string]$Preset['baseUrl']).TrimEnd('/')
|
||||
$ids = @()
|
||||
foreach ($t in $script:Tiers) {
|
||||
$v = [string]$Preset['models'][$t]
|
||||
if ($v -and $ids -notcontains $v) { $ids += $v }
|
||||
}
|
||||
$ps = $null
|
||||
try { $ps = Invoke-RestMethod -Uri "$base/api/ps" -TimeoutSec 5 } catch { }
|
||||
|
||||
$seen = $false; $short = $false
|
||||
foreach ($id in $ids) {
|
||||
try {
|
||||
$show = Invoke-RestMethod -Uri "$base/api/show" -Method Post -ContentType 'application/json' `
|
||||
-Body (@{ model = $id } | ConvertTo-Json) -TimeoutSec 8
|
||||
$max = $null
|
||||
if ($show.model_info) {
|
||||
foreach ($prop in $show.model_info.PSObject.Properties) {
|
||||
if ($prop.Name -like '*.context_length') { $max = [int]$prop.Value; break }
|
||||
}
|
||||
}
|
||||
if ($max -and $max -lt $declared) { Write-Warn2 "$id supports at most $max tokens, below the declared $declared - lower contextTokens" }
|
||||
} catch { }
|
||||
|
||||
$want = @($id)
|
||||
if ($id -notlike '*:*') { $want += "${id}:latest" }
|
||||
$live = $null
|
||||
if ($ps -and $ps.models) {
|
||||
foreach ($m in $ps.models) {
|
||||
if ((($want -contains [string]$m.name) -or ($want -contains [string]$m.model)) -and $m.context_length) {
|
||||
$live = [int]$m.context_length; break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($null -eq $live) { continue }
|
||||
$seen = $true
|
||||
if ($live -lt $declared) {
|
||||
$short = $true
|
||||
Write-Warn2 "$id is loaded with a $live-token context, below the declared $declared - requests past it are cut off"
|
||||
} else {
|
||||
Write-Ok "$id is loaded with a $live-token context"
|
||||
}
|
||||
}
|
||||
if (-not $seen) {
|
||||
Write-Warn2 "none of these models is loaded, so the server's context window cannot be checked"
|
||||
Write-Host " Ollama defaults to 4096 tokens; run it with OLLAMA_CONTEXT_LENGTH=$declared or requests past that are cut off silently"
|
||||
} elseif ($short) {
|
||||
Write-Host " restart it with OLLAMA_CONTEXT_LENGTH=$declared (or lower contextTokens to match)"
|
||||
}
|
||||
}
|
||||
|
||||
# CLAUDE_CODE_MAX_CONTEXT_TOKENS is a single global value, but each tier can
|
||||
# point at a model with a different window. Declaring more context than a tier's
|
||||
# model actually has means requests on that tier can overflow, so name the
|
||||
@@ -1073,10 +1334,10 @@ function Test-ContextWindow {
|
||||
foreach ($t in $script:Tiers) {
|
||||
if (-not $Preset['models'].Contains($t)) { continue }
|
||||
$id = [string]$Preset['models'][$t]
|
||||
$m = $Catalogue | Where-Object { $_.id -eq $id } | Select-Object -First 1
|
||||
if (-not $m) { continue }
|
||||
if ([int]$m.context_length -lt $declared) {
|
||||
Write-Warn2 ("{0} model {1} only has {2:N0} ctx, below the declared {3:N0}." -f $t, $id, $m.context_length, $declared)
|
||||
$m = $Catalogue | Where-Object { $_.Id -eq $id } | Select-Object -First 1
|
||||
if (-not $m -or -not $m.Ctx) { continue }
|
||||
if ([int]$m.Ctx -lt $declared) {
|
||||
Write-Warn2 ("{0} model {1} only has {2:N0} ctx, below the declared {3:N0}." -f $t, $id, [int]$m.Ctx, $declared)
|
||||
if ($t -eq 'haiku') {
|
||||
Write-Warn2 ' haiku only runs short background tasks, so this is usually harmless.'
|
||||
} else {
|
||||
@@ -1251,45 +1512,37 @@ function Invoke-Models {
|
||||
$state = Get-State
|
||||
$mode = [string]$state['mode']
|
||||
|
||||
if ($mode -eq 'lmstudio') {
|
||||
$preset = Get-Preset ([string]$state['preset'])
|
||||
Write-Head "models installed in LM Studio at $($preset['baseUrl'])"
|
||||
$tpl = Get-LmStudioTemplateReport
|
||||
Get-LmStudioModels ([string]$preset['baseUrl']) |
|
||||
Where-Object { -not $Filter -or $_.Id -like "*$Filter*" } |
|
||||
Sort-Object Id |
|
||||
ForEach-Object {
|
||||
$short = ($_.Id -split '/')[-1].ToLower()
|
||||
$t = $tpl | Where-Object { $_.Key -eq $short } | Select-Object -First 1
|
||||
$flag = ''
|
||||
if ($t -and $t.Assertions.Count -gt 0) { $flag = 'TEMPLATE RISK' }
|
||||
[pscustomobject]@{ Id = $_.Id; State = $_.State; Ctx = $_.Ctx; Note = $flag }
|
||||
} | Format-Table -AutoSize
|
||||
Write-Host " 'TEMPLATE RISK' = the model's chat template hard-asserts message order,"
|
||||
Write-Host " which can break tool-call parser generation. Prefer an unflagged model."
|
||||
return
|
||||
# On anthropic, OpenRouter's catalogue is the one worth browsing, as before.
|
||||
$preset = $(if ($mode -ne 'anthropic') { Get-Preset ([string]$state['preset']) } else { [ordered]@{ provider = 'openrouter'; baseUrl = '' } })
|
||||
$prov = Get-Provider ([string]$preset['provider'])
|
||||
$kind = [string]$prov.catalogue.kind
|
||||
switch ($kind) {
|
||||
'static' { Write-Head "$($prov.title) models (from its docs - no public catalogue endpoint)" }
|
||||
'openrouter' { Write-Head 'fetching https://openrouter.ai/api/v1/models ...' }
|
||||
default { Write-Head "models on the $($prov.title) server at $($preset['baseUrl'])" }
|
||||
}
|
||||
|
||||
if ($mode -eq 'zai') {
|
||||
Write-Head 'Z.AI GLM models (from Z.AI docs - no public catalogue endpoint)'
|
||||
@('glm-5.3 - flagship coding model (opus/sonnet tier)',
|
||||
'glm-4.7 - fast/cheap tier (haiku tier)') | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ' Full list: https://docs.z.ai/devpack/tool/claude'
|
||||
return
|
||||
}
|
||||
|
||||
Write-Head 'fetching https://openrouter.ai/api/v1/models ...'
|
||||
$resp = Invoke-RestMethod -Uri 'https://openrouter.ai/api/v1/models' -Method Get -TimeoutSec 30
|
||||
$rows = foreach ($m in $resp.data) {
|
||||
if ($Filter -and $m.id -notlike "*$Filter*") { continue }
|
||||
[pscustomobject]@{
|
||||
Id = $m.id
|
||||
Ctx = $m.context_length
|
||||
'In/M$' = if ($m.pricing -and $m.pricing.prompt) { [math]::Round([double]$m.pricing.prompt * 1e6, 3) } else { $null }
|
||||
'Out/M$' = if ($m.pricing -and $m.pricing.completion) { [math]::Round([double]$m.pricing.completion * 1e6, 3) } else { $null }
|
||||
$tpl = $(if ($kind -eq 'lmstudio') { Get-LmStudioTemplateReport } else { @() })
|
||||
$rows = foreach ($m in (Get-ProviderCatalogue $preset | Sort-Object Id)) {
|
||||
if ($Filter -and $m.Id -notlike "*$Filter*") { continue }
|
||||
if ($kind -eq 'openrouter') {
|
||||
[pscustomobject]@{ Id = $m.Id; Ctx = $m.Ctx; 'In/M$' = $m.InM; 'Out/M$' = $m.OutM }
|
||||
} elseif ($kind -eq 'lmstudio') {
|
||||
$short = ($m.Id -split '/')[-1].ToLower()
|
||||
$t = $tpl | Where-Object { $_.Key -eq $short } | Select-Object -First 1
|
||||
$flag = $(if ($t -and $t.Assertions.Count -gt 0) { 'TEMPLATE RISK' } else { '' })
|
||||
[pscustomobject]@{ Id = $m.Id; State = $m.State; Ctx = $m.Ctx; Note = $flag }
|
||||
} else {
|
||||
[pscustomobject]@{ Id = $m.Id; Note = $m.Note }
|
||||
}
|
||||
}
|
||||
$rows | Sort-Object Id | Format-Table -AutoSize
|
||||
$rows | Format-Table -AutoSize
|
||||
|
||||
if ($kind -eq 'lmstudio') {
|
||||
Write-Host " 'TEMPLATE RISK' = the model's chat template hard-asserts message order,"
|
||||
Write-Host " which can break tool-call parser generation. Prefer an unflagged model."
|
||||
}
|
||||
if ($kind -eq 'static' -and $prov.catalogue.docs) { Write-Host " Full list: $($prov.catalogue.docs)" }
|
||||
}
|
||||
|
||||
function Invoke-Doctor {
|
||||
@@ -1363,7 +1616,7 @@ function Invoke-Doctor {
|
||||
}
|
||||
}
|
||||
|
||||
if ($key -and $mode -eq 'openrouter') {
|
||||
if ($key -and (Test-ProviderDoctor $mode 'openrouter-key')) {
|
||||
try {
|
||||
$r = Invoke-RestMethod -Uri 'https://openrouter.ai/api/v1/key' -Headers @{ Authorization = "Bearer $key" } -TimeoutSec 20
|
||||
Write-Ok "OpenRouter key valid (label: $($r.data.label))"
|
||||
@@ -1382,7 +1635,7 @@ function Invoke-Doctor {
|
||||
Show-GuardrailStatus -Mode $mode -Key $key
|
||||
}
|
||||
|
||||
if ($key -and $mode -eq 'zai') {
|
||||
if ($key -and (Test-ProviderDoctor $mode 'message-check')) {
|
||||
# No key-info endpoint; the cheapest real check is a 1-token
|
||||
# message against the Anthropic-compatible surface.
|
||||
try {
|
||||
@@ -1394,80 +1647,20 @@ function Invoke-Doctor {
|
||||
[void](Invoke-RestMethod -Uri "$base/v1/messages" -Method Post -Body $body `
|
||||
-ContentType 'application/json' -TimeoutSec 45 `
|
||||
-Headers @{ 'x-api-key' = $key; 'Authorization' = "Bearer $key"; 'anthropic-version' = '2023-06-01' })
|
||||
Write-Ok "Z.AI endpoint accepted the key ($base/v1/messages)"
|
||||
Write-Ok "$((Get-Provider $mode).title) endpoint accepted the key ($base/v1/messages)"
|
||||
} catch {
|
||||
$detail = ''
|
||||
if ($_.ErrorDetails) { $detail = ($_.ErrorDetails.Message -replace '\s+', ' ') }
|
||||
Write-Err2 "Z.AI request failed: $($_.Exception.Message) $detail"
|
||||
Write-Err2 "$((Get-Provider $mode).title) request failed: $($_.Exception.Message) $detail"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Ok "inline token '$($auth['token'])' (no secret in settings.json)"
|
||||
}
|
||||
|
||||
if ($mode -eq 'lmstudio') {
|
||||
$catalogue = $null
|
||||
try {
|
||||
$catalogue = Get-LmStudioModels $base
|
||||
Write-Ok "LM Studio server reachable at $base ($($catalogue.Count) models installed)"
|
||||
} catch {
|
||||
Write-Err2 "LM Studio not reachable at $base - start the server (LM Studio > Developer > Start Server)"
|
||||
}
|
||||
|
||||
if ($catalogue) {
|
||||
$wanted = @()
|
||||
foreach ($t in $script:Tiers) {
|
||||
if ($preset['models'].Contains($t)) { $wanted += [string]$preset['models'][$t] }
|
||||
}
|
||||
if ($preset.Contains('subagentModel')) { $wanted += [string]$preset['subagentModel'] }
|
||||
|
||||
foreach ($id in ($wanted | Sort-Object -Unique)) {
|
||||
$m = $catalogue | Where-Object { $_.Id -eq $id } | Select-Object -First 1
|
||||
if (-not $m) {
|
||||
Write-Err2 "model NOT installed in LM Studio: $id (run 'claude-mode models' for valid ids)"
|
||||
continue
|
||||
}
|
||||
if ($m.State -eq 'loaded') { Write-Ok "$id [loaded, ctx $($m.Ctx)]" }
|
||||
else { Write-Ok "$id [$($m.State) - LM Studio will JIT-load it on first request, ctx $($m.Ctx)]" }
|
||||
|
||||
if ($m.Ctx -and [int]$m.Ctx -lt 25000) {
|
||||
Write-Warn2 "$id context is $($m.Ctx); LM Studio recommends >25k for Claude Code."
|
||||
}
|
||||
|
||||
if ($preset.Contains('contextTokens') -and $preset['contextTokens'] -and $m.Ctx) {
|
||||
$declared = [int]$preset['contextTokens']
|
||||
if ([int]$m.Ctx -lt $declared) {
|
||||
Write-Warn2 ("declared contextTokens {0:N0} exceeds {1}'s {2:N0} - lower it." -f $declared, $id, $m.Ctx)
|
||||
} else {
|
||||
Write-Ok ("declared context window: {0:N0} tokens" -f $declared)
|
||||
}
|
||||
} elseif (-not $preset.Contains('contextTokens')) {
|
||||
Write-Warn2 'preset has no contextTokens - Claude Code will guess a small window and auto-compact early.'
|
||||
}
|
||||
|
||||
$tpl = Test-LmStudioTemplate $id
|
||||
if ($tpl -and $tpl.Assertions.Count -gt 0) {
|
||||
Write-Warn2 "$id chat template hard-asserts message order ($($tpl.Assertions -join '; '))."
|
||||
Write-Warn2 " This can surface as: [Server Error] 'Unable to generate parser for this template'."
|
||||
Write-Warn2 " If you hit that, switch to a model without this flag - see 'claude-mode models'."
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($mode -eq 'openrouter') {
|
||||
try {
|
||||
$cat = (Invoke-RestMethod -Uri 'https://openrouter.ai/api/v1/models' -TimeoutSec 30).data
|
||||
foreach ($t in $script:Tiers) {
|
||||
if (-not $preset['models'].Contains($t)) { continue }
|
||||
$id = [string]$preset['models'][$t]
|
||||
$m = $cat | Where-Object { $_.id -eq $id } | Select-Object -First 1
|
||||
if ($m) { Write-Ok ("{0,-6} {1} [ctx {2:N0}]" -f $t, $id, $m.context_length) }
|
||||
else { Write-Err2 "$t model NOT available from this provider: $id" }
|
||||
}
|
||||
Test-ContextWindow -Preset $preset -Catalogue $cat
|
||||
} catch {
|
||||
Write-Warn2 "could not verify model ids: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
# Which checks run is the provider's call, by name, in providers.json.
|
||||
if (Test-ProviderDoctor $mode 'catalogue-models') { Test-PresetCatalogue -Mode $mode -Preset $preset }
|
||||
if (Test-ProviderDoctor $mode 'ollama-context') { Test-OllamaContext -Preset $preset }
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
@@ -1878,37 +2071,26 @@ function Get-ModelChoices {
|
||||
$cacheKey = $provider + '|' + [string]$Preset['baseUrl']
|
||||
if ($script:ModelCatalogueCache.ContainsKey($cacheKey)) { return $script:ModelCatalogueCache[$cacheKey] }
|
||||
|
||||
$out = @()
|
||||
$out = @()
|
||||
$kind = [string](Get-Provider $provider).catalogue.kind
|
||||
$tpl = $(if ($kind -eq 'lmstudio') { Get-LmStudioTemplateReport } else { @() })
|
||||
|
||||
if ($provider -eq 'lmstudio') {
|
||||
$models = @(Get-LmStudioModels ([string]$Preset['baseUrl']))
|
||||
$tpl = Get-LmStudioTemplateReport
|
||||
foreach ($m in ($models | Sort-Object Id)) {
|
||||
foreach ($m in (Get-ProviderCatalogue $Preset | Sort-Object Id)) {
|
||||
$flag = ''
|
||||
if ($kind -eq 'openrouter') {
|
||||
$det = @(("context {0:N0} `$$($m.InM) in / `$$($m.OutM) out per 1M tokens" -f [int]$m.Ctx))
|
||||
} elseif ($kind -eq 'lmstudio') {
|
||||
$det = @("state: $($m.State) max context: $($m.Ctx)")
|
||||
$short = ($m.Id -split '/')[-1].ToLower()
|
||||
$risk = $tpl | Where-Object { $_.Key -eq $short } | Select-Object -First 1
|
||||
$flag = ''
|
||||
$det = @("state: $($m.State) max context: $($m.Ctx)")
|
||||
if ($risk -and $risk.Assertions.Count -gt 0) {
|
||||
$flag = ' [TEMPLATE RISK]'
|
||||
$det += 'chat template asserts message order - can break tool calls'
|
||||
}
|
||||
$out += [pscustomobject]@{ Id = $m.Id; Key = $m.Id; Label = ($m.Id + $flag); Detail = $det }
|
||||
} else {
|
||||
$det = @([string]$m.Note)
|
||||
}
|
||||
}
|
||||
elseif ($provider -eq 'openrouter') {
|
||||
$cat = (Invoke-RestMethod -Uri 'https://openrouter.ai/api/v1/models' -TimeoutSec 30).data
|
||||
foreach ($m in ($cat | Sort-Object id)) {
|
||||
$inp = if ($m.pricing -and $m.pricing.prompt) { [math]::Round([double]$m.pricing.prompt * 1e6, 3) } else { $null }
|
||||
$outp= if ($m.pricing -and $m.pricing.completion) { [math]::Round([double]$m.pricing.completion * 1e6, 3) } else { $null }
|
||||
$det = @(("context {0:N0} `$$inp in / `$$outp out per 1M tokens" -f [int]$m.context_length))
|
||||
$out += [pscustomobject]@{ Id = $m.id; Key = $m.id; Label = $m.id; Detail = $det }
|
||||
}
|
||||
}
|
||||
elseif ($provider -eq 'zai') {
|
||||
# Z.AI publishes no public catalogue endpoint; these are the documented
|
||||
# coding-plan models. Anything else can still be typed manually.
|
||||
$out += [pscustomobject]@{ Id = 'glm-5.3'; Key = 'glm-5.3'; Label = 'glm-5.3'; Detail = @('flagship coding model - opus/sonnet tier') }
|
||||
$out += [pscustomobject]@{ Id = 'glm-4.7'; Key = 'glm-4.7'; Label = 'glm-4.7'; Detail = @('fast/cheap tier - haiku') }
|
||||
$out += [pscustomobject]@{ Id = $m.Id; Key = $m.Id; Label = ($m.Id + $flag); Detail = $det }
|
||||
}
|
||||
|
||||
$script:ModelCatalogueCache[$cacheKey] = $out
|
||||
@@ -1951,36 +2133,30 @@ function Read-ModelId {
|
||||
function New-PresetScaffold {
|
||||
param([string] $Provider)
|
||||
|
||||
# Built from the provider's own template in providers.json; the POSIX
|
||||
# build's `cm-json.py scaffold` produces the same thing from the same file.
|
||||
$p = Get-Provider $Provider
|
||||
if (-not $p) { throw "unknown provider '$Provider'" }
|
||||
$tpl = $p.preset
|
||||
|
||||
$base = [ordered]@{
|
||||
provider = $Provider
|
||||
description = 'new preset'
|
||||
}
|
||||
|
||||
switch ($Provider) {
|
||||
'openrouter' {
|
||||
$base['baseUrl'] = 'https://openrouter.ai/api'
|
||||
$base['auth'] = [ordered]@{ mode = 'vault'; keyRef = 'openrouter' }
|
||||
}
|
||||
'zai' {
|
||||
$base['baseUrl'] = 'https://api.z.ai/api/anthropic'
|
||||
$base['auth'] = [ordered]@{ mode = 'vault'; keyRef = 'zai' }
|
||||
}
|
||||
'lmstudio' {
|
||||
$base['baseUrl'] = 'http://127.0.0.1:1234'
|
||||
$base['auth'] = [ordered]@{ mode = 'literal'; token = 'lmstudio' }
|
||||
}
|
||||
}
|
||||
$base['baseUrl'] = [string]$tpl.baseUrl
|
||||
$auth = [ordered]@{}
|
||||
if ($tpl.auth) { foreach ($prop in $tpl.auth.PSObject.Properties) { $auth[$prop.Name] = $prop.Value } }
|
||||
else { $auth['mode'] = 'vault'; $auth['keyRef'] = $Provider }
|
||||
$base['auth'] = $auth
|
||||
|
||||
$base['models'] = [ordered]@{ opus = ''; sonnet = ''; haiku = ''; fable = '' }
|
||||
$base['subagentModel'] = 'inherit'
|
||||
$base['gatewayModelDiscovery'] = ($Provider -eq 'openrouter')
|
||||
$base['contextTokens'] = if ($Provider -eq 'lmstudio') { 262144 } else { 1000000 }
|
||||
if ($Provider -eq 'lmstudio') { $base['extraEnv'] = [ordered]@{ CLAUDE_CODE_ATTRIBUTION_HEADER = '0' } }
|
||||
if ($Provider -eq 'zai') {
|
||||
$base['extraEnv'] = [ordered]@{
|
||||
API_TIMEOUT_MS = '3000000'
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1'
|
||||
}
|
||||
$base['gatewayModelDiscovery'] = [bool]$tpl.gatewayModelDiscovery
|
||||
$base['contextTokens'] = $(if ($tpl.contextTokens) { [int]$tpl.contextTokens } else { 200000 })
|
||||
if ($tpl.extraEnv) {
|
||||
$extra = [ordered]@{}
|
||||
foreach ($prop in $tpl.extraEnv.PSObject.Properties) { $extra[$prop.Name] = [string]$prop.Value }
|
||||
$base['extraEnv'] = $extra
|
||||
}
|
||||
return $base
|
||||
}
|
||||
@@ -2186,25 +2362,12 @@ Initialize-Root
|
||||
|
||||
try {
|
||||
$cmd = $Command.ToLower()
|
||||
if ($cmd -eq 'z.ai' -or $cmd -eq 'z-ai') { $cmd = 'zai' }
|
||||
|
||||
switch ($cmd) {
|
||||
'' { Invoke-Menu }
|
||||
'menu' { Invoke-Menu }
|
||||
'status' { Invoke-Status }
|
||||
'anthropic' { Set-ClaudeMode -Mode 'anthropic' -PresetName '' }
|
||||
'openrouter' {
|
||||
$req = if ($Rest -and $Rest.Count -ge 1) { $Rest[0] } else { $null }
|
||||
Set-ClaudeMode -Mode 'openrouter' -PresetName (Resolve-PresetForProvider 'openrouter' $req)
|
||||
}
|
||||
'zai' {
|
||||
$req = if ($Rest -and $Rest.Count -ge 1) { $Rest[0] } else { $null }
|
||||
Set-ClaudeMode -Mode 'zai' -PresetName (Resolve-PresetForProvider 'zai' $req)
|
||||
}
|
||||
'lmstudio' {
|
||||
$req = if ($Rest -and $Rest.Count -ge 1) { $Rest[0] } else { $null }
|
||||
Set-ClaudeMode -Mode 'lmstudio' -PresetName (Resolve-PresetForProvider 'lmstudio' $req)
|
||||
}
|
||||
'presets' { Invoke-Presets }
|
||||
'preset' { Invoke-PresetCmd -Argv $Rest }
|
||||
'set-key' {
|
||||
@@ -2226,7 +2389,17 @@ try {
|
||||
'help' { Show-Usage }
|
||||
'--help' { Show-Usage }
|
||||
'-h' { Show-Usage }
|
||||
default { Write-Err2 "unknown command '$Command'"; Show-Usage; exit 1 }
|
||||
default {
|
||||
# Any provider in providers.json, by id or alias, is a mode. Last,
|
||||
# so a provider can never shadow a real command.
|
||||
$prov = Resolve-ProviderId $cmd
|
||||
if ($prov) {
|
||||
$req = if ($Rest -and $Rest.Count -ge 1) { $Rest[0] } else { $null }
|
||||
Set-ClaudeMode -Mode $prov -PresetName (Resolve-PresetForProvider $prov $req)
|
||||
} else {
|
||||
Write-Err2 "unknown command '$Command'"; Show-Usage; exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Err2 $_.Exception.Message
|
||||
|
||||
+7
-1
@@ -91,7 +91,13 @@ if (Test-Path -LiteralPath (Join-Path $src 'VERSION')) {
|
||||
}
|
||||
Copy-Item -LiteralPath (Join-Path $src 'bin\claude-key-helper.ps1') -Destination "$root\bin" -Force
|
||||
Copy-Item -LiteralPath (Join-Path $src 'bin\claude-key-helper.cmd') -Destination "$root\bin" -Force
|
||||
Write-Host ' ok copied claude-mode.ps1 + key helper' -ForegroundColor Green
|
||||
# The table of providers claude-mode can switch to. Always replaced, unlike
|
||||
# presets: it is the tool's own definition of each provider, not user data.
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $src 'providers.json'))) {
|
||||
throw 'providers.json missing from the payload - claude-mode cannot run without it'
|
||||
}
|
||||
Copy-Item -LiteralPath (Join-Path $src 'providers.json') -Destination $root -Force
|
||||
Write-Host ' ok copied claude-mode.ps1 + key helper + providers.json' -ForegroundColor Green
|
||||
|
||||
# --- 3. presets -------------------------------------------------------------
|
||||
foreach ($p in Get-ChildItem -LiteralPath (Join-Path $src 'presets') -Filter '*.json') {
|
||||
|
||||
+352
-202
@@ -32,7 +32,9 @@ JSON="$CM_BIN/cm-json.py"
|
||||
CM_FORCE=0
|
||||
CM_SAME_ENDPOINT=0 # set by reapply_if_active for a tier-only edit
|
||||
|
||||
MODES=(anthropic openrouter zai lmstudio)
|
||||
# anthropic is built in; every gateway provider is appended from providers.json
|
||||
# at startup (see cm_providers_load).
|
||||
MODES=(anthropic)
|
||||
TIERS=(opus sonnet haiku fable)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -219,22 +221,21 @@ err() { printf ' %sFAIL%s %s\n' "$C_BOLD$C_RED" "$C_RESET" "$*" >&2; }
|
||||
head_() { printf '\n%s%s%s\n' "$C_BOLD$C_CYAN" "$*" "$C_RESET"; }
|
||||
|
||||
mode_color() {
|
||||
case "$1" in
|
||||
anthropic) printf '%s' "$C_MAGENTA" ;;
|
||||
openrouter) printf '%s' "$C_CYAN" ;;
|
||||
zai) printf '%s' "$C_GREEN" ;;
|
||||
lmstudio) printf '%s' "$C_YELLOW" ;;
|
||||
*) printf '%s' "$C_GRAY" ;;
|
||||
[ "$1" = anthropic ] && { printf '%s' "$C_MAGENTA"; return; }
|
||||
case "$(prov_field "$1" 5)" in
|
||||
cyan) printf '%s' "$C_CYAN" ;;
|
||||
green) printf '%s' "$C_GREEN" ;;
|
||||
yellow) printf '%s' "$C_YELLOW" ;;
|
||||
magenta) printf '%s' "$C_MAGENTA" ;;
|
||||
white) printf '%s' "$C_WHITE" ;;
|
||||
dkcyan) printf '%s' "$C_DKCYAN" ;;
|
||||
*) printf '%s' "$C_GRAY" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
mode_label() {
|
||||
case "$1" in
|
||||
anthropic) printf 'Anthropic - your subscription login, no gateway' ;;
|
||||
openrouter) printf 'OpenRouter - remote, pay-per-token, any vendor' ;;
|
||||
zai) printf 'Z.AI - GLM coding plan' ;;
|
||||
lmstudio) printf 'LM Studio - local server, offline, free' ;;
|
||||
esac
|
||||
[ "$1" = anthropic ] && { printf 'Anthropic - your subscription login, no gateway'; return; }
|
||||
prov_field "$1" 4
|
||||
}
|
||||
|
||||
# Pure ASCII on purpose - renders identically in every terminal and locale.
|
||||
@@ -254,15 +255,20 @@ show_banner() {
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
claude-mode - switch Claude Code between Anthropic, OpenRouter, Z.AI, LM Studio
|
||||
claude-mode - switch Claude Code between Anthropic and gateway providers
|
||||
|
||||
claude-mode interactive menu
|
||||
claude-mode status active mode, preset, model map
|
||||
|
||||
claude-mode anthropic native login (clears all gateway config)
|
||||
claude-mode openrouter [preset] remote gateway (default: default)
|
||||
claude-mode zai [preset] Z.AI GLM coding plan (default: zai)
|
||||
claude-mode lmstudio [preset] local LM Studio (default: lmstudio)
|
||||
EOF
|
||||
# One line per provider in providers.json, so a new one documents itself.
|
||||
local id
|
||||
for id in $(provider_ids); do
|
||||
printf ' claude-mode %-26s %s (default: %s)\n' "$id [preset]" \
|
||||
"$(prov_field "$id" 4 | sed 's/^[^-]*- //')" "$(default_preset_for "$id")"
|
||||
done
|
||||
cat <<'EOF'
|
||||
|
||||
claude-mode presets list presets
|
||||
claude-mode preset show <name>
|
||||
@@ -350,6 +356,66 @@ tsv_find() {
|
||||
|
||||
tsv_field() { printf '%s' "$1" | cut -f"$2"; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Providers
|
||||
#
|
||||
# Every gateway provider is an entry in providers.json; this script only knows
|
||||
# the *kinds* of behaviour (how a catalogue is fetched, how a server is probed)
|
||||
# and picks one by name from the entry. The table is read once, in the main
|
||||
# shell before dispatch - read inside a $( ) it would be re-read on every
|
||||
# lookup. Columns are cm-json.py's PROVIDER_TSV:
|
||||
#
|
||||
# 1 id 2 aliases 3 title 4 label 5 color 6 defaultPreset
|
||||
# 7 serverEditable 8 probe 9 probePaths 10 catalogueKind 11 perServer
|
||||
# 12 setupKey 13 setupModels 14 keyUrl 15 guardrail 16 doctor
|
||||
# 17 defaultKeyRef 18 literalToken 19 defaultBaseUrl 20 serverHint
|
||||
# 21 serverStart
|
||||
#
|
||||
# Always read with cut, never `IFS=$'\t' read`: tab counts as whitespace to
|
||||
# read, so an empty column (most providers have no aliases) would vanish and
|
||||
# shift every column after it.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CM_PROVIDERS_TSV=''
|
||||
|
||||
cm_providers_load() {
|
||||
CM_PROVIDERS_TSV="$("$PY" "$JSON" provider-tsv)" || {
|
||||
echo "claude-mode: could not read providers.json (next to $CM_BIN)" >&2; exit 1; }
|
||||
local id
|
||||
for id in $(provider_ids); do MODES+=("$id"); done
|
||||
}
|
||||
|
||||
provider_ids() { printf '%s\n' "$CM_PROVIDERS_TSV" | cut -f1; }
|
||||
|
||||
prov_field() {
|
||||
local row
|
||||
row="$(printf '%s\n' "$CM_PROVIDERS_TSV" | tsv_find "$1")" || return 1
|
||||
tsv_field "$row" "$2"
|
||||
}
|
||||
|
||||
# An id or an alias (z.ai, z-ai) to the provider id; non-zero if neither.
|
||||
provider_resolve() {
|
||||
local w line
|
||||
w="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
|
||||
[ -n "$w" ] || return 1
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
if [ "${line%%$'\t'*}" = "$w" ]; then printf '%s\n' "$w"; return 0; fi
|
||||
case ",$(tsv_field "$line" 2)," in
|
||||
*",$w,"*) printf '%s\n' "${line%%$'\t'*}"; return 0 ;;
|
||||
esac
|
||||
done <<EOF_PROVIDERS
|
||||
$CM_PROVIDERS_TSV
|
||||
EOF_PROVIDERS
|
||||
return 1
|
||||
}
|
||||
|
||||
# Whether a provider's entry lists a named doctor check.
|
||||
doctor_has() {
|
||||
case ",$(prov_field "$1" 16)," in *",$2,"*) return 0 ;; esac
|
||||
return 1
|
||||
}
|
||||
|
||||
term_cols() {
|
||||
local c=''
|
||||
if command -v tput >/dev/null 2>&1; then c="$(tput cols 2>/dev/null)"; fi
|
||||
@@ -367,13 +433,7 @@ term_cols() {
|
||||
# The existence test is what lets a stale choice degrade instead of break.
|
||||
# cm-json.py's BUILTIN_DEFAULT_PRESET and the widget's Modes.DEFAULT_PRESET
|
||||
# mirror the built-in names.
|
||||
builtin_default_preset() {
|
||||
case "$1" in
|
||||
openrouter) printf 'default' ;;
|
||||
zai) printf 'zai' ;;
|
||||
lmstudio) printf 'lmstudio' ;;
|
||||
esac
|
||||
}
|
||||
builtin_default_preset() { prov_field "$1" 6; }
|
||||
|
||||
default_preset_for() {
|
||||
local chosen
|
||||
@@ -452,11 +512,11 @@ cm_probe_timeout() { cm_url_is_local "$1" && printf '4' || printf '10'; }
|
||||
# The distinction matters because the remedies are opposites: `refused` means go
|
||||
# and start the server, `auth` means the server is fine and the key is not.
|
||||
cm_probe_server() {
|
||||
local base="${1%/}" token="${2:-}" t code ep
|
||||
local base="${1%/}" token="${2:-}" paths="${3:-/api/v0/models,/v1/models}" t code ep
|
||||
command -v curl >/dev/null 2>&1 || { printf 'skip'; return 0; }
|
||||
t="$(cm_probe_timeout "$base")"
|
||||
|
||||
for ep in /api/v0/models /v1/models; do
|
||||
for ep in $(printf '%s' "$paths" | tr ',' ' '); do
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time "$t" \
|
||||
${token:+-H "Authorization: Bearer $token"} \
|
||||
"$base$ep" 2>/dev/null)"
|
||||
@@ -521,6 +581,15 @@ cm_preflight() {
|
||||
base="$(jget "$pf" baseUrl)"; CM_PF_BASEURL="$base"
|
||||
auth_mode="$(jget "$pf" auth.mode)"; [ -z "$auth_mode" ] && auth_mode=vault
|
||||
|
||||
# A custom endpoint ships with no address, since there is no sensible one
|
||||
# to guess; switching to it would point every session at nothing.
|
||||
if [ -z "$base" ]; then
|
||||
cm_pf_set 'no-url' "Preset '$preset' has no server address" \
|
||||
"$(prov_field "$mode" 3) needs to know where the server is before anything can be sent to it." \
|
||||
"claude-mode preset url $preset <base-url>" 'set-url'
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$auth_mode" = "vault" ]; then
|
||||
key_ref="$(jget "$pf" auth.keyRef)"; [ -z "$key_ref" ] && key_ref=openrouter
|
||||
CM_PF_KEYREF="$key_ref"
|
||||
@@ -538,17 +607,21 @@ cm_preflight() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# LM Studio is checked wherever it is; other providers only when they are
|
||||
# pointed at this machine. A public gateway that is briefly unreachable is
|
||||
# the network's problem and not worth blocking a config change over.
|
||||
if [ -n "$base" ] && { [ "$mode" = "lmstudio" ] || cm_url_is_local "$base"; }; then
|
||||
# A server provider is checked wherever it is (probe "always", or
|
||||
# "lenient" for a proxy that may list no models); a public gateway only
|
||||
# when it has been pointed at this machine (probe "local"). A public
|
||||
# gateway that is briefly unreachable is the network's problem and not
|
||||
# worth blocking a config change over.
|
||||
local probe_rule title
|
||||
probe_rule="$(prov_field "$mode" 8)"; title="$(prov_field "$mode" 3)"
|
||||
if [ "$probe_rule" = always ] || [ "$probe_rule" = lenient ] || cm_url_is_local "$base"; then
|
||||
local token='' probe where
|
||||
if [ "$auth_mode" = "vault" ]; then
|
||||
token="$(cm_vault_get "$key_ref" 2>/dev/null || true)"
|
||||
else
|
||||
token="$(jget "$pf" auth.token)"
|
||||
fi
|
||||
probe="$(cm_probe_server "$base" "$token")"
|
||||
probe="$(cm_probe_server "$base" "$token" "$(prov_field "$mode" 9)")"
|
||||
cm_url_is_local "$base" && where='on this machine' || where='at that address'
|
||||
|
||||
case "$probe" in
|
||||
@@ -560,13 +633,17 @@ cm_preflight() {
|
||||
"claude-mode set-key $key_ref" 'set-key'
|
||||
else
|
||||
cm_pf_set 'server-auth' 'The server wants an API key' \
|
||||
"$base is running but is refusing an unauthenticated request. This preset is set to send LM Studio's placeholder token, which only works on a server with authentication switched off." \
|
||||
"$base is running but is refusing an unauthenticated request. This preset is set to send $title's placeholder token, which only works on a server with authentication switched off." \
|
||||
"claude-mode preset auth $preset key" 'needs-key'
|
||||
fi
|
||||
return 1 ;;
|
||||
notfound)
|
||||
cm_pf_set 'server-wrong' 'That address answered, but not as LM Studio' \
|
||||
"Something is listening at $base, but neither /api/v0/models nor /v1/models is there. Check the port, or whether a proxy in front of it is rewriting the path." \
|
||||
# A proxy in front of a Messages API often serves no model
|
||||
# list at all; for one of those, something answering is
|
||||
# as much as can be checked.
|
||||
[ "$probe_rule" = lenient ] && return 0
|
||||
cm_pf_set 'server-wrong' "That address answered, but not as $title" \
|
||||
"Something is listening at $base, but $(prov_field "$mode" 9 | sed 's/,/ and /g') is not there. Check the port, or whether a proxy in front of it is rewriting the path." \
|
||||
"claude-mode preset url $preset <base-url>" 'set-url'
|
||||
return 1 ;;
|
||||
*)
|
||||
@@ -582,14 +659,11 @@ cm_preflight() {
|
||||
|
||||
cmd_preflight() {
|
||||
local mode="${1:-}" preset="${2:-}" p
|
||||
case "$mode" in
|
||||
anthropic) ;;
|
||||
openrouter|zai|lmstudio)
|
||||
p="$(resolve_preset "$mode" "$preset" 2>/dev/null)" || p="$preset"
|
||||
preset="$p" ;;
|
||||
z.ai|z-ai) mode=zai; p="$(resolve_preset zai "$preset" 2>/dev/null)" || p="$preset"; preset="$p" ;;
|
||||
*) err "unknown mode '$mode'"; return 1 ;;
|
||||
esac
|
||||
if [ "$mode" != anthropic ]; then
|
||||
mode="$(provider_resolve "$mode")" || { err "unknown mode '${1:-}'"; return 1; }
|
||||
p="$(resolve_preset "$mode" "$preset" 2>/dev/null)" || p="$preset"
|
||||
preset="$p"
|
||||
fi
|
||||
|
||||
if cm_preflight "$mode" "$preset"; then
|
||||
"$PY" "$JSON" preflight-json ok "$mode" "$preset" '' '' '' '' '' '' ''
|
||||
@@ -1046,7 +1120,8 @@ write_health() {
|
||||
# nothing actionable to print for them.
|
||||
show_guardrail_status() {
|
||||
local mode="$1" key="$2" code
|
||||
[ "$mode" = "openrouter" ] || return 0
|
||||
# An OpenRouter feature, flagged per provider rather than by name.
|
||||
[ "$(prov_field "$mode" 15)" = 1 ] || return 0
|
||||
[ -n "$key" ] || return 0
|
||||
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 25 \
|
||||
@@ -1187,26 +1262,41 @@ cm_cache_catalogue() {
|
||||
"$PY" "$JSON" cache-models "$CM_MODELS_CACHE" "$1" "$2" "${3:-}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
or_catalogue() {
|
||||
local out rc
|
||||
out="$(curl -fsS --max-time 30 https://openrouter.ai/api/v1/models 2>/dev/null | "$PY" "$JSON" or-models 2>/dev/null)"
|
||||
# One catalogue fetch, for any provider: fetched according to its
|
||||
# catalogue.kind, parsed to TSV, and left in the cache for the panel. Prints
|
||||
# the TSV; the status says whether the fetch itself worked. TSV columns by kind:
|
||||
#
|
||||
# openrouter id ctx $in $out lmstudio id state ctx
|
||||
# ollama id params quant family openai id
|
||||
# static id note (a list kept in providers.json)
|
||||
provider_catalogue() {
|
||||
local pid="$1" base="${2%/}" token="${3:-}" out rc
|
||||
case "$(prov_field "$pid" 10)" in
|
||||
openrouter)
|
||||
out="$(curl -fsS --max-time 30 https://openrouter.ai/api/v1/models 2>/dev/null \
|
||||
| "$PY" "$JSON" or-models 2>/dev/null)" ;;
|
||||
lmstudio)
|
||||
out="$(curl -fsS --max-time 10 ${token:+-H "Authorization: Bearer $token"} \
|
||||
"$base/api/v0/models" 2>/dev/null | "$PY" "$JSON" lms-models 2>/dev/null)" ;;
|
||||
ollama)
|
||||
out="$(curl -fsS --max-time 10 ${token:+-H "Authorization: Bearer $token"} \
|
||||
"$base/api/tags" 2>/dev/null | "$PY" "$JSON" ollama-models 2>/dev/null)" ;;
|
||||
openai)
|
||||
# Both header styles: a proxy in front of Anthropic wants
|
||||
# x-api-key, one in front of anything else wants Bearer.
|
||||
out="$(curl -fsS --max-time 15 ${token:+-H "Authorization: Bearer $token"} \
|
||||
${token:+-H "x-api-key: $token"} -H 'anthropic-version: 2023-06-01' \
|
||||
"$base/v1/models" 2>/dev/null | "$PY" "$JSON" openai-models 2>/dev/null)" ;;
|
||||
static)
|
||||
out="$("$PY" "$JSON" provider-static "$pid" 2>/dev/null)" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
rc=$?
|
||||
cm_cache_catalogue openrouter "$([ "$rc" -eq 0 ] && echo 1 || echo 0)" <<<"$out"
|
||||
cm_cache_catalogue "$pid" "$([ "$rc" -eq 0 ] && echo 1 || echo 0)" "$base" <<<"$out"
|
||||
[ -n "$out" ] && printf '%s\n' "$out"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# Z.AI publishes no catalogue endpoint, so this list comes from its docs - and
|
||||
# this is the one place it is kept.
|
||||
zai_catalogue() {
|
||||
local out
|
||||
out="$(printf '%s\t%s\n' \
|
||||
glm-5.3 'flagship coding model - opus/sonnet tier' \
|
||||
glm-4.7 'fast/cheap tier - haiku')"
|
||||
cm_cache_catalogue zai 1 <<<"$out"
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
# The credential a preset would send. Empty when there is nothing to send.
|
||||
cm_preset_token() {
|
||||
local pf="$1" am ref
|
||||
@@ -1219,19 +1309,11 @@ cm_preset_token() {
|
||||
fi
|
||||
}
|
||||
|
||||
# The token is not optional decoration. An LM Studio server with authentication
|
||||
# switched on answers /api/v0/models with 401 like anything else, so without it
|
||||
# the catalogue comes back empty and every caller silently believes the server
|
||||
# has no models installed - on exactly the setups that need the list most.
|
||||
lms_catalogue() {
|
||||
local base="${1%/}" token="${2:-}" out rc
|
||||
out="$(curl -fsS --max-time 10 ${token:+-H "Authorization: Bearer $token"} \
|
||||
"$base/api/v0/models" 2>/dev/null | "$PY" "$JSON" lms-models 2>/dev/null)"
|
||||
rc=$?
|
||||
cm_cache_catalogue lmstudio "$([ "$rc" -eq 0 ] && echo 1 || echo 0)" "$base" <<<"$out"
|
||||
[ -n "$out" ] && printf '%s\n' "$out"
|
||||
return "$rc"
|
||||
}
|
||||
# The token passed to provider_catalogue is not optional decoration. A local
|
||||
# server with authentication switched on answers its model list with 401 like
|
||||
# anything else, so without it the catalogue comes back empty and every caller
|
||||
# silently believes the server has no models - on exactly the setups that need
|
||||
# the list most.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Commands
|
||||
@@ -1325,18 +1407,16 @@ cmd_models() {
|
||||
provider=openrouter # on anthropic, the catalogue worth browsing
|
||||
fi
|
||||
|
||||
local quiet=$(( refresh || as_json ))
|
||||
case "$provider" in
|
||||
lmstudio)
|
||||
[ "$quiet" -eq 1 ] || head_ "models installed in LM Studio at $base"
|
||||
tsv="$(lms_catalogue "$base" "$(cm_preset_token "$pf")")"; rc=$? ;;
|
||||
zai)
|
||||
[ "$quiet" -eq 1 ] || head_ 'Z.AI GLM models (from Z.AI docs - no public catalogue endpoint)'
|
||||
tsv="$(zai_catalogue)"; rc=$? ;;
|
||||
*)
|
||||
[ "$quiet" -eq 1 ] || head_ 'fetching https://openrouter.ai/api/v1/models ...'
|
||||
tsv="$(or_catalogue)"; rc=$? ;;
|
||||
esac
|
||||
local quiet=$(( refresh || as_json )) kind title
|
||||
kind="$(prov_field "$provider" 10)"; title="$(prov_field "$provider" 3)"
|
||||
if [ "$quiet" -eq 0 ]; then
|
||||
case "$kind" in
|
||||
static) head_ "$title models (from its docs - no public catalogue endpoint)" ;;
|
||||
openrouter) head_ 'fetching https://openrouter.ai/api/v1/models ...' ;;
|
||||
*) head_ "models on the $title server at $base" ;;
|
||||
esac
|
||||
fi
|
||||
tsv="$(provider_catalogue "$provider" "$base" "$(cm_preset_token "$pf")")"; rc=$?
|
||||
|
||||
if [ "$as_json" -eq 1 ]; then
|
||||
jget "$CM_MODELS_CACHE" "providers.$provider"
|
||||
@@ -1346,6 +1426,10 @@ cmd_models() {
|
||||
if [ "$refresh" -eq 1 ]; then
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
err "could not fetch the $provider catalogue${base:+ from $base}; the cached list, if any, is kept"
|
||||
# Not necessarily a fault: plenty of proxies serve Messages and
|
||||
# nothing else.
|
||||
[ "$(prov_field "$provider" 8)" = lenient ] && \
|
||||
say 'some endpoints serve no model list at all - model ids can still be typed by hand'
|
||||
return 1
|
||||
fi
|
||||
ok "cached $(printf '%s\n' "$tsv" | grep -c .) $provider model(s)"
|
||||
@@ -1357,10 +1441,12 @@ cmd_models() {
|
||||
while IFS=$'\t' read -r id c2 c3 c4; do
|
||||
[ -n "$id" ] || continue
|
||||
[ -z "$filter" ] || case "$id" in *"$filter"*) ;; *) continue ;; esac
|
||||
case "$provider" in
|
||||
lmstudio) printf ' %-58s %-11s %s\n' "$id" "$c2" "$c3" ;;
|
||||
zai) say "$(printf '%-8s - %s' "$id" "$c2")" ;;
|
||||
*) printf ' %-52s %10s $%-8s $%s\n' "$id" "$c2" "$c3" "$c4" ;;
|
||||
case "$kind" in
|
||||
lmstudio) printf ' %-58s %-11s %s\n' "$id" "$c2" "$c3" ;;
|
||||
static) say "$(printf '%-8s - %s' "$id" "$c2")" ;;
|
||||
ollama) printf ' %-44s %-8s %s\n' "$id" "$c2" "$c3" ;;
|
||||
openrouter) printf ' %-52s %10s $%-8s $%s\n' "$id" "$c2" "$c3" "$c4" ;;
|
||||
*) printf ' %s\n' "$id" ;;
|
||||
esac
|
||||
done <<<"$tsv"
|
||||
}
|
||||
@@ -1399,6 +1485,106 @@ cmd_set_key() {
|
||||
printf '%s' "$secret" | cm_vault_set "$ref" && ok "stored key '$ref' via $(cm_vault_backend_label)"
|
||||
}
|
||||
|
||||
# The preset's model ids against what the provider actually offers. A server
|
||||
# provider that does not answer is a failure; a hosted catalogue that cannot be
|
||||
# fetched is only a warning, since the endpoint may be fine regardless.
|
||||
doctor_catalogue() {
|
||||
local mode="$1" pf="$2" base="$3" kind title cat t id row declared c2 c3
|
||||
kind="$(prov_field "$mode" 10)"; title="$(prov_field "$mode" 3)"
|
||||
declared="$(jget "$pf" contextTokens)"
|
||||
cat="$(provider_catalogue "$mode" "$base" "$(cm_preset_token "$pf")")"
|
||||
if [ -z "$cat" ]; then
|
||||
if [ "$(prov_field "$mode" 7)" = 1 ]; then
|
||||
local start; start="$(prov_field "$mode" 21)"
|
||||
if [ "$(prov_field "$mode" 8)" = lenient ]; then
|
||||
warn "$title at $base lists no models - fine for a proxy, but the ids below cannot be checked"
|
||||
else
|
||||
err "$title not reachable at $base${start:+ - $start}"
|
||||
fi
|
||||
else
|
||||
warn "could not fetch the $title model list"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
[ "$(prov_field "$mode" 7)" = 1 ] && ok "$title reachable at $base ($(printf '%s\n' "$cat" | grep -c .) models)"
|
||||
|
||||
for t in "${TIERS[@]}"; do
|
||||
id="$(jget "$pf" "models.$t")"; [ -n "$id" ] || continue
|
||||
row="$(printf '%s\n' "$cat" | tsv_find "$id")" || row=''
|
||||
# Ollama lists every model with its tag, and a bare name means
|
||||
# `:latest` - `qwen3-coder` is served as `qwen3-coder:latest`.
|
||||
if [ -z "$row" ] && [ "$kind" = ollama ]; then
|
||||
case "$id" in *:*) ;; *) row="$(printf '%s\n' "$cat" | tsv_find "$id:latest")" || row='' ;; esac
|
||||
fi
|
||||
if [ -z "$row" ]; then
|
||||
# A fixed list is documentation, not the provider's word: an id
|
||||
# missing from it may still be served.
|
||||
if [ "$kind" = static ]; then ok "$(printf '%-6s %s (not in the documented list)' "$t" "$id")"
|
||||
else err "$t model NOT available from $title: $id"; fi
|
||||
continue
|
||||
fi
|
||||
c2="$(tsv_field "$row" 2)"; c3="$(tsv_field "$row" 3)"
|
||||
case "$kind" in
|
||||
openrouter) ok "$(printf '%-6s %s [ctx %s]' "$t" "$id" "$c2")"; doctor_ctx_vs "$t" "$c2" "$declared" ;;
|
||||
lmstudio) ok "$(printf '%-6s %s [%s, ctx %s]' "$t" "$id" "$c2" "$c3")"; doctor_ctx_vs "$t" "$c3" "$declared" ;;
|
||||
ollama) ok "$(printf '%-6s %s [%s %s]' "$t" "$id" "$c2" "$c3")" ;;
|
||||
*) ok "$(printf '%-6s %s' "$t" "$id")" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$declared" ] && ok "declared context window: $declared tokens" \
|
||||
|| warn 'preset has no contextTokens - Claude Code will guess a small window and auto-compact early'
|
||||
}
|
||||
|
||||
doctor_ctx_vs() {
|
||||
local t="$1" ctx="$2" declared="$3"
|
||||
{ [ -n "$declared" ] && [ -n "$ctx" ] && [ "$ctx" -lt "$declared" ]; } 2>/dev/null || return 0
|
||||
if [ "$t" = haiku ]; then
|
||||
warn "$t model has $ctx ctx, below the declared $declared - harmless, haiku runs short background tasks"
|
||||
else
|
||||
warn "$t model has $ctx ctx, below the declared $declared - this tier can overflow"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ollama sets the context window on the server, not per request from Claude
|
||||
# Code: 4096 tokens unless `ollama serve` runs with OLLAMA_CONTEXT_LENGTH, and
|
||||
# anything past it is cut off without an error. contextTokens in a preset only
|
||||
# tells Claude Code what to expect - it cannot change what the server does - so
|
||||
# say what the server is actually running where that can be seen, and what to
|
||||
# set where it cannot.
|
||||
doctor_ollama_context() {
|
||||
local pf="$1" base="$2" declared ids id max live ps seen=0 short=0 t body
|
||||
declared="$(jget "$pf" contextTokens)"; [ -n "$declared" ] || return 0
|
||||
ids="$(for t in "${TIERS[@]}"; do jget "$pf" "models.$t"; done | sort -u | grep . || true)"
|
||||
[ -n "$ids" ] || return 0
|
||||
ps="$(curl -fsS --max-time 5 "$base/api/ps" 2>/dev/null)"
|
||||
while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
body="$("$PY" -c 'import json,sys; print(json.dumps({"model": sys.argv[1]}))' "$id")"
|
||||
max="$(curl -fsS --max-time 8 -X POST -H 'content-type: application/json' -d "$body" \
|
||||
"$base/api/show" 2>/dev/null | "$PY" "$JSON" ollama-ctx show 2>/dev/null)"
|
||||
if [ -n "$max" ] && [ "$max" -lt "$declared" ] 2>/dev/null; then
|
||||
warn "$id supports at most $max tokens, below the declared $declared - lower contextTokens"
|
||||
fi
|
||||
live="$(printf '%s' "$ps" | "$PY" "$JSON" ollama-ctx ps "$id" 2>/dev/null)"
|
||||
[ -n "$live" ] || continue
|
||||
seen=1
|
||||
if [ "$live" -lt "$declared" ] 2>/dev/null; then
|
||||
short=1
|
||||
warn "$id is loaded with a $live-token context, below the declared $declared - requests past it are cut off"
|
||||
else
|
||||
ok "$id is loaded with a $live-token context"
|
||||
fi
|
||||
done <<EOF_IDS
|
||||
$ids
|
||||
EOF_IDS
|
||||
if [ "$seen" -eq 0 ]; then
|
||||
warn "none of these models is loaded, so the server's context window cannot be checked"
|
||||
say "Ollama defaults to 4096 tokens; run it with OLLAMA_CONTEXT_LENGTH=$declared or requests past that are cut off silently"
|
||||
elif [ "$short" -eq 1 ]; then
|
||||
say "restart it with OLLAMA_CONTEXT_LENGTH=$declared (or lower contextTokens to match)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_doctor() {
|
||||
local mode preset pf
|
||||
mode="$(state_mode)"; preset="$(state_preset)"; pf="$(preset_path "$preset")"
|
||||
@@ -1456,7 +1642,7 @@ cmd_doctor() {
|
||||
else err 'apiKeyHelper produced no output'; fi
|
||||
fi
|
||||
|
||||
if [ -n "$key" ] && [ "$mode" = "openrouter" ]; then
|
||||
if [ -n "$key" ] && doctor_has "$mode" openrouter-key; then
|
||||
local kinfo
|
||||
kinfo="$(curl -fsS --max-time 20 -H "Authorization: Bearer $key" https://openrouter.ai/api/v1/key 2>/dev/null)"
|
||||
if [ -n "$kinfo" ]; then
|
||||
@@ -1474,60 +1660,24 @@ else: print(' ok spend %.2f of %.2f limit (%s), %.2f remaining' % (use, lim,
|
||||
|
||||
show_guardrail_status "$mode" "$key"
|
||||
fi
|
||||
if [ -n "$key" ] && [ "$mode" = "zai" ]; then
|
||||
# No key-info endpoint: the cheapest real check is a 1-token
|
||||
# message against the Anthropic-compatible surface itself.
|
||||
if [ -n "$key" ] && doctor_has "$mode" message-check; then
|
||||
if curl -fsS --max-time 45 -X POST "$base/v1/messages" \
|
||||
-H 'content-type: application/json' -H "x-api-key: $key" \
|
||||
-H "authorization: Bearer $key" -H 'anthropic-version: 2023-06-01' \
|
||||
-d "{\"model\":\"$(jget "$pf" models.haiku)\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" >/dev/null 2>&1; then
|
||||
ok "Z.AI endpoint accepted the key ($base/v1/messages)"
|
||||
ok "$(prov_field "$mode" 3) endpoint accepted the key ($base/v1/messages)"
|
||||
else
|
||||
err 'Z.AI request failed'
|
||||
err "$(prov_field "$mode" 3) request failed"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
ok "inline token '$(jget "$pf" auth.token)' (no secret in settings.json)"
|
||||
fi
|
||||
|
||||
if [ "$mode" = "lmstudio" ]; then
|
||||
local cat; cat="$(lms_catalogue "$base" "$(cm_preset_token "$pf")")"
|
||||
if [ -n "$cat" ]; then
|
||||
ok "LM Studio reachable at $base ($(printf '%s\n' "$cat" | wc -l | tr -d ' ') models installed)"
|
||||
local t id row st ctx
|
||||
for t in "${TIERS[@]}"; do
|
||||
id="$(jget "$pf" "models.$t")"; [ -n "$id" ] || continue
|
||||
row="$(printf '%s\n' "$cat" | tsv_find "$id")" || row=''
|
||||
if [ -z "$row" ]; then err "$t model NOT installed in LM Studio: $id"; continue; fi
|
||||
st="$(tsv_field "$row" 2)"; ctx="$(tsv_field "$row" 3)"
|
||||
ok "$t $id [$st, ctx $ctx]"
|
||||
done
|
||||
else
|
||||
err "LM Studio not reachable at $base - start the server (Developer > Start Server)"
|
||||
fi
|
||||
elif [ "$mode" = "openrouter" ]; then
|
||||
local cat; cat="$(or_catalogue)"
|
||||
if [ -n "$cat" ]; then
|
||||
local t id row ctx declared
|
||||
declared="$(jget "$pf" contextTokens)"
|
||||
for t in "${TIERS[@]}"; do
|
||||
id="$(jget "$pf" "models.$t")"; [ -n "$id" ] || continue
|
||||
row="$(printf '%s\n' "$cat" | tsv_find "$id")" || row=''
|
||||
if [ -z "$row" ]; then err "$t model NOT available from OpenRouter: $id"; continue; fi
|
||||
ctx="$(tsv_field "$row" 2)"
|
||||
ok "$(printf '%-6s %s [ctx %s]' "$t" "$id" "$ctx")"
|
||||
if [ -n "$declared" ] && [ -n "$ctx" ] && [ "$ctx" -lt "$declared" ] 2>/dev/null; then
|
||||
if [ "$t" = "haiku" ]; then
|
||||
warn "$t model has $ctx ctx, below the declared $declared - harmless, haiku runs short background tasks"
|
||||
else
|
||||
warn "$t model has $ctx ctx, below the declared $declared - this tier can overflow"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
[ -n "$declared" ] && ok "declared context window: $declared tokens" \
|
||||
|| warn 'preset has no contextTokens - Claude Code will guess a small window and auto-compact early'
|
||||
else
|
||||
warn 'could not fetch the OpenRouter catalogue'
|
||||
fi
|
||||
fi
|
||||
doctor_has "$mode" catalogue-models && doctor_catalogue "$mode" "$pf" "$base"
|
||||
doctor_has "$mode" ollama-context && doctor_ollama_context "$pf" "$base"
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
@@ -1569,17 +1719,18 @@ cmd_preset() {
|
||||
a="$1"; shift
|
||||
case "$a" in
|
||||
--provider)
|
||||
[ $# -gt 0 ] || { err '--provider needs openrouter, zai or lmstudio'; return 1; }
|
||||
[ $# -gt 0 ] || { err "--provider needs one of: $(provider_ids | tr '\n' ' ')"; return 1; }
|
||||
provider="$1"; shift ;;
|
||||
--blank) blank=1 ;;
|
||||
-*) err "unknown option '$a'"; return 1 ;;
|
||||
*) from="$a" ;;
|
||||
esac
|
||||
done
|
||||
case "$provider" in
|
||||
''|openrouter|zai|lmstudio) ;;
|
||||
*) err "unknown provider '$provider' - openrouter, zai or lmstudio"; return 1 ;;
|
||||
esac
|
||||
if [ -n "$provider" ]; then
|
||||
local want="$provider"
|
||||
provider="$(provider_resolve "$want")" || {
|
||||
err "unknown provider '$want' - one of: $(provider_ids | tr '\n' ' ')"; return 1; }
|
||||
fi
|
||||
|
||||
if [ "$blank" -eq 1 ]; then
|
||||
[ -n "$provider" ] || { err '--blank needs --provider to know which template'; return 1; }
|
||||
@@ -1652,7 +1803,7 @@ cmd_preset() {
|
||||
local prov="$name" target="${3:-}" p cur got
|
||||
if [ -z "$prov" ]; then
|
||||
head_ 'default preset per provider'
|
||||
for p in openrouter zai lmstudio; do
|
||||
for p in $(provider_ids); do
|
||||
cur="$(jget "$CM_DEFAULTS" "$p")"
|
||||
if [ -n "$cur" ] && [ -f "$(preset_path "$cur")" ]; then
|
||||
printf ' %-11s %s (chosen)\n' "$p" "$cur"
|
||||
@@ -1671,10 +1822,8 @@ cmd_preset() {
|
||||
printf '\n %sclaude-mode preset default <provider> <name> (or --clear)%s\n' "$C_DIM" "$C_RESET"
|
||||
return 0
|
||||
fi
|
||||
case "$prov" in
|
||||
openrouter|zai|lmstudio) ;;
|
||||
*) err "unknown provider '$prov' - openrouter, zai or lmstudio"; return 1 ;;
|
||||
esac
|
||||
prov="$(provider_resolve "$name")" || {
|
||||
err "unknown provider '$name' - one of: $(provider_ids | tr '\n' ' ')"; return 1; }
|
||||
if [ -z "$target" ]; then
|
||||
resolve_preset "$prov" && printf '\n'
|
||||
return
|
||||
@@ -1722,9 +1871,12 @@ cmd_preset() {
|
||||
reapply_if_active "$name"
|
||||
;;
|
||||
auth)
|
||||
local amode="${3:-}" ref="${4:-lmstudio}"
|
||||
local amode="${3:-}" ref="${4:-}"
|
||||
[ -n "$name" ] && [ -n "$amode" ] || { err 'usage: claude-mode preset auth <name> none|key [keyRef]'; return 1; }
|
||||
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
||||
# The provider's own slot name by default: lmstudio, ollama, custom.
|
||||
[ -n "$ref" ] || ref="$(prov_field "$(jget "$(preset_path "$name")" provider)" 17)"
|
||||
[ -n "$ref" ] || ref=lmstudio
|
||||
"$PY" "$JSON" set-auth "$(preset_path "$name")" "$amode" "$ref" >/dev/null || return 1
|
||||
if [ "$amode" = "key" ]; then
|
||||
ok "$name : auth -> vault key '$ref'"
|
||||
@@ -2012,27 +2164,18 @@ ui_pick_model() {
|
||||
ui_reset_items
|
||||
ui_add_item '<type an id manually>' 'enter any model id by hand'
|
||||
|
||||
local ids=() id ctx a b st
|
||||
case "$provider" in
|
||||
openrouter)
|
||||
while IFS=$'\t' read -r id ctx a b; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id"); ui_add_item "$id" "context $ctx \$$a in / \$$b out per 1M"
|
||||
done < <(or_catalogue)
|
||||
;;
|
||||
lmstudio)
|
||||
while IFS=$'\t' read -r id st ctx; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id"); ui_add_item "$id" "state: $st max context: $ctx"
|
||||
done < <(lms_catalogue "$base" "$(cm_preset_token "$pf")")
|
||||
;;
|
||||
zai)
|
||||
while IFS=$'\t' read -r id a; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id"); ui_add_item "$id" "$a"
|
||||
done < <(zai_catalogue)
|
||||
;;
|
||||
esac
|
||||
local ids=() id ctx a b st kind
|
||||
kind="$(prov_field "$provider" 10)"
|
||||
while IFS=$'\t' read -r id a b st; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id")
|
||||
case "$kind" in
|
||||
openrouter) ui_add_item "$id" "context $a \$$b in / \$$st out per 1M" ;;
|
||||
lmstudio) ui_add_item "$id" "state: $a max context: $b" ;;
|
||||
ollama) ui_add_item "$id" "$a $b" ;;
|
||||
*) ui_add_item "$id" "$a" ;;
|
||||
esac
|
||||
done < <(provider_catalogue "$provider" "$base" "$(cm_preset_token "$pf")")
|
||||
|
||||
if [ "${#ids[@]}" -gt 0 ]; then
|
||||
ui_filter_select "model for '$tier'" "current: $current" || return 1
|
||||
@@ -2083,7 +2226,7 @@ ui_edit_preset() {
|
||||
|
||||
ui_new_preset() {
|
||||
ui_reset_items
|
||||
local provs=(openrouter zai lmstudio) p
|
||||
local provs=($(provider_ids)) p
|
||||
for p in "${provs[@]}"; do ui_add_item "$p" "$(mode_label "$p")"; done
|
||||
ui_select 'new preset - which provider' '' || return
|
||||
local provider="${provs[$UI_SEL]}"
|
||||
@@ -2254,7 +2397,7 @@ setup_models() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$provider" = "lmstudio" ]; then
|
||||
if [ "$(prov_field "$provider" 13)" = one-for-all ]; then
|
||||
# One model for every tier is the normal shape for a local server: it
|
||||
# has one loaded at a time, and mapping tiers to different models just
|
||||
# means paying the load cost on every tier change.
|
||||
@@ -2263,7 +2406,7 @@ setup_models() {
|
||||
while IFS=$'\t' read -r id st ctx; do
|
||||
[ -n "$id" ] || continue
|
||||
ids+=("$id")
|
||||
done < <(lms_catalogue "$base" "$(cm_preset_token "$pf")")
|
||||
done < <(provider_catalogue "$provider" "$base" "$(cm_preset_token "$pf")")
|
||||
|
||||
if [ "${#ids[@]}" -eq 0 ]; then
|
||||
warn 'the server returned no models; type an id by hand instead'
|
||||
@@ -2292,40 +2435,48 @@ setup_models() {
|
||||
done
|
||||
}
|
||||
|
||||
setup_lmstudio_server() {
|
||||
local pf="$1" name="$2" url probe token
|
||||
url="$(jget "$pf" baseUrl)"; [ -n "$url" ] || url='http://127.0.0.1:1234'
|
||||
# Where a server provider (LM Studio, Ollama, a custom endpoint) lives, and
|
||||
# whether it wants a key. None of them has to be on this machine.
|
||||
setup_server() {
|
||||
local pf="$1" name="$2" mode="$3" url probe token title hint start
|
||||
title="$(prov_field "$mode" 3)"; hint="$(prov_field "$mode" 20)"; start="$(prov_field "$mode" 21)"
|
||||
url="$(jget "$pf" baseUrl)"; [ -n "$url" ] || url="$(prov_field "$mode" 19)"
|
||||
|
||||
printf '\n'
|
||||
say 'LM Studio does not have to be on this machine - a LAN address or'
|
||||
say 'anything reachable through a tunnel or proxy works just as well.'
|
||||
[ -n "$hint" ] && say "$hint"
|
||||
url="$(ask_value 'server base URL' "$url")"
|
||||
[ -n "$url" ] || { err 'a server address is needed'; return 1; }
|
||||
"$PY" "$JSON" set-url "$pf" "$url" >/dev/null || return 1
|
||||
ok "baseUrl -> $url"
|
||||
|
||||
printf '\n'
|
||||
if ask_yes 'does that server require an API key?'; then
|
||||
local ref; ref="$(ask_value 'key name to store it under' 'lmstudio')"
|
||||
local ref; ref="$(ask_value 'key name to store it under' "$(prov_field "$mode" 17)")"
|
||||
"$PY" "$JSON" set-auth "$pf" key "$ref" >/dev/null
|
||||
ok "auth -> vault key '$ref'"
|
||||
cm_vault_has "$ref" || cmd_set_key "$ref"
|
||||
token="$(cm_vault_get "$ref" 2>/dev/null || true)"
|
||||
else
|
||||
"$PY" "$JSON" set-auth "$pf" none >/dev/null
|
||||
ok 'auth -> none (inline placeholder token)'
|
||||
token='lmstudio'
|
||||
token="$(prov_field "$mode" 18)"
|
||||
ok "auth -> none (inline placeholder token '$token')"
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
say "checking $url ..."
|
||||
probe="$(cm_probe_server "$url" "$token")"
|
||||
probe="$(cm_probe_server "$url" "$token" "$(prov_field "$mode" 9)")"
|
||||
case "$probe" in
|
||||
ok) ok 'server answered' ;;
|
||||
auth) err 'the server refused that credential'; return 1 ;;
|
||||
notfound) err 'something answered there, but not an LM Studio API' ; return 1 ;;
|
||||
notfound)
|
||||
if [ "$(prov_field "$mode" 8)" = lenient ]; then
|
||||
warn "something answered, but it lists no models - fine for a proxy; model ids are typed by hand"
|
||||
else
|
||||
err "something answered there, but not a $title API"; return 1
|
||||
fi ;;
|
||||
skip) warn 'curl is missing, so the server was not checked' ;;
|
||||
*) err 'nothing answered at that address'
|
||||
say 'start the server and run: claude-mode setup lmstudio'
|
||||
say "${start:+$start, then }run: claude-mode setup $mode"
|
||||
return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
@@ -2339,10 +2490,8 @@ cmd_setup() {
|
||||
head_ 'setup: anthropic'
|
||||
ok 'nothing to configure - it uses your existing Claude login'
|
||||
return 0 ;;
|
||||
openrouter|zai|lmstudio) ;;
|
||||
z.ai|z-ai) mode=zai ;;
|
||||
'') err 'usage: claude-mode setup <mode>'; return 1 ;;
|
||||
*) err "unknown mode '$mode'"; return 1 ;;
|
||||
*) mode="$(provider_resolve "$mode")" || { err "unknown mode '${1:-}'"; return 1; } ;;
|
||||
esac
|
||||
|
||||
name="$(resolve_preset "$mode" "${2:-}")" || return 1
|
||||
@@ -2357,23 +2506,19 @@ cmd_setup() {
|
||||
head_ "setup: $mode / preset '$name'"
|
||||
say "$(mode_label "$mode")"
|
||||
|
||||
case "$mode" in
|
||||
openrouter)
|
||||
printf '\n'
|
||||
setup_key openrouter 'OpenRouter'
|
||||
setup_models "$pf" openrouter "$name"
|
||||
;;
|
||||
zai)
|
||||
printf '\n'
|
||||
say 'get a key from https://z.ai/manage-apikey/apikey-list'
|
||||
setup_key zai 'Z.AI'
|
||||
setup_models "$pf" zai "$name"
|
||||
;;
|
||||
lmstudio)
|
||||
setup_lmstudio_server "$pf" "$name" || return 1
|
||||
setup_models "$pf" lmstudio "$name"
|
||||
;;
|
||||
esac
|
||||
# A server provider is asked where it is and whether it wants a key; a
|
||||
# hosted one always wants a key, and the entry says where to get one.
|
||||
if [ "$(prov_field "$mode" 7)" = 1 ]; then
|
||||
setup_server "$pf" "$name" "$mode" || return 1
|
||||
else
|
||||
local ref url
|
||||
ref="$(jget "$pf" auth.keyRef)"; [ -n "$ref" ] || ref="$(prov_field "$mode" 17)"
|
||||
url="$(prov_field "$mode" 14)"
|
||||
printf '\n'
|
||||
[ -n "$url" ] && say "get a key from $url"
|
||||
setup_key "$ref" "$(prov_field "$mode" 3)"
|
||||
fi
|
||||
setup_models "$pf" "$mode" "$name"
|
||||
|
||||
mark_configured "$pf"
|
||||
printf '\n'
|
||||
@@ -2785,6 +2930,7 @@ else:
|
||||
|
||||
command -v "$PY" >/dev/null 2>&1 || { echo "claude-mode: $PY not found (set CLAUDE_MODE_PYTHON)" >&2; exit 1; }
|
||||
init_root
|
||||
cm_providers_load
|
||||
|
||||
# --force is global and may appear anywhere; strip it before the mode arguments
|
||||
# are positional-matched below.
|
||||
@@ -2803,10 +2949,6 @@ case "$cmd" in
|
||||
''|menu) ui_menu ;;
|
||||
status) cmd_status ;;
|
||||
anthropic) set_mode anthropic ;;
|
||||
openrouter|zai|lmstudio)
|
||||
p="$(resolve_preset "$cmd" "${1:-}")" || exit 1
|
||||
set_mode "$cmd" "$p" ;;
|
||||
z.ai|z-ai) p="$(resolve_preset zai "${1:-}")" || exit 1; set_mode zai "$p" ;;
|
||||
presets) cmd_presets ;;
|
||||
preset) cmd_preset "$@" ;;
|
||||
set-key)
|
||||
@@ -2878,5 +3020,13 @@ case "$cmd" in
|
||||
for a in "$@"; do [ "$a" = "--all" ] && scope=all; done
|
||||
cmd_repair "$scope" ;;
|
||||
help|--help|-h) usage ;;
|
||||
*) err "unknown command '$cmd'"; usage; exit 1 ;;
|
||||
*)
|
||||
# Any provider in providers.json, by id or alias, is a mode.
|
||||
# Last, so a provider can never shadow a real command.
|
||||
if _prov="$(provider_resolve "$cmd")"; then
|
||||
p="$(resolve_preset "$_prov" "${1:-}")" || exit 1
|
||||
set_mode "$_prov" "$p"
|
||||
else
|
||||
err "unknown command '$cmd'"; usage; exit 1
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
+252
-37
@@ -20,6 +20,10 @@ Subcommands:
|
||||
preset-rename <dir> <old> <new> <state> [defaults] rename, repointing state + default
|
||||
auth-of <preset> "mode<TAB>keyRef"; fails if missing
|
||||
set-default <defaults> <provider> [name] choose (or clear) a provider default
|
||||
provider-tsv | provider-resolve <word> | provider-get <id> <path> | provider-static <id>
|
||||
read providers.json
|
||||
ollama-models | openai-models catalogue parsers (JSON on stdin)
|
||||
ollama-ctx show | ps <model> context lengths from Ollama (stdin)
|
||||
"""
|
||||
|
||||
import glob
|
||||
@@ -63,9 +67,162 @@ BASE_MANAGED = [
|
||||
|
||||
TIERS = ["opus", "sonnet", "haiku", "fable"]
|
||||
|
||||
# Mirrors builtin_default_preset() in the CLI: what `claude-mode <provider>`
|
||||
# picks when no preset is named and none has been chosen with `preset default`.
|
||||
BUILTIN_DEFAULT_PRESET = {"openrouter": "default", "zai": "zai", "lmstudio": "lmstudio"}
|
||||
# ---------------------------------------------------------------------------
|
||||
# Providers
|
||||
#
|
||||
# Every gateway provider is described in providers.json - endpoint, auth, how
|
||||
# its model list is fetched, how setup runs, which doctor checks apply, how it
|
||||
# is drawn - so adding one that reuses an existing behaviour is an entry there
|
||||
# rather than code in four places. What differs in *kind* (parsing a catalogue
|
||||
# format, probing a server) stays in code, picked by name from the entry.
|
||||
#
|
||||
# anthropic is not in it: it is the native login, not a gateway.
|
||||
#
|
||||
# The file sits one level above this script in both layouts - the repository
|
||||
# (linux/cm-json.py) and an install (~/.claude-mode/bin/cm-json.py) - so no
|
||||
# path has to be passed around. CM_PROVIDERS overrides it, for tests.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PROVIDERS_PATH = os.environ.get("CM_PROVIDERS") or os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "providers.json")
|
||||
_PROVIDERS = None
|
||||
|
||||
|
||||
def providers():
|
||||
global _PROVIDERS
|
||||
if _PROVIDERS is None:
|
||||
if not os.path.exists(PROVIDERS_PATH):
|
||||
raise SystemExit("providers.json not found at %s - re-run the installer" % PROVIDERS_PATH)
|
||||
data = load(PROVIDERS_PATH, {})
|
||||
items = data.get("providers") if isinstance(data, dict) else None
|
||||
_PROVIDERS = [p for p in (items or []) if isinstance(p, dict) and p.get("id")]
|
||||
return _PROVIDERS
|
||||
|
||||
|
||||
def provider(pid):
|
||||
for p in providers():
|
||||
if p["id"] == pid:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _dig(node, dotted, default=None):
|
||||
for part in dotted.split("."):
|
||||
if isinstance(node, dict) and part in node:
|
||||
node = node[part]
|
||||
else:
|
||||
return default
|
||||
return node
|
||||
|
||||
|
||||
def catalogue_kind(pid):
|
||||
return _dig(provider(pid) or {}, "catalogue.kind", "")
|
||||
|
||||
|
||||
def per_server_catalogue(pid):
|
||||
"""A catalogue that belongs to one server rather than to the provider -
|
||||
two LM Studio presets can point at two machines with different models."""
|
||||
return bool(_dig(provider(pid) or {}, "catalogue.perServer", False))
|
||||
|
||||
|
||||
def builtin_default_preset():
|
||||
"""What `claude-mode <provider>` picks when no preset is named and none has
|
||||
been chosen. Mirrored by builtin_default_preset() in the CLI."""
|
||||
return {p["id"]: p.get("defaultPreset") or p["id"] for p in providers()}
|
||||
|
||||
|
||||
# Column order of `provider-tsv`, which the shell reads once per run and looks
|
||||
# rows up in (bash 3.2 on macOS has no associative arrays). Append only: the
|
||||
# shell addresses these by number.
|
||||
PROVIDER_TSV = [
|
||||
("id", lambda p: p["id"]),
|
||||
("aliases", lambda p: ",".join(p.get("aliases") or [])),
|
||||
("title", lambda p: p.get("title") or p["id"]),
|
||||
("label", lambda p: p.get("label") or p.get("title") or p["id"]),
|
||||
("color", lambda p: p.get("color") or "gray"),
|
||||
("defaultPreset", lambda p: p.get("defaultPreset") or p["id"]),
|
||||
("serverEditable", lambda p: "1" if _dig(p, "server.editable") else "0"),
|
||||
("probe", lambda p: _dig(p, "server.probe", "none")),
|
||||
("probePaths", lambda p: ",".join(_dig(p, "server.paths", []) or [])),
|
||||
("catalogueKind", lambda p: _dig(p, "catalogue.kind", "")),
|
||||
("perServer", lambda p: "1" if _dig(p, "catalogue.perServer") else "0"),
|
||||
("setupKey", lambda p: _dig(p, "setup.key", "required")),
|
||||
("setupModels", lambda p: _dig(p, "setup.models", "per-tier")),
|
||||
("keyUrl", lambda p: _dig(p, "setup.keyUrl", "")),
|
||||
("guardrail", lambda p: "1" if p.get("guardrail") else "0"),
|
||||
("doctor", lambda p: ",".join(p.get("doctor") or [])),
|
||||
("defaultKeyRef", lambda p: _dig(p, "preset.auth.keyRef") or p["id"]),
|
||||
("literalToken", lambda p: _dig(p, "preset.auth.token") or p["id"]),
|
||||
("defaultBaseUrl", lambda p: _dig(p, "preset.baseUrl", "")),
|
||||
("serverHint", lambda p: _dig(p, "server.hint", "")),
|
||||
("serverStart", lambda p: _dig(p, "server.start", "")),
|
||||
]
|
||||
|
||||
|
||||
def cmd_provider_static(argv):
|
||||
"""provider-static <id> - a fixed catalogue from providers.json as id<TAB>note."""
|
||||
for m in _dig(provider(argv[0]) or {}, "catalogue.static", []) or []:
|
||||
if isinstance(m, dict) and m.get("id"):
|
||||
print("%s\t%s" % (m["id"], m.get("note", "")))
|
||||
|
||||
|
||||
def cmd_ollama_ctx(argv):
|
||||
"""ollama-ctx show | ps <model> (the matching Ollama response on stdin)
|
||||
|
||||
show: the model's own maximum, from /api/show's model_info
|
||||
"<architecture>.context_length".
|
||||
ps: the context a loaded model is actually running with, from /api/ps -
|
||||
which is the number that matters, because Ollama sets it on the
|
||||
server and cuts anything longer off without saying so.
|
||||
Prints nothing when the answer is not there to read.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(sys.stdin.read() or "{}")
|
||||
except ValueError:
|
||||
return
|
||||
if argv[0] == "show":
|
||||
info = data.get("model_info") or {}
|
||||
for k, v in info.items():
|
||||
if k.endswith(".context_length") and isinstance(v, int):
|
||||
print(v)
|
||||
return
|
||||
elif argv[0] == "ps" and len(argv) > 1:
|
||||
# A bare name is `:latest` to Ollama, and /api/ps reports the tag.
|
||||
want = {argv[1]} | ({argv[1] + ":latest"} if ":" not in argv[1] else set())
|
||||
for m in data.get("models") or []:
|
||||
if want & {m.get("name"), m.get("model")} and isinstance(m.get("context_length"), int):
|
||||
print(m["context_length"])
|
||||
return
|
||||
|
||||
|
||||
def cmd_provider_tsv(argv):
|
||||
"""provider-tsv - one tab-separated row per provider, columns as PROVIDER_TSV."""
|
||||
for p in providers():
|
||||
print("\t".join(str(fn(p)).replace("\t", " ") for _, fn in PROVIDER_TSV))
|
||||
|
||||
|
||||
def cmd_provider_resolve(argv):
|
||||
"""provider-resolve <word> - the provider id for an id or alias, else exit 1."""
|
||||
word = (argv[0] if argv else "").strip().lower()
|
||||
for p in providers():
|
||||
if word == p["id"] or word in [a.lower() for a in (p.get("aliases") or [])]:
|
||||
print(p["id"])
|
||||
return
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_provider_get(argv):
|
||||
"""provider-get <id> <dotted.path> - one value from a provider entry."""
|
||||
p = provider(argv[0])
|
||||
if p is None:
|
||||
sys.exit(1)
|
||||
val = _dig(p, argv[1])
|
||||
if isinstance(val, (dict, list)):
|
||||
print(json.dumps(val))
|
||||
elif isinstance(val, bool):
|
||||
print("true" if val else "false")
|
||||
elif val is not None:
|
||||
print(val)
|
||||
|
||||
|
||||
def load(path, default=None):
|
||||
@@ -229,31 +386,20 @@ def cmd_set_tier(argv):
|
||||
|
||||
|
||||
def cmd_scaffold(argv):
|
||||
provider = argv[0]
|
||||
base = {"provider": provider, "description": "new preset"}
|
||||
if provider == "openrouter":
|
||||
base["baseUrl"] = "https://openrouter.ai/api"
|
||||
base["auth"] = {"mode": "vault", "keyRef": "openrouter"}
|
||||
elif provider == "zai":
|
||||
base["baseUrl"] = "https://api.z.ai/api/anthropic"
|
||||
base["auth"] = {"mode": "vault", "keyRef": "zai"}
|
||||
elif provider == "lmstudio":
|
||||
base["baseUrl"] = "http://127.0.0.1:1234"
|
||||
base["auth"] = {"mode": "literal", "token": "lmstudio"}
|
||||
else:
|
||||
raise SystemExit("unknown provider '%s'" % provider)
|
||||
|
||||
"""scaffold <provider> - a blank preset built from the provider's template."""
|
||||
p = provider(argv[0])
|
||||
if p is None:
|
||||
raise SystemExit("unknown provider '%s'" % argv[0])
|
||||
tpl = p.get("preset") or {}
|
||||
base = {"provider": p["id"], "description": "new preset"}
|
||||
base["baseUrl"] = tpl.get("baseUrl", "")
|
||||
base["auth"] = dict(tpl.get("auth") or {"mode": "vault", "keyRef": p["id"]})
|
||||
base["models"] = {t: "" for t in TIERS}
|
||||
base["subagentModel"] = "inherit"
|
||||
base["gatewayModelDiscovery"] = provider == "openrouter"
|
||||
base["contextTokens"] = 262144 if provider == "lmstudio" else 1000000
|
||||
if provider == "lmstudio":
|
||||
base["extraEnv"] = {"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"}
|
||||
if provider == "zai":
|
||||
base["extraEnv"] = {
|
||||
"API_TIMEOUT_MS": "3000000",
|
||||
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
|
||||
}
|
||||
base["gatewayModelDiscovery"] = bool(tpl.get("gatewayModelDiscovery"))
|
||||
base["contextTokens"] = int(tpl.get("contextTokens") or 200000)
|
||||
if tpl.get("extraEnv"):
|
||||
base["extraEnv"] = dict(tpl["extraEnv"])
|
||||
print(json.dumps(base, indent=2))
|
||||
|
||||
|
||||
@@ -416,6 +562,8 @@ def cmd_cache_models(argv):
|
||||
"""
|
||||
path, provider, ok = argv[0], argv[1], argv[2] == "1"
|
||||
base = argv[3].rstrip("/") if len(argv) > 3 else ""
|
||||
kind = catalogue_kind(provider)
|
||||
per_server = per_server_catalogue(provider)
|
||||
|
||||
def num(s, cast):
|
||||
try:
|
||||
@@ -423,6 +571,8 @@ def cmd_cache_models(argv):
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
# The TSV columns are whatever that kind's parser prints (or-models,
|
||||
# lms-models, ollama-models, openai-models, or a static list's id/note).
|
||||
rows = []
|
||||
for line in sys.stdin.read().splitlines():
|
||||
parts = line.split("\t")
|
||||
@@ -430,17 +580,20 @@ def cmd_cache_models(argv):
|
||||
if not mid:
|
||||
continue
|
||||
m = {"id": mid}
|
||||
if provider == "openrouter":
|
||||
if kind == "openrouter":
|
||||
ctx, pin, pout = (parts[1:] + ["", "", ""])[:3]
|
||||
m["contextTokens"] = num(ctx, int)
|
||||
m["priceIn"] = num(pin, float)
|
||||
m["priceOut"] = num(pout, float)
|
||||
elif provider == "lmstudio":
|
||||
elif kind == "lmstudio":
|
||||
state, ctx = (parts[1:] + ["", ""])[:2]
|
||||
m["state"] = state
|
||||
m["contextTokens"] = num(ctx, int)
|
||||
else:
|
||||
m["note"] = parts[1] if len(parts) > 1 else ""
|
||||
elif kind == "ollama":
|
||||
params, quant = (parts[1:] + ["", ""])[:2]
|
||||
m["note"] = " ".join(x for x in (params, quant) if x and x != "-")
|
||||
elif len(parts) > 1 and parts[1]:
|
||||
m["note"] = parts[1]
|
||||
rows.append(m)
|
||||
|
||||
try:
|
||||
@@ -458,11 +611,11 @@ def cmd_cache_models(argv):
|
||||
__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
if ok:
|
||||
node = {"fetchedAt": now, "ok": True, "models": rows}
|
||||
if provider == "lmstudio":
|
||||
if per_server:
|
||||
node["baseUrl"] = base
|
||||
else:
|
||||
# A list from a different LM Studio server is not stale, it is wrong.
|
||||
if provider == "lmstudio" and node.get("baseUrl", "") != base:
|
||||
# A list from a different server is not stale, it is wrong.
|
||||
if per_server and node.get("baseUrl", "") != base:
|
||||
node = {"baseUrl": base, "models": []}
|
||||
node["ok"] = False
|
||||
node["failedAt"] = now
|
||||
@@ -479,6 +632,35 @@ def cmd_lms_models(argv):
|
||||
print("%s\t%s\t%s" % (m.get("id", ""), m.get("state", "unknown"), m.get("max_context_length", "")))
|
||||
|
||||
|
||||
def cmd_ollama_models(argv):
|
||||
"""Ollama /api/tags on stdin -> 'id<TAB>parameters<TAB>quantization<TAB>family'.
|
||||
|
||||
The id is `name` (e.g. qwen3-coder:30b), which is what the Anthropic
|
||||
endpoint accepts as a model. /api/tags carries no context length - that is
|
||||
a server setting (OLLAMA_CONTEXT_LENGTH), not a property of the model file.
|
||||
"""
|
||||
data = json.loads(sys.stdin.read())
|
||||
for m in sorted(data.get("models") or [], key=lambda x: x.get("name", "")):
|
||||
d = m.get("details") or {}
|
||||
# "-" rather than empty: bash's `read` with a tab IFS merges empty
|
||||
# fields, which would slide the later columns left.
|
||||
print("%s\t%s\t%s\t%s" % (m.get("name") or m.get("model", ""), d.get("parameter_size") or "-",
|
||||
d.get("quantization_level") or "-", d.get("family") or "-"))
|
||||
|
||||
|
||||
def cmd_openai_models(argv):
|
||||
"""A `GET /v1/models` list on stdin -> one id per line.
|
||||
|
||||
Covers both the OpenAI shape and Anthropic's own ({"data": [{"id": ...}]}),
|
||||
which is what a proxy in front of either tends to serve.
|
||||
"""
|
||||
data = json.loads(sys.stdin.read())
|
||||
items = data.get("data") if isinstance(data, dict) else data
|
||||
for m in sorted((items or []), key=lambda x: (x or {}).get("id", "")):
|
||||
if isinstance(m, dict) and m.get("id"):
|
||||
print(m["id"])
|
||||
|
||||
|
||||
# A model id carrying a bracket suffix - e.g. `claude-fable-5[1m]` - is Claude
|
||||
# Code's extended-context marker. It belongs to Anthropic's 1M models and no
|
||||
# gateway recognises it. A session that had it can carry the tag onto a new id
|
||||
@@ -586,7 +768,8 @@ def cmd_health(argv):
|
||||
"schema": 1,
|
||||
"tool": "claude-mode",
|
||||
"version": version or "0.0.0",
|
||||
"updatedAt": __import__("datetime").datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"updatedAt": __import__("datetime").datetime.now(
|
||||
__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"os": "posix",
|
||||
"mode": mode,
|
||||
"preset": "" if mode == "anthropic" else preset,
|
||||
@@ -662,7 +845,7 @@ def cmd_health(argv):
|
||||
if not isinstance(chosen_raw, dict):
|
||||
chosen_raw = {}
|
||||
effective, chosen = {}, {}
|
||||
for prov, builtin in BUILTIN_DEFAULT_PRESET.items():
|
||||
for prov, builtin in builtin_default_preset().items():
|
||||
pick = chosen_raw.get(prov)
|
||||
if pick and pick in names:
|
||||
effective[prov] = chosen[prov] = pick
|
||||
@@ -674,6 +857,25 @@ def cmd_health(argv):
|
||||
h["defaultPresetFor"] = effective
|
||||
h["defaultPresetChosen"] = chosen
|
||||
|
||||
# The providers as the widget needs them, in menu order: how to draw each
|
||||
# one and which panel features apply. With this published, a provider
|
||||
# added to providers.json shows up in the bar without touching its QML -
|
||||
# or restarting the shell, which a change to Modes.js would need.
|
||||
h["providers"] = [{
|
||||
"id": p["id"],
|
||||
"title": p.get("title") or p["id"],
|
||||
"blurb": p.get("blurb", ""),
|
||||
"logo": p.get("logo", ""),
|
||||
"logoScale": float(p.get("logoScale") or 1.0),
|
||||
"glyph": p.get("glyph", ""),
|
||||
"serverEditable": bool(_dig(p, "server.editable")),
|
||||
"serverHint": _dig(p, "server.hint", ""),
|
||||
"defaultBaseUrl": _dig(p, "preset.baseUrl", ""),
|
||||
"perServerCatalogue": bool(_dig(p, "catalogue.perServer")),
|
||||
"defaultKeyRef": _dig(p, "preset.auth.keyRef") or p["id"],
|
||||
"keyOptional": _dig(p, "setup.key", "required") != "required",
|
||||
} for p in providers()]
|
||||
|
||||
save(os.path.join(root, "health.json"), h)
|
||||
|
||||
|
||||
@@ -760,10 +962,16 @@ def cmd_set_auth(argv):
|
||||
never sees it either way.
|
||||
"""
|
||||
path, mode = argv[0], argv[1]
|
||||
key_ref = argv[2] if len(argv) > 2 and argv[2] else "lmstudio"
|
||||
p = load(path)
|
||||
# The provider's own names, not LM Studio's: an Ollama preset switched to
|
||||
# "none" sends Ollama's placeholder, and one switched to "key" defaults to
|
||||
# a vault slot named after its provider.
|
||||
pid = p.get("provider", "")
|
||||
key_ref = argv[2] if len(argv) > 2 and argv[2] else (
|
||||
_dig(provider(pid) or {}, "preset.auth.keyRef") or pid or "lmstudio")
|
||||
if mode == "none":
|
||||
p["auth"] = {"mode": "literal", "token": "lmstudio"}
|
||||
token = _dig(provider(pid) or {}, "preset.auth.token") or pid or "lmstudio"
|
||||
p["auth"] = {"mode": "literal", "token": token}
|
||||
elif mode == "key":
|
||||
p["auth"] = {"mode": "vault", "keyRef": key_ref}
|
||||
else:
|
||||
@@ -1279,6 +1487,13 @@ COMMANDS = {
|
||||
"strip-tags": cmd_strip_tags,
|
||||
"or-models": cmd_or_models,
|
||||
"lms-models": cmd_lms_models,
|
||||
"ollama-models": cmd_ollama_models,
|
||||
"openai-models": cmd_openai_models,
|
||||
"provider-tsv": cmd_provider_tsv,
|
||||
"provider-resolve": cmd_provider_resolve,
|
||||
"provider-get": cmd_provider_get,
|
||||
"provider-static": cmd_provider_static,
|
||||
"ollama-ctx": cmd_ollama_ctx,
|
||||
"cache-models": cmd_cache_models,
|
||||
"apply": cmd_apply,
|
||||
"summary": cmd_summary,
|
||||
|
||||
@@ -82,6 +82,20 @@ if [ -f "$VERSION_SRC" ]; then
|
||||
green "version $(tr -d '[:space:]' < "$ROOT/VERSION")"
|
||||
fi
|
||||
|
||||
# --- providers (shared with the Windows build) -----------------------------
|
||||
# Always replaced, unlike presets: it is the tool's own table of what each
|
||||
# provider is, not something a user edits. cm-json.py looks for it one level
|
||||
# above bin/.
|
||||
PROVIDERS_SRC="$SRC/../providers.json"
|
||||
[ -f "$PROVIDERS_SRC" ] || PROVIDERS_SRC="$SRC/providers.json"
|
||||
if [ -f "$PROVIDERS_SRC" ]; then
|
||||
install -m 0644 "$PROVIDERS_SRC" "$ROOT/providers.json"
|
||||
green "providers: $("$PY" -c 'import json,sys; print(", ".join(p["id"] for p in json.load(open(sys.argv[1]))["providers"]))' "$ROOT/providers.json")"
|
||||
else
|
||||
fail 'providers.json missing from the payload - claude-mode cannot run without it'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- presets (shared with the Windows build) -------------------------------
|
||||
PRESET_SRC="$SRC/../presets"
|
||||
[ -d "$PRESET_SRC" ] || PRESET_SRC="$SRC/presets"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
.pragma library
|
||||
|
||||
// The four modes claude-mode understands, in the order the CLI menu lists
|
||||
// them. Kept here rather than in either QML file because the bar widget needs
|
||||
// the glyph and the panel needs the prose, and a second copy of this table is
|
||||
// exactly the sort of thing that drifts.
|
||||
// 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
|
||||
|
||||
@@ -72,18 +72,37 @@ Panel {
|
||||
function cli(args) { return [root.cmRoot + "/bin/claude-mode"].concat(args) }
|
||||
function parseJson(t) { try { return JSON.parse(String(t || "")) } catch (e) { return null } }
|
||||
|
||||
// ---- Server settings (LM Studio)
|
||||
// ---- Providers, as the widget publishes them
|
||||
//
|
||||
// LM Studio ships listening on loopback, but it does not have to be there: it
|
||||
// can be another machine on the LAN, or something reached through a tunnel or
|
||||
// a reverse proxy, and it can have authentication switched on. All three are
|
||||
// The widget owns the lookup, and the fallback to Modes.js for anthropic and
|
||||
// for an older health.json. This side only asks, so nothing here needs to
|
||||
// know which providers exist.
|
||||
readonly property var modeOrder: widget ? widget.modeOrder : Modes.ORDER
|
||||
function pInfo(m) { return widget ? widget.providerInfo(m) : null }
|
||||
function pTitle(m) { return widget ? widget.modeTitle(m) : "" }
|
||||
function pBlurb(m) { return widget ? widget.modeBlurb(m) : "" }
|
||||
function pLogo(m) { return widget ? widget.modeLogo(m) : "" }
|
||||
function pLogoScale(m) { return widget ? widget.modeLogoScale(m) : 1.0 }
|
||||
function pGlyph(m) { return widget ? widget.modeGlyph(m) : "" }
|
||||
// Before providers were published only LM Studio had either.
|
||||
function serverEditable(m) { var p = root.pInfo(m); return p ? p.serverEditable === true : m === "lmstudio" }
|
||||
function perServerCatalogue(m) { var p = root.pInfo(m); return p ? p.perServerCatalogue === true : m === "lmstudio" }
|
||||
|
||||
// ---- Server settings
|
||||
//
|
||||
// A server provider (LM Studio, Ollama, a custom endpoint) ships pointed at
|
||||
// its usual local address, but it does not have to be there: it can be
|
||||
// another machine on the LAN, or something reached through a tunnel or a
|
||||
// reverse proxy, and it can have authentication switched on. All of that is
|
||||
// just a baseUrl and an auth block in the preset, so this edits those two
|
||||
// rather than pretending the local default is the only shape.
|
||||
property string serverPreset: ""
|
||||
property string serverProvider: ""
|
||||
property string serverUrl: ""
|
||||
property bool serverNeedsKey: false
|
||||
property string serverKeyRef: "lmstudio"
|
||||
readonly property string serverLocalDefault: "http://127.0.0.1:1234"
|
||||
property string serverKeyRef: ""
|
||||
property string serverDefault: "" // the provider's usual address; "" for custom
|
||||
property string serverHint: ""
|
||||
|
||||
function presetEntry(name) {
|
||||
var all = (health && health.presets) ? health.presets : []
|
||||
@@ -99,9 +118,15 @@ Panel {
|
||||
var e = root.presetEntry(presetName)
|
||||
root.serverReturn = from || ""
|
||||
root.serverPreset = presetName
|
||||
root.serverUrl = e && e.baseUrl ? String(e.baseUrl) : root.serverLocalDefault
|
||||
root.serverProvider = e ? String(e.provider) : ""
|
||||
var p = root.pInfo(root.serverProvider)
|
||||
root.serverDefault = p ? String(p.defaultBaseUrl || "") : "http://127.0.0.1:1234"
|
||||
root.serverHint = p && p.serverHint ? String(p.serverHint)
|
||||
: "The server does not have to be on this machine. Point this at a LAN address, or anything reachable through a tunnel or proxy."
|
||||
root.serverUrl = e && e.baseUrl ? String(e.baseUrl) : root.serverDefault
|
||||
root.serverNeedsKey = !!(e && String(e.authMode) === "vault")
|
||||
root.serverKeyRef = e && e.keyRef ? String(e.keyRef) : "lmstudio"
|
||||
root.serverKeyRef = e && e.keyRef ? String(e.keyRef)
|
||||
: (p && p.defaultKeyRef ? String(p.defaultKeyRef) : (root.serverProvider || "lmstudio"))
|
||||
root.blocker = null
|
||||
root.stage = "server"
|
||||
}
|
||||
@@ -408,7 +433,7 @@ Panel {
|
||||
var all = widget && widget.modelsCache && widget.modelsCache.providers ? widget.modelsCache.providers : null
|
||||
var n = all && root.editProvider !== "" ? all[root.editProvider] : null
|
||||
if (!n) return null
|
||||
if (root.editProvider === "lmstudio") {
|
||||
if (root.perServerCatalogue(root.editProvider)) {
|
||||
var want = root.editEntry && root.editEntry.baseUrl ? String(root.editEntry.baseUrl).replace(/\/+$/, "") : ""
|
||||
if (String(n.baseUrl || "") !== want) return null
|
||||
}
|
||||
@@ -797,17 +822,17 @@ Panel {
|
||||
|
||||
BrandIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: Modes.logo(root.mode) !== ""
|
||||
pathData: Modes.logo(root.mode)
|
||||
opticalScale: Modes.logoScale(root.mode)
|
||||
visible: root.pLogo(root.mode) !== ""
|
||||
pathData: root.pLogo(root.mode)
|
||||
opticalScale: root.pLogoScale(root.mode)
|
||||
color: heroGlyph.tint
|
||||
iconSize: Style.font.display
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: Modes.logo(root.mode) === ""
|
||||
text: Modes.glyph(root.mode)
|
||||
visible: root.pLogo(root.mode) === ""
|
||||
text: root.pGlyph(root.mode)
|
||||
color: heroGlyph.tint
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.font.display
|
||||
@@ -822,7 +847,7 @@ Panel {
|
||||
Text {
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
text: Modes.title(root.mode)
|
||||
text: root.pTitle(root.mode)
|
||||
color: Color.popups.text
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(17)
|
||||
@@ -831,7 +856,7 @@ Panel {
|
||||
Text {
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
text: root.preset !== "" ? "preset · " + root.preset : Modes.blurb(root.mode)
|
||||
text: root.preset !== "" ? "preset · " + root.preset : root.pBlurb(root.mode)
|
||||
color: Color.muted
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(11)
|
||||
@@ -857,7 +882,7 @@ Panel {
|
||||
spacing: Style.space(1)
|
||||
|
||||
Repeater {
|
||||
model: root.stage === "list" ? Modes.ORDER : []
|
||||
model: root.stage === "list" ? root.modeOrder : []
|
||||
|
||||
Column {
|
||||
id: modeEntry
|
||||
@@ -890,8 +915,8 @@ Panel {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
pathData: Modes.logo(modeEntry.thisMode)
|
||||
opticalScale: Modes.logoScale(modeEntry.thisMode)
|
||||
pathData: root.pLogo(modeEntry.thisMode)
|
||||
opticalScale: root.pLogoScale(modeEntry.thisMode)
|
||||
color: modeEntry.isCurrent ? Color.accent : Color.popups.text
|
||||
iconSize: Style.space(16)
|
||||
}
|
||||
@@ -905,7 +930,7 @@ Panel {
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
text: Modes.title(modeEntry.thisMode)
|
||||
text: root.pTitle(modeEntry.thisMode)
|
||||
color: Color.popups.text
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.font.body
|
||||
@@ -914,7 +939,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: Modes.blurb(modeEntry.thisMode)
|
||||
text: root.pBlurb(modeEntry.thisMode)
|
||||
color: Color.muted
|
||||
elide: Text.ElideRight
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
@@ -1080,7 +1105,7 @@ Panel {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(30)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "New " + Modes.title(modeEntry.thisMode) + " preset…"
|
||||
text: "New " + root.pTitle(modeEntry.thisMode) + " preset…"
|
||||
color: newPresetArea.containsMouse ? Color.accent : Color.muted
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(11)
|
||||
@@ -1146,7 +1171,7 @@ Panel {
|
||||
spacing: Style.space(7)
|
||||
|
||||
PillButton {
|
||||
label: "Set up " + Modes.title(root.pendingMode) + "…"
|
||||
label: "Set up " + root.pTitle(root.pendingMode) + "…"
|
||||
primary: true
|
||||
visible: root.blocker && String(root.blocker.remedyKind) === "setup"
|
||||
onTriggered: root.runSetup()
|
||||
@@ -1172,7 +1197,7 @@ Panel {
|
||||
PillButton {
|
||||
label: "Server settings…"
|
||||
primary: root.blocker && ["start-server", "set-url", "needs-key"].indexOf(String(root.blocker.remedyKind)) >= 0
|
||||
visible: root.pendingMode === "lmstudio"
|
||||
visible: root.serverEditable(root.pendingMode)
|
||||
&& !(root.blocker && String(root.blocker.remedyKind) === "setup")
|
||||
onTriggered: root.openServerSettings(root.pendingPreset)
|
||||
}
|
||||
@@ -1187,7 +1212,7 @@ Panel {
|
||||
}
|
||||
|
||||
|
||||
// ---- Where the LM Studio server is, and whether it needs a key.
|
||||
// ---- Where a server provider is, and whether it needs a key.
|
||||
Column {
|
||||
width: parent.width
|
||||
visible: root.stage === "server"
|
||||
@@ -1205,8 +1230,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: "LM Studio does not have to be on this machine. Point this at a "
|
||||
+ "LAN address, or anything reachable through a tunnel or proxy."
|
||||
text: root.serverHint
|
||||
color: Color.muted
|
||||
wrapMode: Text.WordWrap
|
||||
lineHeight: 1.2
|
||||
@@ -1218,7 +1242,7 @@ Panel {
|
||||
id: urlField
|
||||
width: parent.width
|
||||
text: root.serverUrl
|
||||
placeholderText: root.serverLocalDefault
|
||||
placeholderText: root.serverDefault !== "" ? root.serverDefault : "https://…"
|
||||
foreground: Color.popups.text
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
font.pixelSize: Style.space(11)
|
||||
@@ -1232,9 +1256,11 @@ Panel {
|
||||
width: parent.width
|
||||
spacing: Style.space(7)
|
||||
|
||||
// A custom endpoint has no usual address to go back to.
|
||||
PillButton {
|
||||
label: "Use local default"
|
||||
onTriggered: { root.serverUrl = root.serverLocalDefault; urlField.text = root.serverLocalDefault }
|
||||
visible: root.serverDefault !== ""
|
||||
onTriggered: { root.serverUrl = root.serverDefault; urlField.text = root.serverDefault }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1271,7 +1297,7 @@ Panel {
|
||||
width: parent.width
|
||||
text: root.serverNeedsKey
|
||||
? "Kept in the vault as '" + root.serverKeyRef + "', never in settings.json."
|
||||
: "Sends LM Studio's placeholder token, which is not a secret."
|
||||
: "Sends " + root.pTitle(root.serverProvider) + "'s placeholder token, which is not a secret."
|
||||
color: Color.muted
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
@@ -1317,7 +1343,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: "'" + root.editPreset + "' · " + Modes.title(root.editProvider)
|
||||
text: "'" + root.editPreset + "' · " + root.pTitle(root.editProvider)
|
||||
color: Color.popups.text
|
||||
elide: Text.ElideRight
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
@@ -1434,7 +1460,7 @@ Panel {
|
||||
|
||||
PillButton {
|
||||
label: "Server settings…"
|
||||
visible: root.editProvider === "lmstudio"
|
||||
visible: root.serverEditable(root.editProvider)
|
||||
onTriggered: root.openServerSettings(root.editPreset, "preset")
|
||||
}
|
||||
PillButton { label: "Make default"; visible: !root.editIsDefault; onTriggered: root.setDefault(true) }
|
||||
@@ -1470,7 +1496,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: "New " + Modes.title(root.newProvider) + " preset"
|
||||
text: "New " + root.pTitle(root.newProvider) + " preset"
|
||||
color: Color.popups.text
|
||||
elide: Text.ElideRight
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
@@ -1649,8 +1675,8 @@ Panel {
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: parent.lastOne
|
||||
text: "It is the only " + Modes.title(root.editProvider) + " preset, so "
|
||||
+ Modes.title(root.editProvider) + " will have nothing to switch to until you create another."
|
||||
text: "It is the only " + root.pTitle(root.editProvider) + " preset, so "
|
||||
+ root.pTitle(root.editProvider) + " will have nothing to switch to until you create another."
|
||||
color: root.urgentColor
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
@@ -1826,7 +1852,7 @@ Panel {
|
||||
visible: root.modelOptions.length === 0
|
||||
text: root.catalogueNode && root.catalogueNode.ok === false
|
||||
? "The last fetch failed and no list is cached. Type an id, or try fetching again."
|
||||
: "No model list cached for " + Modes.title(root.editProvider) + " yet. Type an id, or fetch the list."
|
||||
: "No model list cached for " + root.pTitle(root.editProvider) + " yet. Type an id, or fetch the list."
|
||||
color: Color.muted
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: root.bar ? root.bar.fontFamily : Style.font.family
|
||||
@@ -1892,7 +1918,7 @@ Panel {
|
||||
// they fail rather than carry on.
|
||||
Text {
|
||||
width: parent.width
|
||||
text: "They keep pointing at " + Modes.title(root.mode) + ", but their key is "
|
||||
text: "They keep pointing at " + root.pTitle(root.mode) + ", but their key is "
|
||||
+ "re-fetched on a timer and will switch under them. Worse, if one takes a "
|
||||
+ "reply from the new provider first, that provider's message-id format "
|
||||
+ "goes into its transcript and Anthropic will refuse to resume it at all."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"id": "smoido.claude-mode",
|
||||
"name": "Claude Mode",
|
||||
"version": "1.11.0",
|
||||
"version": "1.12.0",
|
||||
"author": "smoido",
|
||||
"description": "Which provider Claude Code is pointed at, and a one-click switch between them",
|
||||
"kinds": ["bar-widget"],
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"provider": "custom",
|
||||
"description": "Any Anthropic-compatible endpoint. Setup asks where it is and whether it needs a key.",
|
||||
"baseUrl": "",
|
||||
"auth": {
|
||||
"mode": "vault",
|
||||
"keyRef": "custom"
|
||||
},
|
||||
"models": {
|
||||
"opus": "",
|
||||
"sonnet": "",
|
||||
"haiku": "",
|
||||
"fable": ""
|
||||
},
|
||||
"subagentModel": "inherit",
|
||||
"gatewayModelDiscovery": false,
|
||||
"contextTokens": 200000,
|
||||
"configured": false
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"provider": "ollama",
|
||||
"description": "Local Ollama server. Setup picks the model from whatever the server has pulled.",
|
||||
"baseUrl": "http://127.0.0.1:11434",
|
||||
"auth": {
|
||||
"mode": "literal",
|
||||
"token": "ollama"
|
||||
},
|
||||
"models": {
|
||||
"opus": "qwen3-coder",
|
||||
"sonnet": "qwen3-coder",
|
||||
"haiku": "qwen3-coder",
|
||||
"fable": "qwen3-coder"
|
||||
},
|
||||
"subagentModel": "inherit",
|
||||
"gatewayModelDiscovery": false,
|
||||
"extraEnv": {
|
||||
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"
|
||||
},
|
||||
"contextTokens": 65536,
|
||||
"configured": false
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"_comment": [
|
||||
"Every gateway provider claude-mode can switch to. anthropic is not here: it is the native login.",
|
||||
"Read by linux/cm-json.py (CLI + bar widget, via health.json) and claude-mode.ps1 (Windows).",
|
||||
"Behaviour that differs in kind is chosen by name: catalogue.kind (openrouter | lmstudio | ollama | openai | static),",
|
||||
"server.probe (always | lenient | local), setup.key (required | optional), setup.models (per-tier | one-for-all),",
|
||||
"and the doctor checks. A new provider that reuses those is an entry here and a preset in presets/.",
|
||||
"Logos are single-path 24x24 SVG marks; logoScale corrects apparent size (see Modes.js)."
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"id": "openrouter",
|
||||
"aliases": [],
|
||||
"title": "OpenRouter",
|
||||
"label": "OpenRouter - remote, pay-per-token, any vendor",
|
||||
"blurb": "Remote gateway, pay per token, any vendor.",
|
||||
"color": "cyan",
|
||||
"defaultPreset": "default",
|
||||
"preset": {
|
||||
"baseUrl": "https://openrouter.ai/api",
|
||||
"auth": {
|
||||
"mode": "vault",
|
||||
"keyRef": "openrouter"
|
||||
},
|
||||
"contextTokens": 1000000,
|
||||
"gatewayModelDiscovery": true
|
||||
},
|
||||
"server": {
|
||||
"editable": false,
|
||||
"probe": "local"
|
||||
},
|
||||
"catalogue": {
|
||||
"kind": "openrouter",
|
||||
"url": "https://openrouter.ai/api/v1/models"
|
||||
},
|
||||
"setup": {
|
||||
"key": "required",
|
||||
"models": "per-tier",
|
||||
"keyUrl": "https://openrouter.ai/settings/keys"
|
||||
},
|
||||
"guardrail": true,
|
||||
"doctor": [
|
||||
"openrouter-key",
|
||||
"guardrail",
|
||||
"catalogue-models"
|
||||
],
|
||||
"logo": "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",
|
||||
"logoScale": 1.12,
|
||||
"glyph": "F0469"
|
||||
},
|
||||
{
|
||||
"id": "zai",
|
||||
"aliases": [
|
||||
"z.ai",
|
||||
"z-ai"
|
||||
],
|
||||
"title": "Z.AI",
|
||||
"label": "Z.AI - GLM coding plan",
|
||||
"blurb": "GLM coding plan on Z.AI's Anthropic endpoint.",
|
||||
"color": "green",
|
||||
"defaultPreset": "zai",
|
||||
"preset": {
|
||||
"baseUrl": "https://api.z.ai/api/anthropic",
|
||||
"auth": {
|
||||
"mode": "vault",
|
||||
"keyRef": "zai"
|
||||
},
|
||||
"contextTokens": 1000000,
|
||||
"gatewayModelDiscovery": false,
|
||||
"extraEnv": {
|
||||
"API_TIMEOUT_MS": "3000000",
|
||||
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"editable": false,
|
||||
"probe": "local"
|
||||
},
|
||||
"catalogue": {
|
||||
"kind": "static",
|
||||
"docs": "https://docs.z.ai/devpack/tool/claude",
|
||||
"static": [
|
||||
{
|
||||
"id": "glm-5.3",
|
||||
"note": "flagship coding model - opus/sonnet tier"
|
||||
},
|
||||
{
|
||||
"id": "glm-4.7",
|
||||
"note": "fast/cheap tier - haiku"
|
||||
}
|
||||
]
|
||||
},
|
||||
"setup": {
|
||||
"key": "required",
|
||||
"models": "per-tier",
|
||||
"keyUrl": "https://z.ai/manage-apikey/apikey-list"
|
||||
},
|
||||
"doctor": [
|
||||
"message-check"
|
||||
],
|
||||
"logo": "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",
|
||||
"logoScale": 1.07,
|
||||
"glyph": "F015F"
|
||||
},
|
||||
{
|
||||
"id": "lmstudio",
|
||||
"aliases": [
|
||||
"lm-studio"
|
||||
],
|
||||
"title": "LM Studio",
|
||||
"label": "LM Studio - local server, offline, free",
|
||||
"blurb": "Local LM Studio server. Offline and free.",
|
||||
"color": "yellow",
|
||||
"defaultPreset": "lmstudio",
|
||||
"preset": {
|
||||
"baseUrl": "http://127.0.0.1:1234",
|
||||
"auth": {
|
||||
"mode": "literal",
|
||||
"token": "lmstudio"
|
||||
},
|
||||
"contextTokens": 262144,
|
||||
"gatewayModelDiscovery": false,
|
||||
"extraEnv": {
|
||||
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"editable": true,
|
||||
"probe": "always",
|
||||
"paths": [
|
||||
"/api/v0/models",
|
||||
"/v1/models"
|
||||
],
|
||||
"hint": "LM Studio does not have to be on this machine. Point this at a LAN address, or anything reachable through a tunnel or proxy.",
|
||||
"start": "start the server (Developer > Start Server)"
|
||||
},
|
||||
"catalogue": {
|
||||
"kind": "lmstudio",
|
||||
"perServer": true
|
||||
},
|
||||
"setup": {
|
||||
"key": "optional",
|
||||
"models": "one-for-all"
|
||||
},
|
||||
"doctor": [
|
||||
"catalogue-models",
|
||||
"lmstudio-templates"
|
||||
],
|
||||
"logo": "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",
|
||||
"logoScale": 0.86,
|
||||
"glyph": "F048B"
|
||||
},
|
||||
{
|
||||
"id": "ollama",
|
||||
"aliases": [],
|
||||
"title": "Ollama",
|
||||
"label": "Ollama - local server, offline, free",
|
||||
"blurb": "Local Ollama server. Offline and free.",
|
||||
"color": "white",
|
||||
"defaultPreset": "ollama",
|
||||
"preset": {
|
||||
"baseUrl": "http://127.0.0.1:11434",
|
||||
"auth": {
|
||||
"mode": "literal",
|
||||
"token": "ollama"
|
||||
},
|
||||
"contextTokens": 65536,
|
||||
"gatewayModelDiscovery": false,
|
||||
"extraEnv": {
|
||||
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"editable": true,
|
||||
"probe": "always",
|
||||
"paths": [
|
||||
"/api/tags"
|
||||
],
|
||||
"hint": "Ollama does not have to be on this machine. Serve it with OLLAMA_HOST=0.0.0.0 to reach it over the LAN.",
|
||||
"start": "start it with: ollama serve"
|
||||
},
|
||||
"catalogue": {
|
||||
"kind": "ollama",
|
||||
"perServer": true
|
||||
},
|
||||
"setup": {
|
||||
"key": "optional",
|
||||
"models": "one-for-all"
|
||||
},
|
||||
"doctor": [
|
||||
"catalogue-models",
|
||||
"ollama-context"
|
||||
],
|
||||
"logo": "M16.361 10.26a.894.894 0 0 0-.558.47l-.072.148.001.207c0 .193.004.217.059.353.076.193.152.312.291.448.24.238.51.3.872.205a.86.86 0 0 0 .517-.436.752.752 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.06 1.06 0 0 0-.466 0zm-9.203.005c-.305.096-.533.32-.65.639a1.187 1.187 0 0 0-.06.52c.057.309.31.59.598.667.362.095.632.033.872-.205.14-.136.215-.255.291-.448.055-.136.059-.16.059-.353l.001-.207-.072-.148a.894.894 0 0 0-.565-.472 1.02 1.02 0 0 0-.474.007Zm4.184 2c-.131.071-.223.25-.195.383.031.143.157.288.353.407.105.063.112.072.117.136.004.038-.01.146-.029.243-.02.094-.036.194-.036.222.002.074.07.195.143.253.064.052.076.054.255.059.164.005.198.001.264-.03.169-.082.212-.234.15-.525-.052-.243-.042-.28.087-.355.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48.394.394 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053-.053-.032c-.219-.13-.259-.145-.391-.143a.396.396 0 0 0-.193.032zm.39-2.195c-.373.036-.475.05-.654.086-.291.06-.68.195-.951.328-.94.46-1.589 1.226-1.787 2.114-.04.176-.045.234-.045.53 0 .294.005.357.043.524.264 1.16 1.332 2.017 2.714 2.173.3.033 1.596.033 1.896 0 1.11-.125 2.064-.727 2.493-1.571.114-.226.169-.372.22-.602.039-.167.044-.23.044-.523 0-.297-.005-.355-.045-.531-.288-1.29-1.539-2.304-3.072-2.497a6.873 6.873 0 0 0-.855-.031zm.645.937a3.283 3.283 0 0 1 1.44.514c.223.148.537.458.671.662.166.251.26.508.303.82.02.143.01.251-.043.482-.08.345-.332.705-.672.957a3.115 3.115 0 0 1-.689.348c-.382.122-.632.144-1.525.138-.582-.006-.686-.01-.853-.042-.57-.107-1.022-.334-1.35-.68-.264-.28-.385-.535-.45-.946-.03-.192.025-.509.137-.776.136-.326.488-.73.836-.963.403-.269.934-.46 1.422-.512.187-.02.586-.02.773-.002zm-5.503-11a1.653 1.653 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503 0 .544.064 1.24.155 1.721.02.107.031.202.023.208a8.12 8.12 0 0 1-.187.152 5.324 5.324 0 0 0-.949 1.02 5.49 5.49 0 0 0-.94 2.339 6.625 6.625 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195-.037.064c-.269.452-.498 1.105-.605 1.732-.084.496-.095.629-.095 1.294 0 .67.009.803.088 1.266.095.555.288 1.143.503 1.534.071.128.243.393.264.407.007.003-.014.067-.046.141a7.405 7.405 0 0 0-.548 1.873c-.062.417-.071.552-.071.991 0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.915.915 0 0 0-.194-.25 1.74 1.74 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451.124-.486.329-.918.544-1.154a.787.787 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.136 3.136 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834.563-.814 1.353-1.336 2.237-1.475.199-.033.57-.028.776.01.226.04.367.028.512-.041.179-.085.268-.19.374-.431.093-.215.165-.333.36-.576.234-.29.46-.489.822-.729.413-.27.884-.467 1.352-.561.17-.035.25-.04.569-.04.319 0 .398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602.034.057.095.177.132.267.105.241.195.346.374.43.14.068.286.082.503.045.343-.058.607-.053.943.016 1.144.23 2.14 1.173 2.581 2.437.385 1.108.276 2.267-.296 3.153-.097.15-.193.27-.333.419-.301.322-.301.722-.001 1.053.493.539.801 1.866.708 3.036-.062.772-.26 1.463-.533 1.854a2.096 2.096 0 0 1-.224.258.916.916 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295.253 1.008.231 2.01-.059 2.581a.845.845 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074.036-.134c.019-.076.057-.3.088-.516.029-.217.029-1.016 0-1.258-.11-.875-.295-1.57-.597-2.226-.032-.074-.053-.138-.046-.141.008-.005.057-.074.108-.152.376-.569.607-1.284.724-2.228.031-.26.031-1.378 0-1.628-.083-.645-.182-1.082-.348-1.525a6.083 6.083 0 0 0-.329-.7l-.038-.064.131-.194c.402-.604.636-1.262.727-2.04a6.625 6.625 0 0 0-.024-1.358 5.512 5.512 0 0 0-.939-2.339 5.325 5.325 0 0 0-.95-1.02 8.097 8.097 0 0 1-.186-.152.692.692 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503-.19-.924-.535-1.658-.98-2.082-.354-.338-.716-.482-1.15-.455-.996.059-1.8 1.205-2.116 3.01a6.805 6.805 0 0 0-.097.726c0 .036-.007.066-.015.066a.96.96 0 0 1-.149-.078A4.857 4.857 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a.958.958 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a6.71 6.71 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2.096 2.096 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388.03.113.06.244.069.292.007.047.026.152.041.233.067.365.098.76.102 1.24l.002.475-.12.175-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.438 8.438 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711.067-.05.079-.049.157.013zm9.825-.012c.17.126.358.46.498.888.28.854.36 2.028.212 3.145-.019.14-.024.151-.057.144l-.238-.06a3.693 3.693 0 0 0-.954-.124h-.278l-.119-.178-.119-.175.002-.474c.004-.669.066-1.19.214-1.772.157-.623.434-1.185.68-1.382.078-.062.09-.063.159-.012z",
|
||||
"logoScale": 1.0,
|
||||
"glyph": ""
|
||||
},
|
||||
{
|
||||
"id": "custom",
|
||||
"aliases": [],
|
||||
"title": "Custom",
|
||||
"label": "Custom - any Anthropic-compatible endpoint",
|
||||
"blurb": "Your own endpoint: a proxy, gateway or self-hosted server.",
|
||||
"color": "gray",
|
||||
"defaultPreset": "custom",
|
||||
"preset": {
|
||||
"baseUrl": "",
|
||||
"auth": {
|
||||
"mode": "vault",
|
||||
"keyRef": "custom"
|
||||
},
|
||||
"contextTokens": 200000,
|
||||
"gatewayModelDiscovery": false
|
||||
},
|
||||
"server": {
|
||||
"editable": true,
|
||||
"probe": "lenient",
|
||||
"paths": [
|
||||
"/v1/models"
|
||||
],
|
||||
"hint": "Any server that speaks Anthropic's Messages API: a LiteLLM or Vercel gateway, vLLM, llama.cpp, a company proxy.",
|
||||
"start": ""
|
||||
},
|
||||
"catalogue": {
|
||||
"kind": "openai",
|
||||
"perServer": true
|
||||
},
|
||||
"setup": {
|
||||
"key": "optional",
|
||||
"models": "per-tier"
|
||||
},
|
||||
"doctor": [
|
||||
"catalogue-models"
|
||||
],
|
||||
"logo": "M9 3.5 1 12l8 8.5 2-2L4.8 12 11 5.5zm6 0-2 2 6.2 6.5-6.2 6.5 2 2 8-8.5z",
|
||||
"logoScale": 1.09,
|
||||
"glyph": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -33,6 +33,7 @@ Copy-Item (Join-Path $root 'claude-mode.ps1') $stage -Force
|
||||
Copy-Item (Join-Path $root 'install.ps1') $stage -Force
|
||||
Copy-Item (Join-Path $root 'profile-snippet.ps1') $stage -Force
|
||||
Copy-Item (Join-Path $root 'VERSION') $stage -Force
|
||||
Copy-Item (Join-Path $root 'providers.json') $stage -Force
|
||||
Get-ChildItem (Join-Path $root 'bin') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $stage 'bin') -Force }
|
||||
Get-ChildItem (Join-Path $root 'presets') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $stage 'presets') -Force }
|
||||
|
||||
@@ -81,6 +82,7 @@ New-Item -ItemType Directory -Path (Join-Path $posixStage 'presets') -Force | Ou
|
||||
Get-ChildItem (Join-Path $root 'linux') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $posixStage 'linux') -Force }
|
||||
Get-ChildItem (Join-Path $root 'presets') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $posixStage 'presets') -Force }
|
||||
Copy-Item (Join-Path $root 'VERSION') $posixStage -Force
|
||||
Copy-Item (Join-Path $root 'providers.json') $posixStage -Force
|
||||
|
||||
# Shell scripts authored on Windows carry CRLF, which makes the kernel reject
|
||||
# the "#!/usr/bin/env bash" line. Normalise before packaging.
|
||||
@@ -92,7 +94,7 @@ Get-ChildItem (Join-Path $posixStage 'linux') -File | ForEach-Object {
|
||||
$tarName = "claude-mode-$version-posix.tar.gz"
|
||||
$tarPath = Join-Path $outDir $tarName
|
||||
if (Test-Path -LiteralPath $tarPath) { Remove-Item -LiteralPath $tarPath -Force }
|
||||
& tar.exe -czf $tarPath -C $posixStage 'linux' 'presets' 'VERSION'
|
||||
& tar.exe -czf $tarPath -C $posixStage 'linux' 'presets' 'VERSION' 'providers.json'
|
||||
if ($LASTEXITCODE -ne 0) { throw 'tar failed - cannot build the POSIX package' }
|
||||
|
||||
$shaPosix = (Get-FileHash -LiteralPath $tarPath -Algorithm SHA256).Hash.ToLower()
|
||||
|
||||
Reference in New Issue
Block a user