Skip to content

Commit 5cdabc8

Browse files
committed
feat: provide current-model shortcut defaults for new installations
Signed-off-by: Tiberiu Socaci <tsocaci@makeitfuture.com>
1 parent 2dd0a07 commit 5cdabc8

10 files changed

Lines changed: 147 additions & 8 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ product overview.
1818
1919
## Unreleased
2020

21+
- Supply new installations with Astra, Sol, Terra, Fable, Opus and Sonnet (`:sonet`) shortcuts
22+
using the newest supported stable family versions. Preserve customized lists and thread pins.
23+
2124
- Let organization admins use another requester's Slack menu buttons and question forms. Menu
2225
views open under the clicking admin's own identity; question answers continue the original
2326
requester's session and record which admin responded. Current access, membership and stale-form

‎FEATURES.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,14 @@ control-plane status.
653653

654654
## Gateway model shortcuts
655655

656+
New installations inherit `:astra`, `:sol`, `:terra`, `:fable`, `:opus` and `:sonet` from the
657+
engine model catalogs. Each resolves to the numerically newest stable version of its family,
658+
including Sonnet 5.5 and GPT-6.1 Sol when offered by the catalog. Live Codex discovery can advance
659+
these defaults; Claude uses the curated supported catalog shipped with the gateway. Missing
660+
families are omitted instead of guessed. A stored shortcut map, including an intentionally empty
661+
map, is preserved exactly. Saving unrelated settings keeps inherited defaults; editing the shortcut
662+
list makes it an explicit set of pins. Existing thread pins never follow catalog updates.
663+
656664
Admins can add gateway-wide model shortcuts in Settings → Access & security → Model shortcuts. Each name selects an engine and model. In Slack, `@agent :astra task` pins that engine and model to the current thread and runs the task; `@agent :astra` switches the thread without starting an agent turn. Slack's automatically inserted space after the mention is accepted. Changing a mapping affects later shortcut selections, while threads already pinned keep their selected model. Channel runtime-change access and dedicated Codex-login restrictions apply.
657665

658666
## Public documentation website

‎TEST-PLAN.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,6 +1561,20 @@ Record blocked fixtures and unexecuted gates explicitly for both engines.
15611561

15621562
## Model shortcuts acceptance (2026-10-06)
15631563

1564+
- [x] Automated: fresh settings without `modelShortcuts` inherit the six catalog families. Numeric
1565+
ordering selects 6.10 over 6.9 and Sonnet 5.10 over 5.5 independent of catalog order;
1566+
aliases/previews and missing families never invent targets. Newer catalogs update inherited
1567+
defaults without mutating settings; explicit custom/empty maps survive and unrelated saves
1568+
keep inheritance (`test/model-shortcuts.test.js`).
1569+
- [ ] Live Claude/Codex: on a disposable installation without a saved shortcut map, inspect Settings
1570+
and send `@agent :sonet Reply exactly DEFAULT_SONNET_OK` and `@agent :sol Reply exactly
1571+
DEFAULT_SOL_OK` as Contact in the corresponding private Auto fixtures. Pass: the supported
1572+
latest family IDs are selected and pinned, exact sentinels return on the requested engine,
1573+
and normal followups retain those pins. Save unrelated settings; inherited defaults stay
1574+
inherited. Customize one mapping, then empty the list; reload/upgrade must preserve each
1575+
explicit choice. Use an isolated catalog fixture to introduce newer family versions; only
1576+
inherited mappings advance, never existing thread pins or a customized list.
1577+
15641578
- [x] Automated: `test/model-shortcuts.test.js` checks parsing, saving, repointing, clearing and refusing invalid mappings.
15651579
- [x] Browser: `test/model-shortcuts-browser.test.js` ran in Chromium against a disposable admin API and store. Added a Codex shortcut from the current catalog, saved and reloaded it, repointed its model and engine, and removed it; no page errors.
15661580
- [x] Pipeline: `test/model-shortcut-pipeline.test.js` ran the real Slack pipeline with spawned Claude/Codex fixture CLIs. Verified Slack's inserted space, model flags on new/resumed turns, a Codex→Claude switch, stable pins after repointing a shortcut, stripped prefixes on runtime-card retries, and refusal before pin changes for runtime permissions, disabled engines and a dedicated Codex login. These are integration fixtures, not live provider acceptance.

‎documentation/src/content/docs/configuration/model-defaults.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ Optional provider harnesses must be explicitly enabled and authenticated. At lea
2727

2828
An empty gateway model setting lets the CLI use its own default. An explicit gateway model helps avoid changes in the operator's interactive terminal model choice affecting gateway runs.
2929

30+
## Default model shortcuts
31+
32+
New installations include `:astra`, `:sol`, `:terra`, `:fable`, `:opus`, and `:sonet` (Sonnet).
33+
Use `@agent :sonet your task` in Slack to select and pin the current supported Sonnet model.
34+
Before an administrator saves a custom shortcut list, targets follow the newest stable version
35+
of each family in the gateway's model catalog. Codex uses its authenticated CLI catalog, with a
36+
bundled fallback; Claude uses the curated catalog in the installed gateway version. Updating the
37+
CLI or gateway is therefore still necessary when a provider exposes a model they do not yet list.
38+
Unavailable families are omitted rather than assigned a guessed model ID.
39+
40+
Edit shortcuts under **Settings → Access & security → Model shortcuts**. A saved list uses those
41+
exact mappings until edited again; an empty list disables shortcuts. Existing customized lists and
42+
thread pins survive updates. Saving unrelated settings does not turn inherited defaults into pins.
43+
3044
## Conversation and thread overrides
3145

3246
Set a conversation's **Engine**, **Model**, and **Effort** under **Conversations → Runtime**. On Slack, use `/model` to select a channel-wide or **just this thread** runtime. A thread-scoped choice does not change the default for other threads.

‎public/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,7 @@ <h4>Runtime changes</h4>
965965
</div>
966966
<div class="setcard">
967967
<h4>Model shortcuts</h4>
968-
<p class="desc">Mention the bot, then type a colon and a name after Slack's inserted space, for example <code>@agent :astra review this</code>. The selected engine and model are pinned to that thread. Use the same form without a task to switch an existing thread. Names apply gateway-wide; channel runtime permissions still apply.</p>
968+
<p class="desc">Mention the bot, then type a colon and a name after Slack's inserted space, for example <code>@agent :astra review this</code>. The selected engine and model are pinned to that thread. Use the same form without a task to switch an existing thread. Names apply gateway-wide; channel runtime permissions still apply. New installations include Astra, Sol, Terra, Fable, Opus and <code>:sonet</code> for Sonnet. Until this list is customized, targets follow the newest stable version in each family from the supported model catalog. Saving an edited list keeps those exact choices.</p>
969969
<div id="model-shortcuts-editor"></div>
970970
<button type="button" class="clear-tok" id="add-model-shortcut" disabled>Add shortcut</button>
971971
</div>

‎src/config/model-shortcuts.js‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,43 @@
22
export const MODEL_SHORTCUT_NAME_RE = /^[a-z][a-z0-9_-]{0,23}$/;
33
export const MAX_MODEL_SHORTCUTS = 32;
44

5-
export function modelShortcutsFromSettings(settings) {
5+
// Families are stable shortcut names; exact versions come only from the engine catalogs.
6+
const DEFAULT_SHORTCUT_FAMILIES = [
7+
["astra", "codex", /^gpt-(\d+(?:\.\d+)*)-astra$/],
8+
["sol", "codex", /^gpt-(\d+(?:\.\d+)*)-sol$/],
9+
["terra", "codex", /^gpt-(\d+(?:\.\d+)*)-terra$/],
10+
["fable", "claude", /^claude-fable-(\d+(?:-\d+)*)$/],
11+
["opus", "claude", /^claude-opus-(\d+(?:-\d+)*)$/],
12+
["sonet", "claude", /^claude-sonnet-(\d+(?:-\d+)*)$/],
13+
];
14+
15+
function compareVersions(left, right) {
16+
for (let index = 0; index < Math.max(left.length, right.length); index++) {
17+
const delta = (left[index] || 0) - (right[index] || 0);
18+
if (delta) return delta;
19+
}
20+
return 0;
21+
}
22+
23+
export function defaultModelShortcuts(catalogs = {}) {
24+
const shortcuts = {};
25+
for (const [name, engine, pattern] of DEFAULT_SHORTCUT_FAMILIES) {
26+
let latest = null;
27+
for (const entry of catalogs[engine] || []) {
28+
const match = pattern.exec(String(entry?.value || ""));
29+
if (!match || /-(?:\d{8}|\d{4}-\d{2}-\d{2})$/.test(entry.value)) continue; // Ignore aliases, preview IDs and unrelated model families.
30+
const version = match[1].split(/[.-]/).map(Number);
31+
if (!latest || compareVersions(version, latest.version) > 0) latest = { model: entry.value, version };
32+
}
33+
if (latest) shortcuts[name] = { engine, model: latest.model };
34+
}
35+
return shortcuts;
36+
}
37+
38+
export function modelShortcutsFromSettings(settings, catalogs = {}) {
39+
// A missing key inherits current catalog defaults. A saved list, including {}, is owned by
40+
// the administrator: upgrades must never overwrite custom pins or resurrect deleted names.
41+
if (!Object.hasOwn(settings || {}, "modelShortcuts")) return defaultModelShortcuts(catalogs);
642
const raw = settings?.modelShortcuts;
743
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
844
return Object.fromEntries(Object.entries(raw).filter(([name, target]) =>

‎src/config/settings.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import path from "node:path";
77
import { settingsFile } from "./paths.js";
88
import { writeSecretFile } from "./harden.js";
99
import { getDb } from "../db/index.js";
10-
import { ENGINE_IDS, adapterOr } from "../engines/registry.js";
10+
import { ENGINE_IDS, adapterOr, modelsForEngine } from "../engines/registry.js";
1111
// Value-only import (the default endpoint constant). qwen.js reads settings LAZILY, so this
1212
// direction carries no cycle.
1313
import { QWEN_PROVIDERS, qwenProvider } from "../engines/qwen.js";
@@ -335,7 +335,7 @@ export function getDefaultModel(engine) {
335335
}
336336

337337
export function getModelShortcuts() {
338-
return modelShortcutsFromSettings(getSettings());
338+
return modelShortcutsFromSettings(getSettings(), { claude: modelsForEngine("claude"), codex: modelsForEngine("codex") });
339339
}
340340

341341
// Who may use Slack's /model wizard in a CHANNEL (both channel-wide and thread-scoped picks).

‎test/model-shortcut-pipeline.test.js‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ process.env.PATH = `${fileURLToPath(new URL("./fixtures", import.meta.url))}${pa
1111
process.env.SESSION_KEEPALIVE = "0";
1212
process.env.PROGRESS_VIEW = "shimmer";
1313
const { setUser, upsertChannelEntry, saveChannelMeta } = await import("../src/config/store.js");
14-
const { saveSettings } = await import("../src/config/settings.js");
14+
const { saveSettings, getModelShortcuts, getDefaultModel, getEngine } = await import("../src/config/settings.js");
1515
const { getThreadEngine, getThreadModel, getThreadEffort, setThreadEffort } = await import("../src/gateway/thread-engine.js");
1616
const { processMessageEvent } = await import("../src/slack/message-pipeline.js");
1717
const { stripMentions } = await import("../src/slack/message-normalize.js");
@@ -37,6 +37,23 @@ const client = {
3737
},
3838
};
3939

40+
test("fresh-install defaults select supported Sonnet and Sol without changing gateway defaults", async () => {
41+
await setUser(USER, { name: "Shortcut User", approved: true, isAdmin: false });
42+
const entry = await upsertChannelEntry(CHANNEL, { name: "shortcut-dm", type: "im", isDM: true });
43+
await saveChannelMeta(entry.slug, { channelId: CHANNEL, name: entry.name, type: "im", isDM: true, template: "custom", engine: "claude" });
44+
const engine = getEngine();
45+
const defaults = {claude: getDefaultModel("claude"), codex: getDefaultModel("codex")};
46+
const shortcuts = getModelShortcuts();
47+
for (const [index, name] of ["sonet", "sol"].entries()) {
48+
const ts = `122.00${index}`;
49+
await processMessageEvent({ type: "message", channel: CHANNEL, channel_type: "im", user: USER, text: `<@${BOT}> :${name} short task`, ts }, client, { botUserId: BOT });
50+
assert.equal(await getThreadModel(entry.slug, ts), shortcuts[name].model);
51+
assert.equal(await getThreadEngine(entry.slug, ts), shortcuts[name].engine);
52+
}
53+
assert.equal(getEngine(), engine);
54+
assert.deepEqual({claude: getDefaultModel("claude"), codex: getDefaultModel("codex")}, defaults);
55+
});
56+
4057
test("Slack's inserted space before :astra switches a DM thread and clears an old effort pin", async () => {
4158
await setUser(USER, { name: "Shortcut User", approved: true, isAdmin: false });
4259
const entry = await upsertChannelEntry(CHANNEL, { name: "shortcut-dm", type: "im", isDM: true });

‎test/model-shortcuts-browser.test.js‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ ensureTestEnv();
1010
test("settings creates, repoints and removes model shortcuts through the live catalog", { skip: !process.env.CG_BROWSER_MODULE }, async (t) => {
1111
const { chromium } = await import(process.env.CG_BROWSER_MODULE);
1212
const { createAdminRouter } = await import("../src/web/routes/admin.js");
13-
const { getModelShortcuts } = await import("../src/config/settings.js");
13+
const { getModelShortcuts, saveSettings, getSettings } = await import("../src/config/settings.js");
1414
const app = express();
1515
app.use(express.json());
1616
app.get("/api/skills", (_req, res) => res.json({ skills: [] }));
@@ -35,6 +35,17 @@ test("settings creates, repoints and removes model shortcuts through the live ca
3535
assert.equal((await response).status(), 200);
3636
await page.waitForFunction(() => globalThis.document.querySelector("#settings-dirty-msg")?.textContent === "All changes saved");
3737
};
38+
assert.equal(await page.locator(".model-shortcut-row").count(), 6, "fresh settings render all inherited families");
39+
assert.equal(await page.locator('.shortcut-name').evaluateAll(nodes => nodes.some(n => n.value === "sonet")), true);
40+
await page.locator("#set-keepalive").fill("15m");
41+
await persist();
42+
assert.equal(Object.hasOwn(getSettings(), "modelShortcuts"), false, "an unrelated API save keeps defaults inherited");
43+
await page.reload();
44+
await page.waitForFunction(() => globalThis.document.querySelector("#add-model-shortcut")?.disabled === false);
45+
assert.equal(await page.locator(".model-shortcut-row").count(), 6);
46+
saveSettings({ modelShortcuts: {} });
47+
await page.reload();
48+
await page.waitForFunction(() => globalThis.document.querySelector("#add-model-shortcut")?.disabled === false);
3849
await page.locator("#add-model-shortcut").click();
3950
const row = page.locator(".model-shortcut-row").first();
4051
await row.locator(".shortcut-name").fill("astra");

‎test/model-shortcuts.test.js‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import express from "express";
44
import { ensureTestEnv } from "./helpers.js";
55

66
ensureTestEnv();
7-
const { parseModelShortcut } = await import("../src/config/model-shortcuts.js");
8-
const { getModelShortcuts, settingsForApi, saveSettings } = await import("../src/config/settings.js");
7+
const { parseModelShortcut, defaultModelShortcuts, modelShortcutsFromSettings } = await import("../src/config/model-shortcuts.js");
8+
const { getModelShortcuts, settingsForApi, saveSettings, getSettings } = await import("../src/config/settings.js");
99
const { createAdminRouter } = await import("../src/web/routes/admin.js");
1010

1111
const app = express();
@@ -24,6 +24,42 @@ async function put(modelShortcuts, expectedStatus = 200) {
2424
return response.json();
2525
}
2626

27+
test("fresh installs inherit supported family defaults without persisting version pins", () => {
28+
const defaults = getModelShortcuts();
29+
assert.deepEqual(Object.keys(defaults).sort(), ["astra", "fable", "opus", "sol", "sonet", "terra"]);
30+
assert.equal(defaults.sonet.engine, "claude");
31+
assert.match(defaults.sonet.model, /^claude-sonnet-/);
32+
assert.equal(defaults.sol.engine, "codex");
33+
assert.match(defaults.sol.model, /^gpt-[\d.]+-sol$/);
34+
saveSettings({ sessionKeepalive: "10" });
35+
assert.deepEqual(getModelShortcuts(), defaults, "unrelated saves retain inheritance");
36+
assert.deepEqual(settingsForApi().modelShortcuts, defaults);
37+
assert.equal(Object.hasOwn(getSettings(), "modelShortcuts"), false);
38+
});
39+
40+
test("catalog defaults choose the newest stable family numerically and omit absent families", () => {
41+
const catalogs = {
42+
claude: ["claude-sonnet-5-9", "claude-sonnet-5-10", "claude-sonnet-6-preview", "claude-sonnet-9-20261023", "claude-sonnet-9-2026-10-23", "sonnet", "claude-opus-5-5", "claude-haiku-5-5"].map(value => ({ value })),
43+
codex: ["gpt-6.9-sol", "gpt-6.10-sol", "gpt-6.10-sol-preview", "gpt-7-sol", "gpt-6-astra", "codex"].map(value => ({ value })),
44+
};
45+
const initial = defaultModelShortcuts(catalogs);
46+
assert.equal(initial.sonet.model, "claude-sonnet-5-10");
47+
assert.equal(initial.sol.model, "gpt-7-sol");
48+
assert.equal(defaultModelShortcuts({codex: catalogs.codex.slice(0, 2)}).sol.model, "gpt-6.10-sol");
49+
assert.equal(initial.fable, undefined);
50+
assert.equal(initial.terra, undefined);
51+
const settings = {};
52+
assert.deepEqual(modelShortcutsFromSettings(settings, catalogs), initial);
53+
catalogs.claude.push({ value: "claude-sonnet-6" });
54+
assert.equal(modelShortcutsFromSettings(settings, catalogs).sonet.model, "claude-sonnet-6");
55+
assert.deepEqual(settings, {}, "resolution never freezes a version into settings");
56+
assert.deepEqual(modelShortcutsFromSettings({modelShortcuts: {}}, catalogs), {});
57+
const custom = {sonet: {engine: "claude", model: "claude-sonnet-5-5"}};
58+
assert.deepEqual(modelShortcutsFromSettings({modelShortcuts: custom}, catalogs), custom);
59+
assert.deepEqual(modelShortcutsFromSettings({modelShortcuts: null}, catalogs), {});
60+
assert.deepEqual(defaultModelShortcuts(), {});
61+
});
62+
2763
test("mention suffix parses a shortcut and preserves the task", () => {
2864
assert.deepEqual(parseModelShortcut(":astra build this"), { name: "astra", task: "build this" });
2965
assert.deepEqual(parseModelShortcut(": FABLE"), { name: "fable", task: "" });

0 commit comments

Comments
 (0)