fix: skip model selection prompt, default to openrouter/auto (#2322)

New users don't know what LLM models are — prompting them to pick one
with no context is confusing and openrouter/auto can route to weak
models. Remove the interactive model prompt entirely; agents use their
modelDefault silently (or MODEL_ID env var for power users).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ahmed Abushagur 2026-03-08 00:54:46 -08:00 committed by GitHub
parent ff3a60267c
commit dda6d53db7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 27 additions and 64 deletions

View file

@ -11,7 +11,6 @@ import { afterAll, afterEach, describe, expect, it, mock } from "bun:test";
mock.module("../shared/oauth", () => ({
getOrPromptApiKey: mock(() => Promise.resolve("sk-test")),
getModelIdInteractive: mock(() => Promise.resolve("openrouter/auto")),
}));
// ── Import under test ─────────────────────────────────────────────────────

View file

@ -22,7 +22,6 @@ beforeEach(() => {
mock.module("../shared/oauth", () => ({
getOrPromptApiKey: mock(() => Promise.resolve("sk-or-v1-test-key")),
getModelIdInteractive: mock(() => Promise.resolve("openrouter/auto")),
}));
// ── Import module under test ──────────────────────────────────────────────────

View file

@ -16,11 +16,9 @@ import { isNumber } from "../shared/type-guards.js";
// ── Mock oauth + tarball (needed to avoid interactive prompts / network) ──
const mockGetOrPromptApiKey = mock(() => Promise.resolve("sk-or-v1-test-key"));
const mockGetModelIdInteractive = mock(() => Promise.resolve("openrouter/auto"));
mock.module("../shared/oauth", () => ({
getOrPromptApiKey: mockGetOrPromptApiKey,
getModelIdInteractive: mockGetModelIdInteractive,
}));
// ── Import the real module under test ─────────────────────────────────────
@ -109,8 +107,6 @@ describe("runOrchestration", () => {
});
mockGetOrPromptApiKey.mockClear();
mockGetOrPromptApiKey.mockImplementation(() => Promise.resolve("sk-or-v1-test-key"));
mockGetModelIdInteractive.mockClear();
mockGetModelIdInteractive.mockImplementation(() => Promise.resolve("openrouter/auto"));
mockTryTarballInstall.mockClear();
mockTryTarballInstall.mockImplementation(() => Promise.resolve(false));
});
@ -258,43 +254,53 @@ describe("runOrchestration", () => {
exitSpy.mockRestore();
});
// ── Model selection ─────────────────────────────────────────────────
// ── Model default ──────────────────────────────────────────────────
it("calls getModelIdInteractive when agent.modelPrompt is true", async () => {
it("passes modelDefault to configure without prompting", async () => {
const configure = mock(() => Promise.resolve());
const cloud = createMockCloud();
const agent = createMockAgent({
modelPrompt: true,
modelDefault: "anthropic/claude-3",
configure,
});
await runOrchestrationSafe(cloud, agent, "testagent");
expect(mockGetModelIdInteractive).toHaveBeenCalledTimes(1);
expect(mockGetModelIdInteractive).toHaveBeenCalledWith("anthropic/claude-3", "TestAgent");
expect(configure).toHaveBeenCalledWith("sk-or-v1-test-key", "anthropic/claude-3");
stderrSpy.mockRestore();
exitSpy.mockRestore();
});
it("uses 'openrouter/auto' as default model when modelDefault is not set", async () => {
it("uses MODEL_ID env var when modelDefault is not set", async () => {
const originalModelId = process.env.MODEL_ID;
process.env.MODEL_ID = "google/gemini-pro";
const configure = mock(() => Promise.resolve());
const cloud = createMockCloud();
const agent = createMockAgent({
modelPrompt: true,
}); // no modelDefault
configure,
});
await runOrchestrationSafe(cloud, agent, "testagent");
expect(mockGetModelIdInteractive).toHaveBeenCalledWith("openrouter/auto", "TestAgent");
expect(configure).toHaveBeenCalledWith("sk-or-v1-test-key", "google/gemini-pro");
process.env.MODEL_ID = originalModelId;
stderrSpy.mockRestore();
exitSpy.mockRestore();
});
it("skips model selection when modelPrompt is falsy", async () => {
it("passes undefined modelId when neither modelDefault nor MODEL_ID is set", async () => {
const originalModelId = process.env.MODEL_ID;
delete process.env.MODEL_ID;
const configure = mock(() => Promise.resolve());
const cloud = createMockCloud();
const agent = createMockAgent(); // modelPrompt undefined
const agent = createMockAgent({
configure,
});
await runOrchestrationSafe(cloud, agent, "testagent");
expect(mockGetModelIdInteractive).not.toHaveBeenCalled();
expect(configure).toHaveBeenCalledWith("sk-or-v1-test-key", undefined);
process.env.MODEL_ID = originalModelId;
stderrSpy.mockRestore();
exitSpy.mockRestore();
});

View file

@ -568,7 +568,6 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
name: "OpenClaw",
cloudInitTier: "full",
preProvision: detectGithubAuth,
modelPrompt: true,
modelDefault: "openrouter/auto",
install: () =>
installAgent(

View file

@ -9,9 +9,7 @@ export type CloudInitTier = "minimal" | "node" | "bun" | "full";
export interface AgentConfig {
name: string;
/** If true, prompt for model selection before provisioning. */
modelPrompt?: boolean;
/** Default model ID when modelPrompt is true. */
/** Default model ID passed to configure() (no interactive prompt — override via MODEL_ID env var). */
modelDefault?: string;
/** Pre-provision hook (runs before server creation, e.g., prompt for GitHub auth). */
preProvision?: () => Promise<void>;

View file

@ -3,7 +3,7 @@
import * as v from "valibot";
import { OAUTH_CODE_REGEX } from "./oauth-constants";
import { parseJsonWith } from "./parse";
import { logError, logInfo, logStep, logWarn, openBrowser, prompt, validateModelId } from "./ui";
import { logError, logInfo, logStep, logWarn, openBrowser, prompt } from "./ui";
// ─── Schemas ─────────────────────────────────────────────────────────────────
@ -285,38 +285,3 @@ export async function getOrPromptApiKey(agentSlug?: string, cloudSlug?: string):
logError("No valid API key after 3 attempts");
throw new Error("API key acquisition failed");
}
// ─── Model Selection ─────────────────────────────────────────────────────────
export async function getModelIdInteractive(defaultModel = "openrouter/auto", agentName?: string): Promise<string> {
// Check env var first
if (process.env.MODEL_ID) {
if (!validateModelId(process.env.MODEL_ID)) {
logError("MODEL_ID environment variable contains invalid characters");
throw new Error("Invalid MODEL_ID");
}
return process.env.MODEL_ID;
}
for (let attempt = 1; attempt <= 3; attempt++) {
process.stderr.write("\n");
logInfo("Browse models at: https://openrouter.ai/models");
if (agentName) {
logInfo(`Which model would you like to use with ${agentName}?`);
} else {
logInfo("Which model would you like to use?");
}
const modelId = (await prompt(`Enter model ID [${defaultModel}]: `)) || defaultModel;
if (!validateModelId(modelId)) {
logError("Invalid characters in model ID, try again");
continue;
}
return modelId;
}
logError("No valid model after 3 attempts");
throw new Error("Model selection failed");
}

View file

@ -8,7 +8,7 @@ import { generateSpawnId, saveSpawnRecord } from "../history.js";
import { offerGithubAuth, wrapSshCall } from "./agent-setup";
import { tryTarballInstall } from "./agent-tarball";
import { generateEnvConfig } from "./agents";
import { getModelIdInteractive, getOrPromptApiKey } from "./oauth";
import { getOrPromptApiKey } from "./oauth";
import { logInfo, logStep, logWarn, prepareStdinForHandoff, withRetry } from "./ui";
export interface CloudOrchestrator {
@ -91,11 +91,8 @@ export async function runOrchestration(
// 3. Get API key (before provisioning so user isn't waiting)
const apiKey = await getOrPromptApiKey(agentName, cloud.cloudName);
// 4. Model selection (if agent needs it)
let modelId: string | undefined;
if (agent.modelPrompt) {
modelId = await getModelIdInteractive(agent.modelDefault || "openrouter/auto", agent.name);
}
// 4. Model ID (use agent default — no interactive prompt)
const modelId = agent.modelDefault || process.env.MODEL_ID;
// 5. Size/bundle selection
await cloud.promptSize();