Merge pull request #583 from mitschabaude-bot/feat/agent-model-fallbacks
Config: per-agent model fallbacks
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
- Cron: accept ISO timestamps for one-shot schedules (UTC) and allow optional delete-after-run; wired into CLI + macOS editor.
|
- Cron: accept ISO timestamps for one-shot schedules (UTC) and allow optional delete-after-run; wired into CLI + macOS editor.
|
||||||
- Gateway: add Tailscale binary discovery, custom bind mode, and probe auth retry for password changes. (#740 — thanks @jeffersonwarrior)
|
- Gateway: add Tailscale binary discovery, custom bind mode, and probe auth retry for password changes. (#740 — thanks @jeffersonwarrior)
|
||||||
- Agents: add compaction mode config with optional safeguard summarization for long histories. (#700 — thanks @thewilloftheshadow)
|
- Agents: add compaction mode config with optional safeguard summarization for long histories. (#700 — thanks @thewilloftheshadow)
|
||||||
|
- Agents: support per-agent model fallbacks via `agents.list[].model`. (#583 — thanks @mitschabaude-bot)
|
||||||
- Tools: add tool profiles plus group shorthands for tool policy allow/deny (global, per-agent, sandbox).
|
- Tools: add tool profiles plus group shorthands for tool policy allow/deny (global, per-agent, sandbox).
|
||||||
- Thinking: allow xhigh for GPT-5.2 + Codex models and downgrade on unsupported switches. (#444 — thanks @grp06)
|
- Thinking: allow xhigh for GPT-5.2 + Codex models and downgrade on unsupported switches. (#444 — thanks @grp06)
|
||||||
|
|
||||||
|
|||||||
@@ -569,7 +569,9 @@ Inbound messages are routed to an agent via bindings.
|
|||||||
- `name`: display name for the agent.
|
- `name`: display name for the agent.
|
||||||
- `workspace`: default `~/clawd-<agentId>` (for `main`, falls back to `agents.defaults.workspace`).
|
- `workspace`: default `~/clawd-<agentId>` (for `main`, falls back to `agents.defaults.workspace`).
|
||||||
- `agentDir`: default `~/.clawdbot/agents/<agentId>/agent`.
|
- `agentDir`: default `~/.clawdbot/agents/<agentId>/agent`.
|
||||||
- `model`: per-agent default model (provider/model), overrides `agents.defaults.model` for that agent.
|
- `model`: per-agent default model, overrides `agents.defaults.model` for that agent.
|
||||||
|
- string form: `"provider/model"`, overrides only `agents.defaults.model.primary`
|
||||||
|
- object form: `{ primary, fallbacks }` (fallbacks override `agents.defaults.model.fallbacks`; `[]` disables global fallbacks for that agent)
|
||||||
- `identity`: per-agent name/theme/emoji (used for mention patterns + ack reactions).
|
- `identity`: per-agent name/theme/emoji (used for mention patterns + ack reactions).
|
||||||
- `groupChat`: per-agent mention-gating (`mentionPatterns`).
|
- `groupChat`: per-agent mention-gating (`mentionPatterns`).
|
||||||
- `sandbox`: per-agent sandbox config (overrides `agents.defaults.sandbox`).
|
- `sandbox`: per-agent sandbox config (overrides `agents.defaults.sandbox`).
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { resolveAgentConfig } from "./agent-scope.js";
|
import {
|
||||||
|
resolveAgentConfig,
|
||||||
|
resolveAgentModelFallbacksOverride,
|
||||||
|
resolveAgentModelPrimary,
|
||||||
|
} from "./agent-scope.js";
|
||||||
|
|
||||||
describe("resolveAgentConfig", () => {
|
describe("resolveAgentConfig", () => {
|
||||||
it("should return undefined when no agents config exists", () => {
|
it("should return undefined when no agents config exists", () => {
|
||||||
@@ -47,6 +51,68 @@ describe("resolveAgentConfig", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("supports per-agent model primary+fallbacks", () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: {
|
||||||
|
primary: "anthropic/claude-sonnet-4",
|
||||||
|
fallbacks: ["openai/gpt-4.1"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: "linus",
|
||||||
|
model: {
|
||||||
|
primary: "anthropic/claude-opus-4",
|
||||||
|
fallbacks: ["openai/gpt-5.2"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(resolveAgentModelPrimary(cfg, "linus")).toBe(
|
||||||
|
"anthropic/claude-opus-4",
|
||||||
|
);
|
||||||
|
expect(resolveAgentModelFallbacksOverride(cfg, "linus")).toEqual([
|
||||||
|
"openai/gpt-5.2",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If fallbacks isn't present, we don't override the global fallbacks.
|
||||||
|
const cfgNoOverride: ClawdbotConfig = {
|
||||||
|
agents: {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: "linus",
|
||||||
|
model: {
|
||||||
|
primary: "anthropic/claude-opus-4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(resolveAgentModelFallbacksOverride(cfgNoOverride, "linus")).toBe(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Explicit empty list disables global fallbacks for that agent.
|
||||||
|
const cfgDisable: ClawdbotConfig = {
|
||||||
|
agents: {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: "linus",
|
||||||
|
model: {
|
||||||
|
primary: "anthropic/claude-opus-4",
|
||||||
|
fallbacks: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(resolveAgentModelFallbacksOverride(cfgDisable, "linus")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should return agent-specific sandbox config", () => {
|
it("should return agent-specific sandbox config", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type ResolvedAgentConfig = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
workspace?: string;
|
workspace?: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
model?: string;
|
model?: AgentEntry["model"];
|
||||||
memorySearch?: AgentEntry["memorySearch"];
|
memorySearch?: AgentEntry["memorySearch"];
|
||||||
humanDelay?: AgentEntry["humanDelay"];
|
humanDelay?: AgentEntry["humanDelay"];
|
||||||
identity?: AgentEntry["identity"];
|
identity?: AgentEntry["identity"];
|
||||||
@@ -95,7 +95,11 @@ export function resolveAgentConfig(
|
|||||||
workspace:
|
workspace:
|
||||||
typeof entry.workspace === "string" ? entry.workspace : undefined,
|
typeof entry.workspace === "string" ? entry.workspace : undefined,
|
||||||
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
|
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
|
||||||
model: typeof entry.model === "string" ? entry.model : undefined,
|
model:
|
||||||
|
typeof entry.model === "string" ||
|
||||||
|
(entry.model && typeof entry.model === "object")
|
||||||
|
? entry.model
|
||||||
|
: undefined,
|
||||||
memorySearch: entry.memorySearch,
|
memorySearch: entry.memorySearch,
|
||||||
humanDelay: entry.humanDelay,
|
humanDelay: entry.humanDelay,
|
||||||
identity: entry.identity,
|
identity: entry.identity,
|
||||||
@@ -109,6 +113,28 @@ export function resolveAgentConfig(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveAgentModelPrimary(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
agentId: string,
|
||||||
|
): string | undefined {
|
||||||
|
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||||
|
if (!raw) return undefined;
|
||||||
|
if (typeof raw === "string") return raw.trim() || undefined;
|
||||||
|
const primary = raw.primary?.trim();
|
||||||
|
return primary || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAgentModelFallbacksOverride(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
agentId: string,
|
||||||
|
): string[] | undefined {
|
||||||
|
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||||
|
if (!raw || typeof raw === "string") return undefined;
|
||||||
|
// Important: treat an explicitly provided empty array as an override to disable global fallbacks.
|
||||||
|
if (!Object.hasOwn(raw, "fallbacks")) return undefined;
|
||||||
|
return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveAgentWorkspaceDir(cfg: ClawdbotConfig, agentId: string) {
|
export function resolveAgentWorkspaceDir(cfg: ClawdbotConfig, agentId: string) {
|
||||||
const id = normalizeAgentId(agentId);
|
const id = normalizeAgentId(agentId);
|
||||||
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
const configured = resolveAgentConfig(cfg, id)?.workspace?.trim();
|
||||||
|
|||||||
@@ -124,6 +124,106 @@ describe("runWithModelFallback", () => {
|
|||||||
expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5");
|
expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not append configured primary when fallbacksOverride is set", async () => {
|
||||||
|
const cfg = makeCfg({
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: {
|
||||||
|
primary: "openai/gpt-4.1-mini",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const run = vi
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() =>
|
||||||
|
Promise.reject(Object.assign(new Error("nope"), { status: 401 })),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runWithModelFallback({
|
||||||
|
cfg,
|
||||||
|
provider: "anthropic",
|
||||||
|
model: "claude-opus-4-5",
|
||||||
|
fallbacksOverride: ["anthropic/claude-haiku-3-5"],
|
||||||
|
run,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("All models failed");
|
||||||
|
|
||||||
|
expect(run.mock.calls).toEqual([
|
||||||
|
["anthropic", "claude-opus-4-5"],
|
||||||
|
["anthropic", "claude-haiku-3-5"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses fallbacksOverride instead of agents.defaults.model.fallbacks", async () => {
|
||||||
|
const cfg = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: {
|
||||||
|
fallbacks: ["openai/gpt-5.2"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
const calls: Array<{ provider: string; model: string }> = [];
|
||||||
|
|
||||||
|
const res = await runWithModelFallback({
|
||||||
|
cfg,
|
||||||
|
provider: "anthropic",
|
||||||
|
model: "claude-opus-4-5",
|
||||||
|
fallbacksOverride: ["openai/gpt-4.1"],
|
||||||
|
run: async (provider, model) => {
|
||||||
|
calls.push({ provider, model });
|
||||||
|
if (provider === "anthropic") {
|
||||||
|
throw Object.assign(new Error("nope"), { status: 401 });
|
||||||
|
}
|
||||||
|
if (provider === "openai" && model === "gpt-4.1") {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected candidate: ${provider}/${model}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.result).toBe("ok");
|
||||||
|
expect(calls).toEqual([
|
||||||
|
{ provider: "anthropic", model: "claude-opus-4-5" },
|
||||||
|
{ provider: "openai", model: "gpt-4.1" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats an empty fallbacksOverride as disabling global fallbacks", async () => {
|
||||||
|
const cfg = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: {
|
||||||
|
fallbacks: ["openai/gpt-5.2"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
const calls: Array<{ provider: string; model: string }> = [];
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runWithModelFallback({
|
||||||
|
cfg,
|
||||||
|
provider: "anthropic",
|
||||||
|
model: "claude-opus-4-5",
|
||||||
|
fallbacksOverride: [],
|
||||||
|
run: async (provider, model) => {
|
||||||
|
calls.push({ provider, model });
|
||||||
|
throw new Error("primary failed");
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("primary failed");
|
||||||
|
|
||||||
|
expect(calls).toEqual([
|
||||||
|
{ provider: "anthropic", model: "claude-opus-4-5" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back on missing API key errors", async () => {
|
it("falls back on missing API key errors", async () => {
|
||||||
const cfg = makeCfg();
|
const cfg = makeCfg();
|
||||||
const run = vi
|
const run = vi
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ function resolveFallbackCandidates(params: {
|
|||||||
cfg: ClawdbotConfig | undefined;
|
cfg: ClawdbotConfig | undefined;
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
/** Optional explicit fallbacks list; when provided (even empty), replaces agents.defaults.model.fallbacks. */
|
||||||
|
fallbacksOverride?: string[];
|
||||||
}): ModelCandidate[] {
|
}): ModelCandidate[] {
|
||||||
const provider = params.provider.trim() || DEFAULT_PROVIDER;
|
const provider = params.provider.trim() || DEFAULT_PROVIDER;
|
||||||
const model = params.model.trim() || DEFAULT_MODEL;
|
const model = params.model.trim() || DEFAULT_MODEL;
|
||||||
@@ -159,6 +161,7 @@ function resolveFallbackCandidates(params: {
|
|||||||
addCandidate({ provider, model }, false);
|
addCandidate({ provider, model }, false);
|
||||||
|
|
||||||
const modelFallbacks = (() => {
|
const modelFallbacks = (() => {
|
||||||
|
if (params.fallbacksOverride !== undefined) return params.fallbacksOverride;
|
||||||
const model = params.cfg?.agents?.defaults?.model as
|
const model = params.cfg?.agents?.defaults?.model as
|
||||||
| { fallbacks?: string[] }
|
| { fallbacks?: string[] }
|
||||||
| string
|
| string
|
||||||
@@ -177,7 +180,11 @@ function resolveFallbackCandidates(params: {
|
|||||||
addCandidate(resolved.ref, true);
|
addCandidate(resolved.ref, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (primary?.provider && primary.model) {
|
if (
|
||||||
|
params.fallbacksOverride === undefined &&
|
||||||
|
primary?.provider &&
|
||||||
|
primary.model
|
||||||
|
) {
|
||||||
addCandidate({ provider: primary.provider, model: primary.model }, false);
|
addCandidate({ provider: primary.provider, model: primary.model }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +195,8 @@ export async function runWithModelFallback<T>(params: {
|
|||||||
cfg: ClawdbotConfig | undefined;
|
cfg: ClawdbotConfig | undefined;
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
/** Optional explicit fallbacks list; when provided (even empty), replaces agents.defaults.model.fallbacks. */
|
||||||
|
fallbacksOverride?: string[];
|
||||||
run: (provider: string, model: string) => Promise<T>;
|
run: (provider: string, model: string) => Promise<T>;
|
||||||
onError?: (attempt: {
|
onError?: (attempt: {
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -202,7 +211,12 @@ export async function runWithModelFallback<T>(params: {
|
|||||||
model: string;
|
model: string;
|
||||||
attempts: FallbackAttempt[];
|
attempts: FallbackAttempt[];
|
||||||
}> {
|
}> {
|
||||||
const candidates = resolveFallbackCandidates(params);
|
const candidates = resolveFallbackCandidates({
|
||||||
|
cfg: params.cfg,
|
||||||
|
provider: params.provider,
|
||||||
|
model: params.model,
|
||||||
|
fallbacksOverride: params.fallbacksOverride,
|
||||||
|
});
|
||||||
const attempts: FallbackAttempt[] = [];
|
const attempts: FallbackAttempt[] = [];
|
||||||
let lastError: unknown;
|
let lastError: unknown;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
|
||||||
import { runCliAgent } from "../../agents/cli-runner.js";
|
import { runCliAgent } from "../../agents/cli-runner.js";
|
||||||
import { getCliSessionId, setCliSessionId } from "../../agents/cli-session.js";
|
import { getCliSessionId, setCliSessionId } from "../../agents/cli-session.js";
|
||||||
import { lookupContextTokens } from "../../agents/context.js";
|
import { lookupContextTokens } from "../../agents/context.js";
|
||||||
@@ -394,6 +395,10 @@ export async function runReplyAgent(params: {
|
|||||||
cfg: followupRun.run.config,
|
cfg: followupRun.run.config,
|
||||||
provider: followupRun.run.provider,
|
provider: followupRun.run.provider,
|
||||||
model: followupRun.run.model,
|
model: followupRun.run.model,
|
||||||
|
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||||
|
followupRun.run.config,
|
||||||
|
resolveAgentIdFromSessionKey(followupRun.run.sessionKey),
|
||||||
|
),
|
||||||
run: (provider, model) =>
|
run: (provider, model) =>
|
||||||
runEmbeddedPiAgent({
|
runEmbeddedPiAgent({
|
||||||
sessionId: followupRun.run.sessionId,
|
sessionId: followupRun.run.sessionId,
|
||||||
@@ -586,6 +591,10 @@ export async function runReplyAgent(params: {
|
|||||||
cfg: followupRun.run.config,
|
cfg: followupRun.run.config,
|
||||||
provider: followupRun.run.provider,
|
provider: followupRun.run.provider,
|
||||||
model: followupRun.run.model,
|
model: followupRun.run.model,
|
||||||
|
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||||
|
followupRun.run.config,
|
||||||
|
resolveAgentIdFromSessionKey(followupRun.run.sessionKey),
|
||||||
|
),
|
||||||
run: (provider, model) => {
|
run: (provider, model) => {
|
||||||
if (isCliProvider(provider, followupRun.run.config)) {
|
if (isCliProvider(provider, followupRun.run.config)) {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
resolveAgentConfig,
|
|
||||||
resolveAgentDir,
|
resolveAgentDir,
|
||||||
|
resolveAgentModelPrimary,
|
||||||
resolveDefaultAgentId,
|
resolveDefaultAgentId,
|
||||||
resolveSessionAgentId,
|
resolveSessionAgentId,
|
||||||
} from "../../agents/agent-scope.js";
|
} from "../../agents/agent-scope.js";
|
||||||
@@ -1629,7 +1629,7 @@ export function resolveDefaultModel(params: {
|
|||||||
aliasIndex: ModelAliasIndex;
|
aliasIndex: ModelAliasIndex;
|
||||||
} {
|
} {
|
||||||
const agentModelOverride = params.agentId
|
const agentModelOverride = params.agentId
|
||||||
? resolveAgentConfig(params.cfg, params.agentId)?.model?.trim()
|
? resolveAgentModelPrimary(params.cfg, params.agentId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const cfg =
|
const cfg =
|
||||||
agentModelOverride && agentModelOverride.length > 0
|
agentModelOverride && agentModelOverride.length > 0
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
|
||||||
import { lookupContextTokens } from "../../agents/context.js";
|
import { lookupContextTokens } from "../../agents/context.js";
|
||||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||||
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
||||||
import { hasNonzeroUsage } from "../../agents/usage.js";
|
import { hasNonzeroUsage } from "../../agents/usage.js";
|
||||||
import {
|
import {
|
||||||
|
resolveAgentIdFromSessionKey,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
updateSessionStoreEntry,
|
updateSessionStoreEntry,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
@@ -136,6 +138,10 @@ export function createFollowupRunner(params: {
|
|||||||
cfg: queued.run.config,
|
cfg: queued.run.config,
|
||||||
provider: queued.run.provider,
|
provider: queued.run.provider,
|
||||||
model: queued.run.model,
|
model: queued.run.model,
|
||||||
|
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||||
|
queued.run.config,
|
||||||
|
resolveAgentIdFromSessionKey(queued.run.sessionKey),
|
||||||
|
),
|
||||||
run: (provider, model) =>
|
run: (provider, model) =>
|
||||||
runEmbeddedPiAgent({
|
runEmbeddedPiAgent({
|
||||||
sessionId: queued.run.sessionId,
|
sessionId: queued.run.sessionId,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import {
|
import {
|
||||||
resolveAgentDir,
|
resolveAgentDir,
|
||||||
|
resolveAgentModelFallbacksOverride,
|
||||||
|
resolveAgentModelPrimary,
|
||||||
resolveAgentWorkspaceDir,
|
resolveAgentWorkspaceDir,
|
||||||
} from "../agents/agent-scope.js";
|
} from "../agents/agent-scope.js";
|
||||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||||
@@ -345,9 +347,28 @@ export async function agentCommand(
|
|||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const agentModelPrimary = resolveAgentModelPrimary(cfg, sessionAgentId);
|
||||||
|
const cfgForModelSelection = agentModelPrimary
|
||||||
|
? {
|
||||||
|
...cfg,
|
||||||
|
agents: {
|
||||||
|
...cfg.agents,
|
||||||
|
defaults: {
|
||||||
|
...cfg.agents?.defaults,
|
||||||
|
model: {
|
||||||
|
...(typeof cfg.agents?.defaults?.model === "object"
|
||||||
|
? cfg.agents.defaults.model
|
||||||
|
: undefined),
|
||||||
|
primary: agentModelPrimary,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: cfg;
|
||||||
|
|
||||||
const { provider: defaultProvider, model: defaultModel } =
|
const { provider: defaultProvider, model: defaultModel } =
|
||||||
resolveConfiguredModelRef({
|
resolveConfiguredModelRef({
|
||||||
cfg,
|
cfg: cfgForModelSelection,
|
||||||
defaultProvider: DEFAULT_PROVIDER,
|
defaultProvider: DEFAULT_PROVIDER,
|
||||||
defaultModel: DEFAULT_MODEL,
|
defaultModel: DEFAULT_MODEL,
|
||||||
});
|
});
|
||||||
@@ -477,6 +498,10 @@ export async function agentCommand(
|
|||||||
cfg,
|
cfg,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
|
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||||
|
cfg,
|
||||||
|
sessionAgentId,
|
||||||
|
),
|
||||||
run: (providerOverride, modelOverride) => {
|
run: (providerOverride, modelOverride) => {
|
||||||
if (isCliProvider(providerOverride, cfg)) {
|
if (isCliProvider(providerOverride, cfg)) {
|
||||||
const cliSessionId = getCliSessionId(sessionEntry, providerOverride);
|
const cliSessionId = getCliSessionId(sessionEntry, providerOverride);
|
||||||
|
|||||||
@@ -142,7 +142,15 @@ function resolveAgentModel(cfg: ClawdbotConfig, agentId: string) {
|
|||||||
const entry = listAgentEntries(cfg).find(
|
const entry = listAgentEntries(cfg).find(
|
||||||
(agent) => normalizeAgentId(agent.id) === normalizeAgentId(agentId),
|
(agent) => normalizeAgentId(agent.id) === normalizeAgentId(agentId),
|
||||||
);
|
);
|
||||||
if (entry?.model?.trim()) return entry.model.trim();
|
if (entry?.model) {
|
||||||
|
if (typeof entry.model === "string" && entry.model.trim()) {
|
||||||
|
return entry.model.trim();
|
||||||
|
}
|
||||||
|
if (typeof entry.model === "object") {
|
||||||
|
const primary = entry.model.primary?.trim();
|
||||||
|
if (primary) return primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
const raw = cfg.agents?.defaults?.model;
|
const raw = cfg.agents?.defaults?.model;
|
||||||
if (typeof raw === "string") return raw;
|
if (typeof raw === "string") return raw;
|
||||||
return raw?.primary?.trim() || undefined;
|
return raw?.primary?.trim() || undefined;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { loginOpenAICodex, type OAuthCredentials } from "@mariozechner/pi-ai";
|
import { loginOpenAICodex, type OAuthCredentials } from "@mariozechner/pi-ai";
|
||||||
import { resolveAgentConfig } from "../agents/agent-scope.js";
|
import { resolveAgentModelPrimary } from "../agents/agent-scope.js";
|
||||||
import {
|
import {
|
||||||
CLAUDE_CLI_PROFILE_ID,
|
CLAUDE_CLI_PROFILE_ID,
|
||||||
CODEX_CLI_PROFILE_ID,
|
CODEX_CLI_PROFILE_ID,
|
||||||
@@ -152,7 +152,7 @@ export async function warnIfModelConfigLooksOff(
|
|||||||
options?: { agentId?: string; agentDir?: string },
|
options?: { agentId?: string; agentDir?: string },
|
||||||
) {
|
) {
|
||||||
const agentModelOverride = options?.agentId
|
const agentModelOverride = options?.agentId
|
||||||
? resolveAgentConfig(config, options.agentId)?.model?.trim()
|
? resolveAgentModelPrimary(config, options.agentId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const configWithModel =
|
const configWithModel =
|
||||||
agentModelOverride && agentModelOverride.length > 0
|
agentModelOverride && agentModelOverride.length > 0
|
||||||
|
|||||||
@@ -1127,13 +1127,22 @@ export type ToolsConfig = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AgentModelConfig =
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
/** Primary model (provider/model). */
|
||||||
|
primary?: string;
|
||||||
|
/** Per-agent model fallbacks (provider/model). */
|
||||||
|
fallbacks?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type AgentConfig = {
|
export type AgentConfig = {
|
||||||
id: string;
|
id: string;
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
workspace?: string;
|
workspace?: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
model?: string;
|
model?: AgentModelConfig;
|
||||||
memorySearch?: MemorySearchConfig;
|
memorySearch?: MemorySearchConfig;
|
||||||
/** Human-like delay between block replies for this agent. */
|
/** Human-like delay between block replies for this agent. */
|
||||||
humanDelay?: HumanDelayConfig;
|
humanDelay?: HumanDelayConfig;
|
||||||
|
|||||||
@@ -940,14 +940,20 @@ const MemorySearchSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
const AgentModelSchema = z.union([
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
primary: z.string().optional(),
|
||||||
|
fallbacks: z.array(z.string()).optional(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
const AgentEntrySchema = z.object({
|
const AgentEntrySchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
default: z.boolean().optional(),
|
default: z.boolean().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
workspace: z.string().optional(),
|
workspace: z.string().optional(),
|
||||||
agentDir: z.string().optional(),
|
agentDir: z.string().optional(),
|
||||||
model: z.string().optional(),
|
model: AgentModelSchema.optional(),
|
||||||
memorySearch: MemorySearchSchema,
|
memorySearch: MemorySearchSchema,
|
||||||
humanDelay: HumanDelaySchema.optional(),
|
humanDelay: HumanDelaySchema.optional(),
|
||||||
identity: IdentitySchema,
|
identity: IdentitySchema,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import {
|
import {
|
||||||
resolveAgentConfig,
|
resolveAgentConfig,
|
||||||
|
resolveAgentModelFallbacksOverride,
|
||||||
resolveAgentWorkspaceDir,
|
resolveAgentWorkspaceDir,
|
||||||
resolveDefaultAgentId,
|
resolveDefaultAgentId,
|
||||||
} from "../agents/agent-scope.js";
|
} from "../agents/agent-scope.js";
|
||||||
@@ -458,6 +459,10 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
cfg: cfgWithAgentDefaults,
|
cfg: cfgWithAgentDefaults,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
|
fallbacksOverride: resolveAgentModelFallbacksOverride(
|
||||||
|
params.cfg,
|
||||||
|
agentId,
|
||||||
|
),
|
||||||
run: (providerOverride, modelOverride) => {
|
run: (providerOverride, modelOverride) => {
|
||||||
if (isCliProvider(providerOverride, cfgWithAgentDefaults)) {
|
if (isCliProvider(providerOverride, cfgWithAgentDefaults)) {
|
||||||
const cliSessionId = getCliSessionId(
|
const cliSessionId = getCliSessionId(
|
||||||
|
|||||||
Reference in New Issue
Block a user