Agents: add per-model fallbacks support in models config
This commit is contained in:
parent
9688454a30
commit
34e2c7aba8
@ -6,6 +6,7 @@ Docs: https://docs.molt.bot
|
|||||||
Status: beta.
|
Status: beta.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
- Agents: add per-model fallbacks support in `agents.defaults.models[...].fallbacks` to configure model-specific fallback chains.
|
||||||
- Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope.
|
- Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope.
|
||||||
- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.
|
- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.
|
||||||
- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk).
|
- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk).
|
||||||
|
|||||||
@ -538,4 +538,154 @@ describe("runWithModelFallback", () => {
|
|||||||
expect(result.provider).toBe("openai");
|
expect(result.provider).toBe("openai");
|
||||||
expect(result.model).toBe("gpt-4.1-mini");
|
expect(result.model).toBe("gpt-4.1-mini");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses per-model fallbacks from model catalog entry", async () => {
|
||||||
|
const cfg = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "openai/gpt-4.1-mini" },
|
||||||
|
models: {
|
||||||
|
"openai/gpt-4.1-mini": {
|
||||||
|
alias: "GPT-4",
|
||||||
|
fallbacks: ["anthropic/claude-haiku-3-5", "openai/gpt-4o"],
|
||||||
|
},
|
||||||
|
"anthropic/claude-haiku-3-5": {},
|
||||||
|
"openai/gpt-4o": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
const run = vi
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce(Object.assign(new Error("rate limit"), { status: 429 }))
|
||||||
|
.mockResolvedValueOnce("ok");
|
||||||
|
|
||||||
|
const result = await runWithModelFallback({
|
||||||
|
cfg,
|
||||||
|
provider: "openai",
|
||||||
|
model: "gpt-4.1-mini",
|
||||||
|
run,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.result).toBe("ok");
|
||||||
|
expect(result.provider).toBe("anthropic");
|
||||||
|
expect(result.model).toBe("claude-haiku-3-5");
|
||||||
|
expect(run).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("per-model fallbacks take precedence over global fallbacks", async () => {
|
||||||
|
const cfg = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: {
|
||||||
|
primary: "openrouter/z-ai/glm-4.5-air:free",
|
||||||
|
fallbacks: ["anthropic/claude-opus-4"],
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
"openrouter/z-ai/glm-4.5-air:free": {
|
||||||
|
alias: "GLM-Air",
|
||||||
|
fallbacks: ["zai/glm-4.5-air", "anthropic/claude-haiku-3-5"],
|
||||||
|
},
|
||||||
|
"zai/glm-4.5-air": {},
|
||||||
|
"anthropic/claude-haiku-3-5": {},
|
||||||
|
"anthropic/claude-opus-4": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
const calls: Array<{ provider: string; model: string }> = [];
|
||||||
|
|
||||||
|
const result = await runWithModelFallback({
|
||||||
|
cfg,
|
||||||
|
provider: "openrouter",
|
||||||
|
model: "z-ai/glm-4.5-air:free",
|
||||||
|
run: async (provider, model) => {
|
||||||
|
calls.push({ provider, model });
|
||||||
|
if (calls.length === 1) {
|
||||||
|
throw Object.assign(new Error("rate limit"), { status: 429 });
|
||||||
|
}
|
||||||
|
if (calls.length === 2) {
|
||||||
|
throw Object.assign(new Error("auth error"), { status: 401 });
|
||||||
|
}
|
||||||
|
return "ok";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.result).toBe("ok");
|
||||||
|
expect(result.provider).toBe("anthropic");
|
||||||
|
expect(result.model).toBe("claude-haiku-3-5");
|
||||||
|
expect(calls).toEqual([
|
||||||
|
{ provider: "openrouter", model: "z-ai/glm-4.5-air:free" },
|
||||||
|
{ provider: "zai", model: "glm-4.5-air" },
|
||||||
|
{ provider: "anthropic", model: "claude-haiku-3-5" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("empty per-model fallbacks array disables global fallbacks for that model", async () => {
|
||||||
|
const cfg = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: {
|
||||||
|
primary: "openai/gpt-4.1-mini",
|
||||||
|
fallbacks: ["anthropic/claude-haiku-3-5"],
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
"openai/gpt-4.1-mini": {
|
||||||
|
fallbacks: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
const calls: Array<{ provider: string; model: string }> = [];
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runWithModelFallback({
|
||||||
|
cfg,
|
||||||
|
provider: "openai",
|
||||||
|
model: "gpt-4.1-mini",
|
||||||
|
run: async (provider, model) => {
|
||||||
|
calls.push({ provider, model });
|
||||||
|
throw Object.assign(new Error("rate limit"), { status: 429 });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("rate limit");
|
||||||
|
|
||||||
|
expect(calls).toEqual([{ provider: "openai", model: "gpt-4.1-mini" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("per-model fallbacks respect allowlist", async () => {
|
||||||
|
const cfg = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "openai/gpt-4.1-mini" },
|
||||||
|
models: {
|
||||||
|
"openai/gpt-4.1-mini": {
|
||||||
|
fallbacks: ["anthropic/claude-haiku-3-5"],
|
||||||
|
},
|
||||||
|
"anthropic/claude-haiku-3-5": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
const run = vi
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce(Object.assign(new Error("rate limit"), { status: 429 }))
|
||||||
|
.mockResolvedValueOnce("ok");
|
||||||
|
|
||||||
|
const result = await runWithModelFallback({
|
||||||
|
cfg,
|
||||||
|
provider: "openai",
|
||||||
|
model: "gpt-4.1-mini",
|
||||||
|
run,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.result).toBe("ok");
|
||||||
|
expect(result.provider).toBe("anthropic");
|
||||||
|
expect(result.model).toBe("claude-haiku-3-5");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -165,11 +165,18 @@ function resolveFallbackCandidates(params: {
|
|||||||
|
|
||||||
const modelFallbacks = (() => {
|
const modelFallbacks = (() => {
|
||||||
if (params.fallbacksOverride !== undefined) return params.fallbacksOverride;
|
if (params.fallbacksOverride !== undefined) return params.fallbacksOverride;
|
||||||
const model = params.cfg?.agents?.defaults?.model as
|
|
||||||
|
// Check for per-model fallbacks first
|
||||||
|
const key = modelKey(provider, model);
|
||||||
|
const modelConfig = params.cfg?.agents?.defaults?.models?.[key];
|
||||||
|
if (modelConfig?.fallbacks) return modelConfig.fallbacks;
|
||||||
|
|
||||||
|
// Fall back to global model.fallbacks
|
||||||
|
const modelCfg = params.cfg?.agents?.defaults?.model as
|
||||||
| { fallbacks?: string[] }
|
| { fallbacks?: string[] }
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
if (model && typeof model === "object") return model.fallbacks ?? [];
|
if (modelCfg && typeof modelCfg === "object") return modelCfg.fallbacks ?? [];
|
||||||
return [];
|
return [];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@ export type AgentModelEntryConfig = {
|
|||||||
alias?: string;
|
alias?: string;
|
||||||
/** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
|
/** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
|
||||||
params?: Record<string, unknown>;
|
params?: Record<string, unknown>;
|
||||||
|
/** Per-model fallbacks (tried before global fallbacks). */
|
||||||
|
fallbacks?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AgentModelListConfig = {
|
export type AgentModelListConfig = {
|
||||||
|
|||||||
@ -37,6 +37,8 @@ export const AgentDefaultsSchema = z
|
|||||||
alias: z.string().optional(),
|
alias: z.string().optional(),
|
||||||
/** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
|
/** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
|
||||||
params: z.record(z.string(), z.unknown()).optional(),
|
params: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
/** Per-model fallbacks (tried before global fallbacks). */
|
||||||
|
fallbacks: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user