Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 6 additions & 7 deletions dist/src/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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;
}
/**
Expand Down
4 changes: 2 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion openclaw.plugin.json
Original file line number Diff line number Diff line change
@@ -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/"]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
13 changes: 6 additions & 7 deletions src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
18 changes: 12 additions & 6 deletions test/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,24 @@ 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" },
],
};

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",
]);
});

Expand Down Expand Up @@ -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();
Expand Down
Loading