mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): preserve legacy model picker fallback
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import type { GatewayBrowserClient, GatewayHelloOk } from "../../api/gateway.ts";
|
||||
import type {
|
||||
AgentsFilesListResult,
|
||||
AgentsListResult,
|
||||
@@ -68,8 +68,12 @@ function setPageGateway(
|
||||
client: GatewayBrowserClient | null,
|
||||
connected = true,
|
||||
sourceChanged = false,
|
||||
featureMethods?: string[],
|
||||
) {
|
||||
page.gateway.applySnapshot(snapshot(client, connected), { initial: false, sourceChanged });
|
||||
page.gateway.applySnapshot(snapshot(client, connected, featureMethods), {
|
||||
initial: false,
|
||||
sourceChanged,
|
||||
});
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
@@ -83,13 +87,14 @@ function deferred<T>() {
|
||||
function snapshot(
|
||||
client: GatewayBrowserClient | null,
|
||||
connected = true,
|
||||
featureMethods?: string[],
|
||||
): ApplicationGatewaySnapshot {
|
||||
return {
|
||||
client,
|
||||
phase: connected ? "connected" : "stopped",
|
||||
offlineStable: false,
|
||||
canvasPluginSurfaceUrl: null,
|
||||
hello: null,
|
||||
hello: featureMethods ? ({ features: { methods: featureMethods } } as GatewayHelloOk) : null,
|
||||
assistantAgentId: null,
|
||||
sessionKey: "main",
|
||||
lastError: null,
|
||||
@@ -331,6 +336,31 @@ describe("AgentsPage gateway lifecycle", () => {
|
||||
expect(request).toHaveBeenCalledWith("models.list", { view: "configured", agentId: "main" });
|
||||
});
|
||||
|
||||
it("uses agent-scoped chat metadata when a legacy Gateway lacks models.list", async () => {
|
||||
const request = vi.fn(async () => ({
|
||||
models: [
|
||||
{ id: "gpt-5.5", name: "GPT-5.5", provider: "openai", available: true },
|
||||
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", provider: "openai", available: false },
|
||||
],
|
||||
}));
|
||||
const page = document.createElement("openclaw-agents-page") as TestAgentsPage;
|
||||
page.routeData = { panel: "overview" } as AgentsRouteData;
|
||||
setPageGateway(page, { request } as unknown as GatewayBrowserClient, true, false, [
|
||||
"chat.metadata",
|
||||
]);
|
||||
page.agentsSelectedId = "main";
|
||||
|
||||
page.loadActivePanelData();
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(page.chatModelCatalog).toEqual([
|
||||
{ id: "gpt-5.5", name: "GPT-5.5", provider: "openai", available: true },
|
||||
]),
|
||||
);
|
||||
expect(request).toHaveBeenCalledOnce();
|
||||
expect(request).toHaveBeenCalledWith("chat.metadata", { agentId: "main" });
|
||||
});
|
||||
|
||||
it("caches separate configured model catalogs for the default and worker agents", async () => {
|
||||
const defaultModels = [
|
||||
{ id: "default-model", name: "Default account model", provider: "openai" },
|
||||
|
||||
@@ -40,13 +40,14 @@ import {
|
||||
} from "../../lib/cron/index.ts";
|
||||
import {
|
||||
canCallGatewayMethod,
|
||||
isGatewayMethodAdvertised,
|
||||
type GatewayMethodOperatorScope,
|
||||
} from "../../lib/gateway-methods.ts";
|
||||
import { parseAgentSessionKey } from "../../lib/sessions/session-key.ts";
|
||||
import { GatewayPageController } from "../../lit/gateway-page-controller.ts";
|
||||
import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts";
|
||||
import { SubscriptionsController } from "../../lit/subscriptions-controller.ts";
|
||||
import { loadModels } from "../chat/models.ts";
|
||||
import { loadModelPickerModels } from "../chat/models.ts";
|
||||
import { loadAgentFileContent, saveAgentFile } from "./files.ts";
|
||||
import {
|
||||
resetIdentityDraft,
|
||||
@@ -575,7 +576,11 @@ class AgentsPage
|
||||
const request = { client, generation, agentId };
|
||||
this.chatModelCatalogRequest = request;
|
||||
this.chatModelCatalogError = null;
|
||||
void loadModels(client, { agentId, refresh: true })
|
||||
void loadModelPickerModels(client, {
|
||||
agentId,
|
||||
modelsListAdvertised: isGatewayMethodAdvertised(this.gateway.snapshot ?? {}, "models.list"),
|
||||
refresh: true,
|
||||
})
|
||||
.then((models) => {
|
||||
if (this.isCurrentRequest(client, generation, agentId)) {
|
||||
this.chatModelCatalog = models;
|
||||
|
||||
@@ -73,6 +73,23 @@ export async function loadModels(
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
export async function loadModelPickerModels(
|
||||
client: GatewayBrowserClient,
|
||||
opts: {
|
||||
agentId?: string;
|
||||
modelsListAdvertised: boolean | null;
|
||||
refresh?: boolean;
|
||||
signal?: AbortSignal;
|
||||
timeoutMs?: number;
|
||||
},
|
||||
): Promise<ModelCatalogEntry[]> {
|
||||
const models =
|
||||
opts.modelsListAdvertised === false
|
||||
? await requestMetadataModels(client, opts)
|
||||
: await loadModels(client, opts);
|
||||
return models.filter((entry) => entry.available !== false);
|
||||
}
|
||||
|
||||
export function applyModelCatalogResult(models: unknown): ModelCatalogEntry[] | null {
|
||||
if (!Array.isArray(models)) {
|
||||
return null;
|
||||
@@ -100,3 +117,25 @@ async function requestModels(
|
||||
: await client.request<{ models: ModelCatalogEntry[] }>("models.list", params);
|
||||
return result?.models ?? [];
|
||||
}
|
||||
|
||||
async function requestMetadataModels(
|
||||
client: GatewayBrowserClient,
|
||||
opts: { agentId?: string; signal?: AbortSignal; timeoutMs?: number },
|
||||
): Promise<ModelCatalogEntry[]> {
|
||||
const requestOptions =
|
||||
opts.signal || opts.timeoutMs
|
||||
? {
|
||||
...(opts.signal ? { signal: opts.signal } : {}),
|
||||
...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}),
|
||||
}
|
||||
: undefined;
|
||||
const params = opts.agentId ? { agentId: opts.agentId } : {};
|
||||
const result = requestOptions
|
||||
? await client.request<{ models?: ModelCatalogEntry[] }>(
|
||||
"chat.metadata",
|
||||
params,
|
||||
requestOptions,
|
||||
)
|
||||
: await client.request<{ models?: ModelCatalogEntry[] }>("chat.metadata", params);
|
||||
return result?.models ?? [];
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { NewSessionModelControl } from "./model-control.ts";
|
||||
function contextWith(
|
||||
models: ModelCatalogEntry[],
|
||||
runtime = "openclaw",
|
||||
featureMethods: string[] = [],
|
||||
featureMethods: string[] = ["models.list"],
|
||||
) {
|
||||
const request = vi.fn().mockResolvedValue({ models });
|
||||
const context = {
|
||||
@@ -408,6 +408,30 @@ describe("new-session model runtime", () => {
|
||||
expect(container.querySelector('[data-chat-model-catalog-retry="true"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("uses agent-scoped chat metadata when a legacy Gateway lacks models.list", async () => {
|
||||
const models: ModelCatalogEntry[] = [
|
||||
{ id: "gpt-5.5", name: "GPT-5.5", provider: "openai", available: true },
|
||||
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", provider: "openai", available: false },
|
||||
];
|
||||
const { context, request } = contextWith(models, "openclaw", ["chat.metadata"]);
|
||||
const control = new NewSessionModelControl(() => undefined);
|
||||
|
||||
control.load(context, "main", true);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
renderControl(control, context).querySelectorAll("[data-chat-model-option]"),
|
||||
).toHaveLength(1),
|
||||
);
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"chat.metadata",
|
||||
{ agentId: "main" },
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
);
|
||||
expect(request.mock.calls.map(([method]) => method)).toEqual(["chat.metadata"]);
|
||||
expect(renderControl(control, context).textContent).not.toContain("GPT-5.6 Sol");
|
||||
});
|
||||
|
||||
it("keeps a successful empty catalog explicit when its refresh fails", async () => {
|
||||
const refresh = deferred<{ models: ModelCatalogEntry[] }>();
|
||||
const { context, request } = contextWith([]);
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
renderChatModelControls,
|
||||
type ChatModelCatalogState,
|
||||
} from "../chat/components/chat-model-controls.ts";
|
||||
import { loadModels } from "../chat/models.ts";
|
||||
import { loadModelPickerModels } from "../chat/models.ts";
|
||||
import type { NewSessionPreference } from "./preferences.ts";
|
||||
|
||||
const NEW_SESSION_METADATA_RETRY_WINDOW_MS = 60_000;
|
||||
@@ -88,6 +88,7 @@ async function requestNewSessionMetadata(
|
||||
client: NewSessionMetadataClient,
|
||||
agentId: string,
|
||||
signal: AbortSignal,
|
||||
modelsListAdvertised: boolean | null,
|
||||
): Promise<{ models?: ModelCatalogEntry[] }> {
|
||||
const deadlineAt = Date.now() + NEW_SESSION_METADATA_RETRY_WINDOW_MS;
|
||||
let latestStartupError: Error | undefined;
|
||||
@@ -106,8 +107,9 @@ async function requestNewSessionMetadata(
|
||||
}
|
||||
|
||||
try {
|
||||
const models = await loadModels(client, {
|
||||
const models = await loadModelPickerModels(client, {
|
||||
agentId,
|
||||
modelsListAdvertised,
|
||||
refresh: true,
|
||||
signal,
|
||||
timeoutMs: Math.min(DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS, remainingMs),
|
||||
@@ -316,7 +318,11 @@ export class NewSessionModelControl {
|
||||
this.notify();
|
||||
}
|
||||
|
||||
private startMetadataRequest(client: NewSessionMetadataClient, agentId: string) {
|
||||
private startMetadataRequest(
|
||||
client: NewSessionMetadataClient,
|
||||
agentId: string,
|
||||
modelsListAdvertised: boolean | null,
|
||||
) {
|
||||
this.cancelMetadataRequest();
|
||||
const controller = new AbortController();
|
||||
const requestId = ++this.metadataRequestId;
|
||||
@@ -331,7 +337,7 @@ export class NewSessionModelControl {
|
||||
status: this.metadataState.hasSnapshot ? "refreshing" : "loading",
|
||||
});
|
||||
|
||||
void requestNewSessionMetadata(client, agentId, controller.signal).then(
|
||||
void requestNewSessionMetadata(client, agentId, controller.signal, modelsListAdvertised).then(
|
||||
(result) => {
|
||||
// Aborted transports may still resolve. Only the request that still
|
||||
// owns the control may publish catalog data or restore preferences.
|
||||
@@ -470,7 +476,11 @@ export class NewSessionModelControl {
|
||||
this.notify();
|
||||
return;
|
||||
}
|
||||
this.startMetadataRequest(client, normalizedAgentId);
|
||||
this.startMetadataRequest(
|
||||
client,
|
||||
normalizedAgentId,
|
||||
isGatewayMethodAdvertised(snapshot, "models.list"),
|
||||
);
|
||||
}
|
||||
|
||||
isRestoringPreference(): boolean {
|
||||
|
||||
Reference in New Issue
Block a user