Merge 8f670357faee7437463a54a959d7f59bd308475c into 598f1826d8b2bc969aace2c6459824737667218c
This commit is contained in:
commit
c3c660c1a3
@ -1257,6 +1257,8 @@ async function agentCommandInternal(
|
||||
|
||||
// Update token+model fields in the session store.
|
||||
if (sessionStore && sessionKey) {
|
||||
// Model is from fallback if the successfully-used provider/model differs from the primary.
|
||||
const isFromFallback = fallbackProvider !== provider || fallbackModel !== model;
|
||||
await updateSessionStoreAfterAgentRun({
|
||||
cfg,
|
||||
contextTokensOverride: agentCfg?.contextTokens,
|
||||
@ -1268,6 +1270,7 @@ async function agentCommandInternal(
|
||||
defaultModel: model,
|
||||
fallbackProvider,
|
||||
fallbackModel,
|
||||
isFromFallback,
|
||||
result,
|
||||
});
|
||||
}
|
||||
|
||||
@ -29,6 +29,8 @@ export async function updateSessionStoreAfterAgentRun(params: {
|
||||
defaultModel: string;
|
||||
fallbackProvider?: string;
|
||||
fallbackModel?: string;
|
||||
/** True when the model was selected by the fallback chain rather than being the primary. */
|
||||
isFromFallback?: boolean;
|
||||
result: RunResult;
|
||||
}) {
|
||||
const {
|
||||
@ -71,6 +73,7 @@ export async function updateSessionStoreAfterAgentRun(params: {
|
||||
setSessionRuntimeModel(next, {
|
||||
provider: providerUsed,
|
||||
model: modelUsed,
|
||||
isFromFallback: params.isFromFallback ?? false,
|
||||
});
|
||||
if (isCliProvider(providerUsed, cfg)) {
|
||||
const cliSessionId = result.meta.agentMeta?.sessionId?.trim();
|
||||
|
||||
@ -472,6 +472,8 @@ export async function runReplyAgent(params: {
|
||||
activeSessionEntry?.contextTokens ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
// Model is from fallback if the successfully-used provider/model differs from the primary.
|
||||
const isFromFallback = providerUsed !== selectedProvider || modelUsed !== selectedModel;
|
||||
await persistRunSessionUsage({
|
||||
storePath,
|
||||
sessionKey,
|
||||
@ -481,6 +483,7 @@ export async function runReplyAgent(params: {
|
||||
promptTokens,
|
||||
modelUsed,
|
||||
providerUsed,
|
||||
isFromFallback,
|
||||
contextTokensUsed,
|
||||
systemPromptReport: runResult.meta?.systemPromptReport,
|
||||
cliSessionId,
|
||||
|
||||
@ -268,6 +268,9 @@ export function createFollowupRunner(params: {
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
if (storePath && sessionKey) {
|
||||
// Model is from fallback if the successfully-used provider/model differs from the primary.
|
||||
const isFromFallback =
|
||||
fallbackProvider !== queued.run.provider || fallbackModel !== queued.run.model;
|
||||
await persistRunSessionUsage({
|
||||
storePath,
|
||||
sessionKey,
|
||||
@ -277,6 +280,7 @@ export function createFollowupRunner(params: {
|
||||
promptTokens,
|
||||
modelUsed,
|
||||
providerUsed: fallbackProvider,
|
||||
isFromFallback,
|
||||
contextTokensUsed,
|
||||
systemPromptReport: runResult.meta?.systemPromptReport,
|
||||
logLabel: "followup",
|
||||
|
||||
@ -70,6 +70,8 @@ export async function persistSessionUsageUpdate(params: {
|
||||
lastCallUsage?: NormalizedUsage;
|
||||
modelUsed?: string;
|
||||
providerUsed?: string;
|
||||
/** True when the model was selected by the fallback chain rather than being the primary. */
|
||||
isFromFallback?: boolean;
|
||||
contextTokensUsed?: number;
|
||||
promptTokens?: number;
|
||||
systemPromptReport?: SessionSystemPromptReport;
|
||||
@ -119,6 +121,7 @@ export async function persistSessionUsageUpdate(params: {
|
||||
const patch: Partial<SessionEntry> = {
|
||||
modelProvider: params.providerUsed ?? entry.modelProvider,
|
||||
model: params.modelUsed ?? entry.model,
|
||||
modelIsFromFallback: params.isFromFallback ?? entry.modelIsFromFallback,
|
||||
contextTokens: resolvedContextTokens,
|
||||
systemPromptReport: params.systemPromptReport ?? entry.systemPromptReport,
|
||||
updatedAt: Date.now(),
|
||||
@ -159,6 +162,7 @@ export async function persistSessionUsageUpdate(params: {
|
||||
const patch: Partial<SessionEntry> = {
|
||||
modelProvider: params.providerUsed ?? entry.modelProvider,
|
||||
model: params.modelUsed ?? entry.model,
|
||||
modelIsFromFallback: params.isFromFallback ?? entry.modelIsFromFallback,
|
||||
contextTokens: params.contextTokensUsed ?? entry.contextTokens,
|
||||
systemPromptReport: params.systemPromptReport ?? entry.systemPromptReport,
|
||||
updatedAt: Date.now(),
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
loadSessionStore,
|
||||
mergeSessionEntry,
|
||||
resolveAndPersistSessionFile,
|
||||
setSessionRuntimeModel,
|
||||
updateSessionStore,
|
||||
} from "../sessions.js";
|
||||
import type { SessionConfig } from "../types.base.js";
|
||||
@ -596,3 +597,71 @@ describe("resolveAndPersistSessionFile", () => {
|
||||
expect(saved[sessionKey]?.sessionFile).toBe(fallbackSessionFile);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSessionRuntimeModel", () => {
|
||||
it("sets model and provider on session entry", () => {
|
||||
const entry: SessionEntry = { sessionId: "s1", updatedAt: Date.now() };
|
||||
|
||||
const result = setSessionRuntimeModel(entry, {
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-6",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(entry.modelProvider).toBe("anthropic");
|
||||
expect(entry.model).toBe("claude-opus-4-6");
|
||||
expect(entry.modelIsFromFallback).toBe(false);
|
||||
});
|
||||
|
||||
it("sets modelIsFromFallback when isFromFallback is true (#47705)", () => {
|
||||
const entry: SessionEntry = { sessionId: "s1", updatedAt: Date.now() };
|
||||
|
||||
setSessionRuntimeModel(entry, {
|
||||
provider: "xai",
|
||||
model: "grok-4-1-fast-reasoning",
|
||||
isFromFallback: true,
|
||||
});
|
||||
|
||||
expect(entry.modelIsFromFallback).toBe(true);
|
||||
});
|
||||
|
||||
it("clears modelIsFromFallback when isFromFallback is false", () => {
|
||||
const entry: SessionEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: Date.now(),
|
||||
modelIsFromFallback: true,
|
||||
};
|
||||
|
||||
setSessionRuntimeModel(entry, {
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.3-codex",
|
||||
isFromFallback: false,
|
||||
});
|
||||
|
||||
expect(entry.modelIsFromFallback).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for empty provider", () => {
|
||||
const entry: SessionEntry = { sessionId: "s1", updatedAt: Date.now() };
|
||||
|
||||
const result = setSessionRuntimeModel(entry, {
|
||||
provider: "",
|
||||
model: "claude-opus-4-6",
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(entry.model).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns false for empty model", () => {
|
||||
const entry: SessionEntry = { sessionId: "s1", updatedAt: Date.now() };
|
||||
|
||||
const result = setSessionRuntimeModel(entry, {
|
||||
provider: "anthropic",
|
||||
model: "",
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(entry.modelProvider).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -153,6 +153,13 @@ export type SessionEntry = {
|
||||
cacheWrite?: number;
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
/**
|
||||
* True when the session's runtime model was selected by the fallback chain
|
||||
* rather than being the configured primary. Used to prevent fallback models
|
||||
* from being persisted back to agent config and to ensure the primary model
|
||||
* is retried on subsequent requests.
|
||||
*/
|
||||
modelIsFromFallback?: boolean;
|
||||
/**
|
||||
* Last selected/runtime model pair for which a fallback notice was emitted.
|
||||
* Used to avoid repeating the same fallback notice every turn.
|
||||
@ -231,7 +238,7 @@ export function normalizeSessionRuntimeModelFields(entry: SessionEntry): Session
|
||||
|
||||
export function setSessionRuntimeModel(
|
||||
entry: SessionEntry,
|
||||
runtime: { provider: string; model: string },
|
||||
runtime: { provider: string; model: string; isFromFallback?: boolean },
|
||||
): boolean {
|
||||
const provider = runtime.provider.trim();
|
||||
const model = runtime.model.trim();
|
||||
@ -240,6 +247,7 @@ export function setSessionRuntimeModel(
|
||||
}
|
||||
entry.modelProvider = provider;
|
||||
entry.model = model;
|
||||
entry.modelIsFromFallback = runtime.isFromFallback ?? false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -752,9 +752,12 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
lookupContextTokens(modelUsed, { allowAsyncLoad: false }) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
// Model is from fallback if the successfully-used provider/model differs from the primary.
|
||||
const isFromFallback = providerUsed !== provider || modelUsed !== model;
|
||||
setSessionRuntimeModel(cronSession.sessionEntry, {
|
||||
provider: providerUsed,
|
||||
model: modelUsed,
|
||||
isFromFallback,
|
||||
});
|
||||
cronSession.sessionEntry.contextTokens = contextTokens;
|
||||
if (isCliProvider(providerUsed, cfgWithAgentDefaults)) {
|
||||
|
||||
@ -488,6 +488,58 @@ describe("resolveSessionModelRef", () => {
|
||||
|
||||
expect(resolved).toEqual({ provider: "anthropic", model: "claude-sonnet-4-6" });
|
||||
});
|
||||
|
||||
test("ignores session-stored model when modelIsFromFallback is true (#47705)", () => {
|
||||
// When a fallback model is stored in the session, resolveSessionModelRef should
|
||||
// skip it and return the configured primary model instead, so the primary is
|
||||
// retried on subsequent requests.
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "openai-codex/gpt-5.3-codex",
|
||||
});
|
||||
|
||||
const resolved = resolveSessionModelRef(cfg, {
|
||||
sessionId: "fallback-session",
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: "xai",
|
||||
model: "grok-4-1-fast-reasoning",
|
||||
modelIsFromFallback: true,
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({ provider: "openai-codex", model: "gpt-5.3-codex" });
|
||||
});
|
||||
|
||||
test("uses session-stored model when modelIsFromFallback is false", () => {
|
||||
// When the model was explicitly set (not from fallback), it should be used.
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "openai-codex/gpt-5.3-codex",
|
||||
});
|
||||
|
||||
const resolved = resolveSessionModelRef(cfg, {
|
||||
sessionId: "explicit-session",
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-opus-4-6",
|
||||
modelIsFromFallback: false,
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
|
||||
});
|
||||
|
||||
test("uses session-stored model when modelIsFromFallback is undefined (legacy)", () => {
|
||||
// Legacy sessions without the flag should still work as before.
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "openai-codex/gpt-5.3-codex",
|
||||
});
|
||||
|
||||
const resolved = resolveSessionModelRef(cfg, {
|
||||
sessionId: "legacy-session",
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-opus-4-6",
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSessionModelIdentityRef", () => {
|
||||
|
||||
@ -912,7 +912,10 @@ export function resolveSessionModelRef(
|
||||
cfg: OpenClawConfig,
|
||||
entry?:
|
||||
| SessionEntry
|
||||
| Pick<SessionEntry, "model" | "modelProvider" | "modelOverride" | "providerOverride">,
|
||||
| Pick<
|
||||
SessionEntry,
|
||||
"model" | "modelProvider" | "modelOverride" | "providerOverride" | "modelIsFromFallback"
|
||||
>,
|
||||
agentId?: string,
|
||||
): { provider: string; model: string } {
|
||||
const resolved = agentId
|
||||
@ -923,13 +926,19 @@ export function resolveSessionModelRef(
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
|
||||
// Skip session-stored runtime model if it came from the fallback chain.
|
||||
// This ensures the primary model is retried on subsequent requests rather
|
||||
// than permanently sticking with the fallback. See #47705.
|
||||
const isFromFallback =
|
||||
entry && "modelIsFromFallback" in entry && entry.modelIsFromFallback === true;
|
||||
|
||||
// Prefer the last runtime model recorded on the session entry.
|
||||
// This is the actual model used by the latest run and must win over defaults.
|
||||
let provider = resolved.provider;
|
||||
let model = resolved.model;
|
||||
const runtimeModel = entry?.model?.trim();
|
||||
const runtimeProvider = entry?.modelProvider?.trim();
|
||||
if (runtimeModel) {
|
||||
if (runtimeModel && !isFromFallback) {
|
||||
if (runtimeProvider) {
|
||||
// Provider is explicitly recorded — use it directly. Re-parsing the
|
||||
// model string through parseModelRef would incorrectly split OpenRouter
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user