Skip to content

Commit 2b21beb

Browse files
committed
sync(bfmono): feat(gambit): add gambit.toml model aliases (+19 more) (bfmono@bdc68aae6)
This PR is an automated gambitmono sync of bfmono Gambit packages. - Source: `packages/gambit/` - Core: `packages/gambit-core/` - bfmono rev: bdc68aae6 Changes: - bdc68aae6 feat(gambit): add gambit.toml model aliases - 8ed9e6d9c fix(gambit): include handler decks in model check - 5e10ebb0f infra: add obsidian bft command and fix gambit check mocks - 54a6bfc86 feat(gambit): add model availability check - 951e6c678 feat(gambit): auto-pull missing ollama models - ebc6ed21e feat(gambit): add ollama provider routing - 3f7bf00e7 docs(gambit): document Gambit-first strategy shift - 2cefee730 nits(simulator-ui): small tweaks - 2ae91ed78 fix(simulator-ui): format json consistently - f38dd2df8 feat(simulator-ui): refine tool call display - e1c41537b feat(simulator-ui): refine calibrate summary cards - af2191797 feat(simulator-ui): add status badge component - 02e6d6819 chore(simulator-ui): add "+" to positive scores - 17064a3ab fix(simulator-ui): normalize color tokens - a8a0752ba feat(simulator-ui): make run header toggleable with icon cue - 66bda7f6d docs(gambit): update simulator links for test/grade - 1b7be82fd refactor(simulator-ui): remove session metadata display - 26eb8d7d8 refactor(simulator-ui): simplify raw input details - 642d14f7b refactor(simulator-ui): drop reference sample overlays - 1940ad772 refactor(simulator-ui): drop copy ref button Do not edit this repo directly; make changes in bfmono and re-run the sync.
1 parent f9b9e8f commit 2b21beb

9 files changed

Lines changed: 448 additions & 17 deletions

File tree

docs/external/examples/ollama.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,21 @@ Notes:
2323
Ollama, so the actual model name is `llama3.1`.
2424
- To point at a non-local instance, set `OLLAMA_BASE_URL` (defaults to
2525
`http://localhost:11434/v1`).
26+
27+
## Model aliases
28+
29+
Instead of hard-coding the `ollama/...` string in every deck, define an alias in
30+
`gambit.toml`:
31+
32+
```toml
33+
[models.aliases.randall]
34+
model = "ollama/llama3.1"
35+
36+
[models.aliases.randall.params]
37+
temperature = 0.2
38+
```
39+
40+
Decks can now set `model = "randall"` inside `[modelParams]`. The CLI resolves
41+
the alias before calling Ollama, automatically merging the default params
42+
(`temperature = 0.2` above). CLI flags such as `--model randall` also use the
43+
alias, so swapping the target model only requires editing `gambit.toml`.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
++ area_owner = "engineering" authority = "status" lifecycle = "beta" scope =
2+
"Feature" ++
3+
4+
# Model Aliases in `gambit.toml`
5+
6+
**Published:** 2026-01-24
7+
8+
## What changed
9+
10+
- `gambit.toml` now supports `[models.aliases]` entries so teams can name models
11+
(e.g., `randall`) once and reuse them across decks/CLI flags.
12+
- The CLI automatically resolves aliases, merges default params, and warns when
13+
a deck references an undefined alias.
14+
- `gambit check` validates decks using the resolved models and fails fast if an
15+
alias is missing.
16+
- Demo/init scaffolds include a starter alias plus docs describing how to route
17+
Ollama/OpenRouter targets through the new layer.
18+
19+
## Why
20+
21+
- We needed a single source of truth for per-project model choices so swapping
22+
providers (OpenRouter vs. Ollama) doesn't require editing every deck.
23+
- Bundling default params at the alias level keeps temperature/context-size
24+
tuning consistent and auditable.
25+
- Resolving aliases inside `gambit check` prevents subtle drift between deck
26+
references and provider availability.
27+
28+
## Links
29+
30+
- Code: `packages/gambit/src/project_config.ts`, `packages/gambit/src/cli.ts`,
31+
`packages/gambit/src/commands/check.ts`
32+
- Docs: `packages/gambit/scaffolds/init/gambit.toml`,
33+
`packages/gambit/docs/external/examples/ollama.md`
34+
35+
## Next steps
36+
37+
- Encourage repo owners to define aliases for their canonical models before
38+
adding new decks.
39+
- Extend `gambit check` with richer reporting (e.g., list resolved aliases) if
40+
teams need more visibility.

scaffolds/init/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ workspace with opinionated folders, ready for your own decks/actions/graders.
3434
- `tests/` – synthetic personas/test bots.
3535
- `schemas/` – Zod schemas shared across decks/tests.
3636
- `.gambit/` – local sessions/traces (safe to clear, usually ignored by git).
37-
- `gambit.toml` – workspace configuration for tools/automation.
37+
- `gambit.toml` – workspace configuration (folders + model aliases).
3838

3939
Happy building!

scaffolds/init/gambit.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,10 @@ actions = "actions"
44
graders = "graders"
55
tests = "tests"
66
schemas = "schemas"
7+
8+
[models.aliases.default]
9+
model = "openrouter/openai/gpt-4o-mini"
10+
description = "Workspace default model alias."
11+
12+
[models.aliases.default.params]
13+
temperature = 0.2

src/cli.ts

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import {
2828
printShortUsage,
2929
printUsage,
3030
} from "./cli_args.ts";
31+
import {
32+
createModelAliasResolver,
33+
loadProjectConfig,
34+
} from "./project_config.ts";
3135

3236
const logger = console;
3337
const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434/v1";
@@ -232,6 +236,42 @@ async function main() {
232236
Deno.exit(1);
233237
}
234238

239+
const configHint = deckPath || args.graderPath || args.testDeckPath ||
240+
args.exportDeckPath || Deno.cwd();
241+
let projectConfig: Awaited<ReturnType<typeof loadProjectConfig>> = null;
242+
try {
243+
projectConfig = await loadProjectConfig(configHint);
244+
} catch (err) {
245+
logger.error(
246+
`Failed to load gambit.toml: ${(err as Error).message}`,
247+
);
248+
Deno.exit(1);
249+
}
250+
const modelAliasResolver = createModelAliasResolver(
251+
projectConfig?.config,
252+
);
253+
const warnedMissingAliases = new Set<string>();
254+
const applyModelAlias = (
255+
model?: string,
256+
params?: Record<string, unknown>,
257+
): { model?: string; params?: Record<string, unknown> } => {
258+
const resolution = modelAliasResolver(model);
259+
if (
260+
resolution.missingAlias &&
261+
model &&
262+
!warnedMissingAliases.has(model)
263+
) {
264+
logger.warn(
265+
`[gambit] Model alias "${model}" is not defined in gambit.toml; using literal value.`,
266+
);
267+
warnedMissingAliases.add(model);
268+
}
269+
const mergedParams = resolution.params
270+
? { ...resolution.params, ...(params ?? {}) }
271+
: params;
272+
return { model: resolution.model ?? model, params: mergedParams };
273+
};
274+
235275
if (args.cmd === "grade") {
236276
const graderPath = args.graderPath ?? deckPath;
237277
if (!graderPath) {
@@ -282,6 +322,7 @@ async function main() {
282322
openRouterBaseURL: Deno.env.get("OPENROUTER_BASE_URL") ?? undefined,
283323
ollamaApiKey: Deno.env.get("OLLAMA_API_KEY") ?? undefined,
284324
ollamaBaseURL: Deno.env.get("OLLAMA_BASE_URL") ?? undefined,
325+
modelResolver: modelAliasResolver,
285326
});
286327
return;
287328
}
@@ -304,10 +345,12 @@ async function main() {
304345
baseURL: ollamaBaseURL,
305346
});
306347
const ollamaPrefix = "ollama/";
307-
const ollamaModels = [
348+
const flagModels = [
308349
args.model ?? undefined,
309350
args.modelForce ?? undefined,
310-
]
351+
].filter((model): model is string => Boolean(model));
352+
const ollamaModels = flagModels
353+
.map((model) => applyModelAlias(model).model)
311354
.filter((model): model is string => Boolean(model))
312355
.filter((model) => model.startsWith(ollamaPrefix))
313356
.map((model) => model.slice(ollamaPrefix.length));
@@ -322,16 +365,28 @@ async function main() {
322365
event: import("@bolt-foundry/gambit-core").ResponseEvent,
323366
) => void;
324367
}) => {
325-
if (input.request.model.startsWith(ollamaPrefix)) {
326-
const trimmedModel = input.request.model.slice(ollamaPrefix.length);
368+
const applied = applyModelAlias(
369+
input.request.model,
370+
input.request.params,
371+
);
372+
const request = {
373+
...input.request,
374+
model: applied.model ?? input.request.model,
375+
params: applied.params,
376+
};
377+
if (!request.model) {
378+
throw new Error("Model is required.");
379+
}
380+
if (request.model.startsWith(ollamaPrefix)) {
381+
const trimmedModel = request.model.slice(ollamaPrefix.length);
327382
const ollamaResponses = ollamaProvider.responses;
328383
if (!ollamaResponses) {
329384
throw new Error("Ollama responses are not configured.");
330385
}
331386
return await ollamaResponses({
332387
...input,
333388
request: {
334-
...input.request,
389+
...request,
335390
model: trimmedModel,
336391
},
337392
});
@@ -341,7 +396,10 @@ async function main() {
341396
"OPENROUTER_API_KEY is required for non-ollama models.",
342397
);
343398
}
344-
return await openRouterProvider.responses(input);
399+
return await openRouterProvider.responses({
400+
...input,
401+
request,
402+
});
345403
},
346404
chat: async (input: {
347405
model: string;
@@ -352,16 +410,25 @@ async function main() {
352410
onStreamText?: (chunk: string) => void;
353411
params?: Record<string, unknown>;
354412
}) => {
355-
if (input.model.startsWith(ollamaPrefix)) {
356-
const model = input.model.slice(ollamaPrefix.length);
357-
return await ollamaProvider.chat({ ...input, model });
413+
const applied = applyModelAlias(input.model, input.params);
414+
const request = {
415+
...input,
416+
model: applied.model ?? input.model,
417+
params: applied.params,
418+
};
419+
if (!request.model) {
420+
throw new Error("Model is required.");
421+
}
422+
if (request.model.startsWith(ollamaPrefix)) {
423+
const model = request.model.slice(ollamaPrefix.length);
424+
return await ollamaProvider.chat({ ...request, model });
358425
}
359426
if (!openRouterProvider) {
360427
throw new Error(
361428
"OPENROUTER_API_KEY is required for non-ollama models.",
362429
);
363430
}
364-
return await openRouterProvider.chat(input);
431+
return await openRouterProvider.chat(request);
365432
},
366433
};
367434

src/commands/check.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { assertRejects } from "@std/assert";
22
import * as path from "@std/path";
33
import { handleCheckCommand } from "./check.ts";
4+
import { createModelAliasResolver } from "../project_config.ts";
45

56
async function writeDeck(dir: string, filename: string, contents: string) {
67
const target = path.join(dir, filename);
@@ -200,3 +201,72 @@ Handler deck.`,
200201
globalThis.fetch = originalFetch;
201202
}
202203
});
204+
205+
Deno.test("check resolves model aliases", async () => {
206+
const dir = await Deno.makeTempDir();
207+
const rootDeck = await writeDeck(
208+
dir,
209+
"root.deck.md",
210+
`+++
211+
label = "root"
212+
[modelParams]
213+
model = "randall"
214+
+++
215+
216+
Root deck.`,
217+
);
218+
const originalFetch = globalThis.fetch;
219+
globalThis.fetch = (input: Request | URL | string) => {
220+
const url = resolveUrl(input);
221+
if (url.includes("ollama.test")) {
222+
return Promise.resolve(
223+
new Response(
224+
JSON.stringify({ data: [{ id: "llama3" }] }),
225+
{ status: 200, headers: { "Content-Type": "application/json" } },
226+
),
227+
);
228+
}
229+
return Promise.resolve(new Response("not found", { status: 404 }));
230+
};
231+
try {
232+
await handleCheckCommand({
233+
deckPath: rootDeck,
234+
ollamaBaseURL: "http://ollama.test/v1",
235+
modelResolver: createModelAliasResolver({
236+
models: {
237+
aliases: {
238+
randall: { model: "ollama/llama3" },
239+
},
240+
},
241+
}),
242+
});
243+
} finally {
244+
globalThis.fetch = originalFetch;
245+
}
246+
});
247+
248+
Deno.test("check errors on missing aliases", async () => {
249+
const dir = await Deno.makeTempDir();
250+
const rootDeck = await writeDeck(
251+
dir,
252+
"root.deck.md",
253+
`+++
254+
label = "root"
255+
[modelParams]
256+
model = "randall"
257+
+++
258+
259+
Root deck.`,
260+
);
261+
await assertRejects(
262+
() =>
263+
handleCheckCommand({
264+
deckPath: rootDeck,
265+
modelResolver: createModelAliasResolver({
266+
models: { aliases: { other: { model: "openrouter/foo" } } },
267+
}),
268+
}),
269+
Error,
270+
"Unknown model aliases",
271+
);
272+
});

src/commands/check.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as path from "@std/path";
22
import { loadDeck } from "@bolt-foundry/gambit-core";
3+
import type { ModelAliasResolver } from "../project_config.ts";
34

45
const logger = console;
56
const DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
@@ -109,10 +110,14 @@ function collectDeckRefs(deck: LoadedDeck): Array<string> {
109110
return refs;
110111
}
111112

112-
async function collectDeckModels(rootDeckPath: string): Promise<Set<string>> {
113+
async function collectDeckModels(
114+
rootDeckPath: string,
115+
resolver?: ModelAliasResolver,
116+
): Promise<{ models: Set<string>; missingAliases: Set<string> }> {
113117
const resolvedRoot = path.resolve(rootDeckPath);
114118
const seenDecks = new Set<string>();
115119
const models = new Set<string>();
120+
const missingAliases = new Set<string>();
116121
const queue: Array<string> = [resolvedRoot];
117122

118123
while (queue.length > 0) {
@@ -122,7 +127,15 @@ async function collectDeckModels(rootDeckPath: string): Promise<Set<string>> {
122127
const deck = await loadDeck(deckPath);
123128
seenDecks.add(deck.path);
124129
if (deck.modelParams?.model) {
125-
models.add(deck.modelParams.model);
130+
const resolution = resolver
131+
? resolver(deck.modelParams.model)
132+
: { model: deck.modelParams.model, applied: false };
133+
if (resolution.missingAlias && deck.modelParams.model) {
134+
missingAliases.add(deck.modelParams.model);
135+
}
136+
if (resolution.model) {
137+
models.add(resolution.model);
138+
}
126139
}
127140
const refs = collectDeckRefs(deck);
128141
for (const ref of refs) {
@@ -132,7 +145,7 @@ async function collectDeckModels(rootDeckPath: string): Promise<Set<string>> {
132145
}
133146
}
134147

135-
return models;
148+
return { models, missingAliases };
136149
}
137150

138151
async function fetchProviderModels(opts: {
@@ -182,14 +195,21 @@ export async function handleCheckCommand(opts: {
182195
openRouterBaseURL?: string;
183196
ollamaApiKey?: string;
184197
ollamaBaseURL?: string;
198+
modelResolver?: ModelAliasResolver;
185199
}) {
186-
const models = await collectDeckModels(opts.deckPath);
187-
if (models.size === 0) {
200+
const collected = await collectDeckModels(opts.deckPath, opts.modelResolver);
201+
if (collected.missingAliases.size > 0) {
202+
const missing = Array.from(collected.missingAliases).join(", ");
203+
throw new Error(
204+
`Unknown model aliases: ${missing}. Define them in gambit.toml or update the deck.`,
205+
);
206+
}
207+
if (collected.models.size === 0) {
188208
logger.log("No explicit models found in deck tree.");
189209
return;
190210
}
191211

192-
const resolved = Array.from(models, resolveModelProvider);
212+
const resolved = Array.from(collected.models, resolveModelProvider);
193213
const grouped = new Map<ModelProviderKey, Array<DeckModelInfo>>();
194214
for (const model of resolved) {
195215
const entry = grouped.get(model.provider) ?? [];

0 commit comments

Comments
 (0)