feat: manual model IDs when /models is unavailable

Hosted OpenAI-compatible APIs often serve chat but not GET /models
(Volcengine Ark Agent Plan returns 404), so connect failed. Accept a
typed list of model IDs: merged into discovered models when /models
works, used alone when it does not. The typed list is stored as
customProvider.manualModels so reconnects and pin saves keep it.
This commit is contained in:
iamabhishekt
2026-10-28 16:16:30 -04:00
parent 695948271f
commit b4a13f5c3b
6 changed files with 84 additions and 10 deletions
+2 -1
View File
@@ -99,7 +99,7 @@ export interface EffectiveSettings {
compaction: Required<CompactionSettings>;
/** Custom endpoint, resolved concrete: baseUrl defaults "", models and
* user pins default to empty. */
customProvider: { baseUrl: string; models: CustomModel[]; overrides: Record<string, CustomModelOverride> };
customProvider: { baseUrl: string; models: CustomModel[]; overrides: Record<string, CustomModelOverride>; manualModels: string[] };
}
/** The conversation pair's chain (BYOK, owner 2026-09-22): the chat's own
@@ -230,6 +230,7 @@ export function resolveSettings(record: UserSettings | null): EffectiveSettings
baseUrl: record?.customProvider?.baseUrl ?? "",
models: record?.customProvider?.models ?? [],
overrides: record?.customProvider?.overrides ?? {},
manualModels: record?.customProvider?.manualModels ?? [],
},
};
}
+9
View File
@@ -259,6 +259,13 @@ export interface CustomProviderSettings {
/** User pins keyed by model id — pure user intent, never rewritten by
* discovery, pruned to the endpoint's live ids on connect. */
overrides?: Record<string, CustomModelOverride>;
manualModels?: string[];
}
export function parseModelIds(raw: unknown): string[] {
const items = typeof raw === "string" ? raw.split(/[\s,]+/) : Array.isArray(raw) ? raw : [];
const ids = items.filter((v): v is string => typeof v === "string").map((v) => v.trim()).filter(Boolean);
return [...new Set(ids)];
}
/** Canonical base-URL form (single source): trims, requires absolute http(s),
@@ -644,6 +651,8 @@ function sanitizeCustomProvider(raw: Record<string, unknown>): CustomProviderSet
}
const overrides = sanitizeCustomModelOverrides(raw.overrides);
if (Object.keys(overrides).length) out.overrides = overrides;
const manualModels = parseModelIds(raw.manualModels);
if (manualModels.length) out.manualModels = manualModels;
return out;
}
+6
View File
@@ -131,6 +131,12 @@ export async function discoverModels(baseUrl: string, apiKey?: string): Promise<
}
}
export function mergeManualModels(discovered: readonly CustomModel[], manualIds: readonly string[]): CustomModel[] {
const byId = new Map(discovered.map((m) => [m.id, m]));
for (const id of manualIds) if (!byId.has(id)) byId.set(id, { id });
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
}
/** Model rows from a list response ({data:[…]} | {models:[…]} | bare array),
* or null if unrecognized. */
function modelRows(body: unknown): unknown[] | null {