Merge ee35f505503fe0c3964dd91007b55d9ade9912ae into 598f1826d8b2bc969aace2c6459824737667218c
This commit is contained in:
commit
6e80607b9c
@ -405,7 +405,7 @@ describe("gateway session utils", () => {
|
||||
});
|
||||
|
||||
describe("resolveSessionModelRef", () => {
|
||||
test("prefers runtime model/provider from session entry", () => {
|
||||
test("prefers override over runtime model when both are present", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
});
|
||||
@ -419,7 +419,8 @@ describe("resolveSessionModelRef", () => {
|
||||
providerOverride: "anthropic",
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({ provider: "openai-codex", model: "gpt-5.3-codex" });
|
||||
// Override reflects user intent for the *next* run and wins over last-run runtime model.
|
||||
expect(resolved).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
|
||||
});
|
||||
|
||||
test("preserves openrouter provider when model contains vendor prefix", () => {
|
||||
@ -488,6 +489,38 @@ describe("resolveSessionModelRef", () => {
|
||||
|
||||
expect(resolved).toEqual({ provider: "anthropic", model: "claude-sonnet-4-6" });
|
||||
});
|
||||
|
||||
test("preserves providerOverride for slash-containing modelOverride (wrapper providers)", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
});
|
||||
|
||||
const resolved = resolveSessionModelRef(cfg, {
|
||||
sessionId: "s-wrapper",
|
||||
updatedAt: Date.now(),
|
||||
modelOverride: "anthropic/claude-sonnet-4-6",
|
||||
providerOverride: "openrouter",
|
||||
});
|
||||
|
||||
// providerOverride should be preserved as-is; re-parsing the model string
|
||||
// would incorrectly treat "anthropic" as the provider.
|
||||
expect(resolved).toEqual({ provider: "openrouter", model: "anthropic/claude-sonnet-4-6" });
|
||||
});
|
||||
|
||||
test("uses runtime model when no override is set", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
});
|
||||
|
||||
const resolved = resolveSessionModelRef(cfg, {
|
||||
sessionId: "s-runtime-only",
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: "openai-codex",
|
||||
model: "gpt-5.3-codex",
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({ provider: "openai-codex", model: "gpt-5.3-codex" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSessionModelIdentityRef", () => {
|
||||
|
||||
@ -923,10 +923,33 @@ export function resolveSessionModelRef(
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
|
||||
// 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.
|
||||
// Prefer explicit per-session override (set via /model or at spawn time).
|
||||
// This reflects the user's intent for the next run and should take precedence
|
||||
// over the runtime model recorded from a previous run.
|
||||
let provider = resolved.provider;
|
||||
let model = resolved.model;
|
||||
const storedModelOverride = entry?.modelOverride?.trim();
|
||||
if (storedModelOverride) {
|
||||
const explicitOverrideProvider = entry?.providerOverride?.trim();
|
||||
if (explicitOverrideProvider) {
|
||||
// Provider is explicitly set alongside the override — use it directly.
|
||||
// Re-parsing would incorrectly split vendor-prefixed model names
|
||||
// (e.g. modelOverride="anthropic/claude-sonnet-4-6" with providerOverride="openrouter").
|
||||
return { provider: explicitOverrideProvider, model: storedModelOverride };
|
||||
}
|
||||
const overrideProvider = provider || DEFAULT_PROVIDER;
|
||||
const parsedOverride = parseModelRef(storedModelOverride, overrideProvider);
|
||||
if (parsedOverride) {
|
||||
provider = parsedOverride.provider;
|
||||
model = parsedOverride.model;
|
||||
} else {
|
||||
provider = overrideProvider;
|
||||
model = storedModelOverride;
|
||||
}
|
||||
return { provider, model };
|
||||
}
|
||||
|
||||
// Fall back to the last runtime model recorded on the session entry.
|
||||
const runtimeModel = entry?.model?.trim();
|
||||
const runtimeProvider = entry?.modelProvider?.trim();
|
||||
if (runtimeModel) {
|
||||
@ -948,21 +971,6 @@ export function resolveSessionModelRef(
|
||||
}
|
||||
return { provider, model };
|
||||
}
|
||||
|
||||
// Fall back to explicit per-session override (set at spawn/model-patch time),
|
||||
// then finally to configured defaults.
|
||||
const storedModelOverride = entry?.modelOverride?.trim();
|
||||
if (storedModelOverride) {
|
||||
const overrideProvider = entry?.providerOverride?.trim() || provider || DEFAULT_PROVIDER;
|
||||
const parsedOverride = parseModelRef(storedModelOverride, overrideProvider);
|
||||
if (parsedOverride) {
|
||||
provider = parsedOverride.provider;
|
||||
model = parsedOverride.model;
|
||||
} else {
|
||||
provider = overrideProvider;
|
||||
model = storedModelOverride;
|
||||
}
|
||||
}
|
||||
return { provider, model };
|
||||
}
|
||||
|
||||
|
||||
@ -281,4 +281,129 @@ describe("tui session actions", () => {
|
||||
expect(state.sessionInfo.updatedAt).toBe(50);
|
||||
expect(btw.clear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to defaults model when session entry is not found", async () => {
|
||||
// When the current session key has no entry in the store (brand-new session),
|
||||
// the TUI should show the configured default model, not "unknown".
|
||||
const listSessions = vi.fn().mockResolvedValue({
|
||||
ts: Date.now(),
|
||||
path: "/tmp/sessions.json",
|
||||
count: 0,
|
||||
defaults: {
|
||||
modelProvider: "my-copilot",
|
||||
model: "claude-opus-4.6",
|
||||
contextTokens: 200000,
|
||||
},
|
||||
sessions: [],
|
||||
});
|
||||
|
||||
const state: TuiStateAccess = {
|
||||
agentDefaultId: "main",
|
||||
sessionMainKey: "agent:main:main",
|
||||
sessionScope: "global",
|
||||
agents: [],
|
||||
currentAgentId: "main",
|
||||
currentSessionKey: "agent:main:main",
|
||||
currentSessionId: null,
|
||||
activeChatRunId: null,
|
||||
historyLoaded: false,
|
||||
sessionInfo: {},
|
||||
initialSessionApplied: true,
|
||||
isConnected: true,
|
||||
autoMessageSent: false,
|
||||
toolsExpanded: false,
|
||||
showThinking: false,
|
||||
connectionStatus: "connected",
|
||||
activityStatus: "idle",
|
||||
statusTimeout: null,
|
||||
lastCtrlCAt: 0,
|
||||
};
|
||||
|
||||
const { refreshSessionInfo } = createSessionActions({
|
||||
client: { listSessions } as unknown as GatewayChatClient,
|
||||
chatLog: { addSystem: vi.fn() } as unknown as import("./components/chat-log.js").ChatLog,
|
||||
tui: { requestRender: vi.fn() } as unknown as import("@mariozechner/pi-tui").TUI,
|
||||
opts: {},
|
||||
state,
|
||||
agentNames: new Map(),
|
||||
initialSessionInput: "",
|
||||
initialSessionAgentId: null,
|
||||
resolveSessionKey: vi.fn(),
|
||||
updateHeader: vi.fn(),
|
||||
updateFooter: vi.fn(),
|
||||
updateAutocompleteProvider: vi.fn(),
|
||||
setActivityStatus: vi.fn(),
|
||||
});
|
||||
|
||||
await refreshSessionInfo();
|
||||
|
||||
expect(state.sessionInfo.model).toBe("claude-opus-4.6");
|
||||
expect(state.sessionInfo.modelProvider).toBe("my-copilot");
|
||||
expect(state.sessionInfo.contextTokens).toBe(200000);
|
||||
});
|
||||
|
||||
it("prefers modelOverride over runtime model in session entry", async () => {
|
||||
// After /model switch, the entry has both a runtime model (from last run)
|
||||
// and a modelOverride (user's intent for next run). The override should win.
|
||||
const listSessions = vi.fn().mockResolvedValue({
|
||||
ts: Date.now(),
|
||||
path: "/tmp/sessions.json",
|
||||
count: 1,
|
||||
defaults: {},
|
||||
sessions: [
|
||||
{
|
||||
key: "agent:main:main",
|
||||
model: "gpt-5.3-codex",
|
||||
modelProvider: "openai-codex",
|
||||
modelOverride: "claude-opus-4-6",
|
||||
providerOverride: "anthropic",
|
||||
updatedAt: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const state: TuiStateAccess = {
|
||||
agentDefaultId: "main",
|
||||
sessionMainKey: "agent:main:main",
|
||||
sessionScope: "global",
|
||||
agents: [],
|
||||
currentAgentId: "main",
|
||||
currentSessionKey: "agent:main:main",
|
||||
currentSessionId: null,
|
||||
activeChatRunId: null,
|
||||
historyLoaded: false,
|
||||
sessionInfo: {},
|
||||
initialSessionApplied: true,
|
||||
isConnected: true,
|
||||
autoMessageSent: false,
|
||||
toolsExpanded: false,
|
||||
showThinking: false,
|
||||
connectionStatus: "connected",
|
||||
activityStatus: "idle",
|
||||
statusTimeout: null,
|
||||
lastCtrlCAt: 0,
|
||||
};
|
||||
|
||||
const { refreshSessionInfo } = createSessionActions({
|
||||
client: { listSessions } as unknown as GatewayChatClient,
|
||||
chatLog: { addSystem: vi.fn() } as unknown as import("./components/chat-log.js").ChatLog,
|
||||
tui: { requestRender: vi.fn() } as unknown as import("@mariozechner/pi-tui").TUI,
|
||||
opts: {},
|
||||
state,
|
||||
agentNames: new Map(),
|
||||
initialSessionInput: "",
|
||||
initialSessionAgentId: null,
|
||||
resolveSessionKey: vi.fn(),
|
||||
updateHeader: vi.fn(),
|
||||
updateFooter: vi.fn(),
|
||||
updateAutocompleteProvider: vi.fn(),
|
||||
setActivityStatus: vi.fn(),
|
||||
});
|
||||
|
||||
await refreshSessionInfo();
|
||||
|
||||
// Override should take precedence over runtime model
|
||||
expect(state.sessionInfo.model).toBe("claude-opus-4-6");
|
||||
expect(state.sessionInfo.modelProvider).toBe("anthropic");
|
||||
});
|
||||
});
|
||||
|
||||
@ -120,17 +120,30 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
}
|
||||
};
|
||||
|
||||
const resolveModelSelection = (entry?: SessionInfoEntry) => {
|
||||
const resolveModelSelection = (
|
||||
entry?: SessionInfoEntry,
|
||||
defaults?: SessionInfoDefaults | null,
|
||||
) => {
|
||||
// 1. Explicit user override (set via /model) wins over last-run runtime model.
|
||||
// This matches the priority in resolveSessionModelRef (session-utils.ts).
|
||||
const overrideModel = entry?.modelOverride?.trim();
|
||||
if (overrideModel) {
|
||||
const overrideProvider = entry?.providerOverride?.trim() || state.sessionInfo.modelProvider;
|
||||
return { modelProvider: overrideProvider, model: overrideModel };
|
||||
}
|
||||
// 2. Fall back to runtime model from last run.
|
||||
if (entry?.modelProvider || entry?.model) {
|
||||
return {
|
||||
modelProvider: entry.modelProvider ?? state.sessionInfo.modelProvider,
|
||||
model: entry.model ?? state.sessionInfo.model,
|
||||
};
|
||||
}
|
||||
const overrideModel = entry?.modelOverride?.trim();
|
||||
if (overrideModel) {
|
||||
const overrideProvider = entry?.providerOverride?.trim() || state.sessionInfo.modelProvider;
|
||||
return { modelProvider: overrideProvider, model: overrideModel };
|
||||
// 3. Configured defaults.
|
||||
if (defaults?.modelProvider || defaults?.model) {
|
||||
return {
|
||||
modelProvider: defaults.modelProvider ?? state.sessionInfo.modelProvider,
|
||||
model: defaults.model ?? state.sessionInfo.model,
|
||||
};
|
||||
}
|
||||
return {
|
||||
modelProvider: state.sessionInfo.modelProvider,
|
||||
@ -203,7 +216,7 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
next.updatedAt = entry.updatedAt;
|
||||
}
|
||||
|
||||
const selection = resolveModelSelection(entry);
|
||||
const selection = resolveModelSelection(entry, defaults);
|
||||
if (selection.modelProvider !== undefined) {
|
||||
next.modelProvider = selection.modelProvider;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user