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
6 changes: 6 additions & 0 deletions .changeset/spicy-mice-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@upstash/box-cli": patch
"@upstash/box": patch
---

Rename agent.runner to agent.provider with backwards compatibility
22 changes: 11 additions & 11 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ box create \
--env DEBUG=true
```

| Flag | Description | Default |
| ----------------- | ------------------------------------------------------------------------------------------- | -------- |
| `--token` | Upstash Box API token | |
| `--runtime` | Runtime environment (`node`, `python`, `golang`, `ruby`, `rust`) | |
| `--agent-model` | Agent model identifier | |
| `--agent-runner` | Agent runner (`claude-code`, `codex`, `opencode`) — inferred from model prefix if omitted | inferred |
| `--agent-api-key` | Agent API key — omit for Upstash-managed key, `stored` for a saved key, or a direct API key | Upstash |
| `--git-token` | GitHub personal access token | |
| `--env KEY=VAL` | Environment variable (repeatable) | |
| Flag | Description | Default |
| ------------------ | ------------------------------------------------------------------------------------------- | -------- |
| `--token` | Upstash Box API token | |
| `--runtime` | Runtime environment (`node`, `python`, `golang`, `ruby`, `rust`) | |
| `--agent-model` | Agent model identifier | |
| `--agent-provider` | Agent provider (`claude-code`, `codex`, `opencode`) — inferred from model prefix if omitted | inferred |
| `--agent-api-key` | Agent API key — omit for Upstash-managed key, `stored` for a saved key, or a direct API key | Upstash |
| `--git-token` | GitHub personal access token | |
| `--env KEY=VAL` | Environment variable (repeatable) | |

### `box connect [box-id]`

Expand All @@ -71,7 +71,7 @@ Create a new box from a snapshot and enter the REPL. Accepts the same flags as `

```bash
box from-snapshot snap_abc123 --agent-model claude/sonnet_4_5
box from-snapshot snap_abc123 --agent-model claude/sonnet_4_5 --agent-runner codex --agent-api-key $CLAUDE_KEY
box from-snapshot snap_abc123 --agent-model claude/sonnet_4_5 --agent-provider codex --agent-api-key $CLAUDE_KEY
```

### `box list`
Expand Down Expand Up @@ -147,7 +147,7 @@ Any text entered is sent to the agent by default. You can also use explicit comm
| `git create-pr <title>` | Create a pull request |
| `snapshot [name]` | Save a snapshot of the current state |
| `model` | Change the agent model (interactive picker) |
| `model <runner> <model>` | Change the agent model directly |
| `model <provider> <model>` | Change the agent model directly |
| `pause` | Pause the box and exit |
| `delete` | Delete the box and exit |
| `exit` | Exit the REPL (box keeps running) |
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/__tests__/commands/create-wizard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("createWizard", () => {
expect(result).toEqual({
runtime: "python",
agentModel: "claude/sonnet_4_5",
agentRunner: "claude-code",
agentProvider: "claude-code",
});
});

Expand Down Expand Up @@ -77,7 +77,7 @@ describe("createWizard", () => {
expect(result).toEqual({
runtime: "node",
agentModel: "openai/gpt-5.3-codex",
agentRunner: "codex",
agentProvider: "codex",
agentApiKey: "sk-my-openai-key",
});
});
Expand Down
39 changes: 35 additions & 4 deletions packages/cli/src/__tests__/commands/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ vi.mock("@upstash/box", () => ({
UpstashKey: "UPSTASH_KEY",
StoredKey: "STORED_KEY",
},
inferDefaultRunner: (model: string) => {
inferDefaultProvider: (model: string) => {
if (model.startsWith("openrouter/")) return "claude-code";
if (model.startsWith("opencode/")) return "opencode";
if (model.startsWith("openai/")) return "codex";
Expand Down Expand Up @@ -60,7 +60,7 @@ describe("createCommand", () => {
expect(Box.create).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: "my-key",
agent: { runner: "claude-code", model: "claude/sonnet_4_5", apiKey: "agent-key" },
agent: { provider: "claude-code", model: "claude/sonnet_4_5", apiKey: "agent-key" },
}),
);
expect(startRepl).toHaveBeenCalledWith(mockBox);
Expand All @@ -74,7 +74,7 @@ describe("createCommand", () => {

expect(Box.create).toHaveBeenCalledWith(
expect.objectContaining({
agent: { runner: "claude-code", model: "model", apiKey: undefined },
agent: { provider: "claude-code", model: "model", apiKey: undefined },
}),
);
});
Expand All @@ -87,7 +87,38 @@ describe("createCommand", () => {

expect(Box.create).toHaveBeenCalledWith(
expect.objectContaining({
agent: { runner: "claude-code", model: "model", apiKey: "STORED_KEY" },
agent: { provider: "claude-code", model: "model", apiKey: "STORED_KEY" },
}),
);
});

it("supports deprecated agentRunner flag", async () => {
const mockBox = { id: "box-1" };
vi.mocked(Box.create).mockResolvedValueOnce(mockBox as any);

await createCommand({ token: "key", agentModel: "model", agentRunner: "codex" });

expect(Box.create).toHaveBeenCalledWith(
expect.objectContaining({
agent: { provider: "codex", model: "model", apiKey: undefined },
}),
);
});

it("prioritizes agentProvider over agentRunner", async () => {
const mockBox = { id: "box-1" };
vi.mocked(Box.create).mockResolvedValueOnce(mockBox as any);

await createCommand({
token: "key",
agentModel: "model",
agentProvider: "claude-code",
agentRunner: "codex",
});

expect(Box.create).toHaveBeenCalledWith(
expect.objectContaining({
agent: { provider: "claude-code", model: "model", apiKey: undefined },
}),
);
});
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/__tests__/commands/from-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ vi.mock("@upstash/box", () => ({
UpstashKey: "UPSTASH_KEY",
StoredKey: "STORED_KEY",
},
inferDefaultRunner: (model: string) => {
inferDefaultProvider: (model: string) => {
if (model.startsWith("openrouter/")) return "claude-code";
if (model.startsWith("opencode/")) return "opencode";
if (model.startsWith("openai/")) return "codex";
Expand Down Expand Up @@ -56,7 +56,7 @@ describe("fromSnapshotCommand", () => {
"snap-1",
expect.objectContaining({
apiKey: "key",
agent: { runner: "claude-code", model: "model", apiKey: "agent-key" },
agent: { provider: "claude-code", model: "model", apiKey: "agent-key" },
}),
);
expect(startRepl).toHaveBeenCalledWith(mockBox);
Expand All @@ -71,7 +71,7 @@ describe("fromSnapshotCommand", () => {
expect(Box.fromSnapshot).toHaveBeenCalledWith(
"snap-1",
expect.objectContaining({
agent: { runner: "claude-code", model: "model", apiKey: undefined },
agent: { provider: "claude-code", model: "model", apiKey: undefined },
}),
);
expect(startRepl).toHaveBeenCalledWith(mockBox);
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ program
.option("--token <token>", "Upstash Box API token")
.option("--runtime <runtime>", "Runtime environment (node, python, golang, ruby, rust)")
.option("--agent-model <model>", "Agent model identifier")
.option("--agent-runner <runner>", "Agent runner (claude-code, codex, opencode)")
.option("--agent-provider <provider>", "Agent provider (claude-code, codex, opencode)")
.option("--agent-runner <runner>")
.option(
"--agent-api-key [key]",
'Agent API key — omit to use Upstash-managed key, or pass "stored" to use a key saved in the Upstash console',
Expand All @@ -49,7 +50,8 @@ program
.option("--token <token>", "Upstash Box API token")
.option("--runtime <runtime>", "Runtime environment")
.option("--agent-model <model>", "Agent model identifier")
.option("--agent-runner <runner>", "Agent runner (claude-code, codex, opencode)")
.option("--agent-provider <provider>", "Agent provider (claude-code, codex, opencode)")
.option("--agent-runner <runner>")
.option(
"--agent-api-key [key]",
'Agent API key — omit to use Upstash-managed key, or pass "stored" to use a key saved in the Upstash console',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/create-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async function configureAgent(result: Partial<CreateFlags>): Promise<boolean> {
});
if (model === undefined) return false;
result.agentModel = model;
result.agentRunner = provider;
result.agentProvider = provider;

const keyOption = await interactiveSelect({
prompt: cyan("Agent API key:"),
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/commands/create.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, inferDefaultRunner } from "@upstash/box";
import { Box, inferDefaultProvider } from "@upstash/box";
import type { Runtime } from "@upstash/box";
import { resolveToken } from "../auth.js";
import { resolveAgentApiKey } from "../agent-key.js";
Expand All @@ -10,6 +10,8 @@ export interface CreateFlags {
token?: string;
runtime?: string;
agentModel?: string;
agentProvider?: string;
/** @deprecated Use `agentProvider` instead. */
agentRunner?: string;
agentApiKey?: string | true;
gitToken?: string;
Expand Down Expand Up @@ -53,7 +55,8 @@ export async function createCommand(flags: CreateFlags): Promise<void> {
runtime: flags.runtime as Runtime,
agent: flags.agentModel
? {
runner: flags.agentRunner ?? inferDefaultRunner(flags.agentModel),
provider:
flags.agentProvider ?? flags.agentRunner ?? inferDefaultProvider(flags.agentModel),
model: flags.agentModel,
apiKey: resolveAgentApiKey(flags.agentApiKey),
}
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/commands/from-snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, inferDefaultRunner } from "@upstash/box";
import { Box, inferDefaultProvider } from "@upstash/box";
import type { Runtime } from "@upstash/box";
import { resolveToken } from "../auth.js";
import { resolveAgentApiKey } from "../agent-key.js";
Expand All @@ -8,6 +8,8 @@ interface FromSnapshotFlags {
token?: string;
runtime?: string;
agentModel?: string;
agentProvider?: string;
/** @deprecated Use `agentProvider` instead. */
agentRunner?: string;
agentApiKey?: string | true;
gitToken?: string;
Expand Down Expand Up @@ -38,7 +40,8 @@ export async function fromSnapshotCommand(
runtime: flags.runtime as Runtime,
agent: flags.agentModel
? {
runner: flags.agentRunner ?? inferDefaultRunner(flags.agentModel),
provider:
flags.agentProvider ?? flags.agentRunner ?? inferDefaultProvider(flags.agentModel),
model: flags.agentModel,
apiKey: resolveAgentApiKey(flags.agentApiKey),
}
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/repl/commands/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Box } from "@upstash/box";
import type { BoxREPLEvent } from "../types.js";

/**
* /model [runner] [model]
* /model [provider] [model]
*
* With args: directly set the model via the config API.
* Without args: yield a model-picker event for the terminal/UI to handle.
Expand All @@ -13,7 +13,7 @@ export async function* handleModel(box: Box, args: string): AsyncGenerator<BoxRE
if (parts.length < 2) {
yield {
type: "error",
message: "Usage: /model <runner> <model> (e.g. /model claude-code claude/opus_4_5)",
message: "Usage: /model <provider> <model> (e.g. /model claude-code claude/opus_4_5)",
};
return;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/repl/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function startRepl(box: Box, options?: BoxREPLClientOptions): Promi
const client = new BoxREPLClient(box, {
...options,
onModelConfiguration: async () => {
const agent = box.modelConfig.runner;
const agent = box.modelConfig.provider;
const groups =
MODEL_OPTIONS_BY_AGENT[(agent as Agent) ?? ("claude-code" as Agent)] ??
Object.values(MODEL_OPTIONS_BY_AGENT)[0]!;
Expand Down
37 changes: 34 additions & 3 deletions packages/sdk/src/__tests__/box-create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,51 @@ describe("Box.create", () => {
expect(body.agent_api_key).toBe("test-agent-key");
});

it("sends explicit runner when provided", async () => {
it("sends explicit provider when provided", async () => {
const data = { ...TEST_BOX_DATA, status: "running" };
vi.mocked(fetch).mockResolvedValueOnce(mockResponse(data));

await Box.create({
...TEST_CONFIG,
agent: { runner: Agent.Codex, model: OpenAICodex.GPT_5_3_Codex, apiKey: "k" },
agent: { provider: Agent.Codex, model: OpenAICodex.GPT_5_3_Codex, apiKey: "k" },
});

const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]?.body as string);
expect(body.agent).toBe(Agent.Codex);
expect(body.model).toBe(OpenAICodex.GPT_5_3_Codex);
});

it("supports deprecated runner field", async () => {
const data = { ...TEST_BOX_DATA, status: "running" };
vi.mocked(fetch).mockResolvedValueOnce(mockResponse(data));

await Box.create({
...TEST_CONFIG,
agent: {
provider: Agent.Codex,
runner: "ignored",
model: OpenAICodex.GPT_5_3_Codex,
apiKey: "k",
},
});

const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]?.body as string);
expect(body.agent).toBe(Agent.Codex);
});

it("falls back to runner when provider is absent", async () => {
const data = { ...TEST_BOX_DATA, status: "running" };
vi.mocked(fetch).mockResolvedValueOnce(mockResponse(data));

await Box.create({
...TEST_CONFIG,
agent: { runner: "codex", model: "openai/gpt-5.3-codex", apiKey: "k" } as any,
});

const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]?.body as string);
expect(body.agent).toBe("codex");
});

it("polls until box is ready", async () => {
const creating = { ...TEST_BOX_DATA, status: "creating" };
const running = { ...TEST_BOX_DATA, status: "running" };
Expand Down Expand Up @@ -84,7 +115,7 @@ describe("Box.create", () => {
});

it("throws when agent.model is missing", async () => {
const config = { ...TEST_CONFIG, agent: { runner: "claude-code", model: "", apiKey: "key" } };
const config = { ...TEST_CONFIG, agent: { provider: "claude-code", model: "", apiKey: "key" } };
await expect(Box.create(config)).rejects.toThrow("agent.model is required");
});

Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/src/__tests__/box-from-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ describe("Box.fromSnapshot", () => {
expect(body.model).toBe("claude/sonnet_4_5");
});

it("sends explicit runner when provided", async () => {
it("sends explicit provider when provided", async () => {
const data = { ...TEST_BOX_DATA, status: "running" };
vi.mocked(fetch).mockResolvedValueOnce(mockResponse(data));

await Box.fromSnapshot("snap-1", {
...TEST_CONFIG,
agent: { runner: Agent.Codex, model: OpenAICodex.GPT_5_3_Codex, apiKey: "k" },
agent: { provider: Agent.Codex, model: OpenAICodex.GPT_5_3_Codex, apiKey: "k" },
});

const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]?.body as string);
Expand Down
20 changes: 13 additions & 7 deletions packages/sdk/src/__tests__/infer-runner.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import { describe, it, expect } from "vitest";
import { inferDefaultRunner } from "../client.js";
import { inferDefaultProvider, inferDefaultRunner } from "../client.js";
import { Agent } from "../types.js";

describe("inferDefaultRunner", () => {
describe("inferDefaultProvider", () => {
it("returns ClaudeCode for openrouter/ prefix", () => {
expect(inferDefaultRunner("openrouter/deepseek-r1")).toBe(Agent.ClaudeCode);
expect(inferDefaultProvider("openrouter/deepseek-r1")).toBe(Agent.ClaudeCode);
});

it("returns OpenCode for opencode/ prefix", () => {
expect(inferDefaultRunner("opencode/zen-claude-sonnet-4.5")).toBe(Agent.OpenCode);
expect(inferDefaultProvider("opencode/zen-claude-sonnet-4.5")).toBe(Agent.OpenCode);
});

it("returns Codex for openai/ prefix", () => {
expect(inferDefaultRunner("openai/gpt-5.3-codex")).toBe(Agent.Codex);
expect(inferDefaultProvider("openai/gpt-5.3-codex")).toBe(Agent.Codex);
});

it("returns ClaudeCode for claude/ prefix (default)", () => {
expect(inferDefaultRunner("claude/sonnet_4_5")).toBe(Agent.ClaudeCode);
expect(inferDefaultProvider("claude/sonnet_4_5")).toBe(Agent.ClaudeCode);
});

it("returns ClaudeCode for unknown prefix", () => {
expect(inferDefaultRunner("some-custom-model")).toBe(Agent.ClaudeCode);
expect(inferDefaultProvider("some-custom-model")).toBe(Agent.ClaudeCode);
});
});

describe("inferDefaultRunner (deprecated alias)", () => {
it("is the same function as inferDefaultProvider", () => {
expect(inferDefaultRunner).toBe(inferDefaultProvider);
});
});
Loading
Loading