fix(models): hide unauthenticated catalog entries

This commit is contained in:
Peter Steinberger
2026-04-29 17:55:14 +01:00
parent 2bb16f771b
commit 8a06db084d
12 changed files with 270 additions and 86 deletions

View File

@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Models/UI: hide unauthenticated providers from the default Web chat, `/models`, and model setup pickers while keeping explicit full-catalog browse paths through `view: "all"`, `/models <provider> all`, and `models list --all`. Fixes #74423. Thanks @guarismo and @SymbolStar.
- Exec: reject invalid per-call `host` values instead of silently falling back to the default target, so hostname-like values fail before commands run. Fixes #74426. Thanks @scr00ge-00 and @vyctorbrzezowski.
- Google/Gemini: send non-empty placeholder content when a Gemini run is triggered with empty or filtered user content, avoiding `contents is not specified` API errors. Thanks @CaoYuhaoCarl.
- Heartbeat: preserve non-task `HEARTBEAT.md` context around `tasks:` blocks and apply `agents.defaults.heartbeat` to all agents unless per-agent heartbeat entries restrict scope. Thanks @Sekhar03.

View File

@@ -63,7 +63,7 @@ The same `provider/model` can mean different things depending on where it came f
- User session selections are exact. `/model`, the model picker, `session_status(model=...)`, and `sessions.patch` store `modelOverrideSource: "user"`; if that selected provider/model is unreachable, OpenClaw fails visibly instead of falling through to another configured model.
- Cron `--model` / payload `model` is a per-job primary. It still uses configured fallbacks unless the job supplies explicit payload `fallbacks` (use `fallbacks: []` for a strict cron run).
- CLI default-model and allowlist pickers respect `models.mode: "replace"` by listing explicit `models.providers.*.models` instead of loading the full built-in catalog.
- The Control UI model picker asks the Gateway for its configured model view: `agents.defaults.models` when present, otherwise explicit `models.providers.*.models`, otherwise the full catalog so fresh installs are not blank.
- The Control UI model picker asks the Gateway for its configured model view: `agents.defaults.models` when present, otherwise explicit `models.providers.*.models` plus providers with usable auth. The full built-in catalog is reserved for explicit browse views such as `models.list` with `view: "all"` or `openclaw models list --all`.
## Quick model policy
@@ -219,7 +219,7 @@ openclaw models image-fallbacks clear
### `models list`
Shows configured models by default. Useful flags:
Shows configured/auth-available models by default. Useful flags:
<ParamField path="--all" type="boolean">
Full catalog. Includes bundled provider-owned static catalog rows before auth is configured, so discovery-only views can show models that are unavailable until you add matching provider credentials.

View File

@@ -141,7 +141,7 @@ Current source-of-truth:
- `/elevated [on|off|ask|full]` toggles elevated mode. Alias: `/elev`.
- `/exec host=<auto|sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>` shows or sets exec defaults.
- `/model [name|#|status]` shows or sets the model.
- `/models [provider] [page] [limit=<n>|size=<n>|all]` lists providers or models for a provider.
- `/models [provider] [page] [limit=<n>|size=<n>|all]` lists configured/auth-available providers or models for a provider; add `all` to browse that provider's full catalog.
- `/queue <mode>` manages queue behavior (`steer`, `interrupt`, `followup`, `collect`, `steer-backlog`) plus options like `debounce:2s cap:25 drop:summarize`.
</Accordion>

View File

@@ -155,7 +155,7 @@ Imported themes are stored only in the current browser profile. They are not wri
- During an active send and the final history refresh, the chat view keeps local optimistic user/assistant messages visible if `chat.history` briefly returns an older snapshot; the canonical transcript replaces those local messages once the Gateway history catches up.
- `chat.inject` appends an assistant note to the session transcript and broadcasts a `chat` event for UI-only updates (no agent run, no channel delivery).
- The chat header model and thinking pickers patch the active session immediately through `sessions.patch`; they are persistent session overrides, not one-turn-only send options.
- The chat model picker requests the Gateway's configured model view. If `agents.defaults.models` is present, that allowlist drives the picker. Otherwise the picker shows explicit `models.providers.*.models` entries before falling back to the full catalog for fresh installs.
- The chat model picker requests the Gateway's configured model view. If `agents.defaults.models` is present, that allowlist drives the picker. Otherwise the picker shows explicit `models.providers.*.models` entries plus providers with usable auth. The full catalog stays available through the debug `models.list` RPC with `view: "all"`.
- When fresh Gateway session usage reports show high context pressure, the chat composer area shows a context notice and, at recommended compaction levels, a compact button that runs the normal session compaction path. Stale token snapshots are hidden until the Gateway reports fresh usage again.
</Accordion>

View File

@@ -0,0 +1,65 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { ModelCatalogEntry } from "./model-catalog.js";
import { createProviderAuthChecker } from "./model-provider-auth.js";
import { buildAllowedModelSet, buildConfiguredModelCatalog, modelKey } from "./model-selection.js";
export type ModelCatalogVisibilityView = "default" | "configured" | "all";
function sortModelCatalogEntries(entries: ModelCatalogEntry[]): ModelCatalogEntry[] {
return entries.toSorted(
(a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id),
);
}
function dedupeModelCatalogEntries(entries: ModelCatalogEntry[]): ModelCatalogEntry[] {
const seen = new Set<string>();
const next: ModelCatalogEntry[] = [];
for (const entry of entries) {
const key = modelKey(entry.provider, entry.id);
if (seen.has(key)) {
continue;
}
seen.add(key);
next.push(entry);
}
return next;
}
export function resolveVisibleModelCatalog(params: {
cfg: OpenClawConfig;
catalog: ModelCatalogEntry[];
defaultProvider: string;
defaultModel?: string;
agentId?: string;
agentDir?: string;
env?: NodeJS.ProcessEnv;
view?: ModelCatalogVisibilityView;
}): ModelCatalogEntry[] {
if (params.view === "all") {
return params.catalog;
}
const allowed = buildAllowedModelSet({
cfg: params.cfg,
catalog: params.catalog,
defaultProvider: params.defaultProvider,
defaultModel: params.defaultModel,
agentId: params.agentId,
});
if (!allowed.allowAny && allowed.allowedCatalog.length > 0) {
return sortModelCatalogEntries(allowed.allowedCatalog);
}
const configuredCatalog = sortModelCatalogEntries(
buildConfiguredModelCatalog({ cfg: params.cfg }),
);
const hasAuth = createProviderAuthChecker({
cfg: params.cfg,
agentDir: params.agentDir,
env: params.env,
});
const authBackedCatalog = params.catalog.filter((entry) => hasAuth(entry.provider));
return sortModelCatalogEntries(
dedupeModelCatalogEntries([...configuredCatalog, ...authBackedCatalog]),
);
}

View File

@@ -0,0 +1,60 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
ensureAuthProfileStore,
listProfilesForProvider,
type AuthProfileStore,
} from "./auth-profiles.js";
import { hasUsableCustomProviderApiKey, resolveEnvApiKey } from "./model-auth.js";
import { normalizeProviderId } from "./model-selection.js";
export function hasAuthForModelProvider(params: {
provider: string;
cfg?: OpenClawConfig;
agentDir?: string;
env?: NodeJS.ProcessEnv;
store?: AuthProfileStore;
}): boolean {
const provider = normalizeProviderId(params.provider);
const store =
params.store ??
ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
if (listProfilesForProvider(store, provider).length > 0) {
return true;
}
if (resolveEnvApiKey(provider, params.env)?.apiKey) {
return true;
}
if (hasUsableCustomProviderApiKey(params.cfg, provider, params.env)) {
return true;
}
return false;
}
export function createProviderAuthChecker(params: {
cfg?: OpenClawConfig;
agentDir?: string;
env?: NodeJS.ProcessEnv;
}): (provider: string) => boolean {
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const authCache = new Map<string, boolean>();
return (provider: string) => {
const key = normalizeProviderId(provider);
const cached = authCache.get(key);
if (cached !== undefined) {
return cached;
}
const value = hasAuthForModelProvider({
provider: key,
cfg: params.cfg,
agentDir: params.agentDir,
env: params.env,
store,
});
authCache.set(key, value);
return value;
};
}

View File

@@ -16,6 +16,9 @@ const modelCatalogMocks = vi.hoisted(() => ({
const modelAuthLabelMocks = vi.hoisted(() => ({
resolveModelAuthLabel: vi.fn<(params: unknown) => string | undefined>(() => undefined),
}));
const modelProviderAuthMocks = vi.hoisted(() => ({
authenticatedProviders: new Set(["anthropic", "google", "openai"]),
}));
const MODELS_ADD_DEPRECATED_TEXT =
"⚠️ /models add is deprecated. Use /models to browse providers and /model to switch models.";
@@ -28,6 +31,13 @@ vi.mock("../../agents/model-auth-label.js", () => ({
resolveModelAuthLabel: modelAuthLabelMocks.resolveModelAuthLabel,
}));
vi.mock("../../agents/model-provider-auth.js", () => ({
createProviderAuthChecker: () => (provider: string) =>
modelProviderAuthMocks.authenticatedProviders.has(provider),
hasAuthForModelProvider: ({ provider }: { provider: string }) =>
modelProviderAuthMocks.authenticatedProviders.has(provider),
}));
const telegramModelsTestPlugin: ChannelPlugin = {
...createChannelTestPluginBase({
id: "telegram",
@@ -93,6 +103,7 @@ beforeEach(() => {
]);
modelAuthLabelMocks.resolveModelAuthLabel.mockReset();
modelAuthLabelMocks.resolveModelAuthLabel.mockReturnValue(undefined);
modelProviderAuthMocks.authenticatedProviders = new Set(["anthropic", "google", "openai"]);
setActivePluginRegistry(
createTestRegistry([
...textSurfaceModelsTestPlugins,
@@ -170,6 +181,23 @@ describe("handleModelsCommand", () => {
expect(result?.reply?.text).not.toContain("Add: /models add");
});
it("hides unauthenticated providers by default and keeps all as explicit browse", async () => {
modelProviderAuthMocks.authenticatedProviders = new Set(["anthropic"]);
const providersResult = await handleModelsCommand(buildParams("/models"), true);
expect(providersResult?.reply?.text).toContain("- anthropic (2)");
expect(providersResult?.reply?.text).not.toContain("- google");
expect(providersResult?.reply?.text).not.toContain("- openai");
const defaultListResult = await handleModelsCommand(buildParams("/models openai"), true);
expect(defaultListResult?.reply?.text).toContain("Unknown provider: openai");
const allListResult = await handleModelsCommand(buildParams("/models openai all"), true);
expect(allListResult?.reply?.text).toContain("Models (openai) — showing 1-2 of 2 (page 1/1)");
expect(allListResult?.reply?.text).toContain("- openai/gpt-4.1");
expect(allListResult?.reply?.text).toContain("- openai/gpt-4.1-mini");
});
it("hides legacy runtime providers from /models provider lists", async () => {
modelCatalogMocks.loadModelCatalog.mockResolvedValueOnce([
{ provider: "codex", id: "gpt-5.5", name: "GPT-5.5" },

View File

@@ -1,10 +1,10 @@
import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
import { resolveModelAuthLabel } from "../../agents/model-auth-label.js";
import { resolveVisibleModelCatalog } from "../../agents/model-catalog-visibility.js";
import { loadModelCatalog } from "../../agents/model-catalog.js";
import { isModelPickerVisibleProvider } from "../../agents/model-picker-visibility.js";
import { listLegacyRuntimeModelProviderAliases } from "../../agents/model-runtime-aliases.js";
import {
buildAllowedModelSet,
buildModelAliasIndex,
normalizeProviderId,
resolveBareModelDefaultProvider,
@@ -63,6 +63,7 @@ type ParsedModelsCommand =
export async function buildModelsProviderData(
cfg: OpenClawConfig,
agentId?: string,
options: { view?: "default" | "all" } = {},
): Promise<ModelsProviderData> {
const resolvedDefault = resolveDefaultModelForAgent({
cfg,
@@ -70,12 +71,13 @@ export async function buildModelsProviderData(
});
const catalog = await loadModelCatalog({ config: cfg });
const allowed = buildAllowedModelSet({
const visibleCatalog = resolveVisibleModelCatalog({
cfg,
catalog,
defaultProvider: resolvedDefault.provider,
defaultModel: resolvedDefault.model,
agentId,
view: options.view,
});
const aliasIndex = buildModelAliasIndex({
@@ -140,7 +142,7 @@ export async function buildModelsProviderData(
}
};
for (const entry of allowed.allowedCatalog) {
for (const entry of visibleCatalog) {
add(entry.provider, entry.id);
}
@@ -154,7 +156,7 @@ export async function buildModelsProviderData(
const providers = [...byProvider.keys()].toSorted();
const modelNames = new Map<string, string>();
for (const entry of catalog) {
for (const entry of [...catalog, ...visibleCatalog]) {
if (entry.name && entry.name !== entry.id) {
modelNames.set(`${normalizeProviderId(entry.provider)}/${entry.id}`, entry.name);
}
@@ -340,6 +342,7 @@ export async function resolveModelsCommandReply(params: {
const { byProvider, providers, modelNames } = await buildModelsProviderData(
params.cfg,
params.agentId,
parsed.action === "list" && parsed.all ? { view: "all" } : undefined,
);
const commandPlugin = params.surface ? getChannelPlugin(params.surface) : null;
const providerInfos = buildProviderInfos({ providers, byProvider });

View File

@@ -35,7 +35,9 @@ vi.mock("../agents/auth-profiles.js", () => ({
upsertAuthProfile,
}));
const resolveEnvApiKey = vi.hoisted(() => vi.fn(() => undefined));
const resolveEnvApiKey = vi.hoisted(() =>
vi.fn((_provider: string) => ({ apiKey: "test-key", source: "test" })),
);
const hasUsableCustomProviderApiKey = vi.hoisted(() => vi.fn(() => false));
vi.mock("../agents/model-auth.js", () => ({
resolveEnvApiKey,
@@ -120,6 +122,12 @@ function configuredTextModel(id: string, name: string) {
beforeEach(() => {
vi.clearAllMocks();
loadStaticManifestCatalogRowsForList.mockReturnValue([]);
listProfilesForProvider.mockReturnValue([]);
resolveEnvApiKey.mockImplementation((_provider: string) => ({
apiKey: "test-key",
source: "test",
}));
hasUsableCustomProviderApiKey.mockReturnValue(false);
providerModelPickerContributionRuntime.enabled = false;
resolveOwningPluginIdsForProvider.mockImplementation(({ provider }: { provider: string }) => {
if (provider === "byteplus" || provider === "byteplus-plan") {
@@ -173,6 +181,30 @@ describe("promptDefaultModel", () => {
);
});
it("hides unauthenticated catalog entries from default model choices", async () => {
resolveEnvApiKey.mockReturnValue(undefined);
loadModelCatalog.mockResolvedValue([
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "Claude Sonnet" },
{ provider: "openai", id: "gpt-5.5", name: "GPT-5.5" },
]);
const select = vi.fn(async (params) => params.initialValue as never);
const prompter = makePrompter({ select });
await promptDefaultModel({
config: { agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } } },
prompter,
allowKeep: false,
includeManual: false,
ignoreAllowlist: true,
});
const values = (select.mock.calls[0]?.[0]?.options ?? []).map(
(option: { value: string }) => option.value,
);
expect(values).toEqual(["anthropic/claude-sonnet-4-6"]);
});
it("hides legacy runtime providers from default model choices", async () => {
loadModelCatalog.mockResolvedValue([
{ provider: "codex", id: "gpt-5.5", name: "GPT-5.5" },

View File

@@ -1,15 +1,14 @@
import { ensureAuthProfileStore, listProfilesForProvider } from "../agents/auth-profiles.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { hasUsableCustomProviderApiKey, resolveEnvApiKey } from "../agents/model-auth.js";
import { resolveVisibleModelCatalog } from "../agents/model-catalog-visibility.js";
import { loadModelCatalog } from "../agents/model-catalog.js";
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
import {
isModelPickerVisibleModelRef,
isModelPickerVisibleProvider,
} from "../agents/model-picker-visibility.js";
import { createProviderAuthChecker } from "../agents/model-provider-auth.js";
import { formatLiteralProviderPrefixedModelRef } from "../agents/model-ref-shared.js";
import {
buildAllowedModelSet,
buildConfiguredModelCatalog,
buildModelAliasIndex,
type ModelAliasIndex,
@@ -73,42 +72,6 @@ const loadResolvedModelPickerRuntime = createLazyRuntimeSurface(
({ modelPickerRuntime }) => modelPickerRuntime,
);
function hasAuthForProvider(
provider: string,
cfg: OpenClawConfig,
store: ReturnType<typeof ensureAuthProfileStore>,
) {
if (listProfilesForProvider(store, provider).length > 0) {
return true;
}
if (resolveEnvApiKey(provider)) {
return true;
}
if (hasUsableCustomProviderApiKey(cfg, provider)) {
return true;
}
return false;
}
function createProviderAuthChecker(params: {
cfg: OpenClawConfig;
agentDir?: string;
}): (provider: string) => boolean {
const authStore = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const authCache = new Map<string, boolean>();
return (provider: string) => {
const cached = authCache.get(provider);
if (cached !== undefined) {
return cached;
}
const value = hasAuthForProvider(provider, params.cfg, authStore);
authCache.set(provider, value);
return value;
};
}
function resolveConfiguredModelRaw(cfg: OpenClawConfig): string {
return resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model) ?? "";
}
@@ -744,14 +707,14 @@ export async function promptDefaultModel(
});
const models = ignoreAllowlist
? catalog
: (() => {
const { allowedCatalog } = buildAllowedModelSet({
cfg,
catalog,
defaultProvider: DEFAULT_PROVIDER,
});
return allowedCatalog.length > 0 ? allowedCatalog : catalog;
})();
: resolveVisibleModelCatalog({
cfg,
catalog,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: resolved.model,
agentDir: params.agentDir,
env: params.env,
});
if (models.length === 0) {
return promptManualModel({
prompter: params.prompter,
@@ -786,7 +749,7 @@ export async function promptDefaultModel(
const hasPreferredProvider = preferredProvider
? filteredModels.some((entry) => matchesPreferredProvider?.(entry.provider))
: false;
const hasAuth = createProviderAuthChecker({ cfg, agentDir: params.agentDir });
const hasAuth = createProviderAuthChecker({ cfg, agentDir: params.agentDir, env: params.env });
const literalPrefixProviders = await resolveCachedLiteralPrefixProviders();
// Show the literal form (e.g. nvidia/nvidia/...) in the "Keep current" label
@@ -949,7 +912,7 @@ export async function promptModelAllowlist(params: {
fallbackKeys.length > 0 ||
(params.initialSelections?.length ?? 0) > 0 ||
configuredRaw.length > 0;
const hasAuth = createProviderAuthChecker({ cfg, agentDir: params.agentDir });
const hasAuth = createProviderAuthChecker({ cfg, agentDir: params.agentDir, env: params.env });
const matchesPreferredProvider = preferredProvider
? createPreferredProviderMatcher({
preferredProvider,

View File

@@ -1,6 +1,5 @@
import { DEFAULT_PROVIDER } from "../../agents/defaults.js";
import type { ModelCatalogEntry } from "../../agents/model-catalog.types.js";
import { buildAllowedModelSet, buildConfiguredModelCatalog } from "../../agents/model-selection.js";
import { resolveVisibleModelCatalog } from "../../agents/model-catalog-visibility.js";
import {
ErrorCodes,
errorShape,
@@ -11,12 +10,6 @@ import type { GatewayRequestHandlers } from "./types.js";
type ModelsListView = "default" | "configured" | "all";
function sortModelCatalogEntries(entries: ModelCatalogEntry[]): ModelCatalogEntry[] {
return entries.toSorted(
(a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id),
);
}
function resolveModelsListView(params: Record<string, unknown>): ModelsListView {
return typeof params.view === "string" ? (params.view as ModelsListView) : "default";
}
@@ -42,21 +35,12 @@ export const modelsHandlers: GatewayRequestHandlers = {
respond(true, { models: catalog }, undefined);
return;
}
const allowed = buildAllowedModelSet({
const models = resolveVisibleModelCatalog({
cfg,
catalog,
defaultProvider: DEFAULT_PROVIDER,
view,
});
const configuredCatalog =
view === "configured" ? sortModelCatalogEntries(buildConfiguredModelCatalog({ cfg })) : [];
const models =
view === "configured" && allowed.allowAny && configuredCatalog.length > 0
? configuredCatalog
: allowed.allowedCatalog.length > 0
? allowed.allowedCatalog
: configuredCatalog.length > 0
? configuredCatalog
: catalog;
respond(true, { models }, undefined);
} catch (err) {
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, String(err)));

View File

@@ -3,6 +3,7 @@ import { createServer } from "node:net";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { WebSocket } from "ws";
import { resetModelCatalogCacheForTest } from "../agents/model-catalog.js";
import type { ChannelOutboundAdapter } from "../channels/plugins/types.js";
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js";
import { resolveCanvasHostUrl } from "../infra/canvas-host-url.js";
@@ -153,9 +154,14 @@ describe("gateway server models + voicewake", () => {
: await rpcReq<{ models: ModelCatalogRpcEntry[] }>(ws, "models.list"),
);
const seedPiCatalog = () => {
const setPiCatalog = (entries: PiCatalogFixtureEntry[]) => {
piSdkMock.enabled = true;
piSdkMock.models = buildPiCatalogFixture();
piSdkMock.models = entries;
resetModelCatalogCacheForTest();
};
const seedPiCatalog = () => {
setPiCatalog(buildPiCatalogFixture());
};
const withModelsConfig = async <T>(config: unknown, run: () => Promise<T>): Promise<T> => {
@@ -465,11 +471,11 @@ describe("gateway server models + voicewake", () => {
});
});
test("models.list returns model catalog", async () => {
test("models.list all view returns model catalog", async () => {
seedPiCatalog();
const res1 = await listModels();
const res2 = await listModels();
const res1 = await listModels({ view: "all" });
const res2 = await listModels({ view: "all" });
expect(res1.ok).toBe(true);
expect(res2.ok).toBe(true);
@@ -480,7 +486,7 @@ describe("gateway server models + voicewake", () => {
expect(piSdkMock.discoverCalls).toBe(1);
});
test("models.list keeps default view on the full catalog when no allowlist is configured", async () => {
test("models.list default view uses configured providers instead of the full catalog", async () => {
await withModelsConfig(
{
models: {
@@ -493,10 +499,49 @@ describe("gateway server models + voicewake", () => {
},
},
async () => {
seedPiCatalog();
setPiCatalog([
{ id: "remote-a", provider: "unauth-a", name: "Remote A" },
{ id: "remote-b", provider: "unauth-b", name: "Remote B" },
]);
const res = await listModels();
expect(res.ok).toBe(true);
expect(res.payload?.models).toEqual(expectedSortedCatalog());
expect(res.payload?.models).toEqual([
{
id: "MiniMax-M2.7-highspeed",
name: "MiniMax M2.7 Highspeed",
provider: "minimax",
},
]);
},
);
});
test("models.list configured view includes auth-backed provider catalog entries", async () => {
await withEnvAsync(
{
ANTHROPIC_API_KEY: undefined,
ANTHROPIC_OAUTH_TOKEN: undefined,
OPENAI_API_KEY: "test-openai-key",
},
async () => {
await withModelsConfig({}, async () => {
seedPiCatalog();
const res = await listModels({ view: "configured" });
expect(res.ok).toBe(true);
expect(res.payload?.models).toEqual([
{
id: "gpt-test-a",
name: "A-Model",
provider: "openai",
contextWindow: 8000,
},
{
id: "gpt-test-z",
name: "gpt-test-z",
provider: "openai",
},
]);
});
},
);
});
@@ -518,7 +563,10 @@ describe("gateway server models + voicewake", () => {
},
},
async () => {
seedPiCatalog();
setPiCatalog([
{ id: "remote-a", provider: "unauth-a", name: "Remote A" },
{ id: "remote-b", provider: "unauth-b", name: "Remote B" },
]);
const res = await listModels({ view: "configured" });
expect(res.ok).toBe(true);
expect(res.payload?.models).toEqual([