diff --git a/data/opencode.json b/data/opencode.json index 2c23b21..9077294 100644 --- a/data/opencode.json +++ b/data/opencode.json @@ -9,82 +9,62 @@ "apiKey": "{env:OPPER_API_KEY}" }, "models": { - "gpt-5.5": { - "name": "GPT 5.5", - "cost": { "input": 5.00, "output": 30.00 }, - "limit": { "context": 1050000, "output": 128000 }, - "reasoning": true, - "variants": { - "xhigh": { - "reasoningEffort": "xhigh", - "reasoningSummary": "auto" - } - } + "claude-opus-4-7": { + "name": "Claude Opus 4.7", + "cost": { "input": 5.00, "output": 25.00 }, + "limit": { "context": 1000000, "output": 128000 } }, "claude-sonnet-4-6": { "name": "Claude Sonnet 4.6", "cost": { "input": 3.00, "output": 15.00 }, "limit": { "context": 1000000, "output": 64000 } }, - "claude-opus-4-7": { - "name": "Claude Opus 4.7", - "cost": { "input": 5.00, "output": 25.00 }, - "limit": { "context": 1000000, "output": 128000 } - }, "claude-haiku-4-5": { "name": "Claude Haiku 4.5", "cost": { "input": 1.00, "output": 5.00 }, "limit": { "context": 200000, "output": 64000 } }, - "gpt-5.2": { - "name": "GPT 5.2", - "cost": { "input": 1.75, "output": 14.00 }, - "limit": { "context": 400000, "output": 128000 } - }, - "gpt-5-mini": { - "name": "GPT 5 Mini", - "cost": { "input": 0.25, "output": 1.00 }, - "limit": { "context": 400000, "output": 128000 } - }, - "gpt-5-nano": { - "name": "GPT 5 Nano", - "cost": { "input": 0.05, "output": 0.40 }, - "limit": { "context": 400000, "output": 128000 } - }, - "gemini-2.5-pro": { - "name": "Gemini 2.5 Pro", - "cost": { "input": 1.25, "output": 10.00 }, - "limit": { "context": 1000000, "output": 65536 } + "gpt-5.5": { + "name": "GPT 5.5", + "cost": { "input": 5.00, "output": 30.00 }, + "limit": { "context": 1050000, "output": 128000 }, + "reasoning": true, + "variants": { + "xhigh": { + "reasoningEffort": "xhigh", + "reasoningSummary": "auto" + } + } }, - "gemini-2.5-flash": { - "name": "Gemini 2.5 Flash", - "cost": { "input": 0.30, "output": 2.50 }, - "limit": { "context": 1000000, "output": 65536 } + "gemini-3.1-pro-preview": { + "name": "Gemini 3.1 Pro Preview", + "cost": { "input": 2.00, "output": 12.00 }, + "limit": { "context": 1048576, "output": 65536 } }, - "gemini-2.5-flash-lite": { - "name": "Gemini 2.5 Flash Lite", - "cost": { "input": 0.10, "output": 0.40 }, - "limit": { "context": 1000000, "output": 65536 } + "deepinfra/kimi-k2.6": { + "name": "Kimi K2.6", + "cost": { "input": 0.75, "output": 3.50 }, + "limit": { "context": 262144, "output": 65536 } }, - "gemini-3-flash-preview": { - "name": "Gemini 3 Flash Preview", - "cost": { "input": 0.50, "output": 3.00 }, - "limit": { "context": 1000000, "output": 65536 } + "deepinfra/glm-5.1": { + "name": "GLM 5.1", + "cost": { "input": 1.05, "output": 3.50 }, + "limit": { "context": 202752, "output": 65536 } }, - "kimi-k2.5": { - "name": "Kimi K2.5", - "cost": { "input": 0.60, "output": 3.00 }, - "limit": { "context": 256000, "output": 65536 } + "fireworks/minimax-m2p7": { + "name": "MiniMax M2.7", + "cost": { "input": 0.30, "output": 1.20 }, + "limit": { "context": 196608, "output": 65536 } }, - "glm-4.7": { - "name": "GLM 4.7", - "cost": { "input": 0.60, "output": 2.20 }, - "limit": { "context": 200000, "output": 128000 } + "deepinfra/deepseek-v4-pro": { + "name": "DeepSeek V4 Pro", + "cost": { "input": 1.74, "output": 3.48 }, + "limit": { "context": 1048576, "output": 65536 } }, - "gpt-oss-120b": { - "name": "GPT OSS 120B", - "cost": { "input": 0.15, "output": 0.75 }, - "limit": { "context": 131072, "output": 131072 } + "deepinfra/deepseek-v4-flash": { + "name": "DeepSeek V4 Flash", + "cost": { "input": 0.14, "output": 0.28 }, + "limit": { "context": 1048576, "output": 65536 } } } } diff --git a/src/agents/claude-desktop.ts b/src/agents/claude-desktop.ts index a782d32..054186e 100644 --- a/src/agents/claude-desktop.ts +++ b/src/agents/claude-desktop.ts @@ -4,7 +4,7 @@ import { homedir, platform } from "node:os"; import { join, dirname } from "node:path"; import { OpperError } from "../errors.js"; import { OPPER_COMPAT_URL } from "../config/endpoints.js"; -import { DEFAULT_MODELS } from "../config/models.js"; +import { DEFAULT_MODELS, PICKER_MODELS } from "../config/models.js"; import { run } from "../util/run.js"; import type { AgentAdapter, @@ -240,12 +240,10 @@ async function writeGatewayProfile( cfg.inferenceGatewayAuthScheme = "bearer"; cfg.disableDeploymentModeChooser = true; // First entry is the picker default. Dedupe in case primaryModel - // equals one of the catalog defaults. + // equals one of the catalog ids. const names = Array.from(new Set([ primaryModel, - DEFAULT_MODELS.opus, - DEFAULT_MODELS.sonnet, - DEFAULT_MODELS.haiku, + ...PICKER_MODELS.map((m) => m.id), ])); cfg.inferenceModels = names.map((name) => ({ name })); await writeJson(path, cfg); diff --git a/src/agents/codex.ts b/src/agents/codex.ts index d226c31..8038849 100644 --- a/src/agents/codex.ts +++ b/src/agents/codex.ts @@ -13,13 +13,19 @@ import type { } from "./types.js"; import { OPPER_COMPAT_URL } from "../config/endpoints.js"; -import { DEFAULT_MODELS } from "../config/models.js"; +import { PICKER_MODELS } from "../config/models.js"; const SENTINEL_OPEN = "# >>> opper-cli >>>"; const SENTINEL_CLOSE = "# <<< opper-cli <<<"; const DEFAULT_PROFILE = "opper-opus"; function buildOpperBlock(baseUrl: string): string { + const profileBlocks = PICKER_MODELS.flatMap((m) => [ + "", + `[profiles.opper-${m.codexProfile}]`, + `model = "${m.id}"`, + 'model_provider = "opper"', + ]); return [ SENTINEL_OPEN, "# Managed by `opper`. Edits between these markers will be overwritten", @@ -30,14 +36,7 @@ function buildOpperBlock(baseUrl: string): string { `base_url = "${baseUrl}"`, 'env_key = "OPPER_API_KEY"', 'wire_api = "responses"', - "", - "[profiles.opper-opus]", - `model = "${DEFAULT_MODELS.opus}"`, - 'model_provider = "opper"', - "", - "[profiles.opper-sonnet]", - `model = "${DEFAULT_MODELS.sonnet}"`, - 'model_provider = "opper"', + ...profileBlocks, SENTINEL_CLOSE, "", ].join("\n"); diff --git a/src/agents/hermes.ts b/src/agents/hermes.ts index 8bd1f7c..29029d6 100644 --- a/src/agents/hermes.ts +++ b/src/agents/hermes.ts @@ -6,6 +6,7 @@ import { which } from "../util/which.js"; import { run } from "../util/run.js"; import { opperHome } from "../auth/paths.js"; import { OpperError } from "../errors.js"; +import { PICKER_MODELS } from "../config/models.js"; import type { AgentAdapter, DetectResult, @@ -102,6 +103,30 @@ async function writeOpperConfig(routing: OpperRouting): Promise { default: routing.model, }; + // Register Opper as a named provider so Hermes' `/model` picker shows + // "Opper (N models)" alongside the built-in providers. Without this the + // picker only enumerates first-class providers (OpenRouter, Copilot, + // OpenAI…) with detected creds — our `model.provider: custom` route is + // invisible to it. + // + // We let Hermes auto-discover models from `/v1/models` (the + // default behaviour) — same pattern Claude Code uses with our compat + // endpoint. The curated `models:` dict below is the fallback Hermes + // uses when discovery fails (network error, auth, etc.). + // + // `key_env: OPENAI_API_KEY` matches the env var we already export at + // spawn time, so no api key lands on disk. + const opperModels: Record> = {}; + for (const m of PICKER_MODELS) opperModels[m.id] = {}; + const providers = (existing.providers as Record | undefined) ?? {}; + providers.opper = { + name: "Opper", + base_url: routing.baseUrl, + key_env: "OPENAI_API_KEY", + models: opperModels, + }; + existing.providers = providers; + await writeFile(path, stringify(existing), { mode: 0o600 }); } diff --git a/src/agents/openclaw.ts b/src/agents/openclaw.ts index 4031578..d979726 100644 --- a/src/agents/openclaw.ts +++ b/src/agents/openclaw.ts @@ -8,7 +8,7 @@ import { run } from "../util/run.js"; import { OpperError } from "../errors.js"; import { npmInstallGlobal } from "./npm-install.js"; import { OPPER_COMPAT_URL } from "../config/endpoints.js"; -import { DEFAULT_MODELS } from "../config/models.js"; +import { DEFAULT_MODELS, pickerModelsForLaunch } from "../config/models.js"; import type { AgentAdapter, ConfigureOptions, @@ -53,36 +53,21 @@ async function setOpperProvider( ): Promise { const cfg = await readConfig(); cfg.providers = cfg.providers ?? {}; + // The launch model is reordered to index 0 — OpenClaw has no explicit + // _launch marker, so position-0 is the only signal that picks the + // active default in its picker UI. cfg.providers[PROVIDER_KEY] = { api: "openai-completions", apiKey, baseUrl, - models: [ - { - id: launchModel, - name: launchModel, - api: "openai-completions", - reasoning: true, - input: ["text"], - contextWindow: 200000, - }, - { - id: DEFAULT_MODELS.sonnet, - name: DEFAULT_MODELS.sonnet, - api: "openai-completions", - reasoning: true, - input: ["text"], - contextWindow: 200000, - }, - { - id: DEFAULT_MODELS.haiku, - name: DEFAULT_MODELS.haiku, - api: "openai-completions", - reasoning: false, - input: ["text"], - contextWindow: 200000, - }, - ], + models: pickerModelsForLaunch(launchModel).map((m) => ({ + id: m.id, + name: m.id, + api: "openai-completions", + reasoning: m.reasoning, + input: ["text"], + contextWindow: m.contextWindow, + })), }; await writeConfig(cfg); } diff --git a/src/agents/opencode.ts b/src/agents/opencode.ts index 3f700f1..60fb3ad 100644 --- a/src/agents/opencode.ts +++ b/src/agents/opencode.ts @@ -44,7 +44,11 @@ async function isConfigured(): Promise { } async function configure(): Promise { - await configureOpenCode({ location: "global" }); + // overwrite: true so a re-run pulls in the latest template (model list, + // costs, defaults). Without it, an existing `provider.opper` block from + // an older CLI version would be left in place and the new models would + // never appear in OpenCode's picker. + await configureOpenCode({ location: "global", overwrite: true }); } async function unconfigure(): Promise { @@ -109,9 +113,9 @@ async function spawn( // Explicit opt-in to writing the cwd-local config. We never silently // mutate a project config the user didn't ask us to touch — that file // is usually checked in. - await configureOpenCode({ location: "local" }); + await configureOpenCode({ location: "local", overwrite: true }); } else { - await configureOpenCode({ location: "global" }); + await configureOpenCode({ location: "global", overwrite: true }); // OpenCode reads `./opencode.json` if present and uses it instead of // the user-level config. If one exists without an Opper provider, diff --git a/src/agents/pi.ts b/src/agents/pi.ts index 68bcf40..ae0e692 100644 --- a/src/agents/pi.ts +++ b/src/agents/pi.ts @@ -8,7 +8,7 @@ import { run } from "../util/run.js"; import { OpperError } from "../errors.js"; import { npmInstallGlobal } from "./npm-install.js"; import { OPPER_COMPAT_URL } from "../config/endpoints.js"; -import { DEFAULT_MODELS } from "../config/models.js"; +import { DEFAULT_MODELS, pickerModelsForLaunch } from "../config/models.js"; import type { AgentAdapter, ConfigureOptions, @@ -56,31 +56,20 @@ async function setOpperProvider( ): Promise { const cfg = await readConfig(); cfg.providers = cfg.providers ?? {}; + // The launch model is reordered to index 0 (Pi treats the first entry as + // the active default in its picker UI). `_launch: true` is the explicit + // marker the runtime reads. cfg.providers[PROVIDER_KEY] = { api: "openai-completions", apiKey, baseUrl, - models: [ - { - id: launchModel, - contextWindow: 200000, - input: ["text"], - reasoning: true, - _launch: true, - }, - { - id: DEFAULT_MODELS.sonnet, - contextWindow: 200000, - input: ["text"], - reasoning: true, - }, - { - id: DEFAULT_MODELS.haiku, - contextWindow: 200000, - input: ["text"], - reasoning: false, - }, - ], + models: pickerModelsForLaunch(launchModel).map((m) => ({ + id: m.id, + contextWindow: m.contextWindow, + input: ["text"], + reasoning: m.reasoning, + ...(m.id === launchModel ? { _launch: true } : {}), + })), }; await writeConfig(cfg); } diff --git a/src/config/models.ts b/src/config/models.ts index 6ff4248..47e1d3a 100644 --- a/src/config/models.ts +++ b/src/config/models.ts @@ -14,6 +14,60 @@ export const DEFAULT_MODELS = { opus: "claude-opus-4-7", sonnet: "claude-sonnet-4-6", haiku: "claude-haiku-4-5", + gpt: "gpt-5.5", + gemini: "gemini-3.1-pro-preview", /** Image generation default (Imagen via Opper). */ image: "vertexai/imagen-4.0-fast-generate-001-eu", } as const; + +/** + * The set of models we expose by default in adapter pickers (Pi, OpenClaw, + * Claude Desktop, Codex). Claude Code and OpenCode pull their lists from + * elsewhere (the gateway's /v1/models endpoint and the OpenCode template, + * respectively). + * + * TODO: replace this hardcoded list with a fetch from /v3/models filtered + * to a "featured" set once the platform exposes it. + */ +export interface PickerModel { + /** Gateway model id passed to /v3/call (and compat). */ + id: string; + contextWindow: number; + reasoning: boolean; + /** Codex profile suffix — becomes `[profiles.opper-]`. */ + codexProfile: string; +} + +export const PICKER_MODELS: ReadonlyArray = [ + { id: DEFAULT_MODELS.opus, contextWindow: 1_000_000, reasoning: true, codexProfile: "opus" }, + { id: DEFAULT_MODELS.sonnet, contextWindow: 1_000_000, reasoning: true, codexProfile: "sonnet" }, + { id: DEFAULT_MODELS.haiku, contextWindow: 200_000, reasoning: false, codexProfile: "haiku" }, + { id: DEFAULT_MODELS.gpt, contextWindow: 1_050_000, reasoning: true, codexProfile: "gpt" }, + { id: DEFAULT_MODELS.gemini, contextWindow: 1_048_576, reasoning: true, codexProfile: "gemini" }, + { id: "deepinfra/kimi-k2.6", contextWindow: 262_144, reasoning: true, codexProfile: "kimi" }, + { id: "deepinfra/glm-5.1", contextWindow: 202_752, reasoning: true, codexProfile: "glm" }, + { id: "fireworks/minimax-m2p7", contextWindow: 196_608, reasoning: false, codexProfile: "minimax" }, + { id: "deepinfra/deepseek-v4-pro", contextWindow: 1_048_576, reasoning: true, codexProfile: "deepseek-pro" }, + { id: "deepinfra/deepseek-v4-flash", contextWindow: 1_048_576, reasoning: true, codexProfile: "deepseek-flash" }, +]; + +/** + * Return PICKER_MODELS reordered so `launchModel` is at index 0. When the + * launch model isn't in the picker set (e.g. user passed `--model X` for + * a non-curated id), it's prepended as a minimal entry. + * + * Adapters that bake a list into agent config (Pi, OpenClaw) treat + * `models[0]` as the default — this keeps that contract regardless of + * where the launch model sits in PICKER_MODELS. + */ +export function pickerModelsForLaunch(launchModel: string): PickerModel[] { + const idx = PICKER_MODELS.findIndex((m) => m.id === launchModel); + if (idx === -1) { + return [ + { id: launchModel, contextWindow: 200_000, reasoning: true, codexProfile: "" }, + ...PICKER_MODELS, + ]; + } + const head = PICKER_MODELS[idx]!; + return [head, ...PICKER_MODELS.filter((_, i) => i !== idx)]; +} diff --git a/test/agents/codex.test.ts b/test/agents/codex.test.ts index 6766305..7f25d1b 100644 --- a/test/agents/codex.test.ts +++ b/test/agents/codex.test.ts @@ -86,6 +86,17 @@ describe("codex adapter", () => { expect(await codex.isConfigured()).toBe(true); }); + it("configure writes one profile block per PICKER_MODELS entry", async () => { + const { PICKER_MODELS } = await import("../../src/config/models.js"); + await codex.configure({}); + const text = readFileSync(join(sandbox, ".codex", "config.toml"), "utf8"); + for (const m of PICKER_MODELS) { + // Profile header + the corresponding model id appear in the block. + expect(text).toContain(`[profiles.opper-${m.codexProfile}]`); + expect(text).toContain(`model = "${m.id}"`); + } + }); + it("configure preserves user content outside the sentinels", async () => { const cfgDir = join(sandbox, ".codex"); const cfgPath = join(cfgDir, "config.toml"); diff --git a/test/agents/hermes.test.ts b/test/agents/hermes.test.ts index aff4909..174fe15 100644 --- a/test/agents/hermes.test.ts +++ b/test/agents/hermes.test.ts @@ -174,6 +174,48 @@ describe("hermes adapter — spawn (isolated HERMES_HOME)", () => { expect(after.toolsets).toEqual(["hermes-cli", "web"]); }); + it("writes a providers.opper block with all picker model ids on every spawn", async () => { + runMock.mockReturnValue({ code: 0, stdout: "", stderr: "" }); + const SESSION_URL = "https://api.opper.ai/v3/session/sess_test"; + await hermes.spawn!([], { ...ROUTING, baseUrl: SESSION_URL }); + + const configPath = join(sandbox, ".opper", "hermes-home", "config.yaml"); + const written = parse(readFileSync(configPath, "utf8")) as { + providers?: { opper?: { name?: string; base_url?: string; key_env?: string; models?: Record } }; + }; + expect(written.providers?.opper).toBeDefined(); + expect(written.providers?.opper?.name).toBe("Opper"); + expect(written.providers?.opper?.base_url).toBe(SESSION_URL); + expect(written.providers?.opper?.key_env).toBe("OPENAI_API_KEY"); + const ids = Object.keys(written.providers?.opper?.models ?? {}); + // Spot-check both ends: the curated 5 plus the 5 added later. + expect(ids).toContain("claude-opus-4-7"); + expect(ids).toContain("gpt-5.5"); + expect(ids).toContain("gemini-3.1-pro-preview"); + expect(ids).toContain("deepinfra/kimi-k2.6"); + expect(ids).toContain("fireworks/minimax-m2p7"); + expect(ids).toContain("deepinfra/deepseek-v4-flash"); + }); + + it("rewrites providers.opper.base_url with the per-session URL on each spawn", async () => { + runMock.mockReturnValue({ code: 0, stdout: "", stderr: "" }); + const SESSION_A = "https://api.opper.ai/v3/session/sess_a"; + const SESSION_B = "https://api.opper.ai/v3/session/sess_b"; + + await hermes.spawn!([], { ...ROUTING, baseUrl: SESSION_A }); + await hermes.spawn!([], { ...ROUTING, baseUrl: SESSION_B }); + + const configPath = join(sandbox, ".opper", "hermes-home", "config.yaml"); + const after = parse(readFileSync(configPath, "utf8")) as { + model?: { base_url?: string }; + providers?: { opper?: { base_url?: string } }; + }; + // Both routing surfaces must follow the latest session URL — otherwise + // the picker row keeps pointing at a stale session. + expect(after.model?.base_url).toBe(SESSION_B); + expect(after.providers?.opper?.base_url).toBe(SESSION_B); + }); + it("propagates non-zero exit codes from run()", async () => { runMock.mockReturnValue({ code: 2, stdout: "", stderr: "" }); const code = await hermes.spawn!([], ROUTING); diff --git a/test/agents/openclaw.test.ts b/test/agents/openclaw.test.ts index fe453f0..873f2f4 100644 --- a/test/agents/openclaw.test.ts +++ b/test/agents/openclaw.test.ts @@ -95,6 +95,20 @@ describe("openclaw adapter", () => { expect(raw).not.toContain('"baseUrl": "https://api.opper.ai/v3/compat"'); }); + it("spawn places the launch model at models[0] even when it isn't opus", async () => { + spawnSyncMock.mockReturnValue({ status: 0 }); + await openclaw.spawn!([], { ...ROUTING, model: "gpt-5.5" }); + + const models = readModels(sandbox) as { + providers?: { opper?: { models?: Array<{ id: string }> } }; + }; + const list = models.providers?.opper?.models ?? []; + // OpenClaw has no _launch marker — position 0 is the only signal. + expect(list[0]?.id).toBe("gpt-5.5"); + expect(list.length).toBeGreaterThan(1); + expect(list.some((m) => m.id === "claude-opus-4-7")).toBe(true); + }); + it("spawn defaults to `gateway start` when no args are passed", async () => { spawnSyncMock.mockReturnValue({ status: 0 }); await openclaw.spawn!([], ROUTING); diff --git a/test/agents/opencode.test.ts b/test/agents/opencode.test.ts index e2bbb6f..30a65b5 100644 --- a/test/agents/opencode.test.ts +++ b/test/agents/opencode.test.ts @@ -145,7 +145,7 @@ describe("opencode adapter", () => { const code = await opencode.spawn!(["chat"], ROUTING); expect(code).toBe(0); - expect(configureOpenCodeMock).toHaveBeenCalledWith({ location: "global" }); + expect(configureOpenCodeMock).toHaveBeenCalledWith({ location: "global", overwrite: true }); const call = spawnSyncMock.mock.calls[0]!; expect(call[0]).toBe("opencode"); @@ -202,7 +202,7 @@ describe("opencode adapter", () => { spawnSyncMock.mockReturnValue({ status: 0 }); await opencode.spawn!(["chat"], ROUTING, { configScope: "project" }); - expect(configureOpenCodeMock).toHaveBeenCalledWith({ location: "local" }); + expect(configureOpenCodeMock).toHaveBeenCalledWith({ location: "local", overwrite: true }); }); it("spawn warns when a project opencode.json exists without an Opper provider", async () => { diff --git a/test/agents/pi.test.ts b/test/agents/pi.test.ts index ca08bd7..bdc0404 100644 --- a/test/agents/pi.test.ts +++ b/test/agents/pi.test.ts @@ -93,6 +93,33 @@ describe("pi adapter", () => { expect(raw).not.toContain('"baseUrl": "https://api.opper.ai/v3/compat"'); }); + it("spawn places the launch model at models[0] even when it isn't opus", async () => { + spawnSyncMock.mockReturnValue({ status: 0 }); + await pi.spawn!([], { ...ROUTING, model: "claude-haiku-4-5" }); + + const models = readModels(sandbox) as { + providers?: { opper?: { models?: Array<{ id: string; _launch?: boolean }> } }; + }; + const list = models.providers?.opper?.models ?? []; + expect(list[0]?.id).toBe("claude-haiku-4-5"); + expect(list[0]?._launch).toBe(true); + // Other curated models still present after the launch entry. + expect(list.length).toBeGreaterThan(1); + expect(list.some((m) => m.id === "claude-opus-4-7")).toBe(true); + }); + + it("spawn prepends a non-curated --model id so it still appears in the picker", async () => { + spawnSyncMock.mockReturnValue({ status: 0 }); + await pi.spawn!([], { ...ROUTING, model: "deepinfra/some-future-model" }); + + const models = readModels(sandbox) as { + providers?: { opper?: { models?: Array<{ id: string; _launch?: boolean }> } }; + }; + const list = models.providers?.opper?.models ?? []; + expect(list[0]?.id).toBe("deepinfra/some-future-model"); + expect(list[0]?._launch).toBe(true); + }); + it("spawn auto-injects --provider opper and --model when user doesn't pass --model", async () => { spawnSyncMock.mockReturnValue({ status: 0 }); await pi.spawn!([], ROUTING);