Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.d/maintenance/10779-combo-invocation-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **docs:** Custom combos are only invoked by their exact name in the `model` field — `auto` remains a separate zero-config router, and `openrouter/auto` is a paid OpenRouter product, not an alias ([#10779](https://github.com/diegosouzapw/OmniRoute/pull/10779)) — thanks @maxmad64bis
2 changes: 1 addition & 1 deletion docs/getting-started/AUTO-COMBO-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ Use `auto/smart` when you want the best quality and are okay with occasional exp

### "Can I force a specific provider?"

Yes! Use a combo with `priority` strategy instead of `auto`. See the [Technical Reference](../routing/AUTO-COMBO.md) for details.
Yes! Use a combo with `priority` strategy instead of `auto`, then send the combo's **exact name** as the `model` field (e.g. `model: "my-combo"` — not `auto`). See the [Technical Reference](../routing/AUTO-COMBO.md) for details.

### "How is this different from round-robin?"

Expand Down
22 changes: 22 additions & 0 deletions docs/routing/AUTO-COMBO.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,28 @@ enumerating every existing combo that shadows a model id, so operators who
hit this by accident (rather than intentionally, per #6940) have a signal.
The detection helper lives in `src/lib/combos/modelNameCollision.ts`.

## Calling a Custom Combo From a Client

Persisted combos (Settings → Combos) are only used when the client sends the combo's **exact name** in the `model` field — there is no fuzzy or partial matching of the combo name, and no `auto/` prefix involved. Resolution order (`getComboForModel()` in `src/sse/services/model.ts`):

1. exact combo-name match (`model: "my-combo"`),
2. `combo/<name>` prefix (`model: "combo/my-combo"`),
3. model→combo glob mappings (`/api/model-combo-mappings`).

```bash
curl -X POST http://localhost:20128/v1/chat/completions \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{"model":"my-combo","messages":[{"role":"user","content":"Hello"}]}'
```

Two common pitfalls:

- **`auto` does not use your combos.** `auto`/`auto/*` builds its own zero-config candidate pool and only consults persisted combos if a combo is literally named `auto` (not recommended). To route through a combo, send its exact name — not `auto`.
- **`openrouter/auto` is a real paid OpenRouter product** ("Auto Best Available"), not an OmniRoute alias. It is the single static model entry of the OpenRouter registry (`open-sse/config/providers/registry/openrouter/index.ts`) and is billed separately. Use Settings → Routing → Hide paid models to exclude it from `auto` pools.

See [#7992](https://github.com/diegosouzapw/OmniRoute/issues/7992) and [#7111](https://github.com/diegosouzapw/OmniRoute/issues/7111) for the original confusion this documents.

## How It Works (Persisted Auto-Combos)

The Auto-Combo Engine dynamically selects the best provider/model for each request using a **14-factor scoring function** (defined in `open-sse/services/autoCombo/scoring.ts` → `DEFAULT_WEIGHTS`). Weights form a normalized distribution (custom weights are renormalized by `normalizeScoringWeights()`).
Expand Down
27 changes: 27 additions & 0 deletions src/app/(dashboard)/dashboard/combos/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,33 @@ function ComboUsageGuide({ onHide, onHideForever, onCreateCombo }) {
})}
</div>

<div className="mt-3 rounded-lg border border-primary/20 bg-primary/5 px-3 py-2">
<p className="text-[11px] font-semibold text-primary">
{getI18nOrFallback(t, "usageGuideInvokeTitle", "How to call this combo")}
</p>
<p className="mt-1 text-[11px] leading-4 text-text-muted">
{getI18nOrFallback(
t,
"usageGuideInvokeDesc",
'Send the combo\'s exact name as the model, e.g. model: "my-combo" (or combo/my-combo).'
)}
</p>
<p className="mt-1 text-[11px] leading-4 text-text-muted">
{getI18nOrFallback(
t,
"usageGuideInvokeAutoNote",
"auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto)."
)}
</p>
<p className="mt-1 text-[11px] leading-4 text-text-muted">
{getI18nOrFallback(
t,
"usageGuideInvokeOpenrouterNote",
"openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models."
)}
</p>
</div>

<div className="mt-3 flex items-center gap-2">
<Button size="sm" icon="add" onClick={onCreateCombo}>
{getI18nOrFallback(t, "createFirstCombo", "Create Your First Combo")}
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3606,6 +3606,10 @@
"wizardStep3Desc": "Pick how requests are distributed across your models - 14 strategies available",
"wizardStep4Title": "Review & Save",
"wizardStep4Desc": "Review your configuration and activate the combo",
"usageGuideInvokeTitle": "How to call this combo",
"usageGuideInvokeDesc": "Send the combo's exact name as the model, e.g. model: \"my-combo\" (or combo/my-combo).",
"usageGuideInvokeAutoNote": "auto and auto/* are a separate zero-config router that does not use your combos (unless a combo is literally named auto).",
"usageGuideInvokeOpenrouterNote": "openrouter/auto is a real paid OpenRouter product (Auto Best Available), not an OmniRoute alias — exclude it via Settings → Routing → Hide paid models.",
"emailVisibilityStateOn": "On",
"emailVisibilityStateOff": "Off",
"reorderHandle": "Drag to reorder",
Expand Down
37 changes: 37 additions & 0 deletions tests/unit/combo-guide-invocation-keys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import { readFileSync } from "node:fs";
import path from "node:path";

const require = createRequire(import.meta.url);
const en = require("../../src/i18n/messages/en.json");

const invokeKeys = [
"usageGuideInvokeTitle",
"usageGuideInvokeDesc",
"usageGuideInvokeAutoNote",
"usageGuideInvokeOpenrouterNote",
];

test("combos: usage guide invocation keys exist in en.json with expected copy", () => {
for (const key of invokeKeys) {
assert.ok(
typeof en.combos[key] === "string" && en.combos[key].length > 0,
`combos.${key} missing`
);
}
assert.match(en.combos.usageGuideInvokeDesc, /exact name/);
assert.match(en.combos.usageGuideInvokeAutoNote, /does not use your combos/);
assert.match(en.combos.usageGuideInvokeOpenrouterNote, /paid OpenRouter product/);
});

test("combos: ComboUsageGuide renders the invocation keys via getI18nOrFallback", () => {
const page = readFileSync(
path.join(import.meta.dirname, "../../src/app/(dashboard)/dashboard/combos/page.tsx"),
"utf8"
);
for (const key of invokeKeys) {
assert.ok(page.includes(`"${key}"`), `page.tsx references combos.${key}`);
}
});
Loading