mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 03:32:21 +03:00
feat(radar): build guided combo suggestions
This commit is contained in:
@@ -27,6 +27,8 @@ export interface MergedEntry {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
displayName: string;
|
||||
/** Curated cross-provider model family used for Radar combo suggestions. */
|
||||
familyId?: string | null;
|
||||
monthlyTokens: number;
|
||||
creditTokens: number;
|
||||
freeType:
|
||||
@@ -259,6 +261,9 @@ function mergeOne(
|
||||
if (!overriddenKeys.has("displayName")) {
|
||||
result.displayName = feed.displayName;
|
||||
}
|
||||
if (!overriddenKeys.has("familyId")) {
|
||||
result.familyId = feed.familyId;
|
||||
}
|
||||
if (!overriddenKeys.has("monthlyTokens")) {
|
||||
result.monthlyTokens = feedBudgetToMonthlyTokens(feed.budget);
|
||||
}
|
||||
@@ -293,6 +298,7 @@ function mergeOne(
|
||||
// Apply local overrides (rule 1: they win)
|
||||
if (overrides) {
|
||||
if (overrides.displayName !== undefined) result.displayName = overrides.displayName;
|
||||
if (overrides.familyId !== undefined) result.familyId = overrides.familyId;
|
||||
if (overrides.monthlyTokens !== undefined) result.monthlyTokens = overrides.monthlyTokens;
|
||||
if (overrides.creditTokens !== undefined) result.creditTokens = overrides.creditTokens;
|
||||
if (overrides.freeType !== undefined) result.freeType = overrides.freeType;
|
||||
@@ -330,6 +336,7 @@ function feedModelToMerged(
|
||||
provider: feed.provider,
|
||||
modelId: feed.modelId,
|
||||
displayName: overrides?.displayName ?? feed.displayName,
|
||||
familyId: overrides?.familyId ?? feed.familyId,
|
||||
monthlyTokens: overrides?.monthlyTokens ?? feedBudgetToMonthlyTokens(feed.budget),
|
||||
creditTokens: overrides?.creditTokens ?? 0,
|
||||
freeType: overrides?.freeType ?? feed.freeType,
|
||||
|
||||
164
src/lib/radar/comboSuggestions.ts
Normal file
164
src/lib/radar/comboSuggestions.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { ComboBuilderProviderOption } from "../combos/builderOptions";
|
||||
|
||||
import type { MergedEntry } from "./applyFeed";
|
||||
|
||||
export interface RadarComboSuggestionModel {
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
modelId: string;
|
||||
qualifiedModel: string;
|
||||
displayName: string;
|
||||
monthlyTokens: number;
|
||||
}
|
||||
|
||||
export interface RadarComboSuggestionPayload {
|
||||
name: string;
|
||||
strategy: "priority";
|
||||
models: Array<{
|
||||
kind: "model";
|
||||
providerId: string;
|
||||
model: string;
|
||||
weight: 0;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface RadarComboSuggestion {
|
||||
familyId: string;
|
||||
name: string;
|
||||
alreadyExists: boolean;
|
||||
models: RadarComboSuggestionModel[];
|
||||
payload: RadarComboSuggestionPayload;
|
||||
}
|
||||
|
||||
export interface BuildRadarComboSuggestionsInput {
|
||||
entries: readonly MergedEntry[];
|
||||
providers: readonly ComboBuilderProviderOption[];
|
||||
existingComboNames: Iterable<string>;
|
||||
}
|
||||
|
||||
function compareText(left: string, right: string): number {
|
||||
if (left < right) return -1;
|
||||
if (left > right) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function normalizedIdentity(value: string | null | undefined): string {
|
||||
return value?.trim().toLowerCase() ?? "";
|
||||
}
|
||||
|
||||
function resolveProvider(
|
||||
providerIdentity: string,
|
||||
providers: readonly ComboBuilderProviderOption[]
|
||||
): ComboBuilderProviderOption | null {
|
||||
const identity = normalizedIdentity(providerIdentity);
|
||||
if (!identity) return null;
|
||||
|
||||
const active = providers.filter((provider) => provider.activeConnectionCount > 0);
|
||||
const selectors: Array<(provider: ComboBuilderProviderOption) => string | null | undefined> = [
|
||||
(provider) => provider.providerId,
|
||||
(provider) => provider.alias,
|
||||
(provider) => provider.prefix,
|
||||
];
|
||||
|
||||
for (const select of selectors) {
|
||||
const matches = active.filter((provider) => normalizedIdentity(select(provider)) === identity);
|
||||
if (matches.length === 1) return matches[0];
|
||||
if (matches.length > 1) return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function stableHash(value: string): string {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
hash ^= value.charCodeAt(index);
|
||||
hash = Math.imul(hash, 0x01000193);
|
||||
}
|
||||
return (hash >>> 0).toString(16).padStart(8, "0");
|
||||
}
|
||||
|
||||
function comboNameForFamily(familyId: string): string {
|
||||
const safeFamily =
|
||||
familyId
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9._-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") || "family";
|
||||
const fullName = `radar-${safeFamily}`;
|
||||
if (fullName.length <= 100) return fullName;
|
||||
|
||||
const hash = stableHash(familyId);
|
||||
const availableFamilyLength = 100 - "radar-".length - 1 - hash.length;
|
||||
return `radar-${safeFamily.slice(0, availableFamilyLength)}-${hash}`;
|
||||
}
|
||||
|
||||
function compareModels(left: RadarComboSuggestionModel, right: RadarComboSuggestionModel): number {
|
||||
return (
|
||||
right.monthlyTokens - left.monthlyTokens ||
|
||||
compareText(left.providerId, right.providerId) ||
|
||||
compareText(left.modelId, right.modelId)
|
||||
);
|
||||
}
|
||||
|
||||
/** Build pure, deterministic combo proposals from curated Radar families and live provider options. */
|
||||
export function buildRadarComboSuggestions(
|
||||
input: BuildRadarComboSuggestionsInput
|
||||
): RadarComboSuggestion[] {
|
||||
const families = new Map<string, Map<string, RadarComboSuggestionModel>>();
|
||||
|
||||
for (const entry of input.entries) {
|
||||
const familyId = entry.familyId?.trim();
|
||||
if (!familyId || entry.enabled === false) continue;
|
||||
|
||||
const provider = resolveProvider(entry.provider, input.providers);
|
||||
if (!provider) continue;
|
||||
const model = provider.models.find((candidate) => candidate.id === entry.modelId);
|
||||
if (!model) continue;
|
||||
|
||||
const candidate: RadarComboSuggestionModel = {
|
||||
providerId: provider.providerId,
|
||||
providerName: provider.displayName,
|
||||
modelId: entry.modelId,
|
||||
qualifiedModel: model.qualifiedModel,
|
||||
displayName: entry.displayName,
|
||||
monthlyTokens: entry.monthlyTokens,
|
||||
};
|
||||
const byProvider = families.get(familyId) ?? new Map<string, RadarComboSuggestionModel>();
|
||||
const current = byProvider.get(provider.providerId);
|
||||
if (!current || compareModels(candidate, current) < 0) {
|
||||
byProvider.set(provider.providerId, candidate);
|
||||
}
|
||||
families.set(familyId, byProvider);
|
||||
}
|
||||
|
||||
const existingNames = new Set(
|
||||
[...input.existingComboNames].map((name) => normalizedIdentity(name)).filter(Boolean)
|
||||
);
|
||||
const suggestions: RadarComboSuggestion[] = [];
|
||||
|
||||
for (const familyId of [...families.keys()].sort(compareText)) {
|
||||
const models = [...(families.get(familyId)?.values() ?? [])].sort(compareModels);
|
||||
if (models.length < 2) continue;
|
||||
|
||||
const name = comboNameForFamily(familyId);
|
||||
suggestions.push({
|
||||
familyId,
|
||||
name,
|
||||
alreadyExists: existingNames.has(normalizedIdentity(name)),
|
||||
models,
|
||||
payload: {
|
||||
name,
|
||||
strategy: "priority",
|
||||
models: models.map((model) => ({
|
||||
kind: "model",
|
||||
providerId: model.providerId,
|
||||
model: model.qualifiedModel,
|
||||
weight: 0,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
@@ -746,6 +746,40 @@ test("FIX2 feedModelToMerged path: contextWindow/capabilities/limits/setup survi
|
||||
});
|
||||
});
|
||||
|
||||
test("F3 mergeOne path: familyId survives the feed merge over a baseline entry", () => {
|
||||
const result = applyFeed({
|
||||
baseline: makeBaseline(),
|
||||
feed: [
|
||||
makeFeedModel({
|
||||
provider: "groq",
|
||||
modelId: "llama-3.3-70b-versatile",
|
||||
familyId: "llama-3.3-70b",
|
||||
}),
|
||||
],
|
||||
localOverrides: new Map(),
|
||||
tombstones: new Set(),
|
||||
});
|
||||
|
||||
assert.equal(result.find((entry) => entry.provider === "groq")?.familyId, "llama-3.3-70b");
|
||||
});
|
||||
|
||||
test("F3 feedModelToMerged path: familyId survives for a feed-only entry", () => {
|
||||
const result = applyFeed({
|
||||
baseline: [],
|
||||
feed: [
|
||||
makeFeedModel({
|
||||
provider: "new-provider",
|
||||
modelId: "shared-model",
|
||||
familyId: "shared-family",
|
||||
}),
|
||||
],
|
||||
localOverrides: new Map(),
|
||||
tombstones: new Set(),
|
||||
});
|
||||
|
||||
assert.equal(result[0]?.familyId, "shared-family");
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// Feed `enabled:false` is the safety exception to local override precedence:
|
||||
// a model confirmed dead upstream must not be resurrected locally.
|
||||
|
||||
134
tests/unit/radar-combo-suggestions.test.ts
Normal file
134
tests/unit/radar-combo-suggestions.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import type { ComboBuilderProviderOption } from "../../src/lib/combos/builderOptions.ts";
|
||||
import type { MergedEntry } from "../../src/lib/radar/applyFeed.ts";
|
||||
import { buildRadarComboSuggestions } from "../../src/lib/radar/comboSuggestions.ts";
|
||||
|
||||
function entry(
|
||||
overrides: Partial<MergedEntry> & Pick<MergedEntry, "provider" | "modelId">
|
||||
): MergedEntry {
|
||||
return {
|
||||
provider: overrides.provider,
|
||||
modelId: overrides.modelId,
|
||||
displayName: overrides.displayName ?? overrides.modelId,
|
||||
monthlyTokens: overrides.monthlyTokens ?? 100,
|
||||
creditTokens: 0,
|
||||
freeType: "recurring-daily",
|
||||
poolKey: null,
|
||||
tos: "ok",
|
||||
enabled: true,
|
||||
origin: "radar",
|
||||
familyId: "shared-family",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function provider(
|
||||
providerId: string,
|
||||
modelId: string,
|
||||
overrides: Partial<ComboBuilderProviderOption> = {}
|
||||
): ComboBuilderProviderOption {
|
||||
return {
|
||||
providerId,
|
||||
providerType: providerId,
|
||||
displayName: providerId.toUpperCase(),
|
||||
alias: providerId,
|
||||
icon: "api",
|
||||
color: "#000000",
|
||||
source: "system",
|
||||
acceptsArbitraryModel: false,
|
||||
connectionCount: 1,
|
||||
activeConnectionCount: 1,
|
||||
modelCount: 1,
|
||||
connections: [],
|
||||
models: [
|
||||
{
|
||||
id: modelId,
|
||||
qualifiedModel: `${providerId}/${modelId}`,
|
||||
name: modelId,
|
||||
source: "system",
|
||||
sources: ["system"],
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("two active providers in one family create one deterministic priority suggestion", () => {
|
||||
const suggestions = buildRadarComboSuggestions({
|
||||
entries: [
|
||||
entry({ provider: "groq", modelId: "llama", monthlyTokens: 200 }),
|
||||
entry({ provider: "cerebras", modelId: "llama", monthlyTokens: 300 }),
|
||||
],
|
||||
providers: [provider("groq", "llama"), provider("cerebras", "llama")],
|
||||
existingComboNames: [],
|
||||
});
|
||||
|
||||
assert.equal(suggestions.length, 1);
|
||||
assert.equal(suggestions[0].familyId, "shared-family");
|
||||
assert.equal(suggestions[0].name, "radar-shared-family");
|
||||
assert.equal(suggestions[0].alreadyExists, false);
|
||||
assert.deepEqual(suggestions[0].payload, {
|
||||
name: "radar-shared-family",
|
||||
strategy: "priority",
|
||||
models: [
|
||||
{ kind: "model", providerId: "cerebras", model: "cerebras/llama", weight: 0 },
|
||||
{ kind: "model", providerId: "groq", model: "groq/llama", weight: 0 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("ineligible entries fail closed while alias and prefix match exact provider models", () => {
|
||||
const suggestions = buildRadarComboSuggestions({
|
||||
entries: [
|
||||
entry({ provider: "gq", modelId: "llama", monthlyTokens: 500 }),
|
||||
entry({ provider: "cb", modelId: "llama", monthlyTokens: 400 }),
|
||||
entry({ provider: "inactive", modelId: "llama", monthlyTokens: 900 }),
|
||||
entry({ provider: "disabled", modelId: "llama", enabled: false }),
|
||||
entry({ provider: "missing-model", modelId: "other" }),
|
||||
entry({ provider: "singleton", modelId: "solo", familyId: "solo-family" }),
|
||||
],
|
||||
providers: [
|
||||
provider("groq", "llama", { alias: "gq" }),
|
||||
provider("cerebras", "llama", { prefix: "cb" }),
|
||||
provider("inactive", "llama", { activeConnectionCount: 0 }),
|
||||
provider("disabled", "llama"),
|
||||
provider("missing-model", "llama"),
|
||||
provider("singleton", "solo"),
|
||||
],
|
||||
existingComboNames: new Set(["RADAR-SHARED-FAMILY"]),
|
||||
});
|
||||
|
||||
assert.equal(suggestions.length, 1);
|
||||
assert.equal(suggestions[0].alreadyExists, true);
|
||||
assert.deepEqual(
|
||||
suggestions[0].models.map((model) => model.providerId),
|
||||
["groq", "cerebras"]
|
||||
);
|
||||
});
|
||||
|
||||
test("ambiguous provider aliases, duplicate providers, empty families and unsafe names are closed", () => {
|
||||
const longFamily = `Family / ${"x".repeat(120)}`;
|
||||
const suggestions = buildRadarComboSuggestions({
|
||||
entries: [
|
||||
entry({ provider: "ambiguous", modelId: "m", familyId: "ambiguous-family" }),
|
||||
entry({ provider: "one", modelId: "m", familyId: longFamily, monthlyTokens: 200 }),
|
||||
entry({ provider: "two", modelId: "m", familyId: longFamily, monthlyTokens: 100 }),
|
||||
entry({ provider: "one", modelId: "m", familyId: longFamily, monthlyTokens: 50 }),
|
||||
entry({ provider: "one", modelId: "blank", familyId: " " }),
|
||||
],
|
||||
providers: [
|
||||
provider("ambiguous-a", "m", { alias: "ambiguous" }),
|
||||
provider("ambiguous-b", "m", { alias: "ambiguous" }),
|
||||
provider("one", "m"),
|
||||
provider("two", "m"),
|
||||
],
|
||||
existingComboNames: [],
|
||||
});
|
||||
|
||||
assert.equal(suggestions.length, 1);
|
||||
assert.ok(suggestions[0].name.length <= 100);
|
||||
assert.match(suggestions[0].name, /^[a-zA-Z0-9_/.\-\[\] ]+$/);
|
||||
assert.equal(new Set(suggestions[0].models.map((model) => model.providerId)).size, 2);
|
||||
});
|
||||
Reference in New Issue
Block a user