Skip to content

Commit 07e55b3

Browse files
feat(loop-init): add orcarouter model provider (#569)
Add OrcaRouter as a selectable model provider for the --with-foundry implementer stack. Passing --model-provider orcarouter emits a model/orcarouter provider primitive in .foundry/stack.yaml, mirroring the existing minimax provider wiring. The primitive carries OrcaRouter's gateway endpoints (OpenAI- and Anthropic-compatible on the same base URL) and its routing model options (orcarouter/fusion, fusion-flash, auto, free). OrcaRouter is an OpenAI-compatible AI gateway: a single model id routes across many upstream models with adaptive routing, automatic failover, and zero-markup inference. Co-authored-by: dulcestentaciones2920-debug <dulcestentaciones2920-debug@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 65cfa02 commit 07e55b3

4 files changed

Lines changed: 253 additions & 17 deletions

File tree

tools/loop-init/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,22 @@ Every `loop-init` run prints a Foundry CTA; when Loop Ready is **≥ 80**, the C
4040

4141
### Interface provider (implementer preset)
4242

43-
The `implementer` preset defaults to the built-in interface primitive. Pass `--model-provider minimax` to emit a `model/minimax` provider primitive instead. The generated stack always carries both regional endpoints and both model options, so you can switch region/model without regenerating:
43+
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:
4444

4545
```bash
4646
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax
4747
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax --region cn_zh --model MiniMax-M2.7
48+
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider orcarouter
49+
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider orcarouter --model orcarouter/auto
4850
```
4951

5052
| Flag | Values | Default |
5153
|------|--------|---------|
52-
| `--model-provider` | `anthropic`, `minimax` | `anthropic` |
54+
| `--model-provider` | `anthropic`, `minimax`, `orcarouter` | `anthropic` |
5355
| `--region` | `global_en`, `cn_zh` | `global_en` |
54-
| `--model` | `MiniMax-M3`, `MiniMax-M2.7` | `MiniMax-M3` |
56+
| `--model` | `MiniMax-M3`, `MiniMax-M2.7`, `orcarouter/fusion`, `orcarouter/fusion-flash`, `orcarouter/auto`, `orcarouter/free` | `MiniMax-M3` / `orcarouter/fusion` |
5557

56-
MiniMax model options and both endpoints (`global_en``https://api.minimax.io`, `cn_zh``https://api.minimaxi.com`) are written into `.foundry/stack.yaml`.
58+
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`).
5759

5860
## Patterns
5961

tools/loop-init/dist/cli.js

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,60 @@ const MINIMAX_REGIONS = [
106106
];
107107
const MINIMAX_MODEL_IDS = MINIMAX_MODELS.map((m) => m.id);
108108
const MINIMAX_REGION_IDS = MINIMAX_REGIONS.map((r) => r.region);
109+
const ORCAROUTER_DEFAULT_MODEL = 'orcarouter/fusion';
110+
const ORCAROUTER_BASE_URL = 'https://api.orcarouter.ai';
111+
const ORCAROUTER_MODELS = [
112+
{
113+
id: 'orcarouter/fusion',
114+
contextWindow: 1_000_000,
115+
thinking: ['adaptive', 'disabled'],
116+
notes: 'Adaptive-routing flagship; multi-model aggregate with automatic failover',
117+
},
118+
{
119+
id: 'orcarouter/fusion-flash',
120+
contextWindow: 200_000,
121+
thinking: ['adaptive', 'disabled'],
122+
notes: 'Fast adaptive-routing tier for high-volume, latency-sensitive loops',
123+
},
124+
{
125+
id: 'orcarouter/auto',
126+
contextWindow: 1_000_000,
127+
thinking: ['adaptive', 'disabled'],
128+
notes: 'Cost-optimized routing tier',
129+
},
130+
{
131+
id: 'orcarouter/free',
132+
contextWindow: 200_000,
133+
thinking: ['disabled'],
134+
notes: 'Zero-cost tier for report-only loops',
135+
},
136+
];
137+
const ORCAROUTER_MODEL_IDS = ORCAROUTER_MODELS.map((m) => m.id);
138+
/**
139+
* Render the `model/orcarouter` interface primitive for the implementer stack.
140+
* OrcaRouter exposes an OpenAI-compatible gateway that routes a single model
141+
* id across many upstream models. Both protocol shapes live on the same
142+
* gateway base URL — the OpenAI and Anthropic-compatible paths are identical
143+
* and the client picks the wire format via its request headers.
144+
*/
145+
function orcarouterInterfaceYaml(model) {
146+
const models = ORCAROUTER_MODELS.map((m) => [
147+
` - id: ${m.id}`,
148+
` context_window: ${m.contextWindow}`,
149+
` thinking: [${m.thinking.join(', ')}]`,
150+
` notes: "${m.notes}"`,
151+
].join('\n')).join('\n');
152+
return ` - primitive: model/orcarouter
153+
config:
154+
model: ${model}
155+
models:
156+
${models}
157+
endpoints:
158+
global:
159+
openai_base_url: ${ORCAROUTER_BASE_URL}/v1
160+
anthropic_base_url: ${ORCAROUTER_BASE_URL}/v1
161+
docs_root: https://www.orcarouter.ai`;
162+
}
109163
/** Render the `model/minimax` interface primitive for the implementer stack. */
110164
function minimaxInterfaceYaml(model, region) {
111165
const models = MINIMAX_MODELS.map((m) => [
@@ -176,7 +230,9 @@ function foundryStackYaml(stackName, pattern, preset, provider = 'anthropic', re
176230
if (preset === 'implementer') {
177231
const interfaceLayer = provider === 'minimax'
178232
? minimaxInterfaceYaml(model, region)
179-
: ` - primitive: model/anthropic
233+
: provider === 'orcarouter'
234+
? orcarouterInterfaceYaml(model)
235+
: ` - primitive: model/anthropic
180236
config:
181237
model: claude-sonnet-4-6`;
182238
return `name: ${stackName}
@@ -660,9 +716,9 @@ Options:
660716
--with-foundry Also scaffold .foundry/ stack (harness-foundry runtime)
661717
--with-memory Also scaffold memory-engineering tiers and budget
662718
--with-fleet Also scaffold fleet-engineering registry and inbox
663-
--model-provider Implementer interface provider (default: anthropic; minimax)
719+
--model-provider Implementer interface provider (default: anthropic; minimax, orcarouter)
664720
--region MiniMax region when --model-provider minimax (global_en, cn_zh)
665-
--model MiniMax model when --model-provider minimax (MiniMax-M3, MiniMax-M2.7)
721+
--model Provider model when --model-provider minimax/orcarouter (default: MiniMax-M3 / orcarouter/fusion)
666722
--dry-run Print actions without copying
667723
-h, --help This help
668724
@@ -677,13 +733,14 @@ Examples:
677733
npx @cobusgreyling/loop-init . -p pr-babysitter -t claude --with-foundry
678734
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax
679735
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax --region cn_zh --model MiniMax-M2.7
736+
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider orcarouter --model orcarouter/auto
680737
npx @cobusgreyling/loop-init . -p daily-triage -t opencode
681738
npx @cobusgreyling/loop-init . --with-memory
682739
npx @cobusgreyling/loop-init . --with-fleet
683740
`);
684741
process.exit(0);
685742
}
686-
const { pattern, tool, target, dryRun, withFoundry, withMemory, withFleet, modelProvider, region, model } = args;
743+
let { pattern, tool, target, dryRun, withFoundry, withMemory, withFleet, modelProvider, region, model } = args;
687744
const validPatterns = Object.keys(PATTERN_STARTERS);
688745
const validTools = Object.keys(TOOL_SUFFIX);
689746
if (!validPatterns.includes(pattern)) {
@@ -694,7 +751,7 @@ Examples:
694751
console.error(`Unknown tool: ${tool}. Valid: ${validTools.join(', ')}`);
695752
process.exit(1);
696753
}
697-
const validProviders = ['anthropic', 'minimax'];
754+
const validProviders = ['anthropic', 'minimax', 'orcarouter'];
698755
if (!validProviders.includes(modelProvider)) {
699756
console.error(`Unknown model provider: ${modelProvider}. Valid: ${validProviders.join(', ')}`);
700757
process.exit(1);
@@ -709,6 +766,19 @@ Examples:
709766
process.exit(1);
710767
}
711768
}
769+
if (modelProvider === 'orcarouter') {
770+
// `model` defaults to the MiniMax default; if the user did not pass
771+
// `--model` explicitly, fall back to the OrcaRouter default instead.
772+
if (!ORCAROUTER_MODEL_IDS.includes(model)) {
773+
if (model === MINIMAX_DEFAULT_MODEL) {
774+
model = ORCAROUTER_DEFAULT_MODEL;
775+
}
776+
else {
777+
console.error(`Unknown model: ${model}. Valid: ${ORCAROUTER_MODEL_IDS.join(', ')}`);
778+
process.exit(1);
779+
}
780+
}
781+
}
712782
const targetDir = path.resolve(target);
713783
const baseStarter = PATTERN_STARTERS[pattern];
714784
const suffix = TOOL_AGNOSTIC.has(pattern) ? '' : TOOL_SUFFIX[tool];

tools/loop-init/src/cli.ts

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,11 @@ const FOUNDRY_SHOWCASE =
104104

105105
/**
106106
* Model providers selectable for the implementer preset's `interface` layer.
107-
* The implementer stack defaults to the existing primitive; `minimax` emits a
108-
* `model/minimax` provider primitive with regional endpoints and model options.
107+
* The implementer stack defaults to the existing primitive; `minimax` and
108+
* `orcarouter` emit their own provider primitive with endpoints and model
109+
* options instead.
109110
*/
110-
type ModelProvider = 'anthropic' | 'minimax';
111+
type ModelProvider = 'anthropic' | 'minimax' | 'orcarouter';
111112

112113
type MiniMaxRegion = 'global_en' | 'cn_zh';
113114

@@ -169,6 +170,67 @@ const MINIMAX_REGIONS: MiniMaxRegionEndpoint[] = [
169170
const MINIMAX_MODEL_IDS = MINIMAX_MODELS.map((m) => m.id);
170171
const MINIMAX_REGION_IDS = MINIMAX_REGIONS.map((r) => r.region);
171172

173+
const ORCAROUTER_DEFAULT_MODEL = 'orcarouter/fusion';
174+
const ORCAROUTER_BASE_URL = 'https://api.orcarouter.ai';
175+
176+
const ORCAROUTER_MODELS = [
177+
{
178+
id: 'orcarouter/fusion',
179+
contextWindow: 1_000_000,
180+
thinking: ['adaptive', 'disabled'],
181+
notes: 'Adaptive-routing flagship; multi-model aggregate with automatic failover',
182+
},
183+
{
184+
id: 'orcarouter/fusion-flash',
185+
contextWindow: 200_000,
186+
thinking: ['adaptive', 'disabled'],
187+
notes: 'Fast adaptive-routing tier for high-volume, latency-sensitive loops',
188+
},
189+
{
190+
id: 'orcarouter/auto',
191+
contextWindow: 1_000_000,
192+
thinking: ['adaptive', 'disabled'],
193+
notes: 'Cost-optimized routing tier',
194+
},
195+
{
196+
id: 'orcarouter/free',
197+
contextWindow: 200_000,
198+
thinking: ['disabled'],
199+
notes: 'Zero-cost tier for report-only loops',
200+
},
201+
];
202+
203+
const ORCAROUTER_MODEL_IDS = ORCAROUTER_MODELS.map((m) => m.id);
204+
205+
/**
206+
* Render the `model/orcarouter` interface primitive for the implementer stack.
207+
* OrcaRouter exposes an OpenAI-compatible gateway that routes a single model
208+
* id across many upstream models. Both protocol shapes live on the same
209+
* gateway base URL — the OpenAI and Anthropic-compatible paths are identical
210+
* and the client picks the wire format via its request headers.
211+
*/
212+
function orcarouterInterfaceYaml(model: string): string {
213+
const models = ORCAROUTER_MODELS.map((m) =>
214+
[
215+
` - id: ${m.id}`,
216+
` context_window: ${m.contextWindow}`,
217+
` thinking: [${m.thinking.join(', ')}]`,
218+
` notes: "${m.notes}"`,
219+
].join('\n'),
220+
).join('\n');
221+
222+
return ` - primitive: model/orcarouter
223+
config:
224+
model: ${model}
225+
models:
226+
${models}
227+
endpoints:
228+
global:
229+
openai_base_url: ${ORCAROUTER_BASE_URL}/v1
230+
anthropic_base_url: ${ORCAROUTER_BASE_URL}/v1
231+
docs_root: https://www.orcarouter.ai`;
232+
}
233+
172234
/** Render the `model/minimax` interface primitive for the implementer stack. */
173235
function minimaxInterfaceYaml(model: string, region: MiniMaxRegion): string {
174236
const models = MINIMAX_MODELS.map((m) =>
@@ -247,7 +309,9 @@ function foundryStackYaml(
247309
const interfaceLayer =
248310
provider === 'minimax'
249311
? minimaxInterfaceYaml(model, region)
250-
: ` - primitive: model/anthropic
312+
: provider === 'orcarouter'
313+
? orcarouterInterfaceYaml(model)
314+
: ` - primitive: model/anthropic
251315
config:
252316
model: claude-sonnet-4-6`;
253317
return `name: ${stackName}
@@ -832,9 +896,9 @@ Options:
832896
--with-foundry Also scaffold .foundry/ stack (harness-foundry runtime)
833897
--with-memory Also scaffold memory-engineering tiers and budget
834898
--with-fleet Also scaffold fleet-engineering registry and inbox
835-
--model-provider Implementer interface provider (default: anthropic; minimax)
899+
--model-provider Implementer interface provider (default: anthropic; minimax, orcarouter)
836900
--region MiniMax region when --model-provider minimax (global_en, cn_zh)
837-
--model MiniMax model when --model-provider minimax (MiniMax-M3, MiniMax-M2.7)
901+
--model Provider model when --model-provider minimax/orcarouter (default: MiniMax-M3 / orcarouter/fusion)
838902
--dry-run Print actions without copying
839903
-h, --help This help
840904
@@ -849,14 +913,15 @@ Examples:
849913
npx @cobusgreyling/loop-init . -p pr-babysitter -t claude --with-foundry
850914
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax
851915
npx @cobusgreyling/loop-init . -p ci-sweeper -t grok --with-foundry --model-provider minimax --region cn_zh --model MiniMax-M2.7
916+
npx @cobusgreyling/loop-init . -p ci-sweeper -t claude --with-foundry --model-provider orcarouter --model orcarouter/auto
852917
npx @cobusgreyling/loop-init . -p daily-triage -t opencode
853918
npx @cobusgreyling/loop-init . --with-memory
854919
npx @cobusgreyling/loop-init . --with-fleet
855920
`);
856921
process.exit(0);
857922
}
858923

859-
const { pattern, tool, target, dryRun, withFoundry, withMemory, withFleet, modelProvider, region, model } = args;
924+
let { pattern, tool, target, dryRun, withFoundry, withMemory, withFleet, modelProvider, region, model } = args;
860925

861926
const validPatterns = Object.keys(PATTERN_STARTERS) as Pattern[];
862927
const validTools = Object.keys(TOOL_SUFFIX) as Tool[];
@@ -868,7 +933,7 @@ Examples:
868933
console.error(`Unknown tool: ${tool}. Valid: ${validTools.join(', ')}`);
869934
process.exit(1);
870935
}
871-
const validProviders: ModelProvider[] = ['anthropic', 'minimax'];
936+
const validProviders: ModelProvider[] = ['anthropic', 'minimax', 'orcarouter'];
872937
if (!validProviders.includes(modelProvider)) {
873938
console.error(`Unknown model provider: ${modelProvider}. Valid: ${validProviders.join(', ')}`);
874939
process.exit(1);
@@ -883,6 +948,18 @@ Examples:
883948
process.exit(1);
884949
}
885950
}
951+
if (modelProvider === 'orcarouter') {
952+
// `model` defaults to the MiniMax default; if the user did not pass
953+
// `--model` explicitly, fall back to the OrcaRouter default instead.
954+
if (!ORCAROUTER_MODEL_IDS.includes(model)) {
955+
if (model === MINIMAX_DEFAULT_MODEL) {
956+
model = ORCAROUTER_DEFAULT_MODEL;
957+
} else {
958+
console.error(`Unknown model: ${model}. Valid: ${ORCAROUTER_MODEL_IDS.join(', ')}`);
959+
process.exit(1);
960+
}
961+
}
962+
}
886963

887964
const targetDir = path.resolve(target);
888965
const baseStarter = PATTERN_STARTERS[pattern];

0 commit comments

Comments
 (0)