logancyang_obsidian-copilot/src/modelManagement/chatModel/resolveChatBackendModel.test.ts
Zero Liu 89823e1d03
feat(chat): power legacy chat from the model-management chat backend (#2556)
* feat(chat): power legacy chat from the model-management "chat" backend

Legacy chat, project chat, vault QA, quick command, and quick ask now
select models from the model-management "chat" backend
(backends.chat.enabledModels) instead of the legacy activeModels /
defaultModelKey. A new ConfiguredModel→CustomModel bridge feeds the
existing ChatModelManager (the stubbed v4 ChatModelFactory is untouched),
and a new "Quick Chat" curation section under the Agents settings tab
lets users choose which BYOK/Plus models appear in the chat picker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(skills): apply prettier formatting to builtinSkills.test.ts

Collapse a multi-line expect() onto one line to satisfy the project's
prettier config (surfaced when running npm run format).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(chat): stabilize chat backend model selections

* fix(chat): address chat backend review regressions

* fix(chat): show legacy chat models in a flat list

Drop the per-provider _group on chat picker entries so the legacy chat
model picker renders a single flat list (provider icons inline, no
section headers), matching pre-migration behavior. Agent Mode keeps its
per-backend headers via its own helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(chat): remove dead per-model parameter editing

After the model-management chat migration, per-model parameter editing
is unreachable: the chat selection is now a configuredModelId UUID, so
the ChatSettingsPopover editor (gated on a legacy activeModels match)
never renders, and TokenLimitWarning's activeModels lookup always misses
and falls back to global settings. v4 agent mode no longer supports
per-model parameter editing.

- ChatSettingsPopover: drop the model-param plumbing (originalModel /
  bridgedModel / canEditModelParameters, local-model save/debounce,
  ModelParametersEditor); keep System Prompt + Disable Builtin controls.
- TokenLimitWarning: always open the global Copilot settings tab.
- Delete the now-orphaned ModelEditDialog and ModelParametersEditor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(chat): drop dead legacy model-key fallback in streaming session

The streaming chat session's chain cache key fell back to name|provider
when configuredModelId was absent. Both callers (Quick Ask, custom-command
chat) now source their model from useResolvedChatBackendModel ->
configuredModelToCustomModel, which always stamps configuredModelId, so the
fallback was unreachable. Derive the key directly from configuredModelId;
a model without one is treated as no usable model via the existing guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(chat): remove test-only getLegacyChatModelKey helper

The singular getLegacyChatModelKey was exported but had no production
callers — only the barrel re-export and one test assertion referenced it.
Remove it (and its export + assertion); the load-bearing plural
getLegacyChatModelKeys, which powers legacy-key resolution in
isChatModelSelectionForEntry, is kept and now carries the rationale comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(model-management): dedupe catalog-id to provider map

Drop the duplicate CATALOG_ID_TO_LEGACY_PROVIDER table in chatModelSelection
and reuse the now-exported CATALOG_ID_TO_CHAT_PROVIDER from
configuredModelToCustomModel (extended with anthropic/google for the
legacy-key path). Single source of truth for catalog-id to ChatModelProviders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:52:50 -07:00

104 lines
3.6 KiB
TypeScript

import type { ModelManagementApi } from "@/modelManagement/createModelManagement";
import type { ConfiguredModel, Provider } from "@/modelManagement/types/persisted";
import type { EnabledBackendEntry } from "@/modelManagement/types/runtime";
import { resolveChatBackendModel } from "./resolveChatBackendModel";
function provider(id: string, overrides: Partial<Provider> = {}): Provider {
return {
providerId: id,
providerType: "anthropic",
displayName: id,
origin: { kind: "byok" },
addedAt: 0,
apiKeyKeychainId: null,
...overrides,
};
}
function okEntry(configuredModelId: string, prov: Provider): EnabledBackendEntry {
const configuredModel: ConfiguredModel = {
configuredModelId,
providerId: prov.providerId,
info: { id: `${configuredModelId}-wire`, displayName: configuredModelId },
configuredAt: 0,
};
return { configuredModelId, state: "ok", configuredModel, provider: prov };
}
/** Minimal api stub exposing only what the resolver reads. */
function makeApi(
entries: readonly EnabledBackendEntry[],
keyByProvider: Record<string, string | null> = {}
): Pick<ModelManagementApi, "backendConfigRegistry" | "providerRegistry"> {
return {
backendConfigRegistry: {
resolveEnabled: (backend: string) => (backend === "chat" ? entries : []),
} as unknown as ModelManagementApi["backendConfigRegistry"],
providerRegistry: {
getApiKey: async (providerId: string) => keyByProvider[providerId] ?? null,
} as unknown as ModelManagementApi["providerRegistry"],
};
}
describe("resolveChatBackendModel", () => {
it("resolves the preferred model when it is enabled", async () => {
const p = provider("p1");
const api = makeApi([okEntry("a", p), okEntry("b", p)], { p1: "key" });
const result = await resolveChatBackendModel(api, "b");
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.configuredModelId).toBe("b");
expect(result.customModel.apiKey).toBe("key");
}
});
it("falls back to the first enabled model when the preferred id is gone", async () => {
const p = provider("p1");
const api = makeApi([okEntry("a", p), okEntry("b", p)], { p1: "key" });
const result = await resolveChatBackendModel(api, "stale-uuid");
expect(result.ok).toBe(true);
if (result.ok) expect(result.configuredModelId).toBe("a");
});
it("resolves a legacy name|provider selection", async () => {
const p = provider("p1");
const api = makeApi([okEntry("a", p)], { p1: "key" });
const result = await resolveChatBackendModel(api, "a-wire|anthropic");
expect(result.ok).toBe(true);
if (result.ok) expect(result.configuredModelId).toBe("a");
});
it("falls back to the first enabled model when no preference is given", async () => {
const p = provider("p1");
const api = makeApi([okEntry("a", p)], { p1: "key" });
const result = await resolveChatBackendModel(api, undefined);
expect(result.ok).toBe(true);
if (result.ok) expect(result.configuredModelId).toBe("a");
});
it("returns empty when nothing is enabled", async () => {
const api = makeApi([]);
const result = await resolveChatBackendModel(api, "anything");
expect(result).toEqual({ ok: false, reason: "empty" });
});
it("skips broken refs and resolves the first ok entry", async () => {
const p = provider("p1");
const broken: EnabledBackendEntry = { configuredModelId: "x", state: "broken" };
const api = makeApi([broken, okEntry("a", p)], { p1: "key" });
const result = await resolveChatBackendModel(api, "x");
expect(result.ok).toBe(true);
if (result.ok) expect(result.configuredModelId).toBe("a");
});
});