fix: fallback /model list when catalog is unavailable
This commit is contained in:
@@ -67,6 +67,7 @@
|
|||||||
- Commands: warn when /elevated runs in direct (unsandboxed) runtime.
|
- Commands: warn when /elevated runs in direct (unsandboxed) runtime.
|
||||||
- Commands: treat mention-bypassed group command messages as mentioned so elevated directives respond.
|
- Commands: treat mention-bypassed group command messages as mentioned so elevated directives respond.
|
||||||
- Commands: return /status in directive-only multi-line messages.
|
- Commands: return /status in directive-only multi-line messages.
|
||||||
|
- Models: fall back to configured models when the provider catalog is unavailable.
|
||||||
- Agent system prompt: add messaging guidance for reply routing and cross-session sends. (#526) — thanks @neist
|
- Agent system prompt: add messaging guidance for reply routing and cross-session sends. (#526) — thanks @neist
|
||||||
|
|
||||||
## 2026.1.8
|
## 2026.1.8
|
||||||
|
|||||||
@@ -933,6 +933,36 @@ describe("directive behavior", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to configured models when catalog is unavailable", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
vi.mocked(loadModelCatalog).mockResolvedValueOnce([]);
|
||||||
|
const storePath = path.join(home, "sessions.json");
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{ Body: "/model", From: "+1222", To: "+1222" },
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
agent: {
|
||||||
|
model: { primary: "anthropic/claude-opus-4-5" },
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
models: {
|
||||||
|
"anthropic/claude-opus-4-5": {},
|
||||||
|
"openai/gpt-4.1-mini": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: { store: storePath },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("Model catalog unavailable");
|
||||||
|
expect(text).toContain("anthropic/claude-opus-4-5");
|
||||||
|
expect(text).toContain("openai/gpt-4.1-mini");
|
||||||
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("does not repeat missing auth labels on /model list", async () => {
|
it("does not repeat missing auth labels on /model list", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|||||||
@@ -379,12 +379,92 @@ export async function handleDirectiveOnly(params: {
|
|||||||
modelDirective === "status" || modelDirective === "list";
|
modelDirective === "status" || modelDirective === "list";
|
||||||
if (!directives.rawModelDirective || isModelListAlias) {
|
if (!directives.rawModelDirective || isModelListAlias) {
|
||||||
if (allowedModelCatalog.length === 0) {
|
if (allowedModelCatalog.length === 0) {
|
||||||
|
const resolvedDefault = resolveConfiguredModelRef({
|
||||||
|
cfg: params.cfg,
|
||||||
|
defaultProvider,
|
||||||
|
defaultModel,
|
||||||
|
});
|
||||||
|
const fallbackKeys = new Set<string>();
|
||||||
|
const fallbackCatalog: Array<{
|
||||||
|
provider: string;
|
||||||
|
id: string;
|
||||||
|
}> = [];
|
||||||
|
for (const raw of Object.keys(params.cfg.agent?.models ?? {})) {
|
||||||
|
const resolved = resolveModelRefFromString({
|
||||||
|
raw: String(raw),
|
||||||
|
defaultProvider,
|
||||||
|
aliasIndex,
|
||||||
|
});
|
||||||
|
if (!resolved) continue;
|
||||||
|
const key = modelKey(resolved.ref.provider, resolved.ref.model);
|
||||||
|
if (fallbackKeys.has(key)) continue;
|
||||||
|
fallbackKeys.add(key);
|
||||||
|
fallbackCatalog.push({
|
||||||
|
provider: resolved.ref.provider,
|
||||||
|
id: resolved.ref.model,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (fallbackCatalog.length === 0 && resolvedDefault.model) {
|
||||||
|
const key = modelKey(resolvedDefault.provider, resolvedDefault.model);
|
||||||
|
fallbackKeys.add(key);
|
||||||
|
fallbackCatalog.push({
|
||||||
|
provider: resolvedDefault.provider,
|
||||||
|
id: resolvedDefault.model,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (fallbackCatalog.length === 0) {
|
||||||
return { text: "No models available." };
|
return { text: "No models available." };
|
||||||
}
|
}
|
||||||
const agentDir = resolveClawdbotAgentDir();
|
const agentDir = resolveClawdbotAgentDir();
|
||||||
const modelsPath = `${agentDir}/models.json`;
|
const modelsPath = `${agentDir}/models.json`;
|
||||||
const formatPath = (value: string) => shortenHomePath(value);
|
const formatPath = (value: string) => shortenHomePath(value);
|
||||||
const authByProvider = new Map<string, string>();
|
const authByProvider = new Map<string, string>();
|
||||||
|
for (const entry of fallbackCatalog) {
|
||||||
|
if (authByProvider.has(entry.provider)) continue;
|
||||||
|
const auth = await resolveAuthLabel(
|
||||||
|
entry.provider,
|
||||||
|
params.cfg,
|
||||||
|
modelsPath,
|
||||||
|
);
|
||||||
|
authByProvider.set(entry.provider, formatAuthLabel(auth));
|
||||||
|
}
|
||||||
|
const current = `${params.provider}/${params.model}`;
|
||||||
|
const defaultLabel = `${defaultProvider}/${defaultModel}`;
|
||||||
|
const lines = [
|
||||||
|
`Current: ${current}`,
|
||||||
|
`Default: ${defaultLabel}`,
|
||||||
|
`Auth file: ${formatPath(resolveAuthStorePathForDisplay())}`,
|
||||||
|
`⚠️ Model catalog unavailable; showing configured models only.`,
|
||||||
|
];
|
||||||
|
const byProvider = new Map<string, typeof fallbackCatalog>();
|
||||||
|
for (const entry of fallbackCatalog) {
|
||||||
|
const models = byProvider.get(entry.provider);
|
||||||
|
if (models) {
|
||||||
|
models.push(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byProvider.set(entry.provider, [entry]);
|
||||||
|
}
|
||||||
|
for (const provider of byProvider.keys()) {
|
||||||
|
const models = byProvider.get(provider);
|
||||||
|
if (!models) continue;
|
||||||
|
const authLabel = authByProvider.get(provider) ?? "missing";
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`[${provider}] auth: ${authLabel}`);
|
||||||
|
for (const entry of models) {
|
||||||
|
const label = `${entry.provider}/${entry.id}`;
|
||||||
|
const aliases = aliasIndex.byKey.get(label);
|
||||||
|
const aliasSuffix =
|
||||||
|
aliases && aliases.length > 0 ? ` (${aliases.join(", ")})` : "";
|
||||||
|
lines.push(` • ${label}${aliasSuffix}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { text: lines.join("\n") };
|
||||||
|
}
|
||||||
|
const agentDir = resolveClawdbotAgentDir();
|
||||||
|
const modelsPath = `${agentDir}/models.json`;
|
||||||
|
const formatPath = (value: string) => shortenHomePath(value);
|
||||||
|
const authByProvider = new Map<string, string>();
|
||||||
for (const entry of allowedModelCatalog) {
|
for (const entry of allowedModelCatalog) {
|
||||||
if (authByProvider.has(entry.provider)) continue;
|
if (authByProvider.has(entry.provider)) continue;
|
||||||
const auth = await resolveAuthLabel(
|
const auth = await resolveAuthLabel(
|
||||||
|
|||||||
Reference in New Issue
Block a user