diff --git a/README.md b/README.md index 8f7cd3e..7d88633 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # openclaw-muse-code -Muse Spark in OpenClaw, billed to the **Muse Code monthly subscription** +Muse models in OpenClaw, billed to the **Muse Code monthly subscription** instead of API credits. Meta device-code login, no API key, no third-party CLI. @@ -34,9 +34,9 @@ plugins can be installed side by side. > implicitly: the setup default is the standard `muse-spark-1.3`. Model discovery is **live**: the catalog is fetched from -`GET {baseUrl}/models` (60s in-memory TTL, only `muse-spark-*` ids -admitted — other endpoint families such as `sam-*`, `muse-image-*` or -`muse-voice-*` are not chat models), so newly published models appear +`GET {baseUrl}/models` (60s in-memory TTL) and every returned model id +is served except the endpoint families verified as non-chat (`sam-*`, +`muse-image-*`, `muse-voice-*`), so newly published models appear automatically with no plugin update. The static table above is the fallback used before login and whenever the live fetch fails. diff --git a/dist/index.js b/dist/index.js index ab79e8f..a926034 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,5 +1,5 @@ /** - * Muse Spark model provider plugin for OpenClaw, billed to the Muse Code + * Muse model provider plugin for OpenClaw, billed to the Muse Code * monthly subscription instead of API credits. * * Authentication is a Meta device-code login performed once via @@ -134,7 +134,7 @@ export function resolveMuseCodeDynamicModel(ctx) { export default defineSingleProviderPluginEntry({ id: "muse-code", name: "Muse Code", - description: "Muse Spark (api.meta.ai) model provider billed to the Muse Code monthly subscription.", + description: "Muse models (api.meta.ai) model provider billed to the Muse Code monthly subscription.", provider: { label: "Muse Code", docsPath: "/providers/muse-code", diff --git a/dist/src/catalog.js b/dist/src/catalog.js index 3369075..dfec2df 100644 --- a/dist/src/catalog.js +++ b/dist/src/catalog.js @@ -10,12 +10,11 @@ import { readCacheSync } from "./auth.js"; export const MODELS_PATH = "/models"; export const LIVE_CATALOG_TTL_MS = 60_000; export const FETCH_TIMEOUT_MS = 8_000; -// Allowlist: this provider serves the Muse Spark chat family. The live -// endpoint also returns other families (sam-*, muse-image-*, muse-voice-*) -// that are not usable as chat/completions models — a denylist would leak -// the next such family, so only muse-spark-* is admitted. Anything else -// still resolves through the dynamic resolver if requested explicitly. -const CHAT_FAMILY_PREFIX = "muse-spark-"; +// Dynamic discovery: admit every model id the live account catalog +// returns, except the endpoint families verified as non-chat +// (segmentation, image, voice). New chat models appear without a +// plugin update; only a brand-new non-chat family needs an entry here. +const NON_CHAT_PREFIXES = ["sam-", "muse-image-", "muse-voice-"]; let cache = null; /** Test-only cache reset. */ export function __clearLiveCatalogCache() { @@ -39,7 +38,7 @@ function parseModelIds(body) { (row.id.length > 0)); }) .map((row) => row.id) - .filter((id) => id.startsWith(CHAT_FAMILY_PREFIX)); + .filter((id) => !NON_CHAT_PREFIXES.some((p) => id.startsWith(p))); return ids.length > 0 ? ids : null; } /** diff --git a/index.ts b/index.ts index 18302b3..62029b6 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,5 @@ /** - * Muse Spark model provider plugin for OpenClaw, billed to the Muse Code + * Muse model provider plugin for OpenClaw, billed to the Muse Code * monthly subscription instead of API credits. * * Authentication is a Meta device-code login performed once via @@ -170,7 +170,7 @@ export default defineSingleProviderPluginEntry({ id: "muse-code", name: "Muse Code", description: - "Muse Spark (api.meta.ai) model provider billed to the Muse Code monthly subscription.", + "Muse models (api.meta.ai) model provider billed to the Muse Code monthly subscription.", provider: { label: "Muse Code", docsPath: "/providers/muse-code", diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 7f9dba6..5ef1ffa 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -1,7 +1,7 @@ { "id": "muse-code", "name": "Muse Code", - "description": "Muse Spark (api.meta.ai) model provider plugin billed to the Muse Code monthly subscription.", + "description": "Muse models (api.meta.ai) model provider plugin billed to the Muse Code monthly subscription.", "providers": ["muse-code"], "modelSupport": { "modelPrefixes": ["muse-code/"] diff --git a/package.json b/package.json index 4217974..8cea2fb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@thestreamcode/openclaw-muse-code", "version": "0.2.0", - "description": "Muse Spark in OpenClaw billed to the Muse Code monthly subscription (Meta device login, no API key)", + "description": "Muse models in OpenClaw billed to the Muse Code monthly subscription (Meta device login, no API key)", "type": "module", "license": "MIT", "private": false, diff --git a/src/catalog.ts b/src/catalog.ts index 7b33332..e88842a 100644 --- a/src/catalog.ts +++ b/src/catalog.ts @@ -13,12 +13,11 @@ export const MODELS_PATH = "/models"; export const LIVE_CATALOG_TTL_MS = 60_000; export const FETCH_TIMEOUT_MS = 8_000; -// Allowlist: this provider serves the Muse Spark chat family. The live -// endpoint also returns other families (sam-*, muse-image-*, muse-voice-*) -// that are not usable as chat/completions models — a denylist would leak -// the next such family, so only muse-spark-* is admitted. Anything else -// still resolves through the dynamic resolver if requested explicitly. -const CHAT_FAMILY_PREFIX = "muse-spark-"; +// Dynamic discovery: admit every model id the live account catalog +// returns, except the endpoint families verified as non-chat +// (segmentation, image, voice). New chat models appear without a +// plugin update; only a brand-new non-chat family needs an entry here. +const NON_CHAT_PREFIXES = ["sam-", "muse-image-", "muse-voice-"]; type ModelsResponse = { data?: unknown; @@ -55,7 +54,7 @@ function parseModelIds(body: unknown): string[] | null { ); }) .map((row) => row.id) - .filter((id) => id.startsWith(CHAT_FAMILY_PREFIX)); + .filter((id) => !NON_CHAT_PREFIXES.some((p) => id.startsWith(p))); return ids.length > 0 ? ids : null; } diff --git a/test/catalog.test.ts b/test/catalog.test.ts index c74895b..13fb932 100644 --- a/test/catalog.test.ts +++ b/test/catalog.test.ts @@ -38,6 +38,9 @@ const LIVE_OK = { data: [ { id: "muse-spark-1.3" }, { id: "muse-spark-9.9" }, + { id: "muse-wonder-1.0" }, + { id: "muse-wonder-full-1.0" }, + { id: "muse-nova-9.9" }, { id: "sam-3.1" }, { id: "muse-image-1.0" }, { id: "muse-voice-2.0" }, @@ -45,11 +48,14 @@ const LIVE_OK = { }; describe("fetchLiveModelIds", () => { - it("admits only the muse-spark chat family", async () => { + it("admits the live catalog except non-chat families", async () => { stubFetch(() => ({ status: 200, body: LIVE_OK })); expect(await fetchLiveModelIds("https://x.test/v1", "k")).toEqual([ "muse-spark-1.3", "muse-spark-9.9", + "muse-wonder-1.0", + "muse-wonder-full-1.0", + "muse-nova-9.9", ]); }); @@ -93,21 +99,21 @@ describe("fetchLiveModelIds", () => { describe("getLiveModelIdsCached", () => { it("serves warm entries without refetching", async () => { const counter = stubFetch(() => ({ status: 200, body: LIVE_OK })); - expect(await getLiveModelIdsCached("https://x.test/v1", "k", 60_000)).toHaveLength(2); - expect(await getLiveModelIdsCached("https://x.test/v1", "k", 60_000)).toHaveLength(2); + expect(await getLiveModelIdsCached("https://x.test/v1", "k", 60_000)).toHaveLength(5); + expect(await getLiveModelIdsCached("https://x.test/v1", "k", 60_000)).toHaveLength(5); expect(counter.calls()).toBe(1); }); it("refetches after expiry", async () => { const counter = stubFetch(() => ({ status: 200, body: LIVE_OK })); - expect(await getLiveModelIdsCached("https://x.test/v1", "k", 0)).toHaveLength(2); - expect(await getLiveModelIdsCached("https://x.test/v1", "k", 60_000)).toHaveLength(2); + expect(await getLiveModelIdsCached("https://x.test/v1", "k", 0)).toHaveLength(5); + expect(await getLiveModelIdsCached("https://x.test/v1", "k", 60_000)).toHaveLength(5); expect(counter.calls()).toBe(2); }); it("clears stale entries on failure", async () => { stubFetch(() => ({ status: 200, body: LIVE_OK })); - expect(await getLiveModelIdsCached("https://x.test/v1", "k", 60_000)).toHaveLength(2); + expect(await getLiveModelIdsCached("https://x.test/v1", "k", 60_000)).toHaveLength(5); stubFetch(() => ({ status: 500, body: {} })); __clearLiveCatalogCache(); expect(await getLiveModelIdsCached("https://x.test/v1", "k", 60_000)).toBeNull();