Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions apps/web/test/agents-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,43 @@ describe("agentModelSettledContent (CL-6848)", () => {
).toEqual({ kind: "model", label: "Default" });
});

test("an unset model stays Default when the catalog winner changes (CL-6782)", () => {
const unset = {
status: "ready" as const,
data: { name: "triage-bot" },
};
expect(
agentModelSettledContent(unset, [
{ canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" },
]),
).toEqual({ kind: "model", label: "Default" });
expect(
agentModelSettledContent(unset, [
{ canonicalName: "claude-haiku-5", displayName: "Claude Haiku 5" },
{ canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" },
]),
).toEqual({ kind: "model", label: "Default" });
});

test("a stored model is unchanged when the catalog default becomes a different winner (CL-6782)", () => {
const stored = {
status: "ready" as const,
data: { name: "triage-bot", model: "claude-sonnet-4" },
};
expect(
agentModelSettledContent(stored, [
{ canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" },
{ canonicalName: "claude-haiku-5", displayName: "Claude Haiku 5" },
]),
).toEqual({ kind: "model", label: "Claude Sonnet 4" });
expect(
agentModelSettledContent(stored, [
{ canonicalName: "claude-haiku-5", displayName: "Claude Haiku 5" },
{ canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" },
]),
).toEqual({ kind: "model", label: "Claude Sonnet 4" });
});

test("a ready model maps to the catalog displayName, not the raw id", () => {
expect(
agentModelSettledContent(
Expand Down
8 changes: 6 additions & 2 deletions packages/settings-ui/src/connections-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,10 @@ function ModelRoutePanel({
}

function chooseModel(canonicalName: string) {
// Tenant default is an offering-priority write only (CL-6782). Existing
// agents keep whatever `capabilities.model` they already store; new
// agents may still pick up the default at create. Never PATCH
// `/agent-definitions`.
const target = ordered.find((row) => row.canonicalName === canonicalName);
applyPatches(
target === undefined
Expand All @@ -485,8 +489,8 @@ function ModelRoutePanel({
<div>
<h3 id="model-route-title">Default model & fallbacks</h3>
<p>
Used by Myra and new workbenches unless an agent chooses its own
model.
Used by Myra and new agents. Changing the default does not rewrite
models already stored on existing agents.
</p>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/settings-ui/src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export const SETTINGS_STRINGS = {

connectionsSectionTitle: "AI providers",
connectionsSectionDescription:
"Choose the model route Myra and new workbenches use, then connect the providers behind it.",
"Choose the model route Myra and new agents use, then connect the providers behind it. Changing the default does not rewrite existing agents.",
connectionsLoadError: "connections",
connectionsStatusNotConnected: "Not connected",
connectionsStatusConnected: "Connected",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,78 @@ describe("Global model route", () => {
container.remove();
}
});

test("choosing a default model PATCHes offerings only, never agent capabilities (CL-6782)", async () => {
const calls: { url: string; method: string }[] = [];
let patched = false;

globalThis.fetch = ((url: string, init?: RequestInit) => {
calls.push({
url,
method: init?.method ?? "GET",
});
if (url === "/api/tenants/ten_1/credentials") {
return Promise.resolve(
json({ data: [ANTHROPIC_CREDENTIAL], nextCursor: null }),
);
}
if (url === "/api/tenants/ten_1/providers") {
return Promise.resolve(
json({ data: [ANTHROPIC_PROVIDER], nextCursor: null }),
);
}
if (url === "/api/tenants/ten_1/connections/oauth-configured") {
return Promise.resolve(json({}));
}
if (url === "/api/tenants/ten_1/models") {
return Promise.resolve(
json(resolvedModels(patched ? 0 : 0, patched ? -1 : 1)),
);
}
if (url === "/api/tenants/ten_1/catalog/offerings") {
return Promise.resolve(json({ data: OWN_OFFERINGS, nextCursor: null }));
}
if (url === "/api/tenants/ten_1/catalog/offerings/offering_haiku") {
patched = true;
return Promise.resolve(json({ ...OWN_OFFERINGS[1], priority: -1 }));
}
throw new Error(`unexpected fetch: ${url}`);
}) as unknown as typeof fetch;

const { container, root } = renderSection();
try {
await settle();

expect(container.textContent).toContain(
"does not rewrite models already stored on existing agents",
);

const defaultModel = container.querySelector(
'select[aria-label="Default model"]',
) as HTMLSelectElement | null;
act(() => {
if (defaultModel !== null) {
defaultModel.value = "claude-haiku-5";
defaultModel.dispatchEvent(new Event("change", { bubbles: true }));
}
});
await settle();

const writes = calls.filter(
(call) => call.method !== "GET" && call.method !== "HEAD",
);
expect(writes.length).toBeGreaterThan(0);
expect(
writes.every((call) =>
call.url.startsWith("/api/tenants/ten_1/catalog/offerings/"),
),
).toBe(true);
expect(
calls.some((call) => call.url.includes("/agent-definitions")),
).toBe(false);
} finally {
act(() => root.unmount());
container.remove();
}
});
});
Loading