fix(dashboard): guard null modelAliases values in model picker#5792
Conversation
Opening Create Combo for a custom provider node threw a TypeError whenever `modelAliases` held a null/undefined value: the node-alias branch in ModelSelectModal called `fullModel.startsWith(...)` on every entry without checking its type first. Extract the filter/map logic into a new `buildNodeAliasModels` helper (modelSelectModalHelpers.ts), mirroring the sibling `buildPassthroughAliasModels` guard from #485, which requires `typeof fullModel === "string"` before calling `.startsWith`. Regression guard: tests/unit/model-select-null-alias-guard-2247.test.ts. Co-authored-by: wahyuzero <wahyuzero@users.noreply.github.com> Inspired-by: decolua/9router#2247
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Code Review
This pull request extracts the logic for building alias-derived model rows for custom providers into a new helper function buildNodeAliasModels and adds a null/undefined guard to prevent TypeErrors when parsing stale or partial model aliases. It also adds unit tests to prevent regressions. The review feedback suggests enhancing type safety by updating the parameter type of modelAliases to allow null/undefined values, which in turn allows removing several redundant type assertions in the component and test files. Additionally, it recommends optimizing the helper function to avoid redundant string replacements.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function buildNodeAliasModels( | ||
| modelAliases: Record<string, string>, | ||
| providerId: string, | ||
| nodePrefix: string | ||
| ): NodeAliasModel[] { | ||
| const prefix = `${providerId}/`; | ||
| return Object.entries(modelAliases || {}) | ||
| .filter(([, fullModel]) => typeof fullModel === "string" && fullModel.startsWith(prefix)) | ||
| .map(([aliasName, fullModel]) => ({ | ||
| id: fullModel.replace(prefix, ""), | ||
| name: aliasName, | ||
| value: `${nodePrefix}/${fullModel.replace(prefix, "")}`, | ||
| source: "alias" as const, | ||
| })); | ||
| } |
There was a problem hiding this comment.
Type Safety & Efficiency Improvement
- Type Safety: Since
modelAliasescan containnullorundefinedvalues (which is the core issue this PR addresses), typing the parameter asRecord<string, string>is technically inaccurate and bypasses TypeScript's strict checks. Typing it asRecord<string, string | null | undefined>and using a proper type guard in.filterensures type safety and prevents future developers from accidentally removing thetypeof fullModel === "string"check as "dead code". - Efficiency: The string replacement
fullModel.replace(prefix, "")is currently performed twice per mapped element. We can compute it once and reuse it.
export function buildNodeAliasModels(
modelAliases: Record<string, string | null | undefined> | null | undefined,
providerId: string,
nodePrefix: string
): NodeAliasModel[] {
const prefix = providerId + "/";
return Object.entries(modelAliases || {})
.filter((entry): entry is [string, string] => {
const [, fullModel] = entry;
return typeof fullModel === "string" && fullModel.startsWith(prefix);
})
.map(([aliasName, fullModel]) => {
const modelId = fullModel.replace(prefix, "");
return {
id: modelId,
name: aliasName,
value: nodePrefix + "/" + modelId,
source: "alias" as const,
};
});
}| const nodeModels = buildNodeAliasModels( | ||
| modelAliases as Record<string, string>, | ||
| providerId, | ||
| nodePrefix | ||
| ); |
There was a problem hiding this comment.
With the updated signature of buildNodeAliasModels that properly types modelAliases as nullable, the redundant type assertion as Record<string, string> can be safely removed.
| const nodeModels = buildNodeAliasModels( | |
| modelAliases as Record<string, string>, | |
| providerId, | |
| nodePrefix | |
| ); | |
| const nodeModels = buildNodeAliasModels( | |
| modelAliases, | |
| providerId, | |
| nodePrefix | |
| ); |
| test("buildNodeAliasModels: skips null/undefined alias values instead of throwing", () => { | ||
| const modelAliases = { | ||
| a: "prov/x", | ||
| b: null, | ||
| c: undefined, | ||
| } as unknown as Record<string, string>; |
There was a problem hiding this comment.
With the updated type signature of buildNodeAliasModels, the type assertion as unknown as Record<string, string> is no longer necessary and can be removed.
| test("buildNodeAliasModels: skips null/undefined alias values instead of throwing", () => { | |
| const modelAliases = { | |
| a: "prov/x", | |
| b: null, | |
| c: undefined, | |
| } as unknown as Record<string, string>; | |
| test("buildNodeAliasModels: skips null/undefined alias values instead of throwing", () => { | |
| const modelAliases = { | |
| a: "prov/x", | |
| b: null, | |
| c: undefined, | |
| }; |
| assert.deepEqual( | ||
| buildNodeAliasModels({ x: undefined as unknown as string }, "prov", "prov"), | ||
| [] | ||
| ); |
There was a problem hiding this comment.
With the updated type signature of buildNodeAliasModels, the type assertion as unknown as string is no longer necessary and can be removed.
| assert.deepEqual( | |
| buildNodeAliasModels({ x: undefined as unknown as string }, "prov", "prov"), | |
| [] | |
| ); | |
| assert.deepEqual( | |
| buildNodeAliasModels({ x: undefined }, "prov", "prov"), | |
| [] | |
| ); |
Summary
Opening Create Combo for a custom provider node crashed with a
TypeErrorwhenevermodelAliasesheld anull/undefinedvalue (a stale/partial entry persisted to settings). The custom-provider ("node") branch inModelSelectModalfiltered aliases with a rawObject.entries(modelAliases).filter(([, fullModel]) => fullModel.startsWith(...)), calling.startsWithwithout first checking the value was a string.The sibling passthrough-alias branch already had this guard (
typeof fullModel === "string", viabuildPassthroughAliasModels/ #485); the node-alias branch did not.Changes
buildNodeAliasModels(modelAliases, providerId, nodePrefix)intosrc/shared/components/modelSelectModalHelpers.ts, mirroringbuildPassthroughAliasModelsbut rewritingvaluewith the node displaynodePrefix. It requirestypeof fullModel === "string"before calling.startsWith.src/shared/components/ModelSelectModal.tsxnow calls the extracted helper instead of the inlineObject.entries(...).filter(...).map(...)block.Attribution
Thanks to @wahyuzero for the original implementation.
Testing
TDD (Hard Rule #18): new test
tests/unit/model-select-null-alias-guard-2247.test.tsfails before the fix (helper missing) and passes after (3/3). Siblingmodel-select-passthrough-alias-485.test.tsunaffected (6/6). ExistingModelSelectModalvitest component tests unaffected (16/16).typecheck:core,eslint,check:docs-syncall clean.