Skip to content

Commit 7e12a33

Browse files
authored
feat(openrouter): surface Fusion panel config (#93005)
Signed-off-by: sallyom <[email protected]>
1 parent a42bda5 commit 7e12a33

4 files changed

Lines changed: 454 additions & 0 deletions

File tree

docs/providers/openrouter.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Bundled fallback examples:
8686
| Model ref | Notes |
8787
| --------------------------------- | ---------------------------- |
8888
| `openrouter/auto` | OpenRouter automatic routing |
89+
| `openrouter/openrouter/fusion` | OpenRouter Fusion router |
8990
| `openrouter/moonshotai/kimi-k2.6` | Kimi K2.6 via MoonshotAI |
9091
| `openrouter/moonshotai/kimi-k2.5` | Kimi K2.5 via MoonshotAI |
9192

@@ -213,6 +214,79 @@ media understanding preflight.
213214
OpenClaw sends OpenRouter STT requests as JSON with base64 audio under
214215
`input_audio` (OpenRouter STT contract), not as multipart OpenAI form uploads.
215216

217+
## Fusion router
218+
219+
Use OpenRouter Fusion when you want one OpenClaw model ref to ask several
220+
OpenRouter models in parallel, have OpenRouter judge their answers, and return a
221+
single final response through the normal OpenRouter provider endpoint. Because
222+
the upstream model slug is `openrouter/fusion`, the OpenClaw model ref includes
223+
both the OpenClaw provider prefix and the upstream OpenRouter namespace:
224+
225+
```bash
226+
openclaw models set openrouter/openrouter/fusion
227+
```
228+
229+
Configure Fusion's panel and judge through the model's `params.extraBody`. Those
230+
fields are forwarded into the OpenRouter chat-completions request body. Fusion
231+
works with either OpenRouter OAuth onboarding or API-key onboarding; if you use
232+
OAuth, omit the `env.OPENROUTER_API_KEY` line from the example below.
233+
234+
```json5
235+
{
236+
env: { OPENROUTER_API_KEY: "sk-or-..." },
237+
agents: {
238+
defaults: {
239+
model: { primary: "openrouter/openrouter/fusion" },
240+
models: {
241+
"openrouter/openrouter/fusion": {
242+
params: {
243+
extraBody: {
244+
plugins: [
245+
{
246+
id: "fusion",
247+
analysis_models: [
248+
"google/gemini-3.5-flash",
249+
"moonshotai/kimi-k2.6",
250+
"deepseek/deepseek-v4-pro",
251+
],
252+
model: "google/gemini-3.5-flash",
253+
},
254+
],
255+
},
256+
},
257+
},
258+
},
259+
},
260+
},
261+
}
262+
```
263+
264+
The `analysis_models` list is the parallel panel, and `model` inside the Fusion
265+
plugin config is the judge model. Do not set top-level `tool_choice` to
266+
`"required"` in normal OpenClaw agent/chat turns to try to force Fusion;
267+
OpenClaw turns may include OpenClaw tool definitions, and a top-level required
268+
tool choice can require one of those tools instead of the Fusion router. When
269+
this Fusion plugin config is present, OpenClaw also adds a sanitized
270+
system-prompt note with the configured analysis models and judge model so the
271+
agent can answer questions about its current Fusion panel. Other `extraBody`
272+
fields are not copied into the prompt.
273+
274+
Fusion is slower by design. OpenRouter may send the same OpenClaw prompt to
275+
multiple analysis models and then run a final judge/synthesis step, so latency is
276+
usually higher than a direct single-model request. Use Fusion for deliberate,
277+
high-quality answers or escalation paths, not as the default for
278+
latency-sensitive chat. For faster responses, keep the panel small and choose
279+
faster analysis and judge models.
280+
281+
Test the configured ref with a one-shot local model call:
282+
283+
```bash
284+
openclaw infer model run --local \
285+
--model openrouter/openrouter/fusion \
286+
--prompt "Reply with exactly: FUSION_OK" \
287+
--json
288+
```
289+
216290
## Authentication and headers
217291

218292
OpenRouter uses a Bearer token with your API key under the hood. OpenRouter

extensions/openrouter/index.test.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,179 @@ describe("openrouter provider hooks", () => {
275275
expect(getOpenRouterModelCapabilitiesMock).toHaveBeenCalledWith("openrouter/auto");
276276
});
277277

278+
it("describes configured Fusion analysis models in the system prompt", async () => {
279+
const provider = await registerSingleProviderPlugin(openrouterPlugin);
280+
const contribution = provider.resolveSystemPromptContribution?.({
281+
provider: "openrouter",
282+
modelId: "openrouter/fusion",
283+
promptMode: "full",
284+
config: {
285+
agents: {
286+
defaults: {
287+
models: {
288+
"openrouter/openrouter/fusion": {
289+
params: {
290+
extraBody: {
291+
plugins: [
292+
{
293+
id: "fusion",
294+
analysis_models: [
295+
"google/gemini-3.5-flash",
296+
"moonshotai/kimi-k2.6",
297+
"deepseek/deepseek-v4-pro",
298+
],
299+
model: "google/gemini-3.5-flash",
300+
},
301+
],
302+
},
303+
},
304+
},
305+
},
306+
},
307+
},
308+
},
309+
} as never);
310+
311+
expect(contribution?.dynamicSuffix).toContain("OpenRouter Fusion Configuration");
312+
expect(contribution?.dynamicSuffix).toContain(
313+
"Analysis models: google/gemini-3.5-flash, moonshotai/kimi-k2.6, deepseek/deepseek-v4-pro.",
314+
);
315+
expect(contribution?.dynamicSuffix).toContain("Final Fusion model: google/gemini-3.5-flash.");
316+
});
317+
318+
it("describes Fusion config from the canonical OpenRouter model key", async () => {
319+
const provider = await registerSingleProviderPlugin(openrouterPlugin);
320+
const contribution = provider.resolveSystemPromptContribution?.({
321+
provider: "openrouter",
322+
modelId: "openrouter/fusion",
323+
promptMode: "full",
324+
config: {
325+
agents: {
326+
defaults: {
327+
models: {
328+
"openrouter/fusion": {
329+
params: {
330+
extraBody: {
331+
plugins: [
332+
{
333+
id: "fusion",
334+
analysis_models: ["deepseek/deepseek-v4-pro"],
335+
},
336+
],
337+
},
338+
},
339+
},
340+
},
341+
},
342+
},
343+
},
344+
} as never);
345+
346+
expect(contribution?.dynamicSuffix).toContain("Analysis models: deepseek/deepseek-v4-pro.");
347+
});
348+
349+
it("matches transport alias precedence for Fusion extra body", async () => {
350+
const provider = await registerSingleProviderPlugin(openrouterPlugin);
351+
const contribution = provider.resolveSystemPromptContribution?.({
352+
provider: "openrouter",
353+
modelId: "openrouter/fusion",
354+
promptMode: "full",
355+
config: {
356+
agents: {
357+
defaults: {
358+
params: {
359+
extra_body: {
360+
plugins: [
361+
{
362+
id: "fusion",
363+
analysis_models: ["google/gemini-3.5-flash"],
364+
},
365+
],
366+
},
367+
},
368+
models: {
369+
"openrouter/fusion": {
370+
params: {
371+
extraBody: {
372+
plugins: [
373+
{
374+
id: "fusion",
375+
analysis_models: ["deepseek/deepseek-v4-pro"],
376+
},
377+
],
378+
},
379+
},
380+
},
381+
},
382+
},
383+
},
384+
},
385+
} as never);
386+
387+
expect(contribution?.dynamicSuffix).toContain("Analysis models: google/gemini-3.5-flash.");
388+
expect(contribution?.dynamicSuffix).not.toContain("deepseek/deepseek-v4-pro");
389+
});
390+
391+
it("keeps arbitrary OpenRouter extraBody fields out of the system prompt", async () => {
392+
const provider = await registerSingleProviderPlugin(openrouterPlugin);
393+
const contribution = provider.resolveSystemPromptContribution?.({
394+
provider: "openrouter",
395+
modelId: "openrouter/fusion",
396+
promptMode: "full",
397+
config: {
398+
agents: {
399+
defaults: {
400+
models: {
401+
"openrouter/openrouter/fusion": {
402+
params: {
403+
extraBody: {
404+
metadata: { private: "do-not-render" },
405+
plugins: [{ id: "not-fusion", model: "private-model" }],
406+
},
407+
},
408+
},
409+
},
410+
},
411+
},
412+
},
413+
} as never);
414+
415+
expect(contribution).toBeUndefined();
416+
});
417+
418+
it("does not describe disabled Fusion plugin config in the system prompt", async () => {
419+
const provider = await registerSingleProviderPlugin(openrouterPlugin);
420+
const contribution = provider.resolveSystemPromptContribution?.({
421+
provider: "openrouter",
422+
modelId: "openrouter/fusion",
423+
promptMode: "full",
424+
config: {
425+
agents: {
426+
defaults: {
427+
models: {
428+
"openrouter/fusion": {
429+
params: {
430+
extraBody: {
431+
plugins: [
432+
{
433+
id: "fusion",
434+
enabled: false,
435+
analysis_models: ["deepseek/deepseek-v4-pro"],
436+
model: "google/gemini-3.5-flash",
437+
},
438+
],
439+
},
440+
},
441+
},
442+
},
443+
},
444+
},
445+
},
446+
} as never);
447+
448+
expect(contribution).toBeUndefined();
449+
});
450+
278451
it("does not include retired stealth models in the bundled catalog", () => {
279452
const modelIds = buildOpenrouterProvider().models?.map((model) => model.id) ?? [];
280453
expect(modelIds).not.toContain("openrouter/hunter-alpha");

0 commit comments

Comments
 (0)