mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 16:39:50 +00:00
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:
parent
ff3a60267c
commit
dda6d53db7
7 changed files with 27 additions and 64 deletions
|
|
@ -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 ─────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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 ──────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -568,7 +568,6 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
|
|||
name: "OpenClaw",
|
||||
cloudInitTier: "full",
|
||||
preProvision: detectGithubAuth,
|
||||
modelPrompt: true,
|
||||
modelDefault: "openrouter/auto",
|
||||
install: () =>
|
||||
installAgent(
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue