mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
fix(ui): report a stalled provider-usage refresh on Model Providers
The page observed the incomplete-usage marker but discarded the exhausted outcome, so once the retry budget was spent it rendered ordinary provider cards with no usage and no explanation — indistinguishable from providers that report no usage at all. Keep the outcome and render the warning the Usage page already owns, reusing usage.providerUsage.stalled rather than minting a Model Providers key so no locale baseline churns. A user-initiated refresh now restarts the retry budget. The notice tells the operator to refresh, so the button has to hand back attempts to spend; only the forced path resets it, or the budget could never exhaust. Also fixes tsgo:core:test on the current head: createStore's inferred literal had no usageStats, so the run-bookkeeping case could not stamp it, and view.test.ts needed the new prop. Closes the ClawSweeper P2 at model-providers-page.ts:169-175.
This commit is contained in:
@@ -60,6 +60,9 @@ function createStore(access = "access-one") {
|
||||
expires: 1_000_000,
|
||||
},
|
||||
},
|
||||
// Run bookkeeping stamps this on the real store; declared so a test can add
|
||||
// it without widening the profile shape the rotation case narrows on.
|
||||
usageStats: undefined as Record<string, { lastUsed: number }> | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -220,6 +220,38 @@ describe("ModelProvidersPage usage convergence", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reports a stalled provider refresh once the retry budget is spent", async () => {
|
||||
vi.useFakeTimers();
|
||||
const harness = createHarness("main");
|
||||
harness.setUsageStatus({ updatedAt: 1, providers: [], refreshing: true });
|
||||
const page = appendPage(harness.context);
|
||||
await page.updateComplete;
|
||||
|
||||
// Nothing is visible while retries are still in flight: a converging load is
|
||||
// not a failure and must not warn.
|
||||
expect(page.textContent ?? "").not.toContain("did not finish loading");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(15_000);
|
||||
await page.updateComplete;
|
||||
|
||||
// Budget spent and the payload is still incomplete. Rendering the ordinary
|
||||
// cards with no usage and no notice is indistinguishable from a provider
|
||||
// that simply reports none.
|
||||
expect(page.textContent ?? "").toContain("did not finish loading");
|
||||
|
||||
// The notice says "Refresh to retry", so a manual refresh has to hand back a
|
||||
// budget — otherwise the button is a dead end and nothing ever converges.
|
||||
const callsBeforeManual = harness.request.mock.calls.filter(
|
||||
([method]) => method === "usage.status",
|
||||
).length;
|
||||
page.querySelector<HTMLButtonElement>(".settings-section__actions button")?.click();
|
||||
await page.updateComplete;
|
||||
await vi.advanceTimersByTimeAsync(15_000);
|
||||
expect(
|
||||
harness.request.mock.calls.filter(([method]) => method === "usage.status").length,
|
||||
).toBeGreaterThan(callsBeforeManual + 1);
|
||||
});
|
||||
|
||||
it("replaces a pending pre-disconnect load before it can publish", async () => {
|
||||
const harness = createHarness("main");
|
||||
harness.setUsageStatus({ updatedAt: 1, providers: [] });
|
||||
|
||||
@@ -83,6 +83,8 @@ export class ModelProvidersPage extends OpenClawLightDomElement {
|
||||
@state() private addProviderKey = "";
|
||||
@state() private defaultsDraft: DefaultModelSelection | null = null;
|
||||
@state() private selectedAgentId = "";
|
||||
/** Retry budget spent while usage was still incomplete; the cards lack usage. */
|
||||
@state() private providerUsageStalled = false;
|
||||
|
||||
/** Client the current data was loaded from; a new client means stale data. */
|
||||
private dataClient: GatewayBrowserClient | null = null;
|
||||
@@ -166,13 +168,14 @@ export class ModelProvidersPage extends OpenClawLightDomElement {
|
||||
this.dataClient = client;
|
||||
// The connection epoch scopes the retry budget: a reconnect is a fresh
|
||||
// Gateway cache generation and must not inherit the old attempt count.
|
||||
// The "exhausted" state is deliberately dropped here: these cards render from
|
||||
// models.authStatus and stay useful without usage, and this page's
|
||||
// failed-usage notice is owned separately. Usage owns the visible outcome.
|
||||
this.usageRetry.observe(
|
||||
data !== null && isUsageIncomplete(data.providerUsage),
|
||||
this.connectionLifecycle.epoch,
|
||||
);
|
||||
// Once the budget is spent nothing will converge the payload, so the page
|
||||
// has to say so — cards without usage and without a notice read exactly
|
||||
// like providers that report no usage at all.
|
||||
this.providerUsageStalled =
|
||||
this.usageRetry.observe(
|
||||
data !== null && isUsageIncomplete(data.providerUsage),
|
||||
this.connectionLifecycle.epoch,
|
||||
) === "exhausted";
|
||||
}
|
||||
|
||||
override updated() {
|
||||
@@ -211,6 +214,8 @@ export class ModelProvidersPage extends OpenClawLightDomElement {
|
||||
|
||||
private resetConnectionState(client: GatewayBrowserClient | null, connected: boolean) {
|
||||
this.usageRetry.useConnection(this.connectionLifecycle.epoch);
|
||||
// The new connection gets a fresh budget, so the old verdict is stale.
|
||||
this.providerUsageStalled = false;
|
||||
// A null run advances Task's call id, so a late pre-transition result cannot
|
||||
// publish even when the underlying request ignores AbortSignal.
|
||||
void this.refreshTask.run([null, this.selectedAgentId, false]);
|
||||
@@ -271,6 +276,13 @@ export class ModelProvidersPage extends OpenClawLightDomElement {
|
||||
if (!client || !this.selectedAgentId) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (opts.force) {
|
||||
// The stalled notice tells the operator to refresh, so a user-initiated
|
||||
// refresh has to hand back a budget to spend. Only the forced path resets
|
||||
// it: doing this on the retry callback's own refresh would make the
|
||||
// budget unspendable and the notice unreachable.
|
||||
this.usageRetry.startCycle();
|
||||
}
|
||||
return this.refreshTask.run([client, this.selectedAgentId, opts.force]);
|
||||
}
|
||||
|
||||
@@ -634,6 +646,7 @@ export class ModelProvidersPage extends OpenClawLightDomElement {
|
||||
),
|
||||
canMutate: this.canMutate(),
|
||||
mutationBlockedReason: blockedReason,
|
||||
providerUsageStalled: this.providerUsageStalled,
|
||||
probeAvailable: !this.probeUnsupported && advertised !== false,
|
||||
busy: this.busy,
|
||||
messages: this.messages,
|
||||
|
||||
@@ -46,6 +46,7 @@ function props(overrides: Partial<ModelProvidersViewProps> = {}): ModelProviders
|
||||
unconfiguredProviders: [{ id: "anthropic", displayName: "Anthropic" }],
|
||||
canMutate: true,
|
||||
mutationBlockedReason: null,
|
||||
providerUsageStalled: false,
|
||||
probeAvailable: true,
|
||||
busy: {},
|
||||
messages: {},
|
||||
|
||||
@@ -60,6 +60,8 @@ type ModelProvidersViewProps = {
|
||||
unconfiguredProviders: ProviderOption[];
|
||||
canMutate: boolean;
|
||||
mutationBlockedReason: string | null;
|
||||
/** Usage never converged before the retry budget ran out; cards lack usage. */
|
||||
providerUsageStalled: boolean;
|
||||
probeAvailable: boolean;
|
||||
busy: Record<string, boolean>;
|
||||
messages: Record<string, ModelProviderRowMessage>;
|
||||
@@ -644,6 +646,9 @@ export function renderModelProviders(props: ModelProvidersViewProps) {
|
||||
providerRows,
|
||||
)}
|
||||
${props.quickAddSupported ? renderAddProvider(props) : nothing}
|
||||
${props.providerUsageStalled
|
||||
? html`<div class="callout warning" role="status">${t("usage.providerUsage.stalled")}</div>`
|
||||
: nothing}
|
||||
${props.mutationBlockedReason
|
||||
? html`<div class="callout warning">${props.mutationBlockedReason}</div>`
|
||||
: nothing}
|
||||
|
||||
Reference in New Issue
Block a user