Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 12 additions & 0 deletions .changeset/neon-ai-gateway-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@upstash/box": patch
"@upstash/box-cli": patch
---

Add Neon AI Gateway models: `NeonModel` (`neon/<neon-short-id>`) for the Codex and
OpenCode harnesses, and route `neon/gpt-*` to Codex and the chat-only Neon models
(GPT OSS, Claude, Gemini, Llama, Qwen, Kimi, GLM) to OpenCode in
Comment thread
alitariksahin marked this conversation as resolved.
Outdated
`inferDefaultProvider`. Neon's Claude models run on OpenCode only, because Neon
serves them through chat completions and not the Responses API. Box does not run
Claude Code with Neon. The CLI model picker lists them for Codex and OpenCode.
Runs use the Neon credential (token and branch base URL) saved in the console.
39 changes: 39 additions & 0 deletions packages/cli/src/__tests__/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Agent,
ClaudeCode,
CursorModel,
NeonModel,
OpenAICodex,
OpenCodeModel,
OpenRouterModel,
Expand Down Expand Up @@ -88,6 +89,44 @@ describe("MODEL_OPTIONS_BY_AGENT", () => {
expect(group?.options).toContainEqual({ value, label });
});

it("offers Neon AI Gateway models only to Codex and OpenCode", () => {
const groupFor = (agent: Agent) =>
MODEL_OPTIONS_BY_AGENT[agent].find(({ label }) => label === "Neon AI Gateway");

expect(groupFor(Agent.ClaudeCode)).toBeUndefined();
expect(groupFor(Agent.Cursor)).toBeUndefined();
expect(groupFor(Agent.Codex)?.options).toContainEqual({
value: NeonModel.GPT_5_5,
label: "GPT-5.5 (Neon)",
});
expect(groupFor(Agent.OpenCode)?.options).toContainEqual({
value: NeonModel.Gemini_3_6_Flash,
label: "Gemini 3.6 Flash (Neon)",
});
});

it("splits Neon models by endpoint support", () => {
const values = (agent: Agent) =>
MODEL_OPTIONS_BY_AGENT[agent]
.find(({ label }) => label === "Neon AI Gateway")
?.options.map((o) => o.value) ?? [];

// Responses-only on Neon: Codex yes, OpenCode no.
expect(values(Agent.Codex)).toContain(NeonModel.GPT_5_5_Pro);
expect(values(Agent.Codex)).toContain(NeonModel.GPT_5_3_Codex);
expect(values(Agent.OpenCode)).not.toContain(NeonModel.GPT_5_5_Pro);
expect(values(Agent.OpenCode)).not.toContain(NeonModel.GPT_5_3_Codex);
// Chat-only on Neon: OpenCode yes, Codex no.
expect(values(Agent.OpenCode)).toContain(NeonModel.GPT_OSS_120B);
expect(values(Agent.OpenCode)).toContain(NeonModel.Kimi_K3);
expect(values(Agent.Codex)).not.toContain(NeonModel.GPT_OSS_120B);
expect(values(Agent.Codex)).not.toContain(NeonModel.Kimi_K3);
// Claude on Neon: chat completions only, so OpenCode yes, Codex no.
expect(values(Agent.OpenCode)).toContain(NeonModel.Claude_Haiku_4_5);
expect(values(Agent.OpenCode)).toContain(NeonModel.Claude_Opus_5_5);
expect(values(Agent.Codex).some((v) => v.startsWith("neon/claude-"))).toBe(false);
});

it("includes Cursor models", () => {
const cursorModels = MODEL_OPTIONS_BY_AGENT[Agent.Cursor].flatMap((group) => group.options);

Expand Down
60 changes: 60 additions & 0 deletions packages/cli/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CursorModel,
OpenAICodex,
OpenCodeModel,
NeonModel,
OpenRouterModel,
VercelModel,
} from "@upstash/box";
Expand Down Expand Up @@ -144,6 +145,25 @@ export const MODEL_OPTIONS_BY_AGENT: Record<
{ value: VercelModel.Claude_Sonnet_4_6, label: "Claude Sonnet 4.6 (Vercel)" },
],
},
{
// Responses API models only: Codex cannot use Neon's chat-only models.
label: "Neon AI Gateway",
options: [
Comment thread
alitariksahin marked this conversation as resolved.
{ value: NeonModel.GPT_6_Astra, label: "GPT-6 Astra (Neon)" },
{ value: NeonModel.GPT_5_6_Sol, label: "GPT-5.6 Sol (Neon)" },
{ value: NeonModel.GPT_5_6_Terra, label: "GPT-5.6 Terra (Neon)" },
{ value: NeonModel.GPT_5_6_Luna, label: "GPT-5.6 Luna (Neon)" },
{ value: NeonModel.GPT_5_5, label: "GPT-5.5 (Neon)" },
{ value: NeonModel.GPT_5_5_Pro, label: "GPT-5.5 Pro (Neon)" },
{ value: NeonModel.GPT_5_4, label: "GPT-5.4 (Neon)" },
{ value: NeonModel.GPT_5_4_Mini, label: "GPT-5.4 Mini (Neon)" },
{ value: NeonModel.GPT_5_4_Nano, label: "GPT-5.4 Nano (Neon)" },
{ value: NeonModel.GPT_5_3_Codex, label: "GPT-5.3 Codex (Neon)" },
{ value: NeonModel.GPT_5_Mini, label: "GPT-5 Mini (Neon)" },
{ value: NeonModel.GPT_5_Nano, label: "GPT-5 Nano (Neon)" },
{ value: NeonModel.Grok_4_6, label: "Grok 4.6 (Neon)" },
],
},
],
[Agent.OpenCode]: [
{
Expand Down Expand Up @@ -259,6 +279,46 @@ export const MODEL_OPTIONS_BY_AGENT: Record<
{ value: VercelModel.Grok_4_3, label: "Grok 4.3 (Vercel)" },
],
},
{
// Chat-completions models only: GPT-5.5 Pro and GPT-5.3 Codex are Responses-only on Neon.
label: "Neon AI Gateway",
options: [
{ value: NeonModel.GPT_6_Astra, label: "GPT-6 Astra (Neon)" },
{ value: NeonModel.GPT_5_6_Sol, label: "GPT-5.6 Sol (Neon)" },
{ value: NeonModel.GPT_5_6_Terra, label: "GPT-5.6 Terra (Neon)" },
{ value: NeonModel.GPT_5_6_Luna, label: "GPT-5.6 Luna (Neon)" },
{ value: NeonModel.GPT_5_5, label: "GPT-5.5 (Neon)" },
{ value: NeonModel.GPT_5_4, label: "GPT-5.4 (Neon)" },
{ value: NeonModel.GPT_5_4_Mini, label: "GPT-5.4 Mini (Neon)" },
{ value: NeonModel.GPT_5_4_Nano, label: "GPT-5.4 Nano (Neon)" },
{ value: NeonModel.GPT_5_Mini, label: "GPT-5 Mini (Neon)" },
{ value: NeonModel.GPT_5_Nano, label: "GPT-5 Nano (Neon)" },
{ value: NeonModel.GPT_OSS_120B, label: "GPT OSS 120B (Neon)" },
{ value: NeonModel.GPT_OSS_20B, label: "GPT OSS 20B (Neon)" },
{ value: NeonModel.Gemini_3_6_Flash, label: "Gemini 3.6 Flash (Neon)" },
{ value: NeonModel.Gemini_3_5_Flash, label: "Gemini 3.5 Flash (Neon)" },
{ value: NeonModel.Gemini_3_5_Flash_Lite, label: "Gemini 3.5 Flash Lite (Neon)" },
{ value: NeonModel.Gemini_3_1_Pro, label: "Gemini 3.1 Pro (Neon)" },
{ value: NeonModel.Grok_4_6, label: "Grok 4.6 (Neon)" },
{ value: NeonModel.Kimi_K3, label: "Kimi K3 (Neon)" },
{ value: NeonModel.GLM_5_2, label: "GLM-5.2 (Neon)" },
{ value: NeonModel.Qwen3_5_122B, label: "Qwen3.5 122B (Neon)" },
{ value: NeonModel.Llama_4_Maverick, label: "Llama 4 Maverick (Neon)" },
{ value: NeonModel.Claude_Opus_5_5, label: "Claude Opus 5.5 (Neon)" },
{ value: NeonModel.Claude_Fable_5_1, label: "Claude Fable 5.1 (Neon)" },
{ value: NeonModel.Claude_Opus_5, label: "Claude Opus 5 (Neon)" },
{ value: NeonModel.Claude_Sonnet_5, label: "Claude Sonnet 5 (Neon)" },
{ value: NeonModel.Claude_Fable_5, label: "Claude Fable 5 (Neon)" },
{ value: NeonModel.Claude_Opus_4_8, label: "Claude Opus 4.8 (Neon)" },
{ value: NeonModel.Claude_Opus_4_7, label: "Claude Opus 4.7 (Neon)" },
{ value: NeonModel.Claude_Sonnet_4_6, label: "Claude Sonnet 4.6 (Neon)" },
{ value: NeonModel.Claude_Opus_4_6, label: "Claude Opus 4.6 (Neon)" },
{ value: NeonModel.Claude_Opus_4_5, label: "Claude Opus 4.5 (Neon)" },
{ value: NeonModel.Claude_Haiku_4_5, label: "Claude Haiku 4.5 (Neon)" },
{ value: NeonModel.Claude_Sonnet_4_5, label: "Claude Sonnet 4.5 (Neon)" },
{ value: NeonModel.Claude_Opus_4_1, label: "Claude Opus 4.1 (Neon)" },
],
},
],
[Agent.Cursor]: [
{
Expand Down
5 changes: 5 additions & 0 deletions packages/python-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to `upstash-box` (Python) are documented here.

## Unreleased

- Add `NeonModel` (Neon AI Gateway, `neon/<neon-short-id>`) and route `neon/gpt-*`
to Codex and chat-only Neon models to OpenCode in `infer_default_provider`.
Comment thread
alitariksahin marked this conversation as resolved.
Outdated
Neon's ``CLAUDE_*`` models run on OpenCode only (chat completions); Box does
not run Claude Code with Neon. Neon credentials (token + branch base URL)
must be stored via the console or agent-credentials API.
- Add GPT-6.1 Sol to `OpenAICodex`, `OpenRouterModel`, `VercelModel`, and
`OpenCodeModel`.
- Init commands now work on every box, not only keep-alive ones. `create(init_command=...)`
Expand Down
2 changes: 1 addition & 1 deletion packages/python-sdk/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ JS `Run`/`StreamRun` → Python `Run`/`StreamRun` (+ `AsyncRun`/`AsyncStreamRun`
| `BoxError` | `BoxError` |
| `inferDefaultProvider` | `infer_default_provider` |
| `runCustomHarness` | `run_custom_harness` |
| `Agent`, `ClaudeCode`, `OpenAICodex`, `OpenCodeModel`, `OpenRouterModel`, `VercelModel`, `CursorModel`, `BoxApiKey` | same names (str-Enums) |
| `Agent`, `ClaudeCode`, `OpenAICodex`, `OpenCodeModel`, `OpenRouterModel`, `VercelModel`, `NeonModel`, `CursorModel`, `BoxApiKey` | same names (str-Enums) |

## `Box` instance methods/properties

Expand Down
12 changes: 12 additions & 0 deletions packages/python-sdk/tests/_async/test_infer_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,15 @@ def test_custom_prefix():

def test_unknown_defaults_to_claude_code():
assert infer_default_provider("mystery-model") == Agent.CLAUDE_CODE


def test_neon_gpt_prefix_infers_codex():
assert infer_default_provider("neon/gpt-5-5") == Agent.CODEX
assert infer_default_provider("neon/gpt-5-3-codex") == Agent.CODEX


def test_neon_chat_only_models_infer_opencode():
assert infer_default_provider("neon/gpt-oss-120b") == Agent.OPEN_CODE
assert infer_default_provider("neon/gemini-3-6-flash") == Agent.OPEN_CODE
assert infer_default_provider("neon/kimi-k3") == Agent.OPEN_CODE
assert infer_default_provider("neon/claude-opus-4-8") == Agent.OPEN_CODE
10 changes: 10 additions & 0 deletions packages/python-sdk/tests/_async/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
CursorModel,
FinishChunk,
FinishUsage,
NeonModel,
OpenAICodex,
OpenCodeModel,
OpenRouterModel,
Expand Down Expand Up @@ -83,6 +84,15 @@ def test_claude_opus_5_model_identifier():
}


def test_neon_model_identifiers_use_short_ids():
assert NeonModel.GPT_5_5.value == "neon/gpt-5-5"
assert NeonModel.GEMINI_3_6_FLASH.value == "neon/gemini-3-6-flash"
assert NeonModel.CLAUDE_HAIKU_4_5.value == "neon/claude-haiku-4-5"
for member in NeonModel:
assert member.value.startswith("neon/")
assert "/" not in member.value[len("neon/") :]


def test_box_run_data_fields():
run = BoxRunData.model_validate(
{
Expand Down
2 changes: 2 additions & 0 deletions packages/python-sdk/upstash_box/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
LogEntry,
McpServerConfig,
ModelConfig,
NeonModel,
NetworkPolicy,
OpenAICodex,
OpenCodeAgentOptions,
Expand Down Expand Up @@ -176,6 +177,7 @@
"OpenCodeModel",
"OpenRouterModel",
"VercelModel",
"NeonModel",
# Config / option types
"AgentConfig",
"AgentOptions",
Expand Down
6 changes: 6 additions & 0 deletions packages/python-sdk/upstash_box/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ def infer_default_provider(model: str) -> Agent:
return Agent.CODEX
if model.startswith("vercel/"):
return Agent.CLAUDE_CODE
# Neon AI Gateway has no Anthropic dialect. GPT models go to Codex (Responses
# API); GPT OSS and every non-OpenAI model are chat-only, so they go to OpenCode.
Comment thread
alitariksahin marked this conversation as resolved.
Outdated
if model.startswith("neon/gpt-") and not model.startswith("neon/gpt-oss-"):
return Agent.CODEX
if model.startswith("neon/"):
return Agent.OPEN_CODE
if model.startswith("openrouter/"):
return Agent.CLAUDE_CODE
if model.startswith("opencode/"):
Expand Down
50 changes: 50 additions & 0 deletions packages/python-sdk/upstash_box/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,56 @@ class VercelModel(str, Enum):
GROK_4_20_REASONING = "vercel/spacexai/grok-4.20-reasoning"


class NeonModel(str, Enum):
"""Neon AI Gateway model identifiers (``neon/<neon-short-id>``).

These run on the Codex harness (Responses API) or the OpenCode harness
(chat completions); Box does not run Claude Code with Neon.
``GPT_5_5_PRO`` and ``GPT_5_3_CODEX`` are Responses-only (Codex);
``GPT_OSS_*``, the ``CLAUDE_*`` models and every other non-OpenAI model are
served through chat completions only (OpenCode).
Comment thread
alitariksahin marked this conversation as resolved.
Outdated
Neon credentials are a token plus a per-branch base URL and must be stored
via the console or agent-credentials API; use ``BoxApiKey.STORED_KEY``.
"""

GPT_6_ASTRA = "neon/gpt-6-astra"
GPT_5_6_SOL = "neon/gpt-5-6-sol"
GPT_5_6_TERRA = "neon/gpt-5-6-terra"
GPT_5_6_LUNA = "neon/gpt-5-6-luna"
GPT_5_5 = "neon/gpt-5-5"
GPT_5_5_PRO = "neon/gpt-5-5-pro"
GPT_5_4 = "neon/gpt-5-4"
GPT_5_4_MINI = "neon/gpt-5-4-mini"
GPT_5_4_NANO = "neon/gpt-5-4-nano"
GPT_5_3_CODEX = "neon/gpt-5-3-codex"
GPT_5_MINI = "neon/gpt-5-mini"
GPT_5_NANO = "neon/gpt-5-nano"
GPT_OSS_120B = "neon/gpt-oss-120b"
GPT_OSS_20B = "neon/gpt-oss-20b"
GEMINI_3_6_FLASH = "neon/gemini-3-6-flash"
GEMINI_3_5_FLASH = "neon/gemini-3-5-flash"
GEMINI_3_5_FLASH_LITE = "neon/gemini-3-5-flash-lite"
GEMINI_3_1_PRO = "neon/gemini-3-1-pro"
GROK_4_6 = "neon/grok-4-6"
KIMI_K3 = "neon/kimi-k3"
GLM_5_2 = "neon/glm-5-2"
QWEN3_5_122B = "neon/qwen35-122b-a10b"
LLAMA_4_MAVERICK = "neon/llama-4-maverick"
CLAUDE_OPUS_5_5 = "neon/claude-opus-5-5"
CLAUDE_FABLE_5_1 = "neon/claude-fable-5-1"
CLAUDE_OPUS_5 = "neon/claude-opus-5"
CLAUDE_SONNET_5 = "neon/claude-sonnet-5"
CLAUDE_FABLE_5 = "neon/claude-fable-5"
CLAUDE_OPUS_4_8 = "neon/claude-opus-4-8"
CLAUDE_OPUS_4_7 = "neon/claude-opus-4-7"
CLAUDE_SONNET_4_6 = "neon/claude-sonnet-4-6"
CLAUDE_OPUS_4_6 = "neon/claude-opus-4-6"
CLAUDE_OPUS_4_5 = "neon/claude-opus-4-5"
CLAUDE_HAIKU_4_5 = "neon/claude-haiku-4-5"
CLAUDE_SONNET_4_5 = "neon/claude-sonnet-4-5"
CLAUDE_OPUS_4_1 = "neon/claude-opus-4-1"


class OpenCodeModel(str, Enum):
"""OpenCode model identifiers — supports models from multiple providers."""

Expand Down
14 changes: 14 additions & 0 deletions packages/sdk/src/__tests__/infer-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ describe("inferDefaultProvider", () => {
expect(inferDefaultProvider("vercel/openai/gpt-5.5")).toBe(Agent.Codex);
});

it("returns Codex for neon/gpt-* models", () => {
expect(inferDefaultProvider("neon/gpt-5-5")).toBe(Agent.Codex);
expect(inferDefaultProvider("neon/gpt-5-3-codex")).toBe(Agent.Codex);
expect(inferDefaultProvider("neon/gpt-6-astra")).toBe(Agent.Codex);
});

it("returns OpenCode for chat-only neon models", () => {
Comment thread
alitariksahin marked this conversation as resolved.
Outdated
expect(inferDefaultProvider("neon/gpt-oss-120b")).toBe(Agent.OpenCode);
expect(inferDefaultProvider("neon/gemini-3-6-flash")).toBe(Agent.OpenCode);
expect(inferDefaultProvider("neon/kimi-k3")).toBe(Agent.OpenCode);
expect(inferDefaultProvider("neon/claude-opus-4-8")).toBe(Agent.OpenCode);
expect(inferDefaultProvider("neon/grok-4-6")).toBe(Agent.OpenCode);
});

it("returns OpenCode for opencode/ prefix", () => {
expect(inferDefaultProvider("opencode/zen-claude-sonnet-4.5")).toBe(Agent.OpenCode);
});
Expand Down
22 changes: 22 additions & 0 deletions packages/sdk/src/__tests__/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
ClaudeCode,
CursorModel,
NeonModel,
OpenAICodex,
OpenCodeModel,
OpenRouterModel,
Expand Down Expand Up @@ -77,3 +78,24 @@ describe("Claude Opus 5 model identifiers", () => {
expect(model).toBe(expected);
});
});

describe("Neon AI Gateway model identifiers", () => {
it.each([
[NeonModel.GPT_5_5, "neon/gpt-5-5"],
[NeonModel.GPT_5_3_Codex, "neon/gpt-5-3-codex"],
[NeonModel.GPT_OSS_120B, "neon/gpt-oss-120b"],
[NeonModel.Gemini_3_6_Flash, "neon/gemini-3-6-flash"],
[NeonModel.Kimi_K3, "neon/kimi-k3"],
[NeonModel.Claude_Opus_5_5, "neon/claude-opus-5-5"],
[NeonModel.Claude_Haiku_4_5, "neon/claude-haiku-4-5"],
])("uses Neon's short id under the neon/ namespace: %s", (model, expected) => {
expect(model).toBe(expected);
});

it("never carries a provider segment", () => {
for (const value of Object.values(NeonModel)) {
expect(value.startsWith("neon/")).toBe(true);
expect(value.slice("neon/".length)).not.toContain("/");
}
});
});
4 changes: 4 additions & 0 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export function inferDefaultProvider(model: string): Agent {
if (model.startsWith("cursor/")) return Agent.Cursor;
if (model.startsWith("vercel/openai/")) return Agent.Codex;
if (model.startsWith("vercel/")) return Agent.ClaudeCode;
// Neon AI Gateway has no Anthropic dialect. GPT models go to Codex (Responses API);
// GPT OSS and every non-OpenAI model are chat-completions-only, so they go to OpenCode.
Comment thread
alitariksahin marked this conversation as resolved.
Outdated
if (model.startsWith("neon/gpt-") && !model.startsWith("neon/gpt-oss-")) return Agent.Codex;
if (model.startsWith("neon/")) return Agent.OpenCode;
if (model.startsWith("openrouter/")) return Agent.ClaudeCode;
if (model.startsWith("opencode/")) return Agent.OpenCode;
if (model.startsWith("openai/")) return Agent.Codex;
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
OpenCodeModel,
OpenRouterModel,
VercelModel,
NeonModel,
Agent,
BoxApiKey,
} from "./types.js";
Expand Down
Loading
Loading