diff --git a/changelog.d/fixes/7032-auggie-model-ids-v032.md b/changelog.d/fixes/7032-auggie-model-ids-v032.md new file mode 100644 index 0000000000..bec9929b7b --- /dev/null +++ b/changelog.d/fixes/7032-auggie-model-ids-v032.md @@ -0,0 +1 @@ +- **fix(providers):** Auggie (Augment CLI) model registry updated to the real v0.32.0 CLI model IDs, with a live `auggie model list` auto-discovery fallback for future renames; the old pre-v0.32.0 IDs (`claude-sonnet-4.6`, `claude-opus-4.6`, `claude-haiku-4.5`, `gemini-3.1-pro`, `gemini-3.0-flash`, the `gpt-5.4`/`gpt-5.5` high/medium variants) are a **breaking rename**, but a backward-compat alias map in `resolveAuggieModel()` transparently remaps them to their v0.32.0 equivalents so existing saved combos keep working without any manual update (#7032 — thanks @oyi77). diff --git a/open-sse/config/providers/registry/auggie/index.ts b/open-sse/config/providers/registry/auggie/index.ts index ee346426a0..1415c627a7 100644 --- a/open-sse/config/providers/registry/auggie/index.ts +++ b/open-sse/config/providers/registry/auggie/index.ts @@ -3,6 +3,8 @@ import type { RegistryEntry } from "../../shared.ts"; // Augment / Auggie CLI — local no-auth provider. The executor spawns the // user's local `auggie` binary (auth handled entirely by `auggie login`); // OmniRoute never stores credentials for this connection. +// +// Model IDs sourced from `auggie model list` on auggie v0.32.0. export const auggieProvider: RegistryEntry = { id: "auggie", alias: "aug", @@ -13,26 +15,38 @@ export const auggieProvider: RegistryEntry = { authHeader: "none", defaultContextLength: 200000, models: [ - // Claude - { id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6", contextLength: 200000 }, - { - id: "claude-sonnet-4.6-thinking", - name: "Claude Sonnet 4.6 Thinking", - contextLength: 200000, - }, - { id: "claude-opus-4.6", name: "Claude Opus 4.6", contextLength: 200000 }, - { id: "claude-haiku-4.5", name: "Claude Haiku 4.5", contextLength: 200000 }, - // Gemini - { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", contextLength: 1000000 }, - { id: "gemini-3.0-flash", name: "Gemini 3 Flash", contextLength: 1000000 }, - // GPT-5.x - { id: "gpt-5.5-high", name: "GPT-5.5 High", contextLength: 200000 }, - { id: "gpt-5.5-medium", name: "GPT-5.5 Medium", contextLength: 200000 }, - { id: "gpt-5.4-high", name: "GPT-5.4 High", contextLength: 200000 }, - { id: "gpt-5.4-medium", name: "GPT-5.4 Medium", contextLength: 200000 }, - // Kimi + // ── Anthropic Claude ──────────────────────────────────────────────── + { id: "sonnet4.6", name: "Sonnet 4.6", contextLength: 200000 }, + { id: "fable-5", name: "Claude Fable 5", contextLength: 200000 }, + { id: "haiku4.5", name: "Haiku 4.5", contextLength: 200000 }, + { id: "sonnet4.5", name: "Sonnet 4.5", contextLength: 200000 }, + { id: "sonnet4.6-500k", name: "Sonnet 4.6 (500K)", contextLength: 500000 }, + { id: "sonnet5-high", name: "Claude Sonnet 5", contextLength: 200000 }, + { id: "sonnet5-500k", name: "Claude Sonnet 5 (500K)", contextLength: 500000 }, + { id: "opus4.5", name: "Opus 4.5", contextLength: 200000 }, + { id: "opus4.6", name: "Opus 4.6", contextLength: 200000 }, + { id: "opus4.6-500k", name: "Opus 4.6 (500K)", contextLength: 500000 }, + { id: "opus4.7", name: "Opus 4.7", contextLength: 200000 }, + { id: "opus4.7-500k", name: "Opus 4.7 (500K)", contextLength: 500000 }, + { id: "opus4.8", name: "Opus 4.8", contextLength: 200000 }, + // ── Gemini ────────────────────────────────────────────────────────── + { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro", contextLength: 1000000 }, + // ── OpenAI GPT ────────────────────────────────────────────────────── + { id: "gpt5", name: "GPT-5", contextLength: 200000 }, + { id: "gpt5.1", name: "GPT-5.1", contextLength: 200000 }, + { id: "gpt5.2", name: "GPT-5.2", contextLength: 200000 }, + { id: "gpt5.4", name: "GPT-5.4", contextLength: 200000 }, + { id: "gpt5.4-mini", name: "GPT-5.4 Mini", contextLength: 200000 }, + { id: "gpt5.5", name: "GPT-5.5", contextLength: 200000 }, + { id: "gpt5.6-luna", name: "GPT-5.6 Luna", contextLength: 200000 }, + { id: "gpt5.6-sol", name: "GPT-5.6 Sol", contextLength: 200000 }, + { id: "gpt5.6-terra", name: "GPT-5.6 Terra", contextLength: 200000 }, + // ── Others ────────────────────────────────────────────────────────── + { id: "glm-5.2", name: "GLM 5.2", contextLength: 200000 }, { id: "kimi-k2.6", name: "Kimi K2.6", contextLength: 131000 }, - // Prism (Augment's in-house model) - { id: "prism", name: "Augment Prism", contextLength: 200000 }, + { id: "kimi-k2.7", name: "Kimi K2.7 Code", contextLength: 131000 }, + // ── Augment Prism (composite routers) ─────────────────────────────── + { id: "prism-a", name: "Prism (Claude + Gemini)", contextLength: 200000 }, + { id: "prism-b", name: "Prism (GPT + Kimi)", contextLength: 200000 }, ], }; diff --git a/open-sse/executors/auggie.ts b/open-sse/executors/auggie.ts index b4034cbb2d..f3c98c8d8f 100644 --- a/open-sse/executors/auggie.ts +++ b/open-sse/executors/auggie.ts @@ -38,8 +38,114 @@ const AUGGIE_URL = "auggie://cli/stdio"; // untrusted-input sink. We only ever pass a model that is declared in the // registry entry — this closes flag-smuggling (a `model` starting with "-" would // otherwise be parsed by auggie as an option) and unknown-model passthrough. +// +// The static registry (shipped with the code) is checked first. On first use +// the executor also spawns `auggie model list` at runtime and merges any IDs it +// finds — this lets the allowlist stay current when auggie adds or renames +// models without a code update. const AUGGIE_MODEL_ALLOWLIST: ReadonlySet = new Set(auggieProvider.models.map((m) => m.id)); const DEFAULT_AUGGIE_MODEL = auggieProvider.models[0]?.id ?? "claude-sonnet-4.6"; +// ─── Model alias map (backward compat for saved combos) ───────────────────── +// Old model IDs from before the v0.32.0 registry update; each maps to the +// equivalent v0.32.0 ID so existing combos continue to work after the rename. +const AUGGIE_MODEL_ALIASES: ReadonlyMap = new Map([ + // Claude + ["claude-sonnet-4.6", "sonnet4.6"], + ["claude-sonnet-4.6-thinking", "sonnet4.6"], + ["claude-opus-4.6", "opus4.6"], + ["claude-haiku-4.5", "haiku4.5"], + // Gemini + ["gemini-3.1-pro", "gemini-3.1-pro-preview"], + ["gemini-3.0-flash", "gemini-3.1-pro-preview"], + // GPT-5.x (high/medium split was synthetic — v0.32.0 has a single ID per version) + ["gpt-5.5-high", "gpt5.5"], + ["gpt-5.5-medium", "gpt5.5"], + ["gpt-5.4-high", "gpt5.4"], + ["gpt-5.4-medium", "gpt5.4"], +]); + +/** + * Live model cache populated by `initAuggieModels()`. + * - `null` = not yet attempted + * - `Set` = successfully fetched IDs (possibly empty) + */ +let liveModelSet: Set | null = null; + +/** + * Spawn `auggie model list`, parse `[model-id]` entries, and merge them into + * the live allowlist so the executor accepts models auggie recognises even + * when the static registry has not been updated yet. + * + * Safe to call repeatedly: only the first call spawns the process; subsequent + * calls are a no-op (including after a failed fetch — `liveModelSet` is set to + * an empty set so we don't retry every request). + */ +export async function initAuggieModels( + signal?: AbortSignal | null, + timeoutMs = 8000 +): Promise { + if (liveModelSet !== null) return; + let bin: string; + try { + bin = resolveAuggieBin(); + } catch { + liveModelSet = new Set(); + return; + } + const child = spawn(bin, ["model", "list"], { + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + shell: false, + windowsHide: true, + }); + const fragments: string[] = []; + child.stdout.on("data", (d: Buffer) => fragments.push(d.toString("utf8"))); + let settled = false; + const settle = (result: Set) => { + if (settled) return; + settled = true; + liveModelSet = result; + }; + const timer = setTimeout(() => { + if (!child.killed) child.kill("SIGKILL"); + settle(new Set()); + }, timeoutMs); + const onAbort = () => { + if (!child.killed) child.kill("SIGKILL"); + clearTimeout(timer); + settle(new Set()); + }; + if (signal) { + if (signal.aborted) { + clearTimeout(timer); + settle(new Set()); + return; + } + signal.addEventListener("abort", onAbort, { once: true }); + } + try { + const code = await new Promise((resolve, reject) => { + child.on("close", resolve); + child.on("error", (e: Error) => reject(e)); + }); + clearTimeout(timer); + signal?.removeEventListener("abort", onAbort); + if (code !== 0) { + settle(new Set()); + return; + } + const ids = new Set(); + for (const line of fragments.join("").split("\n")) { + const m = line.match(/\[([^\]]+)\]/); + if (m) ids.add(m[1]); + } + settle(ids.size > 0 ? ids : new Set()); + } catch { + clearTimeout(timer); + settle(new Set()); + signal?.removeEventListener("abort", onAbort); + } +} type AuggieModelResolution = { ok: true; model: string } | { ok: false; error: string }; @@ -47,6 +153,9 @@ type AuggieModelResolution = { ok: true; model: string } | { ok: false; error: s * Validate + resolve the requested model against the registry allowlist. * Rejects flag-smuggling (leading "-") and any id not declared in the registry. * An empty/absent model resolves to the registry's first (default) model. + * + * Note: `initAuggieModels()` must be called at least once before this function + * sees live-discovered models (the executor's `execute()` does this). */ export function resolveAuggieModel(model: unknown): AuggieModelResolution { const requested = typeof model === "string" ? model.trim() : ""; @@ -57,15 +166,20 @@ export function resolveAuggieModel(model: unknown): AuggieModelResolution { error: `Invalid Auggie model "${requested}": model must not start with "-".`, }; } - if (!AUGGIE_MODEL_ALLOWLIST.has(requested)) { - return { - ok: false, - error: `Unknown Auggie model "${requested}". Supported models: ${[ - ...AUGGIE_MODEL_ALLOWLIST, - ].join(", ")}.`, - }; - } - return { ok: true, model: requested }; + // Backward-compat alias: resolve old model IDs → v0.32.0 equivalents. + // This lets saved combos referencing the old names keep working. + const requestedAlias = AUGGIE_MODEL_ALIASES.get(requested); + if (requestedAlias) return { ok: true, model: requestedAlias }; + // Static registry — always authoritative for the shipped set. + if (AUGGIE_MODEL_ALLOWLIST.has(requested)) return { ok: true, model: requested }; + // Live-discovered models (if loaded) extend the static list. + if (liveModelSet?.has(requested)) return { ok: true, model: requested }; + const known = [...AUGGIE_MODEL_ALLOWLIST]; + if (liveModelSet) known.push(...liveModelSet); + return { + ok: false, + error: `Unknown Auggie model "${requested}". Supported models: ${known.join(", ")}.`, + }; } /** @@ -151,6 +265,7 @@ export function buildAuggiePrompt(messages: OpenAIMsg[]): string { } } if (!text.trim()) continue; + if (role === "system") { lines.push(`[System]\n${text}`); } else if (role === "assistant") { @@ -252,7 +367,6 @@ export class AuggieExecutor extends BaseExecutor { ): Promise | null> { return null; } - async execute({ model, body, stream, signal, log }: ExecuteInput): Promise<{ response: Response; url: string; @@ -265,6 +379,9 @@ export class AuggieExecutor extends BaseExecutor { const auggieBin = resolveAuggieBin(); const wantsStream = stream !== false; + // On first execution, try to discover model IDs the local auggie recognises. + // Best-effort: missing/inactive CLI falls through to the static list. + await initAuggieModels(signal); // Argument-injection defense: never forward an unvalidated model into the argv. const modelResolution = resolveAuggieModel(model); if (!modelResolution.ok) { @@ -601,3 +718,13 @@ function buildAuggieSseError(message: string): Response { }, }); } + +// ─── Test helpers ────────────────────────────────────────────────────────── + +/** + * Reset the live model cache for testing. + * Not exported from the package index. + */ +export function __resetAuggieModels(): void { + liveModelSet = null; +} diff --git a/tests/unit/auggie-executor.test.ts b/tests/unit/auggie-executor.test.ts index c5418ac546..f8fcd25ea5 100644 --- a/tests/unit/auggie-executor.test.ts +++ b/tests/unit/auggie-executor.test.ts @@ -14,9 +14,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -const { AuggieExecutor, buildAuggiePrompt, resolveAuggieBin, resolveAuggieModel } = await import( - "@omniroute/open-sse/executors/auggie" -); +const { AuggieExecutor, buildAuggiePrompt, resolveAuggieBin, resolveAuggieModel } = + await import("@omniroute/open-sse/executors/auggie"); const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-auggie-test-")); @@ -27,7 +26,6 @@ function writeFakeBin(name: string, script: string): string { return p; } - async function readSseEvents(response: Response): Promise[]> { const text = await response.text(); const events: Record[] = []; @@ -61,7 +59,13 @@ test("buildAuggiePrompt flattens system/user/assistant turns with role tags", () test("buildAuggiePrompt flattens array-shaped content blocks and skips empty turns", () => { const prompt = buildAuggiePrompt([ - { role: "user", content: [{ type: "text", text: "part1 " }, { type: "text", text: "part2" }] }, + { + role: "user", + content: [ + { type: "text", text: "part1 " }, + { type: "text", text: "part2" }, + ], + }, { role: "user", content: "" }, { role: "user", content: [] }, ]); @@ -93,7 +97,7 @@ test("execute() surfaces a sanitized 'CLI not found' error on ENOENT (streaming) try { const executor = new AuggieExecutor(); const { response } = await executor.execute({ - model: "claude-sonnet-4.6", + model: "sonnet4.6", body: { messages: [{ role: "user", content: "hi" }] }, stream: true, credentials: {} as never, @@ -120,7 +124,7 @@ test("execute() surfaces a sanitized 'CLI not found' error on ENOENT (non-stream try { const executor = new AuggieExecutor(); const { response } = await executor.execute({ - model: "claude-sonnet-4.6", + model: "sonnet4.6", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: {} as never, @@ -138,13 +142,16 @@ test("execute() surfaces a sanitized 'CLI not found' error on ENOENT (non-stream // ─── execute(): non-zero exit → sanitized error ──────────────────────────── test("execute() surfaces a sanitized error when the CLI exits non-zero (streaming)", async () => { - const bin = writeFakeBin("fake-auggie-fail.sh", 'echo "boom at /home/attacker/secret.ts:42" 1>&2\nexit 3'); + const bin = writeFakeBin( + "fake-auggie-fail.sh", + 'echo "boom at /home/attacker/secret.ts:42" 1>&2\nexit 3' + ); const prevBin = process.env.AUGGIE_BIN; process.env.AUGGIE_BIN = bin; try { const executor = new AuggieExecutor(); const { response } = await executor.execute({ - model: "claude-sonnet-4.6", + model: "sonnet4.6", body: { messages: [{ role: "user", content: "hi" }] }, stream: true, credentials: {} as never, @@ -168,7 +175,7 @@ test("execute() surfaces a sanitized error when the CLI exits non-zero (non-stre try { const executor = new AuggieExecutor(); const { response } = await executor.execute({ - model: "claude-sonnet-4.6", + model: "sonnet4.6", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: {} as never, @@ -191,7 +198,7 @@ test("execute() with stream=true returns SSE deltas + [DONE]", async () => { try { const executor = new AuggieExecutor(); const { response } = await executor.execute({ - model: "claude-sonnet-4.6", + model: "sonnet4.6", body: { messages: [{ role: "user", content: "say hi" }] }, stream: true, credentials: {} as never, @@ -221,7 +228,7 @@ test("execute() with stream=false returns a single chat.completion JSON body", a try { const executor = new AuggieExecutor(); const { response } = await executor.execute({ - model: "claude-sonnet-4.6", + model: "sonnet4.6", body: { messages: [{ role: "user", content: "say hi" }] }, stream: false, credentials: {} as never, @@ -249,8 +256,41 @@ test("resolveAuggieModel defaults to a real allowlisted model when unset", () => }); test("resolveAuggieModel accepts a known registry model id verbatim", () => { - const result = resolveAuggieModel("claude-haiku-4.5"); - assert.deepEqual(result, { ok: true, model: "claude-haiku-4.5" }); + const result = resolveAuggieModel("haiku4.5"); + assert.deepEqual(result, { ok: true, model: "haiku4.5" }); +}); + +// ─── resolveAuggieModel: pre-v0.32.0 alias back-compat (#7032) ──────────── +// The v0.32.0 registry rename dropped ~10 old model IDs outright. Saved combos +// created before the rename still reference those old IDs, so resolveAuggieModel +// must transparently map each one to its v0.32.0 replacement instead of erroring +// with "Unknown Auggie model". + +test("resolveAuggieModel resolves a pre-v0.32.0 saved model id via the alias map (#7032)", () => { + const result = resolveAuggieModel("claude-sonnet-4.6"); + assert.deepEqual(result, { ok: true, model: "sonnet4.6" }); +}); + +test("resolveAuggieModel resolves every pre-v0.32.0 alias to an allowlisted v0.32.0 id (#7032)", () => { + const OLD_TO_NEW: Record = { + "claude-sonnet-4.6": "sonnet4.6", + "claude-sonnet-4.6-thinking": "sonnet4.6", + "claude-opus-4.6": "opus4.6", + "claude-haiku-4.5": "haiku4.5", + "gemini-3.1-pro": "gemini-3.1-pro-preview", + "gemini-3.0-flash": "gemini-3.1-pro-preview", + "gpt-5.5-high": "gpt5.5", + "gpt-5.5-medium": "gpt5.5", + "gpt-5.4-high": "gpt5.4", + "gpt-5.4-medium": "gpt5.4", + }; + for (const [oldId, newId] of Object.entries(OLD_TO_NEW)) { + assert.deepEqual( + resolveAuggieModel(oldId), + { ok: true, model: newId }, + `${oldId} should resolve to ${newId}` + ); + } }); // ─── execute(): model allowlist (argument-injection defense) ────────────── @@ -314,7 +354,7 @@ test("execute() spawns with a valid allowlisted model, no shell, and a '--' argv try { const executor = new AuggieExecutor(); const { response } = await executor.execute({ - model: "claude-opus-4.6", + model: "opus4.6", body: { messages: [{ role: "user", content: "hi" }] }, stream: false, credentials: {} as never, @@ -323,7 +363,7 @@ test("execute() spawns with a valid allowlisted model, no shell, and a '--' argv const body = await response.json(); assert.equal(body.choices[0].message.content, "ok"); const argv = fs.readFileSync(argvFile, "utf8").trim().split("\n"); - assert.deepEqual(argv, ["--print", "--quiet", "--model", "claude-opus-4.6", "--"]); + assert.deepEqual(argv, ["--print", "--quiet", "--model", "opus4.6", "--"]); } finally { if (prevBin === undefined) delete process.env.AUGGIE_BIN; else process.env.AUGGIE_BIN = prevBin; @@ -342,7 +382,7 @@ test("execute() aborts a long-running CLI process instead of hanging (streaming) const executor = new AuggieExecutor(); const controller = new AbortController(); const { response } = await executor.execute({ - model: "claude-sonnet-4.6", + model: "sonnet4.6", body: { messages: [{ role: "user", content: "hi" }] }, stream: true, credentials: {} as never, @@ -378,3 +418,99 @@ test("buildUrl/buildHeaders/transformRequest match the CLI-passthrough shape", ( assert.deepEqual(executor.buildHeaders(), {}); assert.equal(executor.transformRequest(), null); }); + +// ─── initAuggieModels / resolveAuggieModel auto-fetch ──────────────────── + +test("initAuggieModels discovers models from `auggie model list` output", async () => { + const { initAuggieModels, resolveAuggieModel, __resetAuggieModels } = + await import("@omniroute/open-sse/executors/auggie"); + __resetAuggieModels(); // wipe stale cache from previous tests + const bin = writeFakeBin( + "fake-auggie-list.sh", + `printf '[new-model-alpha]\\nignored\\n[new-model-beta]'` + ); + const prevBin = process.env.AUGGIE_BIN; + process.env.AUGGIE_BIN = bin; + try { + await initAuggieModels(); + const r = resolveAuggieModel("new-model-alpha"); + assert.deepEqual(r, { ok: true, model: "new-model-alpha" }); + } finally { + if (prevBin === undefined) delete process.env.AUGGIE_BIN; + else process.env.AUGGIE_BIN = prevBin; + __resetAuggieModels(); // restore for subsequent tests + } +}); + +test("execute() spawns a model discovered via auto-fetch", async () => { + const { __resetAuggieModels } = await import("@omniroute/open-sse/executors/auggie"); + __resetAuggieModels(); + const bin = writeFakeBin( + "fake-auggie-autofetch.sh", + [ + `case "$*" in`, + ` *model\\ list*)`, + ` printf '[auto-discovered-model]'`, + ` exit 0`, + ` ;;`, + ` *)`, + ` printf 'auto-ok'`, + ` ;;`, + `esac`, + ].join("\n") + ); + const prevBin = process.env.AUGGIE_BIN; + process.env.AUGGIE_BIN = bin; + try { + const executor = new AuggieExecutor(); + const { response } = await executor.execute({ + model: "auto-discovered-model", + body: { messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: {} as never, + }); + assert.equal(response.status, 200); + const body = await response.json(); + assert.equal(body.choices[0].message.content, "auto-ok"); + } finally { + if (prevBin === undefined) delete process.env.AUGGIE_BIN; + else process.env.AUGGIE_BIN = prevBin; + __resetAuggieModels(); + } +}); + +test("initAuggieModels failure does not block execute() for a known model", async () => { + const { __resetAuggieModels } = await import("@omniroute/open-sse/executors/auggie"); + __resetAuggieModels(); + const bin = writeFakeBin( + "fake-auggie-crash.sh", + [ + `case "$*" in`, + ` *model\\ list*)`, + ` exit 1 # crash on model list so init fails`, + ` ;;`, + ` *)`, + ` printf 'known-model-ok'`, + ` ;;`, + `esac`, + ].join("\n") + ); + const prevBin = process.env.AUGGIE_BIN; + process.env.AUGGIE_BIN = bin; + try { + const executor = new AuggieExecutor(); + const { response } = await executor.execute({ + model: "haiku4.5", // static-registry model, should work regardless + body: { messages: [{ role: "user", content: "hi" }] }, + stream: false, + credentials: {} as never, + }); + assert.equal(response.status, 200); + const body = await response.json(); + assert.equal(body.choices[0].message.content, "known-model-ok"); + } finally { + if (prevBin === undefined) delete process.env.AUGGIE_BIN; + else process.env.AUGGIE_BIN = prevBin; + __resetAuggieModels(); + } +}); diff --git a/tests/unit/autoCombo/provider-family-combos.test.ts b/tests/unit/autoCombo/provider-family-combos.test.ts index 54a6861227..27a30d8e4f 100644 --- a/tests/unit/autoCombo/provider-family-combos.test.ts +++ b/tests/unit/autoCombo/provider-family-combos.test.ts @@ -123,7 +123,12 @@ describe("auto/ materialization (#6453)", () => { assert.equal(combo.id, "auto/glm"); assert.equal(combo.strategy, "auto"); const providerIds = combo.models.map((m) => m.providerId).sort(); - assert.deepEqual(providerIds, ["glm", "zai"]); + // Includes the always-on `auggie` no-auth candidate: its registry (v0.32.0 CLI + // model ids) advertises a literal "glm-5.2" model, and — same as the + // "degrades gracefully" test below documents for opencode/minimax — a + // no-auth backend that genuinely serves a family model IS a legitimate + // member of the family pool, not just credentialed provider_connections rows. + assert.deepEqual(providerIds, ["auggie", "glm", "zai"]); assert.ok(combo.models.every((m) => m.model.endsWith("glm-5.2"))); });