diff --git a/.changeset/neon-ai-gateway-models.md b/.changeset/neon-ai-gateway-models.md new file mode 100644 index 0000000..471a7d8 --- /dev/null +++ b/.changeset/neon-ai-gateway-models.md @@ -0,0 +1,12 @@ +--- +"@upstash/box": patch +"@upstash/box-cli": patch +--- + +Add Neon AI Gateway models: `NeonModel` (`neon/`) for the Codex and +OpenCode harnesses. `inferDefaultProvider` routes `neon/gpt-*` to Codex, except +`neon/gpt-oss-*`, and every other Neon model to OpenCode (GPT OSS, Claude, Gemini, +Llama, Qwen, Kimi, GLM, Grok). 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. diff --git a/packages/cli/src/__tests__/commands/create-wizard.test.ts b/packages/cli/src/__tests__/commands/create-wizard.test.ts index f75e523..25add61 100644 --- a/packages/cli/src/__tests__/commands/create-wizard.test.ts +++ b/packages/cli/src/__tests__/commands/create-wizard.test.ts @@ -82,6 +82,26 @@ describe("createWizard", () => { }); }); + it("Neon model → stored credential without asking for a key", async () => { + vi.mocked(interactiveSelect) + .mockResolvedValueOnce("node") // runtime + .mockResolvedValueOnce("agent") // action: configure agent + .mockResolvedValueOnce("opencode") // provider + .mockResolvedValueOnce("neon/claude-haiku-4-5") // model + .mockResolvedValueOnce("create"); // action: create (no key prompt in between) + + const result = await createWizard(); + + expect(result).toEqual({ + runtime: "node", + agentModel: "neon/claude-haiku-4-5", + agentProvider: "opencode", + agentApiKey: "stored", + }); + // runtime, action, provider, model, action: the key choice was never shown. + expect(vi.mocked(interactiveSelect)).toHaveBeenCalledTimes(5); + }); + it("stored key option → agentApiKey is 'stored'", async () => { vi.mocked(interactiveSelect) .mockResolvedValueOnce("node") // runtime diff --git a/packages/cli/src/__tests__/models.test.ts b/packages/cli/src/__tests__/models.test.ts index 9d8453d..61767f4 100644 --- a/packages/cli/src/__tests__/models.test.ts +++ b/packages/cli/src/__tests__/models.test.ts @@ -3,6 +3,7 @@ import { Agent, ClaudeCode, CursorModel, + NeonModel, OpenAICodex, OpenCodeModel, OpenRouterModel, @@ -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); diff --git a/packages/cli/src/commands/create-wizard.ts b/packages/cli/src/commands/create-wizard.ts index cb76520..2de4f1f 100644 --- a/packages/cli/src/commands/create-wizard.ts +++ b/packages/cli/src/commands/create-wizard.ts @@ -117,6 +117,14 @@ async function configureAgent(result: Partial): Promise { result.agentModel = model; result.agentProvider = provider; + // Neon runs only with the stored credential (token plus branch gateway URL): + // the backend rejects both the Upstash-managed key and a raw key for neon/. + if (model.startsWith("neon/")) { + result.agentApiKey = "stored"; + console.log(dim(" Neon AI Gateway uses your stored Neon credential (token and gateway URL).")); + return true; + } + const keyOption = await interactiveSelect({ prompt: cyan("Agent API key:"), items: API_KEY_OPTIONS, diff --git a/packages/cli/src/models.ts b/packages/cli/src/models.ts index b73ab2f..ca09af9 100644 --- a/packages/cli/src/models.ts +++ b/packages/cli/src/models.ts @@ -4,6 +4,7 @@ import { CursorModel, OpenAICodex, OpenCodeModel, + NeonModel, OpenRouterModel, VercelModel, } from "@upstash/box"; @@ -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: [ + { 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]: [ { @@ -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]: [ { diff --git a/packages/python-sdk/CHANGELOG.md b/packages/python-sdk/CHANGELOG.md index f1dbed9..9158026 100644 --- a/packages/python-sdk/CHANGELOG.md +++ b/packages/python-sdk/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to `upstash-box` (Python) are documented here. ## Unreleased +- Add `NeonModel` (Neon AI Gateway, `neon/`). `infer_default_provider` + routes `neon/gpt-*` to Codex, except `neon/gpt-oss-*`, and every other Neon model + (GPT OSS, Claude, Gemini, Llama, Qwen, Kimi, GLM, Grok) to OpenCode. + 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=...)` diff --git a/packages/python-sdk/PARITY.md b/packages/python-sdk/PARITY.md index 20bd5df..d5e1e59 100644 --- a/packages/python-sdk/PARITY.md +++ b/packages/python-sdk/PARITY.md @@ -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 diff --git a/packages/python-sdk/tests/_async/test_infer_provider.py b/packages/python-sdk/tests/_async/test_infer_provider.py index 43c0766..e8133c0 100644 --- a/packages/python-sdk/tests/_async/test_infer_provider.py +++ b/packages/python-sdk/tests/_async/test_infer_provider.py @@ -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 diff --git a/packages/python-sdk/tests/_async/test_models.py b/packages/python-sdk/tests/_async/test_models.py index 92edaaa..d7a2d4e 100644 --- a/packages/python-sdk/tests/_async/test_models.py +++ b/packages/python-sdk/tests/_async/test_models.py @@ -7,6 +7,7 @@ CursorModel, FinishChunk, FinishUsage, + NeonModel, OpenAICodex, OpenCodeModel, OpenRouterModel, @@ -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( { diff --git a/packages/python-sdk/upstash_box/__init__.py b/packages/python-sdk/upstash_box/__init__.py index 06e24bf..ff5c26f 100644 --- a/packages/python-sdk/upstash_box/__init__.py +++ b/packages/python-sdk/upstash_box/__init__.py @@ -93,6 +93,7 @@ LogEntry, McpServerConfig, ModelConfig, + NeonModel, NetworkPolicy, OpenAICodex, OpenCodeAgentOptions, @@ -176,6 +177,7 @@ "OpenCodeModel", "OpenRouterModel", "VercelModel", + "NeonModel", # Config / option types "AgentConfig", "AgentOptions", diff --git a/packages/python-sdk/upstash_box/_common.py b/packages/python-sdk/upstash_box/_common.py index 0881bdd..deba8b6 100644 --- a/packages/python-sdk/upstash_box/_common.py +++ b/packages/python-sdk/upstash_box/_common.py @@ -107,6 +107,15 @@ def infer_default_provider(model: str) -> Agent: return Agent.CODEX if model.startswith("vercel/"): return Agent.CLAUDE_CODE + # Default routing for Neon AI Gateway (Box does not run Claude Code with Neon). + # GPT models default to Codex (Responses API). Every other Neon model defaults + # to OpenCode (chat completions), which Neon serves for every text model. + # Grok 4.6 is also served on Responses, so Codex can run it when chosen + # explicitly; it only defaults to OpenCode here. + 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/"): diff --git a/packages/python-sdk/upstash_box/types.py b/packages/python-sdk/upstash_box/types.py index c9c7fd1..ab70a29 100644 --- a/packages/python-sdk/upstash_box/types.py +++ b/packages/python-sdk/upstash_box/types.py @@ -145,6 +145,57 @@ class VercelModel(str, Enum): GROK_4_20_REASONING = "vercel/spacexai/grok-4.20-reasoning" +class NeonModel(str, Enum): + """Neon AI Gateway model identifiers (``neon/``). + + 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); + the other GPT models and ``GROK_4_6`` run on both; ``GPT_OSS_*``, the + ``CLAUDE_*`` models and every other non-OpenAI model except Grok are served + through chat completions only (OpenCode). + 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.""" diff --git a/packages/sdk/src/__tests__/infer-runner.test.ts b/packages/sdk/src/__tests__/infer-runner.test.ts index e8761fe..0afbed2 100644 --- a/packages/sdk/src/__tests__/infer-runner.test.ts +++ b/packages/sdk/src/__tests__/infer-runner.test.ts @@ -15,6 +15,22 @@ 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("defaults every non-GPT neon model to OpenCode", () => { + // Chat-completions-only on Neon. + 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); + // Served on both endpoints; Codex can run it when chosen, but the default is 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); }); diff --git a/packages/sdk/src/__tests__/types.test.ts b/packages/sdk/src/__tests__/types.test.ts index 03bb401..d672cdc 100644 --- a/packages/sdk/src/__tests__/types.test.ts +++ b/packages/sdk/src/__tests__/types.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { ClaudeCode, CursorModel, + NeonModel, OpenAICodex, OpenCodeModel, OpenRouterModel, @@ -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("/"); + } + }); +}); diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 7109a24..af3dad5 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -111,6 +111,13 @@ 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; + // Default routing for Neon AI Gateway (Box does not run Claude Code with Neon). GPT + // models default to Codex (Responses API). Every other Neon model defaults to + // OpenCode (chat completions), which Neon serves for every text model. Grok 4.6 is + // also served on Responses, so Codex can run it when chosen explicitly; it only + // defaults to OpenCode here. + 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; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 10e1fa6..13d1627 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -17,6 +17,7 @@ export { OpenCodeModel, OpenRouterModel, VercelModel, + NeonModel, Agent, BoxApiKey, } from "./types.js"; diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index c031377..64bbcc2 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -143,6 +143,57 @@ export enum VercelModel { Grok_4_20_Reasoning = "vercel/spacexai/grok-4.20-reasoning", } +/** + * Neon AI Gateway model identifiers — Neon's short model IDs under the `neon/` namespace. + * + * These models run on the Codex harness (Responses API) or the OpenCode harness + * (chat completions). Box does not run Claude Code with Neon. + * Codex-only: `GPT_5_5_Pro`, `GPT_5_3_Codex` (Responses-only on Neon). + * Both: the other GPT models and `Grok_4_6` (Neon serves them on both endpoints). + * OpenCode-only: `GPT_OSS_120B`, `GPT_OSS_20B`, the Claude models and every other + * non-OpenAI model except Grok (Neon serves them through chat completions only). + * Neon credentials are a token plus a per-branch base URL and must be stored via the + * console or agent-credentials API; use `BoxApiKey.StoredKey`. + */ +export enum NeonModel { + 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", +} + /** * OpenCode model identifiers — supports models from multiple providers */ @@ -291,13 +342,13 @@ type HarnessConfig = } | { harness: Agent.Codex; - model: OpenAICodex | OpenRouterModel | VercelModel; + model: OpenAICodex | OpenRouterModel | VercelModel | NeonModel; provider?: never; runner?: never; } | { harness: Agent.OpenCode; - model: OpenCodeModel | ClaudeCode | OpenAICodex | OpenRouterModel | VercelModel; + model: OpenCodeModel | ClaudeCode | OpenAICodex | OpenRouterModel | VercelModel | NeonModel; provider?: never; runner?: never; } @@ -313,14 +364,14 @@ type HarnessConfig = | { /** @deprecated Use `harness` instead. */ provider: Agent.Codex; - model: OpenAICodex | OpenRouterModel | VercelModel; + model: OpenAICodex | OpenRouterModel | VercelModel | NeonModel; harness?: never; runner?: never; } | { /** @deprecated Use `harness` instead. */ provider: Agent.OpenCode; - model: OpenCodeModel | ClaudeCode | OpenAICodex | OpenRouterModel | VercelModel; + model: OpenCodeModel | ClaudeCode | OpenAICodex | OpenRouterModel | VercelModel | NeonModel; harness?: never; runner?: never; } @@ -341,7 +392,14 @@ type HarnessConfig = | { /** @deprecated Use `harness` instead. */ runner: Exclude; - model: OpenCodeModel | ClaudeCode | OpenAICodex | OpenRouterModel | VercelModel | CursorModel; + model: + | OpenCodeModel + | ClaudeCode + | OpenAICodex + | OpenRouterModel + | VercelModel + | NeonModel + | CursorModel; harness?: never; provider?: never; }