diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 5b5b09244c4..310733b38f0 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -75857675ae94b675f1398444330a8404a4551f59bf7179deabba9884c194b2f8 plugin-sdk-api-baseline.json -1f237dbd11ba7f1d25f6371cb4c5f78b288bca0b92bc397624faaf514f12ea9f plugin-sdk-api-baseline.jsonl +e6da774a43c16fddc77e04b0d2888d06454d1adb84814c8db4fee0f495c1eec1 plugin-sdk-api-baseline.json +ef8b5fd8081dfa05740f6a609144e755d95a19196a1617037dba1213134699df plugin-sdk-api-baseline.jsonl diff --git a/extensions/openai/openai.live.test.ts b/extensions/openai/openai.live.test.ts index 116cd58faea..ae5ffe10eb7 100644 --- a/extensions/openai/openai.live.test.ts +++ b/extensions/openai/openai.live.test.ts @@ -324,9 +324,10 @@ describeLive("openai plugin live", () => { fileName: "reference.png", mime: "image/png", prompt: "Reply with one lowercase word for the dominant center color.", - timeoutMs: 30_000, + timeoutMs: 60_000, agentDir, cfg, + authStore: EMPTY_AUTH_STORE, model: LIVE_VISION_MODEL, provider: "openai", }); @@ -335,5 +336,5 @@ describeLive("openai plugin live", () => { } finally { await fs.rm(agentDir, { recursive: true, force: true }); } - }, 60_000); + }, 120_000); }); diff --git a/src/agents/models.profiles.live.test.ts b/src/agents/models.profiles.live.test.ts index a9f9708ae2e..c76b152addd 100644 --- a/src/agents/models.profiles.live.test.ts +++ b/src/agents/models.profiles.live.test.ts @@ -36,6 +36,7 @@ const LIVE_SETUP_TIMEOUT_MS = Math.max( 1_000, toInt(process.env.OPENCLAW_LIVE_SETUP_TIMEOUT_MS, 45_000), ); +const LIVE_MODELS_JSON_TIMEOUT_MS = resolveLiveModelsJsonTimeoutMs(); const describeLive = LIVE ? describe : describe.skip; @@ -270,6 +271,23 @@ function toInt(value: string | undefined, fallback: number): number { return Number.isFinite(parsed) ? parsed : fallback; } +function resolveLiveModelsJsonTimeoutMs( + modelsJsonTimeoutRaw = process.env.OPENCLAW_LIVE_MODELS_JSON_TIMEOUT_MS, + setupTimeoutMs = LIVE_SETUP_TIMEOUT_MS, +): number { + return Math.max(setupTimeoutMs, toInt(modelsJsonTimeoutRaw, 120_000)); +} + +describe("resolveLiveModelsJsonTimeoutMs", () => { + it("defaults models.json preparation to a longer setup timeout", () => { + expect(resolveLiveModelsJsonTimeoutMs(undefined, 45_000)).toBe(120_000); + }); + + it("never goes below the shared live setup timeout", () => { + expect(resolveLiveModelsJsonTimeoutMs("30000", 45_000)).toBe(45_000); + }); +}); + function resolveTestReasoning( model: Model, ): "minimal" | "low" | "medium" | "high" | "xhigh" | undefined { @@ -427,6 +445,7 @@ describeLive("live models (profile keys)", () => { await withLiveStageTimeout( ensureOpenClawModelsJson(cfg), "[live-models] prepare models.json", + LIVE_MODELS_JSON_TIMEOUT_MS, ); if (!DIRECT_ENABLED) { logProgress( diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index 3170d840701..3fa22cf1ffa 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -318,6 +318,10 @@ function isMeaningful(text: string): boolean { return true; } +function hasEventLoopPromptKeywords(text: string): boolean { + return /\bmicro\s*-?\s*tasks?\b/i.test(text) && /\bmacro\s*-?\s*tasks?\b/i.test(text); +} + function shouldStripAssistantScaffoldingForLiveModel(modelKey?: string): boolean { if (!modelKey) { return false; @@ -708,6 +712,19 @@ describe("isPromptProbeMiss", () => { expect(isPromptProbeMiss(error)).toBe(expected); }); }); + +describe("hasEventLoopPromptKeywords", () => { + it.each([ + { + text: "The event loop drains the microtask queue before running the next macrotask.", + expected: true, + }, + { text: "Micro-tasks run before macro-tasks.", expected: true }, + { text: "Promise callbacks run before timer callbacks.", expected: false }, + ])("returns $expected for $text", ({ text, expected }) => { + expect(hasEventLoopPromptKeywords(text)).toBe(expected); + }); +}); function isMissingProfileError(error: string): boolean { return /no credentials found for profile/i.test(error); } @@ -1531,6 +1548,28 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { phase: "prompt", label: params.label, }); + if (!isMeaningful(text) || !hasEventLoopPromptKeywords(text)) { + logProgress(`${progressLabel}: prompt retry (weak answer)`); + const retryText = await requestGatewayAgentText({ + client, + sessionKey, + idempotencyKey: `idem-${randomUUID()}-keyword-retry`, + modelKey, + message: + "Answer in exactly two short sentences. Include the exact lowercase words microtask and macrotask. No bullets.", + thinkingLevel: params.thinkingLevel, + context: `${progressLabel}: prompt-keyword-retry`, + }); + if (retryText) { + text = retryText; + assertNoReasoningTags({ + text, + model: modelKey, + phase: "prompt-retry", + label: params.label, + }); + } + } if (!isMeaningful(text)) { if (isGoogleishProvider(model.provider) && /gemini/i.test(model.id)) { logProgress(`${progressLabel}: skip (google not meaningful)`); @@ -1538,10 +1577,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { } throw new Error(`not meaningful: ${text}`); } - if ( - !/\bmicro\s*-?\s*tasks?\b/i.test(text) || - !/\bmacro\s*-?\s*tasks?\b/i.test(text) - ) { + if (!hasEventLoopPromptKeywords(text)) { throw new Error(`missing required keywords: ${text}`); } diff --git a/src/media-understanding/image.test.ts b/src/media-understanding/image.test.ts index e567da91568..16750172c2b 100644 --- a/src/media-understanding/image.test.ts +++ b/src/media-understanding/image.test.ts @@ -90,6 +90,7 @@ describe("describeImageWithModel", () => { }); it("routes minimax-portal image models through the MiniMax VLM endpoint", async () => { + const authStore = { version: 1, profiles: {} }; const result = await describeImageWithModel({ cfg: {}, agentDir: "/tmp/openclaw-agent", @@ -100,6 +101,7 @@ describe("describeImageWithModel", () => { mime: "image/png", prompt: "Describe the image.", timeoutMs: 1000, + authStore, }); expect(result).toEqual({ @@ -107,7 +109,9 @@ describe("describeImageWithModel", () => { model: "MiniMax-VL-01", }); expect(ensureOpenClawModelsJsonMock).toHaveBeenCalled(); - expect(getApiKeyForModelMock).toHaveBeenCalled(); + expect(getApiKeyForModelMock).toHaveBeenCalledWith( + expect.objectContaining({ store: authStore }), + ); expect(requireApiKeyMock).toHaveBeenCalled(); expect(setRuntimeApiKeyMock).toHaveBeenCalledWith("minimax-portal", "oauth-test"); expect(fetchMock).toHaveBeenCalledWith( diff --git a/src/media-understanding/image.ts b/src/media-understanding/image.ts index 21ea1bd5fd3..4de9c000da5 100644 --- a/src/media-understanding/image.ts +++ b/src/media-understanding/image.ts @@ -43,6 +43,7 @@ async function resolveImageRuntime(params: { model: string; profile?: string; preferredProfile?: string; + authStore?: ImageDescriptionRequest["authStore"]; }): Promise<{ apiKey: string; model: Model }> { await ensureOpenClawModelsJson(params.cfg, params.agentDir); const { discoverAuthStorage, discoverModels } = await loadPiModelDiscoveryRuntime(); @@ -62,6 +63,7 @@ async function resolveImageRuntime(params: { agentDir: params.agentDir, profileId: params.profile, preferredProfile: params.preferredProfile, + store: params.authStore, }); const apiKey = requireApiKey(apiKeyInfo, model.provider); authStorage.setRuntimeApiKey(model.provider, apiKey); @@ -226,6 +228,7 @@ export async function describeImageWithModel( timeoutMs: params.timeoutMs, profile: params.profile, preferredProfile: params.preferredProfile, + authStore: params.authStore, agentDir: params.agentDir, cfg: params.cfg, }); diff --git a/src/media-understanding/types.ts b/src/media-understanding/types.ts index cfaa9fe5571..3d705504f81 100644 --- a/src/media-understanding/types.ts +++ b/src/media-understanding/types.ts @@ -1,3 +1,4 @@ +import type { AuthProfileStore } from "../agents/auth-profiles/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; export type MediaUnderstandingKind = @@ -136,6 +137,7 @@ export type ImageDescriptionRequest = { timeoutMs: number; profile?: string; preferredProfile?: string; + authStore?: AuthProfileStore; agentDir: string; cfg: OpenClawConfig; model: string; @@ -157,6 +159,7 @@ export type ImagesDescriptionRequest = { timeoutMs: number; profile?: string; preferredProfile?: string; + authStore?: AuthProfileStore; agentDir: string; cfg: OpenClawConfig; };