From 86caa94baaa412ce41f91a1aaf23284bb556f03e Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 8 Aug 2026 20:33:48 -0300 Subject: [PATCH] feat(radar): build guided combo suggestions --- src/lib/radar/applyFeed.ts | 7 + src/lib/radar/comboSuggestions.ts | 164 +++++++++++++++++++++ tests/unit/radar-apply-feed.test.ts | 34 +++++ tests/unit/radar-combo-suggestions.test.ts | 134 +++++++++++++++++ 4 files changed, 339 insertions(+) create mode 100644 src/lib/radar/comboSuggestions.ts create mode 100644 tests/unit/radar-combo-suggestions.test.ts diff --git a/src/lib/radar/applyFeed.ts b/src/lib/radar/applyFeed.ts index 55ba532099..a8e9f07ce7 100644 --- a/src/lib/radar/applyFeed.ts +++ b/src/lib/radar/applyFeed.ts @@ -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, diff --git a/src/lib/radar/comboSuggestions.ts b/src/lib/radar/comboSuggestions.ts new file mode 100644 index 0000000000..f3fbdf7547 --- /dev/null +++ b/src/lib/radar/comboSuggestions.ts @@ -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; +} + +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>(); + + 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(); + 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; +} diff --git a/tests/unit/radar-apply-feed.test.ts b/tests/unit/radar-apply-feed.test.ts index 868d0cd76a..cd1a3fc012 100644 --- a/tests/unit/radar-apply-feed.test.ts +++ b/tests/unit/radar-apply-feed.test.ts @@ -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. diff --git a/tests/unit/radar-combo-suggestions.test.ts b/tests/unit/radar-combo-suggestions.test.ts new file mode 100644 index 0000000000..9a83b3555b --- /dev/null +++ b/tests/unit/radar-combo-suggestions.test.ts @@ -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 & Pick +): 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 { + 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); +});