Preselect the recommended model while none is connected (#150)
Without a model key the settings opened on the keyless deployment default (OpenRouter). The connect flow now opens on Claude Opus 5, so a new user only adds a key or signs in. A connected credential, an owner-chosen default, or a deployment key still decides as before. Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2f8d7175f6
commit
b3f20967db
@ -224,7 +224,11 @@ describe("model setup gate", () => {
|
|||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
await expect(response.json()).resolves.toEqual({
|
await expect(response.json()).resolves.toEqual({
|
||||||
json: expect.objectContaining({ needsModel: false }),
|
json: expect.objectContaining({
|
||||||
|
needsModel: false,
|
||||||
|
defaultProvider: "openrouter",
|
||||||
|
defaultModel: "test-model",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -238,7 +242,12 @@ describe("model setup gate", () => {
|
|||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
await expect(response.json()).resolves.toEqual({
|
await expect(response.json()).resolves.toEqual({
|
||||||
json: expect.objectContaining({ needsModel: true }),
|
// The connect flow opens on the recommendation, not the keyless deployment default.
|
||||||
|
json: expect.objectContaining({
|
||||||
|
needsModel: true,
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModel: "claude-opus-5",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -71,6 +71,7 @@ import {
|
|||||||
probeOpenAiCompatibleModels,
|
probeOpenAiCompatibleModels,
|
||||||
provisionComputer,
|
provisionComputer,
|
||||||
queueComputerUpdate,
|
queueComputerUpdate,
|
||||||
|
RECOMMENDED_MODEL,
|
||||||
readProjectWorkspaceFile,
|
readProjectWorkspaceFile,
|
||||||
releaseComputerExecutionLease,
|
releaseComputerExecutionLease,
|
||||||
replaceComputer,
|
replaceComputer,
|
||||||
@ -5511,6 +5512,10 @@ async function meDto(deps: RouterDeps, actor: Actor): Promise<Me> {
|
|||||||
deps.prisma.user.findUniqueOrThrow({ where: { id: actor.userId } }),
|
deps.prisma.user.findUniqueOrThrow({ where: { id: actor.userId } }),
|
||||||
modelSetup(deps, actor),
|
modelSetup(deps, actor),
|
||||||
]);
|
]);
|
||||||
|
// Without a key the deployment default cannot run, so the connect flow opens on the recommendation.
|
||||||
|
const fallback = setup.needsModel
|
||||||
|
? RECOMMENDED_MODEL
|
||||||
|
: { provider: deps.env.defaultProvider, model: deps.env.defaultModel };
|
||||||
return {
|
return {
|
||||||
userId: actor.userId,
|
userId: actor.userId,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
@ -5519,11 +5524,9 @@ async function meDto(deps: RouterDeps, actor: Actor): Promise<Me> {
|
|||||||
isDeploymentOwner: actor.isDeploymentOwner,
|
isDeploymentOwner: actor.isDeploymentOwner,
|
||||||
needsModel: setup.needsModel,
|
needsModel: setup.needsModel,
|
||||||
defaultProvider:
|
defaultProvider:
|
||||||
setup.credential?.provider ??
|
setup.credential?.provider ?? setup.settings?.defaultModelProvider ?? fallback.provider,
|
||||||
setup.settings?.defaultModelProvider ??
|
|
||||||
deps.env.defaultProvider,
|
|
||||||
defaultModel:
|
defaultModel:
|
||||||
setup.credential?.defaultModel ?? setup.settings?.defaultModelId ?? deps.env.defaultModel,
|
setup.credential?.defaultModel ?? setup.settings?.defaultModelId ?? fallback.model,
|
||||||
computerHost: computerHostFor(setup.settings?.computerHost, deps.env.sandboxProvider),
|
computerHost: computerHostFor(setup.settings?.computerHost, deps.env.sandboxProvider),
|
||||||
canChooseHostComputer: actor.isDeploymentOwner && deps.env.sandboxProvider === "docker",
|
canChooseHostComputer: actor.isDeploymentOwner && deps.env.sandboxProvider === "docker",
|
||||||
sandboxProvider: deps.env.sandboxProvider,
|
sandboxProvider: deps.env.sandboxProvider,
|
||||||
|
|||||||
@ -26,10 +26,13 @@ test("without a model the composer offers to connect one", async ({ page }) => {
|
|||||||
await signup(page, `model-needed-${Date.now()}@rakazo.test`, "password12", "Model needed");
|
await signup(page, `model-needed-${Date.now()}@rakazo.test`, "password12", "Model needed");
|
||||||
await completeOnboarding(page, undefined, { mainConversation: true });
|
await completeOnboarding(page, undefined, { mainConversation: true });
|
||||||
|
|
||||||
|
// Scripted CI has a model; this is what the API returns without one.
|
||||||
|
const keyless = { needsModel: true, defaultProvider: "anthropic", defaultModel: "claude-opus-5" };
|
||||||
await routePatchRpc(page, "bootstrap", (json) => ({
|
await routePatchRpc(page, "bootstrap", (json) => ({
|
||||||
...json,
|
...json,
|
||||||
me: { ...(json.me as Record<string, unknown>), needsModel: true },
|
me: { ...(json.me as Record<string, unknown>), ...keyless },
|
||||||
}));
|
}));
|
||||||
|
await routePatchRpc(page, "me", (json) => ({ ...json, ...keyless }));
|
||||||
await page.reload({ waitUntil: "domcontentloaded" });
|
await page.reload({ waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
const notice = page.getByTestId("composer-model-needed");
|
const notice = page.getByTestId("composer-model-needed");
|
||||||
@ -37,5 +40,12 @@ test("without a model the composer offers to connect one", async ({ page }) => {
|
|||||||
await captureScreenshot(page, testInfo, "first-conversation-model-needed");
|
await captureScreenshot(page, testInfo, "first-conversation-model-needed");
|
||||||
|
|
||||||
await notice.getByRole("button", { name: "Connect model" }).click();
|
await notice.getByRole("button", { name: "Connect model" }).click();
|
||||||
await expect(page.getByTestId("model-settings")).toBeVisible();
|
const settings = page.getByTestId("model-settings");
|
||||||
|
await expect(settings).toBeVisible();
|
||||||
|
// The recommendation is preselected, so only the key or sign-in is left.
|
||||||
|
await expect(settings.getByRole("combobox", { name: "Model" })).toHaveText("Claude Opus 5");
|
||||||
|
await captureScreenshot(page, testInfo, "model-settings-recommended");
|
||||||
|
await page.emulateMedia({ colorScheme: "dark" });
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
|
||||||
|
await captureScreenshot(page, testInfo, "model-settings-recommended-dark");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
export const DEFAULT_OPENROUTER_MODEL_ID = "openai/gpt-5.6-luna";
|
export const DEFAULT_OPENROUTER_MODEL_ID = "openai/gpt-5.6-luna";
|
||||||
|
|
||||||
|
/** What the connect flow preselects while no model is connected: the user only adds a key or signs in. */
|
||||||
|
export const RECOMMENDED_MODEL = { provider: "anthropic", model: "claude-opus-5" } as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The deployment-wide model default: which provider a run falls back to when no user
|
* The deployment-wide model default: which provider a run falls back to when no user
|
||||||
* credential applies, and the key for that provider.
|
* credential applies, and the key for that provider.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user