Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions tools/loop-init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,24 @@ Every `loop-init` run prints a Foundry CTA; when Loop Ready is **≥ 80**, the C

### Interface provider (implementer preset)

The `implementer` preset defaults to the built-in interface primitive. Pass `--model-provider minimax` to emit a `model/minimax` provider primitive instead, or `--model-provider orcarouter` to emit a `model/orcarouter` provider primitive. The generated stack always carries the provider endpoints and model options, so you can switch provider/model without regenerating:
The `implementer` preset defaults to the built-in interface primitive. Pass `--model-provider minimax` to emit a `model/minimax` provider primitive instead, `--model-provider orcarouter` to emit a `model/orcarouter` provider primitive, or `--model-provider cheaperinference` to emit a `model/cheaperinference` provider primitive. The generated stack always carries the provider endpoints and model options, so you can switch provider/model without regenerating:

```bash
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax --region cn_zh --model MiniMax-M2.7
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider orcarouter
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider orcarouter --model orcarouter/auto
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider cheaperinference
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider cheaperinference --model claude-sonnet-5
```

| Flag | Values | Default |
|------|--------|---------|
| `--model-provider` | `anthropic`, `minimax`, `orcarouter` | `anthropic` |
| `--model-provider` | `anthropic`, `minimax`, `orcarouter`, `cheaperinference` | `anthropic` |
| `--region` | `global_en`, `cn_zh` | `global_en` |
| `--model` | `MiniMax-M3`, `MiniMax-M2.7`, `orcarouter/fusion`, `orcarouter/fusion-flash`, `orcarouter/auto`, `orcarouter/free` | `MiniMax-M3` / `orcarouter/fusion` |
| `--model` | `MiniMax-M3`, `MiniMax-M2.7`, `orcarouter/fusion`, `orcarouter/fusion-flash`, `orcarouter/auto`, `orcarouter/free`, `gpt-5.4-mini`, `gpt-5.4`, `claude-sonnet-5`, `gemini-3.1-pro`, `deepseek-v4-flash`, `glm-5.3` | `MiniMax-M3` / `orcarouter/fusion` / `gpt-5.4-mini` |

MiniMax model options and both endpoints (`global_en` → `https://api.minimax.io`, `cn_zh` → `https://api.minimaxi.com`) are written into `.foundry/stack.yaml`. The OrcaRouter provider writes the gateway's OpenAI- and Anthropic-compatible endpoints (`https://api.orcarouter.ai`) plus its routing model options (`orcarouter/fusion`, `orcarouter/fusion-flash`, `orcarouter/auto`, `orcarouter/free`).
MiniMax model options and both endpoints (`global_en` → `https://api.minimax.io`, `cn_zh` → `https://api.minimaxi.com`) are written into `.foundry/stack.yaml`. The OrcaRouter provider writes the gateway's OpenAI- and Anthropic-compatible endpoints (`https://api.orcarouter.ai`) plus its routing model options (`orcarouter/fusion`, `orcarouter/fusion-flash`, `orcarouter/auto`, `orcarouter/free`). The Cheaper Inference provider writes the gateway's OpenAI-compatible (`https://api.cheaperinference.com/v1`) and Anthropic-compatible (`https://api.cheaperinference.com`) endpoints plus its model options. Set `CHEAPER_INFERENCE_API_KEY` to a key from [cheaperinference.com/signup](https://cheaperinference.com/signup).

## Patterns

Expand Down
58 changes: 54 additions & 4 deletions tools/loop-init/dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,40 @@ ${models}
anthropic_base_url: ${ORCAROUTER_BASE_URL}/v1
docs_root: https://www.orcarouter.ai`;
}
const CHEAPERINFERENCE_DEFAULT_MODEL = 'gpt-5.4-mini';
const CHEAPERINFERENCE_BASE_URL = 'https://api.cheaperinference.com';
const CHEAPERINFERENCE_MODELS = [
{ id: 'gpt-5.4-mini', contextWindow: 400_000, notes: 'OpenAI' },
{ id: 'gpt-5.4', contextWindow: 1_000_000, notes: 'OpenAI' },
{ id: 'claude-sonnet-5', contextWindow: 1_000_000, notes: 'Anthropic' },
{ id: 'gemini-3.1-pro', contextWindow: 1_048_576, notes: 'Google' },
{ id: 'deepseek-v4-flash', contextWindow: 1_000_000, notes: 'DeepSeek' },
{ id: 'glm-5.3', contextWindow: 1_000_000, notes: 'Z.ai' },
];
const CHEAPERINFERENCE_MODEL_IDS = CHEAPERINFERENCE_MODELS.map((m) => m.id);
/**
* Render the `model/cheaperinference` interface primitive for the implementer
* stack. Cheaper Inference is an OpenAI-compatible LLM gateway with bare model
* ids from several labs. It also serves the Anthropic Messages API; Anthropic
* clients append `/v1/messages` to the base URL.
*/
function cheaperinferenceInterfaceYaml(model) {
const models = CHEAPERINFERENCE_MODELS.map((m) => [
` - id: ${m.id}`,
` context_window: ${m.contextWindow}`,
` notes: "${m.notes}"`,
].join('\n')).join('\n');
return ` - primitive: model/cheaperinference
config:
model: ${model}
models:
${models}
endpoints:
global:
openai_base_url: ${CHEAPERINFERENCE_BASE_URL}/v1
anthropic_base_url: ${CHEAPERINFERENCE_BASE_URL}
docs_root: https://cheaperinference.com/docs`;
}
/** Render the `model/minimax` interface primitive for the implementer stack. */
function minimaxInterfaceYaml(model, region) {
const models = MINIMAX_MODELS.map((m) => [
Expand Down Expand Up @@ -232,7 +266,9 @@ function foundryStackYaml(stackName, pattern, preset, provider = 'anthropic', re
? minimaxInterfaceYaml(model, region)
: provider === 'orcarouter'
? orcarouterInterfaceYaml(model)
: ` - primitive: model/anthropic
: provider === 'cheaperinference'
? cheaperinferenceInterfaceYaml(model)
: ` - primitive: model/anthropic
config:
model: claude-sonnet-4-6`;
return `name: ${stackName}
Expand Down Expand Up @@ -716,9 +752,9 @@ Options:
--with-foundry Also scaffold .foundry/ stack (harness-foundry runtime)
--with-memory Also scaffold memory-engineering tiers and budget
--with-fleet Also scaffold fleet-engineering registry and inbox
--model-provider Implementer interface provider (default: anthropic; minimax, orcarouter)
--model-provider Implementer interface provider (default: anthropic; minimax, orcarouter, cheaperinference)
--region MiniMax region when --model-provider minimax (global_en, cn_zh)
--model Provider model when --model-provider minimax/orcarouter (default: MiniMax-M3 / orcarouter/fusion)
--model Provider model when --model-provider minimax/orcarouter/cheaperinference (default: MiniMax-M3 / orcarouter/fusion / gpt-5.4-mini)
--dry-run Print actions without copying
-h, --help This help

Expand All @@ -734,6 +770,7 @@ Examples:
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax --region cn_zh --model MiniMax-M2.7
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider orcarouter --model orcarouter/auto
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider cheaperinference --model claude-sonnet-5
npx @cobusgreyling/loop-init . -p daily-triage -t opencode
npx @cobusgreyling/loop-init . --with-memory
npx @cobusgreyling/loop-init . --with-fleet
Expand All @@ -751,7 +788,7 @@ Examples:
console.error(`Unknown tool: ${tool}. Valid: ${validTools.join(', ')}`);
process.exit(1);
}
const validProviders = ['anthropic', 'minimax', 'orcarouter'];
const validProviders = ['anthropic', 'minimax', 'orcarouter', 'cheaperinference'];
if (!validProviders.includes(modelProvider)) {
console.error(`Unknown model provider: ${modelProvider}. Valid: ${validProviders.join(', ')}`);
process.exit(1);
Expand Down Expand Up @@ -779,6 +816,19 @@ Examples:
}
}
}
if (modelProvider === 'cheaperinference') {
// `model` defaults to the MiniMax default; if the user did not pass
// `--model` explicitly, fall back to the Cheaper Inference default instead.
if (!CHEAPERINFERENCE_MODEL_IDS.includes(model)) {
if (model === MINIMAX_DEFAULT_MODEL) {
model = CHEAPERINFERENCE_DEFAULT_MODEL;
}
else {
console.error(`Unknown model: ${model}. Valid: ${CHEAPERINFERENCE_MODEL_IDS.join(', ')}`);
process.exit(1);
}
}
}
const targetDir = path.resolve(target);
const baseStarter = PATTERN_STARTERS[pattern];
const suffix = TOOL_AGNOSTIC.has(pattern) ? '' : TOOL_SUFFIX[tool];
Expand Down
72 changes: 64 additions & 8 deletions tools/loop-init/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ const FOUNDRY_SHOWCASE =

/**
* Model providers selectable for the implementer preset's `interface` layer.
* The implementer stack defaults to the existing primitive; `minimax` and
* `orcarouter` emit their own provider primitive with endpoints and model
* options instead.
* The implementer stack defaults to the existing primitive; `minimax`,
* `orcarouter` and `cheaperinference` emit their own provider primitive with
* endpoints and model options instead.
*/
type ModelProvider = 'anthropic' | 'minimax' | 'orcarouter';
type ModelProvider = 'anthropic' | 'minimax' | 'orcarouter' | 'cheaperinference';

type MiniMaxRegion = 'global_en' | 'cn_zh';

Expand Down Expand Up @@ -231,6 +231,47 @@ ${models}
docs_root: https://www.orcarouter.ai`;
}

const CHEAPERINFERENCE_DEFAULT_MODEL = 'gpt-5.4-mini';
const CHEAPERINFERENCE_BASE_URL = 'https://api.cheaperinference.com';

const CHEAPERINFERENCE_MODELS = [
{ id: 'gpt-5.4-mini', contextWindow: 400_000, notes: 'OpenAI' },
{ id: 'gpt-5.4', contextWindow: 1_000_000, notes: 'OpenAI' },
{ id: 'claude-sonnet-5', contextWindow: 1_000_000, notes: 'Anthropic' },
{ id: 'gemini-3.1-pro', contextWindow: 1_048_576, notes: 'Google' },
{ id: 'deepseek-v4-flash', contextWindow: 1_000_000, notes: 'DeepSeek' },
{ id: 'glm-5.3', contextWindow: 1_000_000, notes: 'Z.ai' },
];

const CHEAPERINFERENCE_MODEL_IDS = CHEAPERINFERENCE_MODELS.map((m) => m.id);

/**
* Render the `model/cheaperinference` interface primitive for the implementer
* stack. Cheaper Inference is an OpenAI-compatible LLM gateway with bare model
* ids from several labs. It also serves the Anthropic Messages API; Anthropic
* clients append `/v1/messages` to the base URL.
*/
function cheaperinferenceInterfaceYaml(model: string): string {
const models = CHEAPERINFERENCE_MODELS.map((m) =>
[
` - id: ${m.id}`,
` context_window: ${m.contextWindow}`,
` notes: "${m.notes}"`,
].join('\n'),
).join('\n');

return ` - primitive: model/cheaperinference
config:
model: ${model}
models:
${models}
endpoints:
global:
openai_base_url: ${CHEAPERINFERENCE_BASE_URL}/v1
anthropic_base_url: ${CHEAPERINFERENCE_BASE_URL}
docs_root: https://cheaperinference.com/docs`;
}

/** Render the `model/minimax` interface primitive for the implementer stack. */
function minimaxInterfaceYaml(model: string, region: MiniMaxRegion): string {
const models = MINIMAX_MODELS.map((m) =>
Expand Down Expand Up @@ -311,7 +352,9 @@ function foundryStackYaml(
? minimaxInterfaceYaml(model, region)
: provider === 'orcarouter'
? orcarouterInterfaceYaml(model)
: ` - primitive: model/anthropic
: provider === 'cheaperinference'
? cheaperinferenceInterfaceYaml(model)
: ` - primitive: model/anthropic
config:
model: claude-sonnet-4-6`;
return `name: ${stackName}
Expand Down Expand Up @@ -896,9 +939,9 @@ Options:
--with-foundry Also scaffold .foundry/ stack (harness-foundry runtime)
--with-memory Also scaffold memory-engineering tiers and budget
--with-fleet Also scaffold fleet-engineering registry and inbox
--model-provider Implementer interface provider (default: anthropic; minimax, orcarouter)
--model-provider Implementer interface provider (default: anthropic; minimax, orcarouter, cheaperinference)
--region MiniMax region when --model-provider minimax (global_en, cn_zh)
--model Provider model when --model-provider minimax/orcarouter (default: MiniMax-M3 / orcarouter/fusion)
--model Provider model when --model-provider minimax/orcarouter/cheaperinference (default: MiniMax-M3 / orcarouter/fusion / gpt-5.4-mini)
--dry-run Print actions without copying
-h, --help This help

Expand All @@ -914,6 +957,7 @@ Examples:
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax --region cn_zh --model MiniMax-M2.7
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider orcarouter --model orcarouter/auto
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider cheaperinference --model claude-sonnet-5
npx @cobusgreyling/loop-init . -p daily-triage -t opencode
npx @cobusgreyling/loop-init . --with-memory
npx @cobusgreyling/loop-init . --with-fleet
Expand All @@ -933,7 +977,7 @@ Examples:
console.error(`Unknown tool: ${tool}. Valid: ${validTools.join(', ')}`);
process.exit(1);
}
const validProviders: ModelProvider[] = ['anthropic', 'minimax', 'orcarouter'];
const validProviders: ModelProvider[] = ['anthropic', 'minimax', 'orcarouter', 'cheaperinference'];
if (!validProviders.includes(modelProvider)) {
console.error(`Unknown model provider: ${modelProvider}. Valid: ${validProviders.join(', ')}`);
process.exit(1);
Expand All @@ -960,6 +1004,18 @@ Examples:
}
}
}
if (modelProvider === 'cheaperinference') {
// `model` defaults to the MiniMax default; if the user did not pass
// `--model` explicitly, fall back to the Cheaper Inference default instead.
if (!CHEAPERINFERENCE_MODEL_IDS.includes(model)) {
if (model === MINIMAX_DEFAULT_MODEL) {
model = CHEAPERINFERENCE_DEFAULT_MODEL;
} else {
console.error(`Unknown model: ${model}. Valid: ${CHEAPERINFERENCE_MODEL_IDS.join(', ')}`);
process.exit(1);
}
}
}

const targetDir = path.resolve(target);
const baseStarter = PATTERN_STARTERS[pattern];
Expand Down
88 changes: 88 additions & 0 deletions tools/loop-init/test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,94 @@ test('loop-init --help documents --model-provider orcarouter', async () => {
assert.match(stdout, /orcarouter/);
});

test('loop-init --with-foundry --model-provider cheaperinference emits Cheaper Inference provider primitive', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'loop-init-foundry-cheaperinference-'));
try {
const { stdout } = await exec('node', [
CLI,
dir,
'--pattern',
'ci-sweeper',
'--tool',
'grok',
'--with-foundry',
'--model-provider',
'cheaperinference',
]);
const stack = await readFile(path.join(dir, '.foundry', 'stack.yaml'), 'utf8');
assert.match(stack, /primitive: model\/cheaperinference/);
// default model
assert.match(stack, /model: gpt-5\.4-mini/);
// model options table
assert.match(stack, /- id: gpt-5\.4-mini/);
assert.match(stack, /- id: claude-sonnet-5/);
assert.match(stack, /- id: gemini-3\.1-pro/);
assert.match(stack, /- id: glm-5\.3/);
// OpenAI- and Anthropic-compatible endpoints on the same gateway
assert.match(stack, /openai_base_url: https:\/\/api\.cheaperinference\.com\/v1/);
assert.match(stack, /anthropic_base_url: https:\/\/api\.cheaperinference\.com$/m);
assert.doesNotMatch(stack, /model\/anthropic/);
assert.doesNotMatch(stack, /model\/minimax/);
assert.doesNotMatch(stack, /model\/orcarouter/);
assert.match(stdout, /preset: implementer/);
} finally {
await rm(dir, { recursive: true, force: true });
}
});

test('loop-init --with-foundry cheaperinference --model selects a different model', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'loop-init-foundry-cheaperinference-model-'));
try {
await exec('node', [
CLI,
dir,
'--pattern',
'ci-sweeper',
'--tool',
'grok',
'--with-foundry',
'--model-provider',
'cheaperinference',
'--model',
'claude-sonnet-5',
]);
const stack = await readFile(path.join(dir, '.foundry', 'stack.yaml'), 'utf8');
assert.match(stack, /model: claude-sonnet-5/);
// all model options remain available in config
assert.match(stack, /- id: gpt-5\.4-mini/);
assert.match(stack, /- id: deepseek-v4-flash/);
} finally {
await rm(dir, { recursive: true, force: true });
}
});

test('loop-init rejects unknown cheaperinference model', async () => {
await assert.rejects(
() =>
exec('node', [
CLI,
'.',
'--pattern',
'ci-sweeper',
'--tool',
'grok',
'--with-foundry',
'--model-provider',
'cheaperinference',
'--model',
'not-a-model',
'--dry-run',
]),
(err) => err.stderr?.includes('Unknown model') || err.message?.includes('Unknown model'),
);
});

test('loop-init --help documents --model-provider cheaperinference', async () => {
const { stdout } = await exec('node', [CLI, '--help']);
assert.match(stdout, /--model-provider/);
assert.match(stdout, /cheaperinference/);
});

test('loop-init rejects unknown model provider', async () => {
await assert.rejects(
() =>
Expand Down
Loading