diff --git a/CHANGELOG.md b/CHANGELOG.md index d667b2d33fd..349edd4edfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ Docs: https://docs.openclaw.ai - Agents/Models: probe the primary model when its auth-profile cooldown is near expiry (with per-provider throttling), so runs recover from temporary rate limits without staying on fallback models until restart. (#17478) Thanks @PlayerGhost. - Agents/Failover: classify provider abort stop-reason errors (`Unhandled stop reason: abort`, `stop reason: abort`, `reason: abort`) as timeout-class failures so configured model fallback chains trigger instead of surfacing raw abort failures. (#18618) Thanks @sauerdaniel. - Models/CLI: sync auth-profiles credentials into agent `auth.json` before registry availability checks so `openclaw models list --all` reports auth correctly for API-key/token providers, normalize provider-id aliases when bridging credentials, and skip expired token mirrors. (#18610, #18615) +- CLI/Models: revert `models set` catalog validation to restore prior behavior while the validation logic is reworked. (#19194) - Agents/Context: raise default total bootstrap prompt cap from `24000` to `150000` chars (keeping `bootstrapMaxChars` at `20000`), include total-cap visibility in `/context`, and mark truncation from injected-vs-raw sizes so total-cap clipping is reflected accurately. - Memory/QMD: scope managed collection names per agent and precreate glob-backed collection directories before registration, preventing cross-agent collection clobbering and startup ENOENT failures in fresh workspaces. (#17194) Thanks @jonathanadams96. - Cron: preserve per-job schedule-error isolation in post-run maintenance recompute so malformed sibling jobs no longer abort persistence of successful runs. (#17852) Thanks @pierreeurope. diff --git a/src/agents/chutes-oauth.e2e.test.ts b/src/agents/chutes-oauth.e2e.test.ts index dd960b49981..a4292b3eb28 100644 --- a/src/agents/chutes-oauth.e2e.test.ts +++ b/src/agents/chutes-oauth.e2e.test.ts @@ -16,7 +16,7 @@ const urlToString = (url: Request | URL | string): string => { describe("chutes-oauth", () => { it("exchanges code for tokens and stores username as email", async () => { - const fetchFn = withFetchPreconnect(async (input, init) => { + const fetchFn = withFetchPreconnect(async (input: RequestInfo | URL, init?: RequestInit) => { const url = urlToString(input); if (url === CHUTES_TOKEN_ENDPOINT) { expect(init?.method).toBe("POST"); @@ -66,7 +66,7 @@ describe("chutes-oauth", () => { }); it("refreshes tokens using stored client id and falls back to old refresh token", async () => { - const fetchFn = withFetchPreconnect(async (input, init) => { + const fetchFn = withFetchPreconnect(async (input: RequestInfo | URL, init?: RequestInit) => { const url = urlToString(input); if (url !== CHUTES_TOKEN_ENDPOINT) { return new Response("not found", { status: 404 }); diff --git a/src/agents/tools/web-fetch.ssrf.e2e.test.ts b/src/agents/tools/web-fetch.ssrf.e2e.test.ts index e61b56952b2..9a02821cb7f 100644 --- a/src/agents/tools/web-fetch.ssrf.e2e.test.ts +++ b/src/agents/tools/web-fetch.ssrf.e2e.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as ssrf from "../../infra/net/ssrf.js"; -import { withFetchPreconnect } from "../../test-utils/fetch-mock.js"; +import { type FetchMock, withFetchPreconnect } from "../../test-utils/fetch-mock.js"; const lookupMock = vi.fn(); const resolvePinnedHostname = ssrf.resolvePinnedHostname; @@ -29,8 +29,10 @@ function textResponse(body: string): Response { } as unknown as Response; } -function setMockFetch(impl?: (...args: unknown[]) => unknown) { - const fetchSpy = vi.fn(impl); +function setMockFetch( + impl: FetchMock = async (_input: RequestInfo | URL, _init?: RequestInit) => textResponse(""), +) { + const fetchSpy = vi.fn(impl); global.fetch = withFetchPreconnect(fetchSpy); return fetchSpy; } diff --git a/src/agents/tools/web-tools.enabled-defaults.e2e.test.ts b/src/agents/tools/web-tools.enabled-defaults.e2e.test.ts index b6f2f2f24ef..e8ca5a00092 100644 --- a/src/agents/tools/web-tools.enabled-defaults.e2e.test.ts +++ b/src/agents/tools/web-tools.enabled-defaults.e2e.test.ts @@ -216,7 +216,7 @@ describe("web_search external content wrapping", () => { it("wraps Brave result descriptions", async () => { vi.stubEnv("BRAVE_API_KEY", "test-key"); - const mockFetch = vi.fn(() => + const mockFetch = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => Promise.resolve({ ok: true, json: () => @@ -233,7 +233,7 @@ describe("web_search external content wrapping", () => { }), } as Response), ); - global.fetch = mockFetch; + global.fetch = withFetchPreconnect(mockFetch); const tool = createWebSearchTool({ config: undefined, sandboxed: true }); const result = await tool?.execute?.("call-1", { query: "test" }); @@ -254,7 +254,7 @@ describe("web_search external content wrapping", () => { it("does not wrap Brave result urls (raw for tool chaining)", async () => { vi.stubEnv("BRAVE_API_KEY", "test-key"); const url = "https://example.com/some-page"; - const mockFetch = vi.fn(() => + const mockFetch = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => Promise.resolve({ ok: true, json: () => @@ -271,7 +271,7 @@ describe("web_search external content wrapping", () => { }), } as Response), ); - global.fetch = mockFetch; + global.fetch = withFetchPreconnect(mockFetch); const tool = createWebSearchTool({ config: undefined, sandboxed: true }); const result = await tool?.execute?.("call-1", { query: "unique-test-url-not-wrapped" }); @@ -284,7 +284,7 @@ describe("web_search external content wrapping", () => { it("does not wrap Brave site names", async () => { vi.stubEnv("BRAVE_API_KEY", "test-key"); - const mockFetch = vi.fn(() => + const mockFetch = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => Promise.resolve({ ok: true, json: () => @@ -301,7 +301,7 @@ describe("web_search external content wrapping", () => { }), } as Response), ); - global.fetch = mockFetch; + global.fetch = withFetchPreconnect(mockFetch); const tool = createWebSearchTool({ config: undefined, sandboxed: true }); const result = await tool?.execute?.("call-1", { query: "unique-test-site-name-wrapping" }); @@ -313,7 +313,7 @@ describe("web_search external content wrapping", () => { it("does not wrap Brave published ages", async () => { vi.stubEnv("BRAVE_API_KEY", "test-key"); - const mockFetch = vi.fn(() => + const mockFetch = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => Promise.resolve({ ok: true, json: () => @@ -331,7 +331,7 @@ describe("web_search external content wrapping", () => { }), } as Response), ); - global.fetch = mockFetch; + global.fetch = withFetchPreconnect(mockFetch); const tool = createWebSearchTool({ config: undefined, sandboxed: true }); const result = await tool?.execute?.("call-1", { @@ -345,7 +345,7 @@ describe("web_search external content wrapping", () => { it("wraps Perplexity content", async () => { vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test"); - const mockFetch = vi.fn(() => + const mockFetch = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => Promise.resolve({ ok: true, json: () => @@ -355,7 +355,7 @@ describe("web_search external content wrapping", () => { }), } as Response), ); - global.fetch = mockFetch; + global.fetch = withFetchPreconnect(mockFetch); const tool = createWebSearchTool({ config: { tools: { web: { search: { provider: "perplexity" } } } }, @@ -371,7 +371,7 @@ describe("web_search external content wrapping", () => { it("does not wrap Perplexity citations (raw for tool chaining)", async () => { vi.stubEnv("PERPLEXITY_API_KEY", "pplx-test"); const citation = "https://example.com/some-article"; - const mockFetch = vi.fn((_input?: unknown, _init?: unknown) => + const mockFetch = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => Promise.resolve({ ok: true, json: () => @@ -381,7 +381,7 @@ describe("web_search external content wrapping", () => { }), } as Response), ); - global.fetch = mockFetch; + global.fetch = withFetchPreconnect(mockFetch); const tool = createWebSearchTool({ config: { tools: { web: { search: { provider: "perplexity" } } } }, diff --git a/src/commands/chutes-oauth.e2e.test.ts b/src/commands/chutes-oauth.e2e.test.ts index 812f0ba0a52..bb30707a805 100644 --- a/src/commands/chutes-oauth.e2e.test.ts +++ b/src/commands/chutes-oauth.e2e.test.ts @@ -32,8 +32,8 @@ function createOAuthFetchFn(params: { refreshToken: string; username: string; passthrough?: boolean; -}): typeof fetch { - return withFetchPreconnect(async (input, init) => { +}) { + return withFetchPreconnect(async (input: RequestInfo | URL, init?: RequestInit) => { const url = urlToString(input); if (url === CHUTES_TOKEN_ENDPOINT) { return new Response( diff --git a/src/commands/models/set.ts b/src/commands/models/set.ts index dd0054994fb..6b0e79e8c33 100644 --- a/src/commands/models/set.ts +++ b/src/commands/models/set.ts @@ -1,46 +1,12 @@ -import { loadModelCatalog } from "../../agents/model-catalog.js"; -import { modelKey } from "../../agents/model-selection.js"; -import { readConfigFileSnapshot } from "../../config/config.js"; import { logConfigUpdated } from "../../config/logging.js"; import type { RuntimeEnv } from "../../runtime.js"; -import { applyDefaultModelPrimaryUpdate, resolveModelTarget, updateConfig } from "./shared.js"; +import { applyDefaultModelPrimaryUpdate, updateConfig } from "./shared.js"; export async function modelsSetCommand(modelRaw: string, runtime: RuntimeEnv) { - // 1. Read config and resolve the model reference - const snapshot = await readConfigFileSnapshot(); - if (!snapshot.valid) { - const issues = snapshot.issues.map((i) => `- ${i.path}: ${i.message}`).join("\n"); - throw new Error(`Invalid config at ${snapshot.path}\n${issues}`); - } - const cfg = snapshot.config; - const resolved = resolveModelTarget({ raw: modelRaw, cfg }); - const key = `${resolved.provider}/${resolved.model}`; - - // 2. Validate against catalog (skip when catalog is empty — initial setup) - const catalog = await loadModelCatalog({ config: cfg }); - if (catalog.length > 0) { - const catalogKeys = new Set(catalog.map((e) => modelKey(e.provider, e.id))); - if (!catalogKeys.has(key)) { - throw new Error( - `Unknown model: ${key}\nModel not found in catalog. Run "openclaw models list" to see available models.`, - ); - } - } - - // 3. Track whether this is a new entry - const isNewEntry = !cfg.agents?.defaults?.models?.[key]; - - // 4. Update config (using upstream's helper for the actual mutation) - const updated = await updateConfig((c) => { - return applyDefaultModelPrimaryUpdate({ cfg: c, modelRaw, field: "model" }); + const updated = await updateConfig((cfg) => { + return applyDefaultModelPrimaryUpdate({ cfg, modelRaw, field: "model" }); }); - // 5. Warn and log - if (isNewEntry) { - runtime.log( - `Warning: "${key}" had no entry in models config. Added with empty config (no provider routing).`, - ); - } logConfigUpdated(runtime); runtime.log(`Default model: ${updated.agents?.defaults?.model?.primary ?? modelRaw}`); } diff --git a/src/memory/embeddings-voyage.test.ts b/src/memory/embeddings-voyage.test.ts index 65b6e4306ed..822eabd55ae 100644 --- a/src/memory/embeddings-voyage.test.ts +++ b/src/memory/embeddings-voyage.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import * as authModule from "../agents/model-auth.js"; -import { withFetchPreconnect } from "../test-utils/fetch-mock.js"; +import { type FetchMock, withFetchPreconnect } from "../test-utils/fetch-mock.js"; import { createVoyageEmbeddingProvider, normalizeVoyageModel } from "./embeddings-voyage.js"; vi.mock("../agents/model-auth.js", () => ({ @@ -14,13 +14,14 @@ vi.mock("../agents/model-auth.js", () => ({ })); const createFetchMock = () => { - const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => { - return new Response(JSON.stringify({ data: [{ embedding: [0.1, 0.2, 0.3] }] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }); - return withFetchPreconnect(fetchMock) as typeof fetch & typeof fetchMock; + const fetchMock = vi.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + new Response(JSON.stringify({ data: [{ embedding: [0.1, 0.2, 0.3] }] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + return withFetchPreconnect(fetchMock); }; describe("voyage embedding provider", () => { @@ -95,17 +96,15 @@ describe("voyage embedding provider", () => { it("passes input_type=document for embedBatch", async () => { const fetchMock = withFetchPreconnect( - vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => { - return new Response( - JSON.stringify({ - data: [{ embedding: [0.1, 0.2] }, { embedding: [0.3, 0.4] }], - }), - { - status: 200, - headers: { "Content-Type": "application/json" }, - }, - ); - }), + vi.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => + new Response( + JSON.stringify({ + data: [{ embedding: [0.1, 0.2] }, { embedding: [0.3, 0.4] }], + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ), + ), ); vi.stubGlobal("fetch", fetchMock); diff --git a/src/slack/monitor/media.test.ts b/src/slack/monitor/media.test.ts index 05b30615e84..b3d0bcb51b7 100644 --- a/src/slack/monitor/media.test.ts +++ b/src/slack/monitor/media.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as ssrf from "../../infra/net/ssrf.js"; import type { SavedMedia } from "../../media/store.js"; import * as mediaStore from "../../media/store.js"; -import { withFetchPreconnect } from "../../test-utils/fetch-mock.js"; +import { type FetchMock, withFetchPreconnect } from "../../test-utils/fetch-mock.js"; import { fetchWithSlackAuth, resolveSlackAttachmentContent, @@ -12,7 +12,7 @@ import { // Store original fetch const originalFetch = globalThis.fetch; -let mockFetch: ReturnType; +let mockFetch: ReturnType>; const createSavedMedia = (filePath: string, contentType: string): SavedMedia => ({ id: "saved-media-id", path: filePath, @@ -23,7 +23,9 @@ const createSavedMedia = (filePath: string, contentType: string): SavedMedia => describe("fetchWithSlackAuth", () => { beforeEach(() => { // Create a new mock for each test - mockFetch = vi.fn(); + mockFetch = vi.fn( + async (_input: RequestInfo | URL, _init?: RequestInit) => new Response(), + ); globalThis.fetch = withFetchPreconnect(mockFetch); }); @@ -366,8 +368,9 @@ describe("resolveSlackMedia", () => { return createSavedMedia("/tmp/unknown", "application/octet-stream"); }); - mockFetch.mockImplementation(async (input) => { - const url = String(input); + mockFetch.mockImplementation(async (input: RequestInfo | URL) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("/a.jpg")) { return new Response(Buffer.from("image a"), { status: 200, diff --git a/src/test-utils/fetch-mock.ts b/src/test-utils/fetch-mock.ts index 7eec83cd231..430a110431f 100644 --- a/src/test-utils/fetch-mock.ts +++ b/src/test-utils/fetch-mock.ts @@ -1,5 +1,14 @@ +export type FetchMock = (input: RequestInfo | URL, init?: RequestInit) => Promise; + +type FetchPreconnectOptions = { + dns?: boolean; + tcp?: boolean; + http?: boolean; + https?: boolean; +}; + type FetchWithPreconnect = { - preconnect: (url: string, init?: { credentials?: RequestCredentials }) => void; + preconnect: (url: string | URL, options?: FetchPreconnectOptions) => void; }; export function withFetchPreconnect(fn: T): T & FetchWithPreconnect; @@ -8,6 +17,6 @@ export function withFetchPreconnect( ): T & FetchWithPreconnect & typeof fetch; export function withFetchPreconnect(fn: object) { return Object.assign(fn, { - preconnect: (_url: string, _init?: { credentials?: RequestCredentials }) => {}, + preconnect: (_url: string | URL, _options?: FetchPreconnectOptions) => {}, }); }