From b3f20967dba6e22c2a937bb194408d2964d1c3c4 Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:17:33 +0200 Subject: [PATCH] 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 --- apps/api/src/router.test.ts | 13 +++++++++++-- apps/api/src/router.ts | 11 +++++++---- apps/web/e2e/first-conversation.spec.ts | 14 ++++++++++++-- packages/adapters/src/deployment-model.ts | 3 +++ 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/apps/api/src/router.test.ts b/apps/api/src/router.test.ts index a43a74b..9cb1b54 100644 --- a/apps/api/src/router.test.ts +++ b/apps/api/src/router.test.ts @@ -224,7 +224,11 @@ describe("model setup gate", () => { expect(response.status).toBe(200); 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); 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", + }), }); }); diff --git a/apps/api/src/router.ts b/apps/api/src/router.ts index 76abe30..6fc50df 100644 --- a/apps/api/src/router.ts +++ b/apps/api/src/router.ts @@ -71,6 +71,7 @@ import { probeOpenAiCompatibleModels, provisionComputer, queueComputerUpdate, + RECOMMENDED_MODEL, readProjectWorkspaceFile, releaseComputerExecutionLease, replaceComputer, @@ -5511,6 +5512,10 @@ async function meDto(deps: RouterDeps, actor: Actor): Promise { deps.prisma.user.findUniqueOrThrow({ where: { id: actor.userId } }), 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 { userId: actor.userId, email: user.email, @@ -5519,11 +5524,9 @@ async function meDto(deps: RouterDeps, actor: Actor): Promise { isDeploymentOwner: actor.isDeploymentOwner, needsModel: setup.needsModel, defaultProvider: - setup.credential?.provider ?? - setup.settings?.defaultModelProvider ?? - deps.env.defaultProvider, + setup.credential?.provider ?? setup.settings?.defaultModelProvider ?? fallback.provider, 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), canChooseHostComputer: actor.isDeploymentOwner && deps.env.sandboxProvider === "docker", sandboxProvider: deps.env.sandboxProvider, diff --git a/apps/web/e2e/first-conversation.spec.ts b/apps/web/e2e/first-conversation.spec.ts index 3722b6f..60aee0f 100644 --- a/apps/web/e2e/first-conversation.spec.ts +++ b/apps/web/e2e/first-conversation.spec.ts @@ -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 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) => ({ ...json, - me: { ...(json.me as Record), needsModel: true }, + me: { ...(json.me as Record), ...keyless }, })); + await routePatchRpc(page, "me", (json) => ({ ...json, ...keyless })); await page.reload({ waitUntil: "domcontentloaded" }); 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 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"); }); diff --git a/packages/adapters/src/deployment-model.ts b/packages/adapters/src/deployment-model.ts index 9535761..fe4ea8f 100644 --- a/packages/adapters/src/deployment-model.ts +++ b/packages/adapters/src/deployment-model.ts @@ -1,5 +1,8 @@ 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 * credential applies, and the key for that provider.