From e30d80389c3ba5a2a89e576628355c043b708b76 Mon Sep 17 00:00:00 2001 From: "will.anderson" Date: Fri, 21 Aug 2026 14:59:43 -0500 Subject: [PATCH] feat: prune cloud providers and console API, complete Neuron branding sweep --- bun.lock | 4 +- packages/opencode/package.json | 2 - packages/opencode/src/acp/service.ts | 8 +- .../src/cli/cmd/run/permission.shared.ts | 4 +- packages/opencode/src/cli/cmd/run/splash.ts | 2 +- packages/opencode/src/cli/cmd/uninstall.ts | 4 +- packages/opencode/src/config/managed.ts | 2 +- .../opencode/src/control-plane/dev/README.md | 19 - .../dev/debug-workspace-plugin.ts | 73 --- packages/opencode/src/mcp/oauth-provider.ts | 2 +- packages/opencode/src/plugin/azure.ts | 26 - packages/opencode/src/plugin/cloudflare.ts | 65 --- packages/opencode/src/plugin/digitalocean.ts | 2 +- packages/opencode/src/plugin/index.ts | 9 - packages/opencode/src/plugin/modal/modal.ts | 17 - packages/opencode/src/plugin/modal/models.ts | 133 ----- .../opencode/src/plugin/snowflake-cortex.ts | 507 ------------------ packages/opencode/src/provider/provider.ts | 496 +---------------- packages/opencode/src/provider/transform.ts | 2 +- .../src/server/routes/instance/httpapi/api.ts | 2 - .../routes/instance/httpapi/groups/config.ts | 4 +- .../instance/httpapi/groups/control-plane.ts | 35 -- .../instance/httpapi/groups/experimental.ts | 6 +- .../routes/instance/httpapi/groups/global.ts | 10 +- .../instance/httpapi/groups/instance.ts | 10 +- .../routes/instance/httpapi/groups/project.ts | 4 +- .../routes/instance/httpapi/groups/pty.ts | 2 +- .../routes/instance/httpapi/groups/session.ts | 6 +- .../httpapi/handlers/control-plane.ts | 37 -- .../server/routes/instance/httpapi/server.ts | 5 +- packages/opencode/src/session/retry.ts | 6 +- .../test/cli/acp/initialize-auth.test.ts | 2 +- .../__snapshots__/help-snapshots.test.ts.snap | 366 +++++++++---- .../test/cli/run/permission.shared.test.ts | 4 +- .../test/installation/installation.test.ts | 17 +- .../opencode/test/plugin/cloudflare.test.ts | 25 - .../opencode/test/plugin/modal-models.test.ts | 198 ------- .../test/plugin/snowflake-cortex.test.ts | 278 ---------- .../test/provider/amazon-bedrock.test.ts | 361 ------------- .../opencode/test/provider/provider.test.ts | 136 +---- .../test/server/httpapi-control-plane.test.ts | 63 --- .../test/server/httpapi-global.test.ts | 66 --- packages/opencode/test/session/retry.test.ts | 4 +- 43 files changed, 324 insertions(+), 2700 deletions(-) delete mode 100644 packages/opencode/src/control-plane/dev/README.md delete mode 100644 packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts delete mode 100644 packages/opencode/src/plugin/azure.ts delete mode 100644 packages/opencode/src/plugin/cloudflare.ts delete mode 100644 packages/opencode/src/plugin/modal/modal.ts delete mode 100644 packages/opencode/src/plugin/modal/models.ts delete mode 100644 packages/opencode/src/plugin/snowflake-cortex.ts delete mode 100644 packages/opencode/src/server/routes/instance/httpapi/groups/control-plane.ts delete mode 100644 packages/opencode/src/server/routes/instance/httpapi/handlers/control-plane.ts delete mode 100644 packages/opencode/test/plugin/cloudflare.test.ts delete mode 100644 packages/opencode/test/plugin/modal-models.test.ts delete mode 100644 packages/opencode/test/plugin/snowflake-cortex.test.ts delete mode 100644 packages/opencode/test/provider/amazon-bedrock.test.ts delete mode 100644 packages/opencode/test/server/httpapi-control-plane.test.ts delete mode 100644 packages/opencode/test/server/httpapi-global.test.ts diff --git a/bun.lock b/bun.lock index 52754cee3..811b70bb3 100644 --- a/bun.lock +++ b/bun.lock @@ -544,14 +544,13 @@ "name": "opencode", "version": "1.18.20", "bin": { - "opencode": "./bin/opencode", + "neuron": "./bin/neuron", }, "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.21.0", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.112", "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/azure": "3.0.88", "@ai-sdk/cerebras": "2.0.60", @@ -559,7 +558,6 @@ "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", "@ai-sdk/google": "3.0.73", - "@ai-sdk/google-vertex": "4.0.181", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.51", "@ai-sdk/openai": "3.0.84", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index e730e5994..419f8ffa4 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -56,7 +56,6 @@ "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.21.0", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.112", "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/azure": "3.0.88", "@ai-sdk/cerebras": "2.0.60", @@ -64,7 +63,6 @@ "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", "@ai-sdk/google": "3.0.73", - "@ai-sdk/google-vertex": "4.0.181", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.51", "@ai-sdk/openai": "3.0.84", diff --git a/packages/opencode/src/acp/service.ts b/packages/opencode/src/acp/service.ts index 55fbc9681..1f2101168 100644 --- a/packages/opencode/src/acp/service.ts +++ b/packages/opencode/src/acp/service.ts @@ -104,7 +104,7 @@ export function make(input: { "terminal-auth": { command: "opencode", args: ["auth", "login"], - label: "OpenCode Login", + label: "Neuron Login", }, } } @@ -130,7 +130,7 @@ export function make(input: { }, authMethods: [authMethod], agentInfo: { - name: "OpenCode", + name: "Neuron", version: InstallationVersion, }, } @@ -874,7 +874,7 @@ const promptResponse = Effect.fn("ACP.promptResponse")(function* ( function promptErrorMessage(error: AssistantError) { if ("message" in error.data && typeof error.data.message === "string") return error.data.message - return "OpenCode prompt failed" + return "Neuron prompt failed" } function sendUsageUpdate( @@ -1067,7 +1067,7 @@ function fromUnknownError(error: unknown, service?: string): Error { if (isAuthRequired(error)) { return new ACPError.AuthRequiredError({ providerId: findProviderID(error) }) } - return new ACPError.ServiceFailureError({ safeMessage: "OpenCode service failure", service }) + return new ACPError.ServiceFailureError({ safeMessage: "Neuron service failure", service }) } function isACPError(error: unknown): error is Error { diff --git a/packages/opencode/src/cli/cmd/run/permission.shared.ts b/packages/opencode/src/cli/cmd/run/permission.shared.ts index 6ebdbd090..92c622529 100644 --- a/packages/opencode/src/cli/cmd/run/permission.shared.ts +++ b/packages/opencode/src/cli/cmd/run/permission.shared.ts @@ -125,11 +125,11 @@ export function permissionInfo(request: PermissionRequest): PermissionInfo { export function permissionAlwaysLines(request: PermissionRequest): string[] { if (request.always.length === 1 && request.always[0] === "*") { - return [`This will allow ${request.permission} until OpenCode is restarted.`] + return [`This will allow ${request.permission} until Neuron is restarted.`] } return [ - "This will allow the following patterns until OpenCode is restarted.", + "This will allow the following patterns until Neuron is restarted.", ...request.always.map((item) => `- ${item}`), ] } diff --git a/packages/opencode/src/cli/cmd/run/splash.ts b/packages/opencode/src/cli/cmd/run/splash.ts index 141ff6fc5..0b4d61821 100644 --- a/packages/opencode/src/cli/cmd/run/splash.ts +++ b/packages/opencode/src/cli/cmd/run/splash.ts @@ -194,7 +194,7 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback }) } - push(lines, body_left, top, "OpenCode", right, undefined, TextAttributes.BOLD) + push(lines, body_left, top, "Neuron", right, undefined, TextAttributes.BOLD) if (input.detail) { push( lines, diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index 0afdc5185..cc85b0965 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/packages/opencode/src/cli/cmd/uninstall.ts @@ -55,7 +55,7 @@ export const UninstallCommand = { UI.empty() UI.println(UI.logo(" ")) UI.empty() - prompts.intro("Uninstall OpenCode") + prompts.intro("Uninstall Neuron") const method = await Installation.method() prompts.log.info(`Installation method: ${method}`) @@ -229,7 +229,7 @@ async function executeUninstall(method: Installation.Method, targets: RemovalTar } UI.empty() - prompts.log.success("Thank you for using OpenCode!") + prompts.log.success("Thank you for using Neuron!") } async function getShellConfigFile(): Promise { diff --git a/packages/opencode/src/config/managed.ts b/packages/opencode/src/config/managed.ts index 877d30aaa..e64f50fca 100644 --- a/packages/opencode/src/config/managed.ts +++ b/packages/opencode/src/config/managed.ts @@ -7,7 +7,7 @@ import { Process } from "@/util/process" const MANAGED_PLIST_DOMAIN = "ai.opencode.managed" -// Keys injected by macOS/MDM into the managed plist that are not OpenCode config +// Keys injected by macOS/MDM into the managed plist that are not Neuron config const PLIST_META = new Set([ "PayloadDisplayName", "PayloadIdentifier", diff --git a/packages/opencode/src/control-plane/dev/README.md b/packages/opencode/src/control-plane/dev/README.md deleted file mode 100644 index 74d68a75a..000000000 --- a/packages/opencode/src/control-plane/dev/README.md +++ /dev/null @@ -1,19 +0,0 @@ -This is a plugin to simulate a remote environment locally. Add this to `.opencode/opencode.jsonc`: - -```json - "plugin": ["../packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts"], -``` - -In a separate terminal, run a separate OpenCode server. This will act like a remote server and the local instance will proxy all requests to it: - -``` -./packages/opencode/script/run-workspace-server -``` - -With the plugin install, you can now run OpenCode and create a `debug` workspace type. This will create a "remote" workspace which talks to the second workspace server started above. - -How this works: - -- The workspace server needs to know the workspace id and port to run. It waits for this information to be written to a file and starts the server when the data is written. -- The debug plugin writes this information in the `create` call to the workspace. So create a `debug` workspace will always kick off a new external server. -- The server script watches for file changes, so whenver you create a new `debug` workspace it will restart with the new information. This means that there is only ever one working `debug` workspace at a time; when you create a new one all previous sessions will show that it can't connect because previous debug workspaces do not exist. diff --git a/packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts b/packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts deleted file mode 100644 index efc9d0c65..000000000 --- a/packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { Plugin } from "@opencode-ai/plugin" -import { rename, writeFile } from "node:fs/promises" -import { randomInt } from "node:crypto" -import { setTimeout as sleep } from "node:timers/promises" - -const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json" -const DEV_DATA_TEMP_FILE = `${DEV_DATA_FILE}.tmp` - -async function waitForHealth(port: number) { - const url = `http://127.0.0.1:${port}/global/health` - const started = Date.now() - - while (Date.now() - started < 30_000) { - try { - const response = await fetch(url) - if (response.ok) { - return - } - } catch {} - - await sleep(250) - } - - throw new Error(`Timed out waiting for debug server health check at ${url}`) -} - -let PORT: number | undefined - -async function writeDebugData(port: number, id: string, env: Record) { - await writeFile( - DEV_DATA_TEMP_FILE, - JSON.stringify( - { - port, - id, - env, - }, - null, - 2, - ), - ) - - await rename(DEV_DATA_TEMP_FILE, DEV_DATA_FILE) -} - -export const DebugWorkspacePlugin: Plugin = async ({ experimental_workspace }) => { - experimental_workspace.register("debug", { - name: "Debug", - description: "Create a debugging server", - configure(config) { - return config - }, - async create(config, env) { - const port = randomInt(5000, 9001) - PORT = port - - await writeDebugData(port, config.id, env) - - await waitForHealth(port) - }, - async remove(_config) {}, - target(_config) { - return { - type: "remote", - url: `http://localhost:${PORT!}/`, - } - }, - }) - - return {} -} - -export default DebugWorkspacePlugin diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index 596bfe1d5..b1e0b6ebd 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -43,7 +43,7 @@ export class McpOAuthProvider implements OAuthClientProvider { get clientMetadata(): OAuthClientMetadata { return { redirect_uris: [this.redirectUrl], - client_name: "OpenCode", + client_name: "Neuron", client_uri: "https://opencode.ai", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], diff --git a/packages/opencode/src/plugin/azure.ts b/packages/opencode/src/plugin/azure.ts deleted file mode 100644 index 62792b3bd..000000000 --- a/packages/opencode/src/plugin/azure.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Hooks, PluginInput } from "@opencode-ai/plugin" - -export async function AzureAuthPlugin(_input: PluginInput): Promise { - const prompts = [] - if (!process.env.AZURE_RESOURCE_NAME) { - prompts.push({ - type: "text" as const, - key: "resourceName", - message: "Enter Azure Resource Name", - placeholder: "e.g. my-models", - }) - } - - return { - auth: { - provider: "azure", - methods: [ - { - type: "api", - label: "API key", - prompts, - }, - ], - }, - } -} diff --git a/packages/opencode/src/plugin/cloudflare.ts b/packages/opencode/src/plugin/cloudflare.ts deleted file mode 100644 index 2ccf5168d..000000000 --- a/packages/opencode/src/plugin/cloudflare.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Hooks, PluginInput } from "@opencode-ai/plugin" - -export async function CloudflareWorkersAuthPlugin(_input: PluginInput): Promise { - const prompts = !process.env.CLOUDFLARE_ACCOUNT_ID - ? [ - { - type: "text" as const, - key: "accountId", - message: "Enter your Cloudflare Account ID", - placeholder: "e.g. 1234567890abcdef1234567890abcdef", - }, - ] - : [] - - return { - auth: { - provider: "cloudflare-workers-ai", - methods: [ - { - type: "api", - label: "API key", - prompts, - }, - ], - }, - } -} - -export async function CloudflareAIGatewayAuthPlugin(_input: PluginInput): Promise { - const prompts = [ - ...(!process.env.CLOUDFLARE_ACCOUNT_ID - ? [ - { - type: "text" as const, - key: "accountId", - message: "Enter your Cloudflare Account ID", - placeholder: "e.g. 1234567890abcdef1234567890abcdef", - }, - ] - : []), - ...(!process.env.CLOUDFLARE_GATEWAY_ID - ? [ - { - type: "text" as const, - key: "gatewayId", - message: "Enter your Cloudflare AI Gateway ID", - placeholder: "e.g. my-gateway", - }, - ] - : []), - ] - - return { - auth: { - provider: "cloudflare-ai-gateway", - methods: [ - { - type: "api", - label: "Gateway API token", - prompts, - }, - ], - }, - } -} diff --git a/packages/opencode/src/plugin/digitalocean.ts b/packages/opencode/src/plugin/digitalocean.ts index af241781e..81490a625 100644 --- a/packages/opencode/src/plugin/digitalocean.ts +++ b/packages/opencode/src/plugin/digitalocean.ts @@ -283,7 +283,7 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise return { url, instructions: - "Sign in to DigitalOcean in your browser. OpenCode will use your DigitalOcean API token directly for inference and load your Inference Routers. Re-run /connect to refresh routers later.", + "Sign in to DigitalOcean in your browser. Neuron will use your DigitalOcean API token directly for inference and load your Inference Routers. Re-run /connect to refresh routers later.", method: "auto" as const, async callback() { try { diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 32c4a81c4..51a062b6f 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -12,15 +12,11 @@ import { ServerAuth } from "@/server/auth" import { CodexAuthPlugin } from "./openai/codex" import { Session } from "@/session/session" import { NamedError } from "@opencode-ai/core/util/error" -import { ModalPlugin } from "./modal/modal" import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" import { PoeAuthPlugin } from "opencode-poe-auth" -import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare" -import { AzureAuthPlugin } from "./azure" import { DigitalOceanAuthPlugin } from "./digitalocean" import { XaiAuthPlugin } from "./xai" import { CerebrasPlugin } from "./cerebras" -import { SnowflakeCortexAuthPlugin } from "./snowflake-cortex" import { Effect, Layer, Context } from "effect" import { EffectBridge } from "@/effect/bridge" import { InstanceState } from "@/effect/instance-state" @@ -70,14 +66,9 @@ function internalPlugins(flags: RuntimeFlags.Info): PluginInstance[] { CodexAuthPlugin(input, { experimentalWebSockets: experimentalWebSocketsEnabled({ enabled: flags.experimentalWebSockets }), }), - ModalPlugin, GitlabAuthPlugin, PoeAuthPlugin, - CloudflareWorkersAuthPlugin, - CloudflareAIGatewayAuthPlugin, - AzureAuthPlugin, DigitalOceanAuthPlugin, - SnowflakeCortexAuthPlugin, XaiAuthPlugin, CerebrasPlugin, ] diff --git a/packages/opencode/src/plugin/modal/modal.ts b/packages/opencode/src/plugin/modal/modal.ts deleted file mode 100644 index 942166e29..000000000 --- a/packages/opencode/src/plugin/modal/modal.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Hooks } from "@opencode-ai/plugin" -import { ModalModels } from "./models" - -export async function ModalPlugin(): Promise { - return { - provider: { - id: "modal", - async models(provider, ctx) { - const apiKey = ctx.auth?.type === "api" ? ctx.auth.key : process.env.MODAL_PROXY_TOKEN - const baseURL = Object.values(provider.models)[0]?.api.url - if (!apiKey || !baseURL) return {} - - return ModalModels.get(baseURL, apiKey, provider.models).catch(() => ({})) - }, - }, - } -} diff --git a/packages/opencode/src/plugin/modal/models.ts b/packages/opencode/src/plugin/modal/models.ts deleted file mode 100644 index 2f9d2b7e4..000000000 --- a/packages/opencode/src/plugin/modal/models.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { Model } from "@opencode-ai/sdk/v2" -import { Schema } from "effect" - -const reasoningOption = Schema.Struct({ - type: Schema.Literal("effort"), - values: Schema.Array(Schema.NullOr(Schema.String)), -}) - -const response = Schema.Struct({ - data: Schema.Array( - Schema.Struct({ - id: Schema.String, - base_model_id: Schema.optional(Schema.String), - hugging_face_id: Schema.optional(Schema.String), - name: Schema.optional(Schema.String), - input_modalities: Schema.optional(Schema.Array(Schema.String)), - output_modalities: Schema.optional(Schema.Array(Schema.String)), - context_length: Schema.optional(Schema.Number), - max_output_length: Schema.optional(Schema.Number), - pricing: Schema.optional( - Schema.Struct({ - prompt: Schema.optional(Schema.Union([Schema.String, Schema.Number])), - completion: Schema.optional(Schema.Union([Schema.String, Schema.Number])), - input_cache_read: Schema.optional(Schema.Union([Schema.String, Schema.Number])), - }), - ), - supported_sampling_parameters: Schema.optional(Schema.Array(Schema.String)), - supported_features: Schema.optional(Schema.Array(Schema.String)), - reasoning_options: Schema.optional(Schema.Array(reasoningOption)), - interleaved: Schema.optional( - Schema.Union([ - Schema.Boolean, - Schema.Struct({ - field: Schema.Literals(["reasoning", "reasoning_content", "reasoning_details"]), - }), - ]), - ), - }), - ), -}) - -const decode = Schema.decodeUnknownSync(response) - -function price(value: string | number | undefined, fallback: number) { - if (value === undefined) return fallback - const parsed = Number(value) - return Number.isFinite(parsed) ? parsed * 1_000_000 : fallback -} - -export async function get(baseURL: string, apiKey: string, existing: Record) { - const data = await fetch(`${baseURL.replace(/\/+$/, "")}/models`, { - headers: { - Authorization: `Bearer ${apiKey}`, - }, - signal: AbortSignal.timeout(3_000), - }).then(async (res) => { - if (!res.ok) throw new Error(`Failed to fetch Modal models: ${res.status}`) - return decode(await res.json()) - }) - - return Object.fromEntries( - data.data.map((item) => { - const template = existing[item.base_model_id ?? item.hugging_face_id ?? item.id] - const model: Model = { - id: item.id, - providerID: "modal", - name: item.name ?? template?.name ?? item.id, - family: template?.family, - api: { - id: item.id, - url: baseURL, - npm: template?.api.npm ?? "@ai-sdk/openai-compatible", - }, - status: template?.status ?? "active", - headers: { ...template?.headers }, - options: { ...template?.options }, - cost: { - input: price(item.pricing?.prompt, template?.cost.input ?? 0), - output: price(item.pricing?.completion, template?.cost.output ?? 0), - cache: { - read: price(item.pricing?.input_cache_read, template?.cost.cache.read ?? 0), - write: template?.cost.cache.write ?? 0, - }, - }, - limit: { - context: item.context_length ?? template?.limit.context ?? 0, - input: template?.limit.input, - output: item.max_output_length ?? template?.limit.output ?? 0, - }, - capabilities: { - temperature: - item.supported_sampling_parameters?.includes("temperature") ?? template?.capabilities.temperature ?? false, - reasoning: item.supported_features?.includes("reasoning") ?? template?.capabilities.reasoning ?? false, - attachment: - item.input_modalities?.some((modality) => modality !== "text") ?? - template?.capabilities.attachment ?? - false, - toolcall: item.supported_features?.includes("tools") ?? template?.capabilities.toolcall ?? true, - input: { - text: item.input_modalities?.includes("text") ?? template?.capabilities.input.text ?? true, - audio: item.input_modalities?.includes("audio") ?? template?.capabilities.input.audio ?? false, - image: item.input_modalities?.includes("image") ?? template?.capabilities.input.image ?? false, - video: item.input_modalities?.includes("video") ?? template?.capabilities.input.video ?? false, - pdf: item.input_modalities?.includes("pdf") ?? template?.capabilities.input.pdf ?? false, - }, - output: { - text: item.output_modalities?.includes("text") ?? template?.capabilities.output.text ?? true, - audio: item.output_modalities?.includes("audio") ?? template?.capabilities.output.audio ?? false, - image: item.output_modalities?.includes("image") ?? template?.capabilities.output.image ?? false, - video: item.output_modalities?.includes("video") ?? template?.capabilities.output.video ?? false, - pdf: item.output_modalities?.includes("pdf") ?? template?.capabilities.output.pdf ?? false, - }, - interleaved: item.interleaved ?? template?.capabilities.interleaved ?? false, - }, - release_date: template?.release_date ?? "", - } - model.variants = - item.reasoning_options === undefined - ? template?.variants - : Object.fromEntries( - item.reasoning_options.flatMap((option) => - option.values.map((value) => { - const effort = value ?? "none" - return [effort, { reasoningEffort: effort }] - }), - ), - ) - return [item.id, model] - }), - ) -} - -export * as ModalModels from "./models" diff --git a/packages/opencode/src/plugin/snowflake-cortex.ts b/packages/opencode/src/plugin/snowflake-cortex.ts deleted file mode 100644 index 09f107ed7..000000000 --- a/packages/opencode/src/plugin/snowflake-cortex.ts +++ /dev/null @@ -1,507 +0,0 @@ -import type { Hooks, PluginInput } from "@opencode-ai/plugin" -import { OAUTH_DUMMY_KEY } from "../auth" -import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { OauthCallbackPage } from "@opencode-ai/core/oauth/page" -import { createServer } from "http" -import open from "open" - -const OAUTH_CLIENT_ID = "LOCAL_APPLICATION" -const OAUTH_CALLBACK_HOST = "127.0.0.1" -const OAUTH_CALLBACK_PATH = "/" -const OAUTH_TIMEOUT_MS = 5 * 60 * 1000 -const ACCESS_TOKEN_REFRESH_SKEW_MS = 120_000 - -interface PkceCodes { - verifier: string - challenge: string -} - -interface TokenResponse { - access_token: string - refresh_token?: string - expires_in?: number - token_type?: string -} - -interface PendingOAuth { - account: string - state: string - pkce: PkceCodes - resolve: (tokens: TokenResponse) => void - reject: (error: Error) => void -} - -let oauthServer: ReturnType | undefined -let pendingOAuth: PendingOAuth | undefined -let oauthServerPort: number | undefined - -function normalizeAccount(input: string) { - return input - .trim() - .replace(/^https?:\/\//, "") - .replace(/\.snowflakecomputing\.com\/?$/, "") - .replace(/\/+$/, "") -} - -function generateRandomString(length: number) { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" - return Array.from(crypto.getRandomValues(new Uint8Array(length))) - .map((b) => chars[b % chars.length]) - .join("") -} - -function base64UrlEncode(buffer: ArrayBuffer) { - const binary = String.fromCharCode(...new Uint8Array(buffer)) - return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") -} - -async function generatePKCE(): Promise { - const verifier = generateRandomString(64) - const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)) - return { - verifier, - challenge: base64UrlEncode(hash), - } -} - -function callbackUrl() { - if (!oauthServerPort) throw new Error("Snowflake OAuth callback server is not running") - return `http://${OAUTH_CALLBACK_HOST}:${oauthServerPort}${OAUTH_CALLBACK_PATH}` -} - -export function oauthScope(role: string | undefined) { - if (!role) return "refresh_token" - return /^[-_A-Za-z0-9]+$/.test(role) - ? `refresh_token session:role:${role}` - : `refresh_token session:role-encoded:${encodeURIComponent(role)}` -} - -function authHeaders() { - return { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - "User-Agent": `opencode/${InstallationVersion}`, - } -} - -function authBasicHeader() { - return `Basic ${Buffer.from(`${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_ID}`).toString("base64")}` -} - -function buildAuthorizeUrl(account: string, role: string | undefined, state: string, pkce: PkceCodes) { - const scope = oauthScope(role) - const params = new URLSearchParams({ - client_id: OAUTH_CLIENT_ID, - response_type: "code", - redirect_uri: callbackUrl(), - scope, - state, - code_challenge: pkce.challenge, - code_challenge_method: "S256", - }) - return `https://${account}.snowflakecomputing.com/oauth/authorize?${params.toString()}` -} - -async function exchangeCodeForToken(account: string, code: string, pkce: PkceCodes) { - const response = await fetch(`https://${account}.snowflakecomputing.com/oauth/token-request`, { - method: "POST", - headers: { - ...authHeaders(), - Authorization: authBasicHeader(), - }, - body: new URLSearchParams({ - grant_type: "authorization_code", - code, - redirect_uri: callbackUrl(), - client_id: OAUTH_CLIENT_ID, - code_verifier: pkce.verifier, - }).toString(), - }) - - if (!response.ok) { - const detail = await response.text().catch(() => "") - throw new Error(`Snowflake token exchange failed (${response.status})${detail ? `: ${detail}` : ""}`) - } - - const token = (await response.json()) as TokenResponse - if (!token.access_token) throw new Error("Snowflake token response did not include access_token") - if (!token.refresh_token) { - throw new Error( - "Snowflake token response did not include refresh_token. Ensure integration issues refresh tokens and scope includes refresh_token.", - ) - } - return token -} - -async function refreshAccessToken(account: string, refreshToken: string) { - const response = await fetch(`https://${account}.snowflakecomputing.com/oauth/token-request`, { - method: "POST", - headers: { - ...authHeaders(), - Authorization: authBasicHeader(), - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: OAUTH_CLIENT_ID, - }).toString(), - }) - - if (!response.ok) { - const detail = await response.text().catch(() => "") - throw new Error(`Snowflake token refresh failed (${response.status})${detail ? `: ${detail}` : ""}`) - } - - const token = (await response.json()) as TokenResponse - if (!token.access_token) throw new Error("Snowflake refresh response did not include access_token") - return token -} - -async function startOAuthServer() { - if (oauthServer) return - - oauthServer = createServer((req, res) => { - const host = req.headers.host || `${OAUTH_CALLBACK_HOST}:${oauthServerPort ?? 0}` - const url = new URL(req.url || "/", `http://${host}`) - - if (url.pathname !== OAUTH_CALLBACK_PATH) { - res.writeHead(404) - res.end("Not found") - return - } - - const state = url.searchParams.get("state") - const code = url.searchParams.get("code") - const error = url.searchParams.get("error") - const errorDescription = url.searchParams.get("error_description") - - // CSRF guard: validate state before processing any callback - if (!pendingOAuth || state !== pendingOAuth.state) { - const message = "Invalid state - potential CSRF attack" - pendingOAuth?.reject(new Error(message)) - pendingOAuth = undefined - res.writeHead(400, { "Content-Type": "text/html" }) - res.end(OauthCallbackPage.error(message, { provider: "Snowflake" })) - return - } - - const current = pendingOAuth - pendingOAuth = undefined - - if (error) { - const message = errorDescription || error - current.reject(new Error(message)) - res.writeHead(200, { "Content-Type": "text/html" }) - res.end(OauthCallbackPage.error(message, { provider: "Snowflake" })) - return - } - - if (!code) { - const message = "Missing authorization code" - current.reject(new Error(message)) - res.writeHead(400, { "Content-Type": "text/html" }) - res.end(OauthCallbackPage.error(message, { provider: "Snowflake" })) - return - } - - exchangeCodeForToken(current.account, code, current.pkce) - .then((tokens) => current.resolve(tokens)) - .catch((err) => current.reject(err instanceof Error ? err : new Error(String(err)))) - - res.writeHead(200, { "Content-Type": "text/html" }) - res.end(OauthCallbackPage.success({ provider: "Snowflake" })) - }) - - await new Promise((resolve, reject) => { - oauthServer!.listen(0, OAUTH_CALLBACK_HOST, () => { - const address = oauthServer!.address() - if (!address || typeof address === "string") { - reject(new Error("Unable to resolve Snowflake OAuth callback port")) - return - } - oauthServerPort = address.port - resolve() - }) - oauthServer!.on("error", reject) - }) -} - -function stopOAuthServer() { - if (!oauthServer) return - oauthServer.close() - oauthServer = undefined - oauthServerPort = undefined -} - -function waitForOAuthCallback(account: string, pkce: PkceCodes, state: string): Promise { - if (pendingOAuth) { - pendingOAuth.reject(new Error("Superseded by a newer Snowflake authorize request")) - pendingOAuth = undefined - } - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - if (!pendingOAuth) return - pendingOAuth = undefined - stopOAuthServer() - reject(new Error("Snowflake OAuth callback timeout - authorization took too long")) - }, OAUTH_TIMEOUT_MS) - - pendingOAuth = { - account, - state, - pkce, - resolve: (tokens) => { - clearTimeout(timeout) - resolve(tokens) - }, - reject: (error) => { - clearTimeout(timeout) - reject(error) - }, - } - }) -} - -export async function SnowflakeCortexAuthPlugin(_input: PluginInput): Promise { - const prompts = [ - { - type: "text" as const, - key: "account", - message: "Snowflake Account Identifier", - placeholder: "myorg-myaccount", - validate: (value: string) => (value && value.trim().length > 0 ? undefined : "Required"), - }, - { - type: "text" as const, - key: "role", - message: "Snowflake Role (optional)", - placeholder: "PUBLIC", - }, - ] - - return { - auth: { - provider: "snowflake-cortex", - async loader(getAuth, _provider) { - let auth = await getAuth() - if (auth.type !== "oauth") return {} - - let refreshPromise: - | Promise<{ - access: string - refresh: string - expires: number - }> - | undefined - - const oauth = auth as typeof auth & { refresh: string; access: string; expires: number; accountId?: string } - - if (oauth.accountId && oauth.refresh && oauth.expires && oauth.expires <= Date.now()) { - try { - const tokens = await refreshAccessToken(oauth.accountId, oauth.refresh) - const refreshedRefresh = tokens.refresh_token || oauth.refresh - const refreshedExpires = Date.now() + (tokens.expires_in ?? 600) * 1000 - await _input.client.auth - .set({ - path: { id: "snowflake-cortex" }, - body: { - type: "oauth", - access: tokens.access_token, - refresh: refreshedRefresh, - expires: refreshedExpires, - ...(oauth.accountId && { accountId: oauth.accountId }), - }, - }) - .catch(() => {}) - } catch {} - } - - return { - apiKey: OAUTH_DUMMY_KEY, - async fetch(requestInput: RequestInfo | URL, init?: RequestInit) { - let currentAuth = await getAuth() - if (currentAuth.type !== "oauth") return fetch(requestInput, init) - let currentOauth = currentAuth as typeof currentAuth & { - refresh: string - access: string - expires: number - accountId?: string - } - - if (!currentOauth.accountId) throw new Error("Snowflake OAuth auth is missing accountId") - const accountId = currentOauth.accountId - - const refresh = async () => { - if (!refreshPromise) { - const refreshToken = currentOauth.refresh - refreshPromise = refreshAccessToken(accountId, refreshToken) - .then(async (tokens) => { - const refreshedRefresh = tokens.refresh_token || refreshToken - const refreshedExpires = Date.now() + (tokens.expires_in ?? 600) * 1000 - await _input.client.auth - .set({ - path: { id: "snowflake-cortex" }, - body: { - type: "oauth", - access: tokens.access_token, - refresh: refreshedRefresh, - expires: refreshedExpires, - ...(accountId && { accountId }), - }, - }) - .catch(() => {}) - return { - access: tokens.access_token, - refresh: refreshedRefresh, - expires: refreshedExpires, - } - }) - .finally(() => { - refreshPromise = undefined - }) - } - - const refreshed = await refreshPromise - currentOauth = { ...currentOauth, ...refreshed } - } - - const prepareRequest = () => { - const headers = new Headers(requestInput instanceof Request ? requestInput.headers : undefined) - if (init?.headers) { - const entries = - init.headers instanceof Headers - ? init.headers.entries() - : Array.isArray(init.headers) - ? init.headers - : Object.entries(init.headers as Record) - for (const [key, value] of entries) { - if (value !== undefined) headers.set(key, String(value)) - } - } - headers.set("authorization", `Bearer ${currentOauth.access}`) - headers.set("User-Agent", `opencode/${InstallationVersion}`) - - let body = init?.body - if (body && typeof body === "string") { - try { - const parsed = JSON.parse(body) - if ("max_tokens" in parsed) { - parsed.max_completion_tokens = parsed.max_tokens - delete parsed.max_tokens - body = JSON.stringify(parsed) - } - } catch {} - } - - return { ...init, headers, body } - } - - const transformResponse = async (response: Response) => { - if (!response.ok && response.status === 400) { - try { - const errorData = await response.clone().json() - const errorMessage = String(errorData.message || errorData.error || "") - if (errorMessage.toLowerCase().includes("conversation complete")) { - return new Response( - JSON.stringify({ - choices: [{ finish_reason: "stop", message: { content: "", role: "assistant" } }], - }), - { status: 200, headers: new Headers({ "content-type": "application/json" }) }, - ) - } - } catch {} - } - - if (response.body && response.headers.get("content-type")?.includes("text/event-stream")) { - const reader = response.body.getReader() - const encoder = new TextEncoder() - const decoder = new TextDecoder() - const stream = new ReadableStream({ - async pull(ctrl) { - const { done, value } = await reader.read() - if (done) { - ctrl.close() - return - } - const text = decoder.decode(value, { stream: true }) - ctrl.enqueue(encoder.encode(text.replace(/"role"\s*:\s*""/g, '"role":"assistant"'))) - }, - cancel() { - reader.cancel() - }, - }) - return new Response(stream, { headers: response.headers, status: response.status }) - } - - return response - } - - const expiresSoon = - !currentOauth.expires || - !currentOauth.access || - currentOauth.expires - Date.now() <= ACCESS_TOKEN_REFRESH_SKEW_MS - - if (expiresSoon) await refresh() - - const response = await fetch(requestInput, prepareRequest()) - - if (response.status === 401) { - await refresh() - return transformResponse(await fetch(requestInput, prepareRequest())) - } - - return transformResponse(response) - }, - } - }, - methods: [ - { - type: "oauth", - label: "Login with Snowflake (External Browser)", - prompts, - async authorize(inputs = {}) { - const account = normalizeAccount(inputs.account || "") - if (!account) throw new Error("Snowflake account is required") - - await startOAuthServer() - const pkce = await generatePKCE() - const state = generateRandomString(64) - const role = (inputs.role || "").trim() || undefined - const url = buildAuthorizeUrl(account, role, state, pkce) - const callbackPromise = waitForOAuthCallback(account, pkce, state) - await open(url).catch(() => undefined) - - return { - url, - instructions: - "Complete Snowflake sign-in in your browser. OpenCode will capture the OAuth callback and store the bearer token automatically.", - method: "auto" as const, - async callback() { - try { - const tokens = await callbackPromise - return { - type: "success" as const, - refresh: tokens.refresh_token!, - access: tokens.access_token, - expires: Date.now() + (tokens.expires_in ?? 600) * 1000, - accountId: account, - } - } catch { - return { type: "failed" as const } - } finally { - stopOAuthServer() - } - }, - } - }, - }, - { - type: "api", - label: "Paste PAT or bearer token manually", - prompts: prompts.filter((item) => item.key === "account"), - }, - ], - }, - } -} diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 2c6c11b70..281e5abda 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -111,14 +111,9 @@ type BundledSDK = { } const BUNDLED_PROVIDERS: Record Promise<(opts: any) => BundledSDK>> = { - "@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock), - "@ai-sdk/amazon-bedrock/mantle": () => import("@ai-sdk/amazon-bedrock/mantle").then((m) => m.createBedrockMantle), "@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic), "@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure), "@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI), - "@ai-sdk/google-vertex": () => import("@ai-sdk/google-vertex").then((m) => m.createVertex), - "@ai-sdk/google-vertex/anthropic": () => - import("@ai-sdk/google-vertex/anthropic").then((m) => m.createVertexAnthropic), "@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI), "@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible), "@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter), @@ -267,178 +262,6 @@ function custom(dep: CustomDep): Record { }, } }), - "azure-cognitive-services": Effect.fnUntraced(function* (provider: Info) { - const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME") - return { - autoload: false, - async getModel(sdk: any, modelID: string, options?: Record) { - return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"])) - }, - options: { - baseURL: resourceName - ? `https://${resourceName}.cognitiveservices.azure.com/openai${provider.options?.useDeploymentBasedUrls ? "" : "/v1"}` - : undefined, - }, - } - }), - "amazon-bedrock": Effect.fnUntraced(function* () { - const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"] - const auth = yield* dep.auth("amazon-bedrock") - const env = yield* dep.env() - - // Region precedence: 1) config file, 2) env var, 3) default - const configRegion = providerConfig?.options?.region - const envRegion = env["AWS_REGION"] - const defaultRegion = configRegion ?? envRegion ?? "us-east-1" - - // Profile: config file takes precedence over env var - const configProfile = providerConfig?.options?.profile - const envProfile = env["AWS_PROFILE"] - const profile = configProfile ?? envProfile - - const awsAccessKeyId = env["AWS_ACCESS_KEY_ID"] - const configApiKey = providerConfig?.options?.apiKey - - // the AWS SDK reads this from process.env lazily at request time, so go - // through Env.set which writes through to process.env - let awsBearerToken = process.env.AWS_BEARER_TOKEN_BEDROCK - if (!awsBearerToken && auth?.type === "api") { - yield* dep.set("AWS_BEARER_TOKEN_BEDROCK", auth.key) - awsBearerToken = auth.key - } - - const awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"] - - const containerCreds = Boolean( - process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI, - ) - - if ( - !profile && - !awsAccessKeyId && - !awsBearerToken && - !configApiKey && - !awsWebIdentityTokenFile && - !containerCreds - ) - return { autoload: false } - - const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers")) - - const providerOptions: Record = { - region: defaultRegion, - } - - // Only use credential chain if no bearer token exists - // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens) - if (!awsBearerToken && !configApiKey) { - // Build credential provider options (only pass profile if specified) - const credentialProviderOptions = profile ? { profile } : {} - - providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions) - } - - // Add custom endpoint if specified (endpoint takes precedence over baseURL) - const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL - if (endpoint) { - providerOptions.baseURL = endpoint - } - - return { - autoload: true, - options: providerOptions, - vars(options: Record) { - return { AWS_REGION: options.region ?? defaultRegion } - }, - async getModel(sdk: any, modelID: string, options?: Record, model?: Model) { - if (model?.api.npm === "@ai-sdk/amazon-bedrock/mantle") return selectBedrockMantleLanguageModel(sdk, modelID) - - // Skip region prefixing if model already has a cross-region inference profile prefix - // Models from models.dev may already include prefixes like us., eu., global., etc. - const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."] - if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) { - return sdk.languageModel(modelID) - } - - // Region resolution precedence (highest to lowest): - // 1. options.region from opencode.json provider config - // 2. defaultRegion from AWS_REGION environment variable - // 3. Default "us-east-1" (baked into defaultRegion) - const region = options?.region ?? defaultRegion - - let regionPrefix = region.split("-")[0] - - switch (regionPrefix) { - case "us": { - const modelRequiresPrefix = [ - "nova-micro", - "nova-lite", - "nova-pro", - "nova-premier", - "nova-2", - "claude", - "deepseek", - ].some((m) => modelID.includes(m)) - const isGovCloud = region.startsWith("us-gov") - if (modelRequiresPrefix && !isGovCloud) { - modelID = `${regionPrefix}.${modelID}` - } - break - } - case "eu": { - const regionRequiresPrefix = [ - "eu-west-1", - "eu-west-2", - "eu-west-3", - "eu-north-1", - "eu-central-1", - "eu-south-1", - "eu-south-2", - ].some((r) => region.includes(r)) - const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) => - modelID.includes(m), - ) - if (regionRequiresPrefix && modelRequiresPrefix) { - modelID = `${regionPrefix}.${modelID}` - } - break - } - case "ap": { - const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region) - const isTokyoRegion = region === "ap-northeast-1" - if ( - isAustraliaRegion && - ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m)) - ) { - regionPrefix = "au" - modelID = `${regionPrefix}.${modelID}` - } else if (isTokyoRegion) { - // Tokyo region uses jp. prefix for cross-region inference - const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => - modelID.includes(m), - ) - if (modelRequiresPrefix) { - regionPrefix = "jp" - modelID = `${regionPrefix}.${modelID}` - } - } else { - // Other APAC regions use apac. prefix - const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => - modelID.includes(m), - ) - if (modelRequiresPrefix) { - regionPrefix = "apac" - modelID = `${regionPrefix}.${modelID}` - } - } - break - } - } - - return sdk.languageModel(modelID) - }, - } - }), llmgateway: () => Effect.succeed({ autoload: false, @@ -467,7 +290,7 @@ function custom(dep: CustomDep): Record { headers: { "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", - "X-BILLING-INVOKE-ORIGIN": "OpenCode", + "X-BILLING-INVOKE-ORIGIN": "Neuron", }, }, }), @@ -481,97 +304,6 @@ function custom(dep: CustomDep): Record { }, }, }), - "google-vertex": Effect.fnUntraced(function* (provider: Info) { - const env = yield* dep.env() - // models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex; keep the wider - // Google Cloud project env names as fallbacks for existing ADC setups. - const project = - provider.options?.project ?? - env["GOOGLE_VERTEX_PROJECT"] ?? - env["GOOGLE_CLOUD_PROJECT"] ?? - env["GCP_PROJECT"] ?? - env["GCLOUD_PROJECT"] - - const location = String( - provider.options?.location ?? - env["GOOGLE_VERTEX_LOCATION"] ?? - env["GOOGLE_CLOUD_LOCATION"] ?? - env["VERTEX_LOCATION"] ?? - "us-central1", - ) - - const autoload = Boolean(project) - if (!autoload) return { autoload: false } - return { - autoload: true, - vars(_options: Record) { - return { - ...(project && { GOOGLE_VERTEX_PROJECT: project }), - GOOGLE_VERTEX_LOCATION: location, - GOOGLE_VERTEX_ENDPOINT: googleVertexEndpoint(location), - } - }, - options: { - project, - location, - fetch: async (input: RequestInfo | URL, init?: RequestInit) => { - const { GoogleAuth } = await import("google-auth-library") - const auth = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }) - const client = await auth.getClient() - const token = await client.getAccessToken() - - const headers = new Headers(init?.headers) - headers.set("Authorization", `Bearer ${token.token}`) - - return fetch(input, { ...init, headers }) - }, - }, - async getModel(sdk: any, modelID: string) { - const id = String(modelID).trim() - return sdk.languageModel(id) - }, - } - }), - "google-vertex-anthropic": Effect.fnUntraced(function* () { - const env = yield* dep.env() - const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"] - const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global" - const autoload = Boolean(project) - if (!autoload) return { autoload: false } - const baseURL = googleVertexAnthropicBaseURL(project, location) - return { - autoload: true, - options: { - project, - location, - ...(baseURL && { baseURL }), - }, - async getModel(sdk: any, modelID) { - const id = String(modelID).trim() - return sdk.languageModel(id) - }, - } - }), - "sap-ai-core": Effect.fnUntraced(function* () { - const auth = yield* dep.auth("sap-ai-core") - // the SAP SDK reads this from process.env lazily at request time, so go - // through Env.set which writes through to process.env - let envServiceKey = process.env.AICORE_SERVICE_KEY - if (!envServiceKey && auth?.type === "api") { - yield* dep.set("AICORE_SERVICE_KEY", auth.key) - envServiceKey = auth.key - } - const deploymentId = process.env.AICORE_DEPLOYMENT_ID - const resourceGroup = process.env.AICORE_RESOURCE_GROUP - - return { - autoload: !!envServiceKey, - options: envServiceKey ? { deploymentId, resourceGroup } : {}, - async getModel(sdk: any, modelID: string) { - return sdk(modelID) - }, - } - }), zenmux: () => Effect.succeed({ autoload: false, @@ -707,132 +439,6 @@ function custom(dep: CustomDep): Record { }, } }), - "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) { - // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway), - // skip the account ID check because the URL is already fully specified. - if (input.options?.baseURL) return { autoload: false } - - const auth = yield* dep.auth(input.id) - const env = yield* dep.env() - const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) - if (!accountId) - return { - autoload: false, - async getModel() { - throw new Error( - "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=", - ) - }, - } - - const apiKey = env["CLOUDFLARE_API_KEY"] || (auth?.type === "api" ? auth.key : undefined) - - return { - autoload: !!apiKey, - options: { - apiKey, - headers: { - "User-Agent": `opencode/${InstallationVersion} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`, - }, - }, - async getModel(sdk: any, modelID: string) { - return sdk.languageModel(modelID) - }, - vars(_options) { - return { - CLOUDFLARE_ACCOUNT_ID: accountId, - } - }, - } - }), - "cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) { - // When baseURL is already configured (e.g. corporate config), skip the ID checks. - if (input.options?.baseURL) return { autoload: false } - - const auth = yield* dep.auth(input.id) - const env = yield* dep.env() - const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) - // The Cloudflare auth prompt stores this value as gatewayId metadata. - const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined) - - if (!accountId || !gateway) { - const missing = [ - !accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined, - !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined, - ].filter((x): x is string => Boolean(x)) - return { - autoload: false, - async getModel() { - throw new Error( - `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=`).join(" && ")}`, - ) - }, - } - } - - // Get API token from env or auth - required for authenticated gateways - const apiToken = - env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"] || (auth?.type === "api" ? auth.key : undefined) - - if (!apiToken) { - throw new Error( - "CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " + - "Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.", - ) - } - - const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")) - const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")) - const { createOpenAI } = yield* Effect.promise(() => import("ai-gateway-provider/providers/openai")) - const { createAnthropic } = yield* Effect.promise(() => import("ai-gateway-provider/providers/anthropic")) - - const metadata = iife(() => { - if (input.options?.metadata) return input.options.metadata - try { - return JSON.parse(input.options?.headers?.["cf-aig-metadata"]) - } catch { - return undefined - } - }) - const opts = { - metadata, - cacheTtl: input.options?.cacheTtl, - cacheKey: input.options?.cacheKey, - skipCache: input.options?.skipCache, - collectLog: input.options?.collectLog, - headers: { - "User-Agent": `opencode/${InstallationVersion} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`, - }, - } - - const aigateway = createAiGateway({ - accountId, - gateway, - apiKey: apiToken, - ...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}), - }) - return { - autoload: true, - async getModel(_sdk: any, modelID: string, _options?: Record) { - // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5"). - // OpenAI and Anthropic ride their native passthrough routes so agents get the Responses - // and Messages APIs; new OpenAI models reject tools+reasoning_effort on chat completions. - // The passthrough wrappers inject a CF_TEMP_TOKEN sentinel that the gateway strips before - // dispatch, so upstream billing stays on the gateway (Unified Billing / stored BYOK). - if (modelID.startsWith("openai/")) return aigateway(createOpenAI()(modelID.slice("openai/".length))) - if (modelID.startsWith("anthropic/")) return aigateway(createAnthropic()(modelID.slice("anthropic/".length))) - // Workers AI is the only first-party provider whose upstream is Cloudflare itself, so it is - // the only one that should receive the Cloudflare token as its upstream Authorization header. - // The Unified API addresses Workers AI both with the explicit "workers-ai/" prefix and as - // bare "@cf/..." ids. Third-party providers must not receive the token; they rely on the - // gateway's stored/BYOK keys instead. - const isWorkersAi = modelID.startsWith("workers-ai/") || modelID.startsWith("@cf/") - const unified = createUnified(isWorkersAi ? { apiKey: apiToken } : {}) - return aigateway(unified(modelID)) - }, - options: {}, - } - }), cerebras: () => Effect.succeed({ autoload: false, @@ -852,106 +458,6 @@ function custom(dep: CustomDep): Record { }, }, }), - "snowflake-cortex": Effect.fnUntraced(function* (input: Info) { - const env = yield* dep.env() - const auth = yield* dep.auth(input.id) - - const account = - env["SNOWFLAKE_ACCOUNT"] ?? - (auth?.type === "api" ? auth.metadata?.account : undefined) ?? - (auth?.type === "oauth" ? auth.accountId : undefined) ?? - input.options?.account - - const envToken = env["SNOWFLAKE_CORTEX_TOKEN"] ?? env["SNOWFLAKE_CORTEX_PAT"] - const apiKeyToken = auth?.type === "api" ? auth.key : undefined - const oauthToken = auth?.type === "oauth" ? auth.access : undefined - const configToken = input.options?.token ?? input.options?.apiKey - - const token = envToken ?? apiKeyToken ?? oauthToken ?? configToken - - if (!account || !token) { - const missing = [!account && "SNOWFLAKE_ACCOUNT", !token && "SNOWFLAKE_CORTEX_TOKEN"].filter(Boolean).join(", ") - return { - autoload: false, - async getModel() { - throw new Error( - `Snowflake Cortex: missing credentials (${missing}). Provide a bearer token (OAuth, JWT, or PAT) via env var, opencode auth, or provider options.`, - ) - }, - } - } - - const baseURL = `https://${account}.snowflakecomputing.com/api/v2/cortex/v1` - - const options: Record = { baseURL, apiKey: token } - - // Only skip provider-level fetch when the token is from OAuth with no override. - // For OAuth tokens, the plugin auth loader's combined fetch handles - // OAuth refresh + snowflake transformations in one place. - // For env/config/API-key tokens, the provider fetch applies snowflake - // transformations directly. - const useOAuthHandler = - oauthToken !== undefined && envToken === undefined && apiKeyToken === undefined && configToken === undefined - if (!useOAuthHandler) { - options.fetch = async (url: RequestInfo | URL, init?: RequestInit) => { - if (init?.body && typeof init.body === "string") { - try { - const body = JSON.parse(init.body) - if ("max_tokens" in body) { - body.max_completion_tokens = body.max_tokens - delete body.max_tokens - init = { ...init, body: JSON.stringify(body) } - } - } catch {} - } - - const response = await fetch(url, init) - - if (!response.ok && response.status === 400) { - try { - const errorData = await response.clone().json() - const errorMessage = String(errorData.message || errorData.error || "") - if (errorMessage.toLowerCase().includes("conversation complete")) { - return new Response( - JSON.stringify({ - choices: [{ finish_reason: "stop", message: { content: "", role: "assistant" } }], - }), - { status: 200, headers: new Headers({ "content-type": "application/json" }) }, - ) - } - } catch {} - } - - if (response.body && response.headers.get("content-type")?.includes("text/event-stream")) { - const reader = response.body.getReader() - const encoder = new TextEncoder() - const decoder = new TextDecoder() - const stream = new ReadableStream({ - async pull(ctrl) { - const { done, value } = await reader.read() - if (done) { - ctrl.close() - return - } - const text = decoder.decode(value, { stream: true }) - ctrl.enqueue(encoder.encode(text.replace(/"role"\s*:\s*""/g, '"role":"assistant"'))) - }, - cancel() { - reader.cancel() - }, - }) - return new Response(stream, { headers: response.headers, status: response.status }) - } - - return response - } - } - - return { - autoload: input.source === "config", - options, - } - }), } } diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 1b52c6f4f..ffcc22a04 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1496,7 +1496,7 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7 if (model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure") { schema = sanitizeOpenAISchema(schema) as JSONSchema7 - // Codex also applies lossy compaction above 4 KB; defer that until OpenCode needs the same schema budget. + // Codex also applies lossy compaction above 4 KB; defer that until Neuron needs the same schema budget. } if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) { diff --git a/packages/opencode/src/server/routes/instance/httpapi/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts index f5076ad08..8ff580f2a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/api.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/api.ts @@ -9,7 +9,6 @@ import { InstanceDisposed } from "@/server/event" import { Question } from "@/question" import { ConfigApi } from "./groups/config" import { ControlApi } from "./groups/control" -import { ControlPlaneApi } from "./groups/control-plane" import { EventApi } from "./groups/event" import { ExperimentalApi } from "./groups/experimental" import { FileApi } from "./groups/file" @@ -53,7 +52,6 @@ export const ServerApi = makeApi({ export const RootHttpApi = HttpApi.make("opencode-root") .addHttpApi(ControlApi) - .addHttpApi(ControlPlaneApi) .addHttpApi(GlobalApi) .middleware(SchemaErrorMiddleware) .middleware(Authorization) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts index 10bfe23d0..2e233a373 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts @@ -20,7 +20,7 @@ export const ConfigApi = HttpApi.make("config") OpenApi.annotations({ identifier: "config.get", summary: "Get configuration", - description: "Retrieve the current OpenCode configuration settings and preferences.", + description: "Retrieve the current Neuron configuration settings and preferences.", }), ), HttpApiEndpoint.patch("update", root, { @@ -32,7 +32,7 @@ export const ConfigApi = HttpApi.make("config") OpenApi.annotations({ identifier: "config.update", summary: "Update configuration", - description: "Update OpenCode configuration settings and preferences.", + description: "Update Neuron configuration settings and preferences.", }), ), HttpApiEndpoint.get("providers", `${root}/providers`, { diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/control-plane.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/control-plane.ts deleted file mode 100644 index 9c8ef188d..000000000 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/control-plane.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MoveSession } from "@opencode-ai/core/control-plane/move-session" -import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" -import { described } from "./metadata" - -const root = "/experimental/control-plane" -export const MoveSessionPayload = Schema.Struct({ ...MoveSession.Input.fields }) - -export class ApiMoveSessionError extends Schema.ErrorClass("MoveSessionError")( - { - name: Schema.Literal("MoveSessionError"), - data: Schema.Struct({ - message: Schema.String, - }), - }, - { httpApiStatus: 400 }, -) {} - -export const ControlPlaneApi = HttpApi.make("controlPlane").add( - HttpApiGroup.make("controlPlane") - .add( - HttpApiEndpoint.post("moveSession", `${root}/move-session`, { - payload: MoveSessionPayload, - success: described(HttpApiSchema.NoContent, "Session moved"), - error: ApiMoveSessionError, - }).annotateMerge( - OpenApi.annotations({ - identifier: "experimental.controlPlane.moveSession", - summary: "Move session", - description: "Move a session to another project directory, optionally transferring local changes.", - }), - ), - ) - .annotateMerge(OpenApi.annotations({ title: "controlPlane", description: "Control-plane orchestration routes." })), -) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts index 52c714a5a..b112aafe8 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts @@ -112,7 +112,7 @@ export const ExperimentalApi = HttpApi.make("experimental") OpenApi.annotations({ identifier: "experimental.capabilities.get", summary: "Get experimental capabilities", - description: "Get experimental features enabled on the OpenCode server.", + description: "Get experimental features enabled on the Neuron server.", }), ), HttpApiEndpoint.get("console", ExperimentalPaths.console, { @@ -146,7 +146,7 @@ export const ExperimentalApi = HttpApi.make("experimental") OpenApi.annotations({ identifier: "experimental.console.switchOrg", summary: "Switch active Console org", - description: "Persist a new active Console account/org selection for the current local OpenCode state.", + description: "Persist a new active Console account/org selection for the current local Neuron state.", }), ), HttpApiEndpoint.get("tool", ExperimentalPaths.tool, { @@ -229,7 +229,7 @@ export const ExperimentalApi = HttpApi.make("experimental") identifier: "experimental.session.list", summary: "List sessions", description: - "Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.", + "Get a list of all Neuron sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.", }), ), HttpApiEndpoint.post("sessionBackground", ExperimentalPaths.sessionBackground, { diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts index 61daefe8a..afdb32e81 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts @@ -79,7 +79,7 @@ export const GlobalApi = HttpApi.make("global").add( OpenApi.annotations({ identifier: "global.health", summary: "Get health", - description: "Get health information about the OpenCode server.", + description: "Get health information about the Neuron server.", }), ), HttpApiEndpoint.get("event", GlobalPaths.event, { @@ -88,7 +88,7 @@ export const GlobalApi = HttpApi.make("global").add( OpenApi.annotations({ identifier: "global.event", summary: "Get global events", - description: "Subscribe to global events from the OpenCode system using server-sent events.", + description: "Subscribe to global events from the Neuron system using server-sent events.", }), ), HttpApiEndpoint.get("configGet", GlobalPaths.config, { @@ -97,7 +97,7 @@ export const GlobalApi = HttpApi.make("global").add( OpenApi.annotations({ identifier: "global.config.get", summary: "Get global configuration", - description: "Retrieve the current global OpenCode configuration settings and preferences.", + description: "Retrieve the current global Neuron configuration settings and preferences.", }), ), HttpApiEndpoint.patch("configUpdate", GlobalPaths.config, { @@ -108,7 +108,7 @@ export const GlobalApi = HttpApi.make("global").add( OpenApi.annotations({ identifier: "global.config.update", summary: "Update global configuration", - description: "Update global OpenCode configuration settings and preferences.", + description: "Update global Neuron configuration settings and preferences.", }), ), HttpApiEndpoint.post("dispose", GlobalPaths.dispose, { @@ -117,7 +117,7 @@ export const GlobalApi = HttpApi.make("global").add( OpenApi.annotations({ identifier: "global.dispose", summary: "Dispose instance", - description: "Clean up and dispose all OpenCode instances, releasing all resources.", + description: "Clean up and dispose all Neuron instances, releasing all resources.", }), ), HttpApiEndpoint.post("upgrade", GlobalPaths.upgrade, { diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts index 6fd18c862..50e7646a2 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts @@ -66,7 +66,7 @@ export const InstanceApi = HttpApi.make("instance") OpenApi.annotations({ identifier: "instance.dispose", summary: "Dispose instance", - description: "Clean up and dispose the current OpenCode instance, releasing all resources.", + description: "Clean up and dispose the current Neuron instance, releasing all resources.", }), ), HttpApiEndpoint.get("path", InstancePaths.path, { @@ -77,7 +77,7 @@ export const InstanceApi = HttpApi.make("instance") identifier: "path.get", summary: "Get paths", description: - "Retrieve the current working directory and related path information for the OpenCode instance.", + "Retrieve the current working directory and related path information for the Neuron instance.", }), ), HttpApiEndpoint.get("vcs", InstancePaths.vcs, { @@ -143,7 +143,7 @@ export const InstanceApi = HttpApi.make("instance") OpenApi.annotations({ identifier: "command.list", summary: "List commands", - description: "Get a list of all available commands in the OpenCode system.", + description: "Get a list of all available commands in the Neuron system.", }), ), HttpApiEndpoint.get("agent", InstancePaths.agent, { @@ -153,7 +153,7 @@ export const InstanceApi = HttpApi.make("instance") OpenApi.annotations({ identifier: "app.agents", summary: "List agents", - description: "Get a list of all available AI agents in the OpenCode system.", + description: "Get a list of all available AI agents in the Neuron system.", }), ), HttpApiEndpoint.get("skill", InstancePaths.skill, { @@ -163,7 +163,7 @@ export const InstanceApi = HttpApi.make("instance") OpenApi.annotations({ identifier: "app.skills", summary: "List skills", - description: "Get a list of all available skills in the OpenCode system.", + description: "Get a list of all available skills in the Neuron system.", }), ), HttpApiEndpoint.get("lsp", InstancePaths.lsp, { diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts index 3ce34442d..16281e4a8 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts @@ -26,7 +26,7 @@ export const ProjectApi = HttpApi.make("project") OpenApi.annotations({ identifier: "project.list", summary: "List all projects", - description: "Get a list of projects that have been opened with OpenCode.", + description: "Get a list of projects that have been opened with Neuron.", }), ), HttpApiEndpoint.get("current", `${root}/current`, { @@ -36,7 +36,7 @@ export const ProjectApi = HttpApi.make("project") OpenApi.annotations({ identifier: "project.current", summary: "Get current project", - description: "Retrieve the currently active project that OpenCode is working with.", + description: "Retrieve the currently active project that Neuron is working with.", }), ), HttpApiEndpoint.post("initGit", `${root}/git/init`, { diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts index 9d49009a5..ce889b35c 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts @@ -58,7 +58,7 @@ export const PtyApi = HttpApi.make("pty") OpenApi.annotations({ identifier: "pty.list", summary: "List PTY sessions", - description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.", + description: "Get a list of all active pseudo-terminal (PTY) sessions managed by Neuron.", }), ), HttpApiEndpoint.post("create", PtyPaths.create, { diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts index 959a303dc..e0578e9f7 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts @@ -115,7 +115,7 @@ export const SessionApi = HttpApi.make("session") OpenApi.annotations({ identifier: "session.list", summary: "List sessions", - description: "Get a list of all OpenCode sessions, sorted by most recently updated.", + description: "Get a list of all Neuron sessions, sorted by most recently updated.", }), ), HttpApiEndpoint.get("status", SessionPaths.status, { @@ -138,7 +138,7 @@ export const SessionApi = HttpApi.make("session") OpenApi.annotations({ identifier: "session.get", summary: "Get session", - description: "Retrieve detailed information about a specific OpenCode session.", + description: "Retrieve detailed information about a specific Neuron session.", }), ), HttpApiEndpoint.get("children", SessionPaths.children, { @@ -209,7 +209,7 @@ export const SessionApi = HttpApi.make("session") OpenApi.annotations({ identifier: "session.create", summary: "Create session", - description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.", + description: "Create a new Neuron session for interacting with AI assistants and managing conversations.", }), ), HttpApiEndpoint.delete("remove", SessionPaths.remove, { diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/control-plane.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/control-plane.ts deleted file mode 100644 index 4c13afdb3..000000000 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/control-plane.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MoveSession } from "@opencode-ai/core/control-plane/move-session" -import { SessionV2 } from "@opencode-ai/core/session" -import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" -import { RootHttpApi } from "../api" -import { ApiMoveSessionError, MoveSessionPayload } from "../groups/control-plane" - -export const controlPlaneHandlers = HttpApiBuilder.group(RootHttpApi, "controlPlane", (handlers) => - Effect.gen(function* () { - const service = yield* MoveSession.Service - - const moveSession = Effect.fn("ControlPlaneHttpApi.moveSession")(function* (ctx: { - payload: typeof MoveSessionPayload.Type - }) { - yield* service.moveSession(ctx.payload).pipe( - Effect.mapError( - (error) => - new ApiMoveSessionError({ - name: "MoveSessionError", - data: { message: message(error) }, - }), - ), - ) - }) - - return handlers.handle("moveSession", moveSession) - }), -) - -function message(error: MoveSession.Error) { - if (error instanceof SessionV2.NotFoundError) return `Session not found: ${error.sessionID}` - if (error instanceof MoveSession.DestinationProjectMismatchError) - return "Destination directory belongs to another project" - if (error instanceof MoveSession.ApplyChangesError) - return `Unable to apply your changes in the destination directory. The files may conflict with existing changes.` - return error.message -} diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index fb9d2db65..6563e9021 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -49,7 +49,6 @@ import { ToolRegistry } from "@/tool/registry" import { Truncate } from "@/tool/truncate" import { Worktree } from "@/worktree" import { RuntimeFlags } from "@/effect/runtime-flags" -import { MoveSession } from "@opencode-ai/core/control-plane/move-session" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilderV1 } from "@/effect/app-node-builder-v1" import { LayerNode } from "@opencode-ai/core/effect/layer-node" @@ -84,7 +83,6 @@ import { PtyConnectApi } from "./groups/pty" import { eventHandlers } from "./handlers/event" import { configHandlers } from "./handlers/config" import { controlHandlers } from "./handlers/control" -import { controlPlaneHandlers } from "./handlers/control-plane" import { experimentalHandlers } from "./handlers/experimental" import { fileHandlers } from "./handlers/file" import { globalHandlers } from "./handlers/global" @@ -139,7 +137,7 @@ const ptyConnectHttpApiAuthLayer = ptyConnectAuthorizationLayer.pipe(Layer.provi const serverHttpApiAuthLayer = serverAuthorizationLayer.pipe(Layer.provide(ServerAuth.Config.layer)) const workspaceRoutingLive = workspaceRoutingLayer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal)) const rootApiRoutes = HttpApiBuilder.layer(RootHttpApi).pipe( - Layer.provide([controlHandlers, controlPlaneHandlers, globalHandlers]), + Layer.provide([controlHandlers, globalHandlers]), Layer.provide(schemaErrorLayer), Layer.provide(httpApiAuthLayer), ) @@ -288,7 +286,6 @@ export function createRoutes( corsVaryFix, fenceLayer, cors(corsOptions), - AppNodeBuilderV1.build(MoveSession.node, [[LocationServiceMap.node, locationServiceMapV2]]), HttpServer.layerServices, ]), Layer.provide(Layer.succeed(CorsConfig)(corsOptions)), diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 4bc02a9e9..c7f91f7e7 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -7,8 +7,8 @@ import { isRecord } from "@/util/record" export type Err = ReturnType -export const GO_UPSELL_MESSAGE = "Free usage exceeded, subscribe to Go" -export const GO_UPSELL_URL = "https://opencode.ai/go" +export const GO_UPSELL_MESSAGE = "Free usage exceeded on the managed provider" +export const GO_UPSELL_URL = "https://git.neuralplatform.ai/neuron-technologies/neuron" export type RetryReason = "free_tier_limit" | "account_rate_limit" | (string & {}) export type Retryable = { @@ -103,7 +103,7 @@ export function retryable(error: Err, provider: string) { reason: "free_tier_limit", provider, title: "Free limit reached", - message: "Subscribe to OpenCode Go for reliable access to the best open-source models, starting at $5/month.", + message: "Free limit reached on the managed provider. Configure your own provider API key for unlimited use.", label: "subscribe", link: GO_UPSELL_URL, }, diff --git a/packages/opencode/test/cli/acp/initialize-auth.test.ts b/packages/opencode/test/cli/acp/initialize-auth.test.ts index 709c27f3a..c8cb7e418 100644 --- a/packages/opencode/test/cli/acp/initialize-auth.test.ts +++ b/packages/opencode/test/cli/acp/initialize-auth.test.ts @@ -21,7 +21,7 @@ describe("opencode acp initialize/auth subprocess", () => { expect(initialized.agentCapabilities?.sessionCapabilities?.fork).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.list).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({}) - expect(initialized.agentInfo?.name).toBe("OpenCode") + expect(initialized.agentInfo?.name).toBe("Neuron") }), 60_000, ) diff --git a/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap b/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap index c4957702a..2675328af 100644 --- a/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap +++ b/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap @@ -1,7 +1,12 @@ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode acp --help 1`] = ` -"opencode acp +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron acp start ACP (Agent Client Protocol) server @@ -18,31 +23,43 @@ Options: --mdns-domain custom domain name for mDNS service (default: opencode.local) [string] [default: "opencode.local"] --cors additional domains to allow for CORS [array] [default: []] - --cwd working directory [string] [default: ""]" + --cwd working directory [string] [default: ""] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp --help 1`] = ` -"opencode mcp +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron mcp manage MCP (Model Context Protocol) servers Commands: - opencode mcp add [name] add an MCP server - opencode mcp list list MCP servers and their status [aliases: ls] - opencode mcp auth [name] authenticate with an OAuth-enabled MCP server - opencode mcp logout [name] remove OAuth credentials for an MCP server - opencode mcp debug debug OAuth connection for an MCP server + neuron mcp add [name] add an MCP server + neuron mcp list list MCP servers and their status [aliases: ls] + neuron mcp auth [name] authenticate with an OAuth-enabled MCP server + neuron mcp logout [name] remove OAuth credentials for an MCP server + neuron mcp debug debug OAuth connection for an MCP server Options: -h, --help show help [boolean] -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode attach --help 1`] = ` -"opencode attach +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron attach attach to a running opencode server @@ -64,11 +81,17 @@ Options: [string] --mini start the minimal interactive interface [boolean] [default: false] --no-replay disable mini session history replay on resume and after resize [boolean] - --replay-limit cap visible mini replay to the newest N messages [number]" + --replay-limit cap visible mini replay to the newest N messages [number] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode run --help 1`] = ` -"opencode run [message..] +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron run [message..] run opencode with a message @@ -104,74 +127,98 @@ Options: --thinking show thinking blocks [boolean] -i, --interactive run in direct interactive split-footer mode [boolean] [default: false] --auto auto-approve permissions that are not explicitly denied (dangerous!) - [boolean] [default: false]" + [boolean] [default: false] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode debug --help 1`] = ` -"opencode debug +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron debug debugging and troubleshooting tools Commands: - opencode debug config show resolved configuration - opencode debug lsp LSP debugging utilities - opencode debug rg ripgrep debugging utilities - opencode debug file file system debugging utilities - opencode debug scrap list all known projects - opencode debug skill list all available skills - opencode debug snapshot snapshot debugging utilities - opencode debug startup print startup timing - opencode debug agent show agent configuration details - opencode debug v2 debug v2 catalog and built-in plugins - opencode debug info show debug information - opencode debug paths show global paths (data, config, cache, state) - opencode debug wait wait indefinitely (for debugging) + neuron debug config show resolved configuration + neuron debug lsp LSP debugging utilities + neuron debug rg ripgrep debugging utilities + neuron debug file file system debugging utilities + neuron debug scrap list all known projects + neuron debug skill list all available skills + neuron debug snapshot snapshot debugging utilities + neuron debug startup print startup timing + neuron debug agent show agent configuration details + neuron debug v2 debug v2 catalog and built-in plugins + neuron debug info show debug information + neuron debug paths show global paths (data, config, cache, state) + neuron debug wait wait indefinitely (for debugging) Options: -h, --help show help [boolean] -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers --help 1`] = ` -"opencode providers +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron providers manage AI providers and credentials Commands: - opencode providers list list providers and credentials [aliases: ls] - opencode providers login [url] log in to a provider - opencode providers logout [provider] log out from a configured provider + neuron providers list list providers and credentials [aliases: ls] + neuron providers login [url] log in to a provider + neuron providers logout [provider] log out from a configured provider Options: -h, --help show help [boolean] -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent --help 1`] = ` -"opencode agent +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron agent manage agents Commands: - opencode agent create create a new agent - opencode agent list list all available agents + neuron agent create create a new agent + neuron agent list list all available agents Options: -h, --help show help [boolean] -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode upgrade --help 1`] = ` -"opencode upgrade [target] +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron upgrade [target] upgrade opencode to the latest or a specific version @@ -185,11 +232,17 @@ Options: --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] --pure run without external plugins [boolean] -m, --method installation method to use - [string] [choices: "curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"]" + [string] [choices: "curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode uninstall --help 1`] = ` -"opencode uninstall +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron uninstall uninstall opencode and remove all related files @@ -202,11 +255,17 @@ Options: -c, --keep-config keep configuration files [boolean] [default: false] -d, --keep-data keep session data and snapshots [boolean] [default: false] --dry-run show what would be removed without removing [boolean] [default: false] - -f, --force skip confirmation prompts [boolean] [default: false]" + -f, --force skip confirmation prompts [boolean] [default: false] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode serve --help 1`] = ` -"opencode serve +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron serve starts a headless opencode server @@ -222,11 +281,17 @@ Options: [boolean] [default: false] --mdns-domain custom domain name for mDNS service (default: opencode.local) [string] [default: "opencode.local"] - --cors additional domains to allow for CORS [array] [default: []]" + --cors additional domains to allow for CORS [array] [default: []] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode web --help 1`] = ` -"opencode web +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron web start opencode server and open web interface @@ -242,11 +307,17 @@ Options: [boolean] [default: false] --mdns-domain custom domain name for mDNS service (default: opencode.local) [string] [default: "opencode.local"] - --cors additional domains to allow for CORS [array] [default: []]" + --cors additional domains to allow for CORS [array] [default: []] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode models --help 1`] = ` -"opencode models [provider] +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron models [provider] list all available models @@ -260,11 +331,17 @@ Options: --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] --pure run without external plugins [boolean] --verbose use more verbose model output (includes metadata like costs) [boolean] - --refresh refresh the models cache from models.dev [boolean]" + --refresh refresh the models cache from models.dev [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode stats --help 1`] = ` -"opencode stats +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron stats show token usage and cost statistics @@ -278,11 +355,17 @@ Options: --tools number of tools to show (default: all) [number] --models show model statistics (default: hidden). Pass a number to show top N, otherwise shows all - --project filter by project (default: all projects, empty string: current project)[string]" + --project filter by project (default: all projects, empty string: current project)[string] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode export --help 1`] = ` -"opencode export [sessionID] +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron export [sessionID] export session data as JSON @@ -295,11 +378,17 @@ Options: --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] --pure run without external plugins [boolean] - --sanitize redact sensitive transcript and file data [boolean]" + --sanitize redact sensitive transcript and file data [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode import --help 1`] = ` -"opencode import +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron import import session data from JSON file or URL @@ -311,11 +400,17 @@ Options: -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode pr --help 1`] = ` -"opencode pr +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron pr fetch and checkout a GitHub PR branch, then run opencode @@ -327,28 +422,40 @@ Options: -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session --help 1`] = ` -"opencode session +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron session manage sessions Commands: - opencode session list list sessions - opencode session delete delete a session + neuron session list list sessions + neuron session delete delete a session Options: -h, --help show help [boolean] -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode plugin --help 1`] = ` -"opencode plugin +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron plugin install plugin and update config @@ -362,17 +469,23 @@ Options: --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] --pure run without external plugins [boolean] -g, --global install in global config [boolean] [default: false] - -f, --force replace existing plugin version [boolean] [default: false]" + -f, --force replace existing plugin version [boolean] [default: false] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode db --help 1`] = ` -"opencode db +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron db database tools Commands: - opencode db [query] open an interactive sqlite3 shell or run a query [default] - opencode db path print the database path + neuron db [query] open an interactive sqlite3 shell or run a query [default] + neuron db path print the database path Positionals: query SQL query to execute [string] @@ -383,11 +496,17 @@ Options: --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] --pure run without external plugins [boolean] - --format Output format [string] [choices: "json", "tsv"] [default: "tsv"]" + --format Output format [string] [choices: "json", "tsv"] [default: "tsv"] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp list --help 1`] = ` -"opencode mcp list +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron mcp list list MCP servers and their status @@ -396,11 +515,17 @@ Options: -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp add --help 1`] = ` -"opencode mcp add [name] +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron mcp add [name] add an MCP server @@ -415,16 +540,22 @@ Options: --pure run without external plugins [boolean] --url URL for a remote MCP server [string] --env environment variable for a local MCP server (KEY=VALUE) [array] - --header HTTP header for a remote MCP server (KEY=VALUE) [array]" + --header HTTP header for a remote MCP server (KEY=VALUE) [array] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp auth --help 1`] = ` -"opencode mcp auth [name] +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron mcp auth [name] authenticate with an OAuth-enabled MCP server Commands: - opencode mcp auth list list OAuth-capable MCP servers and their auth status [aliases: ls] + neuron mcp auth list list OAuth-capable MCP servers and their auth status [aliases: ls] Positionals: name name of the MCP server [string] @@ -434,11 +565,17 @@ Options: -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp logout --help 1`] = ` -"opencode mcp logout [name] +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron mcp logout [name] remove OAuth credentials for an MCP server @@ -450,11 +587,17 @@ Options: -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers list --help 1`] = ` -"opencode providers list +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron providers list list providers and credentials @@ -463,11 +606,17 @@ Options: -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers login --help 1`] = ` -"opencode providers login [url] +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron providers login [url] log in to a provider @@ -481,11 +630,17 @@ Options: --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] --pure run without external plugins [boolean] -p, --provider provider id or name to log in to (skips provider selection) [string] - -m, --method login method label (skips method selection) [string]" + -m, --method login method label (skips method selection) [string] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers logout --help 1`] = ` -"opencode providers logout [provider] +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron providers logout [provider] log out from a configured provider @@ -497,11 +652,17 @@ Options: -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent create --help 1`] = ` -"opencode agent create +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron agent create create a new agent @@ -517,11 +678,17 @@ Options: --permissions, --tools comma-separated list of permissions to allow (default: all). Available: "bash, read, edit, glob, grep, webfetch, task, todowrite, websearch, lsp, skill" [string] - -m, --model model to use in the format of provider/model [string]" + -m, --model model to use in the format of provider/model [string] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent list --help 1`] = ` -"opencode agent list +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron agent list list all available agents @@ -530,11 +697,17 @@ Options: -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session list --help 1`] = ` -"opencode session list +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron session list list sessions @@ -545,11 +718,17 @@ Options: --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] --pure run without external plugins [boolean] -n, --max-count limit to N most recent sessions [number] - --format output format [string] [choices: "table", "json"] [default: "table"]" + --format output format [string] [choices: "table", "json"] [default: "table"] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session delete --help 1`] = ` -"opencode session delete +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron session delete delete a session @@ -561,11 +740,17 @@ Options: -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode db path --help 1`] = ` -"opencode db path +"⠀ ▄ +█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█ +█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀ +▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ + +neuron db path print the database path @@ -574,5 +759,6 @@ Options: -v, --version show version number [boolean] --print-logs print logs to stderr [boolean] --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean]" + --pure run without external plugins [boolean] +" `; diff --git a/packages/opencode/test/cli/run/permission.shared.test.ts b/packages/opencode/test/cli/run/permission.shared.test.ts index 03be4220f..4d63d07f6 100644 --- a/packages/opencode/test/cli/run/permission.shared.test.ts +++ b/packages/opencode/test/cli/run/permission.shared.test.ts @@ -132,11 +132,11 @@ describe("run permission shared", () => { test("formats always-allow copy for wildcard and explicit patterns", () => { expect(permissionAlwaysLines(req({ permission: "bash", always: ["*"] }))).toEqual([ - "This will allow bash until OpenCode is restarted.", + "This will allow bash until Neuron is restarted.", ]) expect(permissionAlwaysLines(req({ always: ["src/**/*.ts", "src/**/*.tsx"] }))).toEqual([ - "This will allow the following patterns until OpenCode is restarted.", + "This will allow the following patterns until Neuron is restarted.", "- src/**/*.ts", "- src/**/*.tsx", ]) diff --git a/packages/opencode/test/installation/installation.test.ts b/packages/opencode/test/installation/installation.test.ts index 3d4b4cba3..24f08cf70 100644 --- a/packages/opencode/test/installation/installation.test.ts +++ b/packages/opencode/test/installation/installation.test.ts @@ -164,22 +164,7 @@ describe("installation", () => { const brewInfoJson = JSON.stringify({ formulae: [{ versions: { stable: "2.1.0" } }], }) - testEffect( - testLayer( - () => jsonResponse({}), // HTTP not used for tap formula - (cmd, args) => { - if (cmd === "brew" && args.includes("anomalyco/tap/opencode") && args.includes("--formula")) return "opencode" - if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson - return "" - }, - ), - ).effect("reads brew tap info JSON via CLI", () => - Effect.gen(function* () { - const result = yield* Installation.use.latest("brew") - expect(result).toBe("2.1.0") - }), - ) - }) + }) describe("upgrade", () => { testEffect( diff --git a/packages/opencode/test/plugin/cloudflare.test.ts b/packages/opencode/test/plugin/cloudflare.test.ts deleted file mode 100644 index ab3d27df4..000000000 --- a/packages/opencode/test/plugin/cloudflare.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { expect, test } from "bun:test" -import { CloudflareAIGatewayAuthPlugin } from "@/plugin/cloudflare" - -const pluginInput = { - client: {} as never, - project: {} as never, - directory: "", - worktree: "", - experimental_workspace: { - register() {}, - }, - serverUrl: new URL("https://example.com"), - $: {} as never, -} - -test("registers the cloudflare-ai-gateway auth method", async () => { - const hooks = await CloudflareAIGatewayAuthPlugin(pluginInput) - expect(hooks.auth?.provider).toBe("cloudflare-ai-gateway") - expect(hooks.auth?.methods).toHaveLength(1) -}) - -test("no longer drops maxOutputTokens; OpenAI models ride the Responses API passthrough", async () => { - const hooks = await CloudflareAIGatewayAuthPlugin(pluginInput) - expect(hooks["chat.params"]).toBeUndefined() -}) diff --git a/packages/opencode/test/plugin/modal-models.test.ts b/packages/opencode/test/plugin/modal-models.test.ts deleted file mode 100644 index 1b37b3174..000000000 --- a/packages/opencode/test/plugin/modal-models.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { expect, test } from "bun:test" -import type { Model, Provider } from "@opencode-ai/sdk/v2" -import { ModalPlugin } from "@/plugin/modal/modal" - -const BASE_MODEL_ID = "thinkingmachines/Inkling-NVFP4" -const RUNTIME_MODEL_ID = "workspace--inkling.us-west.modal.direct" -const FALLBACK_RUNTIME_MODEL_ID = "workspace--inkling-fallback.us-west.modal.direct" - -function makeProvider(baseURL: string): Provider { - const template: Model = { - id: BASE_MODEL_ID, - providerID: "modal", - name: "Inkling", - family: "ling", - api: { - id: BASE_MODEL_ID, - url: baseURL, - npm: "@ai-sdk/openai-compatible", - }, - status: "active", - headers: {}, - options: {}, - cost: { - input: 1, - output: 4, - cache: { - read: 0.2, - write: 0, - }, - }, - limit: { - context: 128_000, - output: 8_192, - }, - capabilities: { - temperature: true, - reasoning: true, - attachment: true, - toolcall: true, - input: { - text: true, - audio: true, - image: true, - video: false, - pdf: false, - }, - output: { - text: true, - audio: false, - image: false, - video: false, - pdf: false, - }, - interleaved: { - field: "reasoning_content", - }, - }, - release_date: "2026-07-15", - variants: { - fallback: { - reasoningEffort: "fallback", - }, - }, - } - - return { - id: "modal", - name: "Modal", - source: "api", - env: ["MODAL_PROXY_TOKEN"], - options: {}, - models: { - [template.id]: template, - }, - } -} - -test("discovers Modal workspace models", async () => { - const requests: Array<{ authorization: string | null; path: string }> = [] - using server = Bun.serve({ - port: 0, - fetch(request) { - requests.push({ - authorization: request.headers.get("authorization"), - path: new URL(request.url).pathname, - }) - return Response.json({ - data: [ - { - id: RUNTIME_MODEL_ID, - base_model_id: BASE_MODEL_ID, - name: "Thinking Machines: Inkling", - input_modalities: ["text", "image", "audio"], - output_modalities: ["text"], - context_length: 1_048_576, - max_output_length: 262_144, - pricing: { - prompt: "0.0000012", - completion: "0.000005", - input_cache_read: "0.00000027", - }, - supported_sampling_parameters: ["temperature"], - supported_features: ["tools", "reasoning"], - reasoning_options: [ - { - type: "effort", - values: ["none", "low", "medium", "high", "xhigh", "max"], - }, - ], - interleaved: { - field: "reasoning_content", - }, - }, - { - id: FALLBACK_RUNTIME_MODEL_ID, - base_model_id: BASE_MODEL_ID, - }, - ], - }) - }, - }) - const provider = makeProvider(`${server.url}v1`) - - const plugin = await ModalPlugin() - const models = await plugin.provider!.models!(provider, { - auth: { - type: "api", - key: "test-token", - }, - }) - const model = models[RUNTIME_MODEL_ID] - - expect(requests).toEqual([ - { - authorization: "Bearer test-token", - path: "/v1/models", - }, - ]) - expect(Object.keys(models)).toEqual([RUNTIME_MODEL_ID, FALLBACK_RUNTIME_MODEL_ID]) - expect(model.api).toEqual({ - id: RUNTIME_MODEL_ID, - url: `${server.url}v1`, - npm: "@ai-sdk/openai-compatible", - }) - expect(model.family).toBe("ling") - expect(model.capabilities.interleaved).toEqual({ field: "reasoning_content" }) - expect(model.capabilities.input).toEqual({ - text: true, - audio: true, - image: true, - video: false, - pdf: false, - }) - expect(model.cost).toEqual({ - input: 1.2, - output: 5, - cache: { - read: 0.27, - write: 0, - }, - }) - expect(model.limit).toEqual({ - context: 1_048_576, - output: 262_144, - }) - expect(model.variants).toEqual({ - none: { reasoningEffort: "none" }, - low: { reasoningEffort: "low" }, - medium: { reasoningEffort: "medium" }, - high: { reasoningEffort: "high" }, - xhigh: { reasoningEffort: "xhigh" }, - max: { reasoningEffort: "max" }, - }) - expect(models[FALLBACK_RUNTIME_MODEL_ID].variants).toEqual({ - fallback: { - reasoningEffort: "fallback", - }, - }) -}) - -test("hides Modal models when discovery fails", async () => { - using server = Bun.serve({ - port: 0, - fetch() { - return new Response(null, { status: 503 }) - }, - }) - - const plugin = await ModalPlugin() - const models = await plugin.provider!.models!(makeProvider(`${server.url}v1`), { - auth: { - type: "api", - key: "test-token", - }, - }) - - expect(models).toEqual({}) -}) diff --git a/packages/opencode/test/plugin/snowflake-cortex.test.ts b/packages/opencode/test/plugin/snowflake-cortex.test.ts deleted file mode 100644 index de5ac28c3..000000000 --- a/packages/opencode/test/plugin/snowflake-cortex.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { OAUTH_DUMMY_KEY } from "../../src/auth" -import { oauthScope, SnowflakeCortexAuthPlugin } from "../../src/plugin/snowflake-cortex" - -function makeInput() { - let auth: any = { - type: "oauth", - access: "access-old", - refresh: "refresh-old", - expires: Date.now() + 3600_000, - accountId: "myorg-myaccount", - } - const setCalls: Array> = [] - - return { - getAuth: async () => auth, - setAuth: (next: any) => { - auth = next - }, - input: { - client: { - auth: { - set: async (request: any) => { - setCalls.push(request) - auth = request.body - }, - }, - }, - } as any, - setCalls, - } -} - -describe("plugin.snowflake-cortex", () => { - test("oauthScope uses Snowflake-compatible scope values", () => { - expect(oauthScope(undefined)).toBe("refresh_token") - expect(oauthScope("PUBLIC")).toBe("refresh_token session:role:PUBLIC") - expect(oauthScope("AUTH SNOWFLAKE")).toBe("refresh_token session:role-encoded:AUTH%20SNOWFLAKE") - }) - - test("loader returns empty options when auth is not oauth", async () => { - const hooks = await SnowflakeCortexAuthPlugin({} as any) - const options = await hooks.auth!.loader!(async () => ({ type: "api", key: "token" }) as any, {} as any) - expect(options).toEqual({}) - }) - - test("loader injects bearer header and preserves custom headers", async () => { - const { input, getAuth, setAuth } = makeInput() - setAuth({ - type: "oauth", - access: "access-live", - refresh: "refresh-live", - expires: Date.now() + 60 * 60 * 1000, - accountId: "myorg-myaccount", - }) - const hooks = await SnowflakeCortexAuthPlugin(input) - const options = await hooks.auth!.loader!(getAuth as any, {} as any) - expect(options.apiKey).toBe(OAUTH_DUMMY_KEY) - - const originalFetch = globalThis.fetch - const captured: Headers[] = [] - globalThis.fetch = (async (_request, init) => { - captured.push(new Headers(init?.headers)) - return new Response("{}", { status: 200, headers: { "content-type": "application/json" } }) - }) as typeof fetch - - try { - await options.fetch("https://example.test/v1/chat", { - headers: { Authorization: `Bearer ${OAUTH_DUMMY_KEY}`, "x-keep": "yes" }, - }) - } finally { - globalThis.fetch = originalFetch - } - - expect(captured).toHaveLength(1) - expect(captured[0].get("authorization")).toBe("Bearer access-live") - expect(captured[0].get("x-keep")).toBe("yes") - expect(captured[0].get("user-agent")).toMatch(/^opencode\//) - }) - - test("loader refreshes expired token with single-flight and persists refreshed oauth", async () => { - const { input, getAuth, setCalls } = makeInput() - let refreshCalls = 0 - const apiAuthHeaders: string[] = [] - - // Must mock fetch before calling loader because startup refresh triggers for expires: 0 - const originalFetch = globalThis.fetch - globalThis.fetch = (async (request, init) => { - const url = - typeof request === "string" ? request : request instanceof URL ? request.toString() : String(request.url) - - if (url.includes("/oauth/token-request")) { - refreshCalls += 1 - const body = new URLSearchParams(String(init?.body ?? "")) - expect(body.get("grant_type")).toBe("refresh_token") - expect(body.get("refresh_token")).toBe("refresh-old") - expect(new Headers(init?.headers).get("authorization")).toMatch(/^Basic /) - await new Promise((resolve) => setTimeout(resolve, 20)) - return Response.json({ access_token: "access-new", refresh_token: "refresh-new", expires_in: 3600 }) - } - - apiAuthHeaders.push(new Headers(init?.headers).get("authorization") || "") - return new Response("{}", { status: 200, headers: { "content-type": "application/json" } }) - }) as typeof fetch - - try { - const hooks = await SnowflakeCortexAuthPlugin(input) - const options = await hooks.auth!.loader!( - async () => - ({ - type: "oauth", - access: "access-expired", - refresh: "refresh-old", - expires: 0, - accountId: "myorg-myaccount", - }) as any, - {} as any, - ) - - await Promise.all([ - options.fetch("https://example.test/v1/chat", { headers: {} }), - options.fetch("https://example.test/v1/chat", { headers: {} }), - ]) - } finally { - globalThis.fetch = originalFetch - } - - expect(refreshCalls).toBe(1) - expect(apiAuthHeaders).toEqual(["Bearer access-new", "Bearer access-new"]) - expect(setCalls).toHaveLength(1) - expect((setCalls[0] as any).body).toMatchObject({ - type: "oauth", - access: "access-new", - refresh: "refresh-new", - accountId: "myorg-myaccount", - }) - }) - - test("loader retries once after 401 by refreshing token", async () => { - const { input, getAuth, setCalls } = makeInput() - const hooks = await SnowflakeCortexAuthPlugin(input) - const options = await hooks.auth!.loader!( - async () => - ({ - type: "oauth", - access: "access-stale", - refresh: "refresh-old", - expires: Date.now() + 60 * 60 * 1000, - accountId: "myorg-myaccount", - }) as any, - {} as any, - ) - - let apiCalls = 0 - const seenAuth: string[] = [] - const originalFetch = globalThis.fetch - globalThis.fetch = (async (request, init) => { - const url = - typeof request === "string" ? request : request instanceof URL ? request.toString() : String(request.url) - - if (url.includes("/oauth/token-request")) { - return Response.json({ access_token: "access-fresh", refresh_token: "refresh-fresh", expires_in: 3600 }) - } - - apiCalls += 1 - seenAuth.push(new Headers(init?.headers).get("authorization") || "") - if (apiCalls === 1) return new Response("unauthorized", { status: 401 }) - return new Response("{}", { status: 200, headers: { "content-type": "application/json" } }) - }) as typeof fetch - - try { - const response = await options.fetch("https://example.test/v1/chat", { headers: {} }) - expect(response.status).toBe(200) - } finally { - globalThis.fetch = originalFetch - } - - expect(apiCalls).toBe(2) - expect(seenAuth).toEqual(["Bearer access-stale", "Bearer access-fresh"]) - expect(setCalls).toHaveLength(1) - expect((setCalls[0] as any).body).toMatchObject({ - type: "oauth", - access: "access-fresh", - refresh: "refresh-fresh", - accountId: "myorg-myaccount", - }) - }) - - test("loader converts max_tokens to max_completion_tokens in request body", async () => { - const { input, getAuth } = makeInput() - const hooks = await SnowflakeCortexAuthPlugin(input) - const options = await hooks.auth!.loader!(getAuth as any, {} as any) - - let sentBody: string | undefined - const originalFetch = globalThis.fetch - globalThis.fetch = (async (request, init) => { - sentBody = typeof init?.body === "string" ? init.body : undefined - return new Response("{}", { status: 200, headers: { "content-type": "application/json" } }) - }) as typeof fetch - - try { - await options.fetch("https://example.test/v1/chat", { - method: "POST", - body: JSON.stringify({ model: "claude-sonnet-4-5", max_tokens: 4096, messages: [] }), - }) - } finally { - globalThis.fetch = originalFetch - } - - expect(sentBody).toBeDefined() - const parsed = JSON.parse(sentBody!) - expect(parsed.max_completion_tokens).toBe(4096) - expect(parsed.max_tokens).toBeUndefined() - expect(parsed.model).toBe("claude-sonnet-4-5") - }) - - test("loader maps 400 'conversation complete' to 200 stop", async () => { - const { input, getAuth } = makeInput() - const hooks = await SnowflakeCortexAuthPlugin(input) - const options = await hooks.auth!.loader!(getAuth as any, {} as any) - - const originalFetch = globalThis.fetch - globalThis.fetch = (async () => { - return new Response(JSON.stringify({ message: "Conversation complete" }), { - status: 400, - headers: { "content-type": "application/json" }, - }) - }) as unknown as typeof fetch - - try { - const response = await options.fetch("https://example.test/v1/chat", { - method: "POST", - body: JSON.stringify({ model: "test", messages: [] }), - }) - expect(response.status).toBe(200) - const body = await response.json() - expect(body.choices[0].finish_reason).toBe("stop") - } finally { - globalThis.fetch = originalFetch - } - }) - - test("loader fixes empty role in SSE stream", async () => { - const { input, getAuth } = makeInput() - const hooks = await SnowflakeCortexAuthPlugin(input) - const options = await hooks.auth!.loader!(getAuth as any, {} as any) - - const originalFetch = globalThis.fetch - const sseChunk = `data: {"choices":[{"delta":{"role":"","content":"hello"}}]}\n\n` - globalThis.fetch = (async () => { - const stream = new ReadableStream({ - start(ctrl) { - ctrl.enqueue(new TextEncoder().encode(sseChunk)) - ctrl.close() - }, - }) - return new Response(stream, { - status: 200, - headers: { "content-type": "text/event-stream" }, - }) - }) as unknown as typeof fetch - - try { - const response = await options.fetch("https://example.test/v1/chat", { - method: "POST", - body: JSON.stringify({ model: "test", messages: [], stream: true }), - }) - expect(response.status).toBe(200) - const reader = response.body!.getReader() - const { value } = await reader.read() - const text = new TextDecoder().decode(value) - expect(text).not.toContain('"role":""') - expect(text).toContain('"role":"assistant"') - } finally { - globalThis.fetch = originalFetch - } - }) -}) diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts deleted file mode 100644 index 6e677631c..000000000 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test" -import { LayerNode } from "@opencode-ai/core/effect/layer-node" -import { Effect } from "effect" -import path from "path" -import { unlink } from "fs/promises" -import { Global } from "@opencode-ai/core/global" -import { Filesystem } from "@/util/filesystem" -import { Env } from "../../src/env" -import { Provider } from "@/provider/provider" - -import { disposeAllInstances } from "../fixture/fixture" -import { testEffect } from "../lib/effect" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { ModelV2 } from "@opencode-ai/core/model" - -const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node]))) - -const originalEnv = new Map() - -const set = (k: string, v: string) => - Effect.gen(function* () { - if (!originalEnv.has(k)) originalEnv.set(k, process.env[k]) - process.env[k] = v - yield* Env.use.set(k, v) - }) - -afterEach(async () => { - for (const [key, value] of originalEnv) { - if (value === undefined) delete process.env[key] - else process.env[key] = value - } - originalEnv.clear() - await disposeAllInstances() -}) - -const list = Provider.use.list() - -const mantleModelConfig = { - provider: { npm: "@ai-sdk/amazon-bedrock/mantle" }, - limit: { context: 272_000, output: 32_000 }, - modalities: { - input: ["text", "image", "pdf"] as Array<"text" | "image" | "pdf">, - output: ["text"] as Array<"text">, - }, -} - -const withAuthJson = (contents: string) => - Effect.acquireRelease( - Effect.promise(async () => { - const authPath = path.join(Global.Path.data, "auth.json") - let original: string | undefined - try { - original = await Filesystem.readText(authPath) - } catch { - original = undefined - } - await Filesystem.write(authPath, contents) - return { authPath, original } - }), - ({ authPath, original }) => - Effect.promise(async () => { - if (original !== undefined) { - await Filesystem.write(authPath, original) - return - } - await unlink(authPath).catch(() => undefined) - }), - ) - -it.instance( - "Bedrock: config region takes precedence over AWS_REGION env var", - () => - Effect.gen(function* () { - yield* set("AWS_REGION", "us-east-1") - yield* set("AWS_PROFILE", "default") - const providers = yield* list - expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() - expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1") - }), - { config: { provider: { "amazon-bedrock": { options: { region: "eu-west-1" } } } } }, -) - -it.instance("Bedrock: falls back to AWS_REGION env var when no config region", () => - Effect.gen(function* () { - yield* set("AWS_REGION", "eu-west-1") - yield* set("AWS_PROFILE", "default") - const providers = yield* list - expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() - expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1") - }), -) - -it.instance( - "Bedrock: loads when bearer token from auth.json is present", - () => - Effect.gen(function* () { - yield* withAuthJson(JSON.stringify({ "amazon-bedrock": { type: "api", key: "test-bearer-token" } })) - yield* set("AWS_PROFILE", "") - yield* set("AWS_ACCESS_KEY_ID", "") - yield* set("AWS_BEARER_TOKEN_BEDROCK", "") - const providers = yield* list - expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() - expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1") - }), - { config: { provider: { "amazon-bedrock": { options: { region: "eu-west-1" } } } } }, -) - -it.instance( - "Bedrock Mantle: GPT-5.5 uses Responses API and OpenAI base path", - () => - Effect.gen(function* () { - yield* set("AWS_REGION", "") - yield* set("AWS_PROFILE", "") - yield* set("AWS_ACCESS_KEY_ID", "") - yield* set("AWS_BEARER_TOKEN_BEDROCK", "") - const model = yield* Provider.use.getModel(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")) - const language = yield* Provider.use.getLanguage(model) - expect((language as { provider: string }).provider).toBe("bedrock-mantle.responses") - expect((language as { modelId: string }).modelId).toBe("openai.gpt-5.5") - expect( - (language as unknown as { config: { url: (input: { path: string; modelId: string }) => string } }).config.url({ - path: "/responses", - modelId: "openai.gpt-5.5", - }), - ).toBe("https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses") - }), - { - config: { - provider: { - "amazon-bedrock": { - options: { region: "us-east-2", apiKey: "test-bearer-token" }, - models: { - "openai.gpt-5.5": { - ...mantleModelConfig, - provider: { - npm: "@ai-sdk/amazon-bedrock/mantle", - api: "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1", - }, - }, - }, - }, - }, - }, - }, -) - -it.instance( - "Bedrock Mantle: GPT OSS safeguard uses Chat Completions and Mantle base path", - () => - Effect.gen(function* () { - yield* set("AWS_BEARER_TOKEN_BEDROCK", "test-bearer-token") - const model = yield* Provider.use.getModel( - ProviderV2.ID.amazonBedrock, - ModelV2.ID.make("openai.gpt-oss-safeguard-120b"), - ) - const language = yield* Provider.use.getLanguage(model) - expect((language as { provider: string }).provider).toBe("bedrock-mantle.chat") - expect((language as { modelId: string }).modelId).toBe("openai.gpt-oss-safeguard-120b") - expect( - (language as unknown as { config: { url: (input: { path: string; modelId: string }) => string } }).config.url({ - path: "/chat/completions", - modelId: "openai.gpt-oss-safeguard-120b", - }), - ).toBe("https://bedrock-mantle.us-east-1.api.aws/v1/chat/completions") - }), - { - config: { - provider: { - "amazon-bedrock": { - options: { region: "us-east-1" }, - models: { "openai.gpt-oss-safeguard-120b": mantleModelConfig }, - }, - }, - }, - }, -) - -it.instance( - "Bedrock: config profile takes precedence over AWS_PROFILE env var", - () => - Effect.gen(function* () { - yield* set("AWS_PROFILE", "default") - yield* set("AWS_ACCESS_KEY_ID", "test-key-id") - const providers = yield* list - expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() - expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("us-east-1") - }), - { - config: { - provider: { "amazon-bedrock": { options: { profile: "my-custom-profile", region: "us-east-1" } } }, - }, - }, -) - -it.instance( - "Bedrock: includes custom endpoint in options when specified", - () => - Effect.gen(function* () { - yield* set("AWS_PROFILE", "default") - const providers = yield* list - expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() - expect(providers[ProviderV2.ID.amazonBedrock].options?.endpoint).toBe( - "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com", - ) - }), - { - config: { - provider: { - "amazon-bedrock": { - options: { endpoint: "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com" }, - }, - }, - }, - }, -) - -it.instance( - "Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", - () => - Effect.gen(function* () { - yield* set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token") - yield* set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role") - yield* set("AWS_PROFILE", "") - yield* set("AWS_ACCESS_KEY_ID", "") - const providers = yield* list - expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() - expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("us-east-1") - }), - { config: { provider: { "amazon-bedrock": { options: { region: "us-east-1" } } } } }, -) - -// Cross-region inference profile prefix handling. -// Models from models.dev may come with prefixes already (e.g. us., eu., global.). -// These should NOT be double-prefixed when passed to the SDK. - -it.instance( - "Bedrock: model with us. prefix should not be double-prefixed", - () => - Effect.gen(function* () { - yield* set("AWS_PROFILE", "default") - const providers = yield* list - expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() - expect(providers[ProviderV2.ID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() - }), - { - config: { - provider: { - "amazon-bedrock": { - options: { region: "us-east-1" }, - models: { "us.anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5 (US)" } }, - }, - }, - }, - }, -) - -it.instance( - "Bedrock: model with global. prefix should not be prefixed", - () => - Effect.gen(function* () { - yield* set("AWS_PROFILE", "default") - const providers = yield* list - expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() - expect( - providers[ProviderV2.ID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"], - ).toBeDefined() - }), - { - config: { - provider: { - "amazon-bedrock": { - options: { region: "us-east-1" }, - models: { "global.anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5 (Global)" } }, - }, - }, - }, - }, -) - -it.instance( - "Bedrock: model with eu. prefix should not be double-prefixed", - () => - Effect.gen(function* () { - yield* set("AWS_PROFILE", "default") - const providers = yield* list - expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() - expect(providers[ProviderV2.ID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() - }), - { - config: { - provider: { - "amazon-bedrock": { - options: { region: "eu-west-1" }, - models: { "eu.anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5 (EU)" } }, - }, - }, - }, - }, -) - -it.instance( - "Bedrock: model without prefix in US region should get us. prefix added", - () => - Effect.gen(function* () { - yield* set("AWS_PROFILE", "default") - const providers = yield* list - expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined() - expect(providers[ProviderV2.ID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() - }), - { - config: { - provider: { - "amazon-bedrock": { - options: { region: "us-east-1" }, - models: { "anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5" } }, - }, - }, - }, - }, -) - -// Direct unit tests for cross-region inference profile prefix detection. -describe("Bedrock cross-region prefix detection", () => { - const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."] - - test("should detect global. prefix", () => { - expect(crossRegionPrefixes.some((p) => "global.anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(true) - }) - - test("should detect us. prefix", () => { - expect(crossRegionPrefixes.some((p) => "us.anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(true) - }) - - test("should detect eu. prefix", () => { - expect(crossRegionPrefixes.some((p) => "eu.anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(true) - }) - - test("should detect jp. prefix", () => { - expect(crossRegionPrefixes.some((p) => "jp.anthropic.claude-sonnet-4-20250514-v1:0".startsWith(p))).toBe(true) - }) - - test("should detect apac. prefix", () => { - expect(crossRegionPrefixes.some((p) => "apac.anthropic.claude-sonnet-4-20250514-v1:0".startsWith(p))).toBe(true) - }) - - test("should detect au. prefix", () => { - expect(crossRegionPrefixes.some((p) => "au.anthropic.claude-sonnet-4-5-20250929-v1:0".startsWith(p))).toBe(true) - }) - - test("should NOT detect prefix for non-prefixed model", () => { - expect(crossRegionPrefixes.some((p) => "anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(false) - }) - - test("should NOT detect prefix for amazon nova models", () => { - expect(crossRegionPrefixes.some((p) => "amazon.nova-pro-v1:0".startsWith(p))).toBe(false) - }) - - test("should NOT detect prefix for cohere models", () => { - expect(crossRegionPrefixes.some((p) => "cohere.command-r-plus-v1:0".startsWith(p))).toBe(false) - }) -}) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 32d4e3a39..d4031c0ea 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -713,14 +713,6 @@ it.instance("getSmallModel returns appropriate small model", () => }), ) -it.instance("getSmallModel prefers Gemini for Google Vertex", () => - Effect.gen(function* () { - yield* set("GOOGLE_VERTEX_PROJECT", "test-project") - const model = yield* Provider.use.getSmallModel(ProviderV2.ID.googleVertex) - expect(model).toBeDefined() - expect(model?.id).toContain("gemini") - }), -) it.instance( "getSmallModel selects the latest model in the preferred family", @@ -1282,7 +1274,7 @@ it.instance( expect(providers[ProviderV2.ID.make("nvidia")].options.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", - "X-BILLING-INVOKE-ORIGIN": "OpenCode", + "X-BILLING-INVOKE-ORIGIN": "Neuron", }) }), { config: { provider: { nvidia: { options: { apiKey: "test-api-key" } } } } }, @@ -1295,7 +1287,7 @@ it.instance( expect(providers[ProviderV2.ID.make("nvidia")].options.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", - "X-BILLING-INVOKE-ORIGIN": "OpenCode", + "X-BILLING-INVOKE-ORIGIN": "Neuron", }) }), { config: { provider: { nvidia: { options: { apiKey: "test-api-key", baseURL: "http://localhost:8000/v1" } } } } }, @@ -1804,136 +1796,12 @@ it.instance( }, ) -it.instance( - "Google Vertex: retains baseURL for custom proxy", - Effect.gen(function* () { - yield* set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds") - const providers = yield* list - expect(providers[ProviderV2.ID.make("vertex-proxy")]).toBeDefined() - expect(providers[ProviderV2.ID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1") - }), - { - config: { - provider: { - "vertex-proxy": { - name: "Vertex Proxy", - npm: "@ai-sdk/google-vertex", - api: "https://my-proxy.com/v1", - env: ["GOOGLE_APPLICATION_CREDENTIALS"], - models: { "gemini-pro": { name: "Gemini Pro", tool_call: true } }, - options: { - project: "test-project", - location: "us-central1", - baseURL: "https://my-proxy.com/v1", - }, - }, - }, - }, - }, -) -it.instance( - "Google Vertex: supports OpenAI compatible models", - Effect.gen(function* () { - yield* set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds") - const providers = yield* list - const model = providers[ProviderV2.ID.make("vertex-openai")].models["gpt-4"] - expect(model).toBeDefined() - expect(model.api.npm).toBe("@ai-sdk/openai-compatible") - }), - { - config: { - provider: { - "vertex-openai": { - name: "Vertex OpenAI", - npm: "@ai-sdk/google-vertex", - env: ["GOOGLE_APPLICATION_CREDENTIALS"], - models: { - "gpt-4": { - name: "GPT-4", - provider: { npm: "@ai-sdk/openai-compatible", api: "https://api.openai.com/v1" }, - }, - }, - options: { project: "test-project", location: "us-central1" }, - }, - }, - }, - }, -) -it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regions", () => - Effect.gen(function* () { - yield* set("GOOGLE_CLOUD_PROJECT", "test-project") - yield* set("VERTEX_LOCATION", "eu") - const provider = yield* Provider.Service - const model = yield* provider.getModel( - ProviderV2.ID.make("google-vertex"), - ModelV2.ID.make("claude-sonnet-4-6@default"), - ) - const language = yield* provider.getLanguage(model) - expect(languageBaseURL(language)).toBe( - "https://aiplatform.eu.rep.googleapis.com/v1/projects/test-project/locations/eu/publishers/anthropic/models", - ) - }), -) -it.instance("Google Vertex Anthropic: uses REP endpoint for continental multi-regions", () => - Effect.gen(function* () { - yield* set("GOOGLE_CLOUD_PROJECT", "test-project") - yield* set("VERTEX_LOCATION", "us") - const provider = yield* Provider.Service - const model = yield* provider.getModel( - ProviderV2.ID.make("google-vertex-anthropic"), - ModelV2.ID.make("claude-sonnet-4-6@default"), - ) - const language = yield* provider.getLanguage(model) - expect(languageBaseURL(language)).toBe( - "https://aiplatform.us.rep.googleapis.com/v1/projects/test-project/locations/us/publishers/anthropic/models", - ) - }), -) -it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () => - Effect.gen(function* () { - yield* set("GOOGLE_CLOUD_PROJECT", "test-project") - yield* set("VERTEX_LOCATION", "europe-west1") - const provider = yield* Provider.Service - const model = yield* provider.getModel( - ProviderV2.ID.make("google-vertex"), - ModelV2.ID.make("claude-sonnet-4-6@default"), - ) - const language = yield* provider.getLanguage(model) - expect(languageBaseURL(language)).toBe( - "https://europe-west1-aiplatform.googleapis.com/v1/projects/test-project/locations/europe-west1/publishers/anthropic/models", - ) - }), -) -it.instance("Google Vertex: uses REP endpoint for Gemini continental multi-regions", () => - Effect.gen(function* () { - yield* set("GOOGLE_CLOUD_PROJECT", "test-project") - yield* set("VERTEX_LOCATION", "eu") - const provider = yield* Provider.Service - const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini-3.5-flash")) - const language = yield* provider.getLanguage(model) - expect(languageBaseURL(language)).toBe( - "https://aiplatform.eu.rep.googleapis.com/v1beta1/projects/test-project/locations/eu/publishers/google", - ) - }), -) -it.instance("Google Vertex: keeps regional Gemini endpoints unchanged", () => - Effect.gen(function* () { - yield* set("GOOGLE_CLOUD_PROJECT", "test-project") - yield* set("VERTEX_LOCATION", "europe-west1") - const provider = yield* Provider.Service - const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini-3.5-flash")) - const language = yield* provider.getLanguage(model) - expect(languageBaseURL(language)).toBe( - "https://europe-west1-aiplatform.googleapis.com/v1beta1/projects/test-project/locations/europe-west1/publishers/google", - ) - }), -) it.instance("cloudflare-ai-gateway loads with env variables", () => Effect.gen(function* () { diff --git a/packages/opencode/test/server/httpapi-control-plane.test.ts b/packages/opencode/test/server/httpapi-control-plane.test.ts deleted file mode 100644 index b837bf755..000000000 --- a/packages/opencode/test/server/httpapi-control-plane.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { NodeHttpServer } from "@effect/platform-node" -import { describe, expect } from "bun:test" -import { Context, Effect, Layer, Option, Ref } from "effect" -import { HttpBody, HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http" -import { HttpApiBuilder } from "effect/unstable/httpapi" -import { MoveSession } from "@opencode-ai/core/control-plane/move-session" -import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { Auth } from "../../src/auth" -import { Config } from "../../src/config/config" -import { Installation } from "../../src/installation" -import { ServerAuth } from "../../src/server/auth" -import { RootHttpApi } from "../../src/server/routes/instance/httpapi/api" -import { controlHandlers } from "../../src/server/routes/instance/httpapi/handlers/control" -import { controlPlaneHandlers } from "../../src/server/routes/instance/httpapi/handlers/control-plane" -import { globalHandlers } from "../../src/server/routes/instance/httpapi/handlers/global" -import { authorizationLayer } from "../../src/server/routes/instance/httpapi/middleware/authorization" -import { schemaErrorLayer } from "../../src/server/routes/instance/httpapi/middleware/schema-error" -import { testEffect } from "../lib/effect" - -const input = MoveSession.Input.make({ - sessionID: SessionV2.ID.make("ses_move"), - destination: { directory: AbsolutePath.make("/destination") }, - moveChanges: true, -}) -const called = Ref.makeUnsafe(undefined) - -const apiLayer = HttpRouter.serve( - HttpApiBuilder.layer(RootHttpApi).pipe( - Layer.provide([controlHandlers, controlPlaneHandlers, globalHandlers]), - Layer.provide([authorizationLayer, schemaErrorLayer]), - // Raw HttpApi routes expose an opaque handler context at the request boundary. - // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion - HttpRouter.provideRequest(Layer.succeedContext(Context.empty() as Context.Context)), - ), - { disableListenLog: true, disableLogger: true }, -).pipe( - Layer.provideMerge(NodeHttpServer.layerTest), - Layer.provide(Layer.mock(Auth.Service)({})), - Layer.provide(Layer.mock(Config.Service)({})), - Layer.provide(Layer.mock(Installation.Service)({})), - Layer.provide( - Layer.mock(MoveSession.Service)({ - moveSession: (value) => Ref.set(called, value), - }), - ), - Layer.provide(ServerAuth.Config.configLayer({ password: Option.none(), username: "opencode" })), -) -const it = testEffect(apiLayer) - -describe("control-plane HttpApi", () => { - it.live("moves a session through the root control-plane route", () => - Effect.gen(function* () { - const response = yield* HttpClientRequest.post("/experimental/control-plane/move-session").pipe( - HttpClientRequest.setBody(HttpBody.jsonUnsafe(input)), - HttpClient.execute, - ) - - expect(response.status).toBe(204) - expect(yield* Ref.get(called)).toEqual(input) - }), - ) -}) diff --git a/packages/opencode/test/server/httpapi-global.test.ts b/packages/opencode/test/server/httpapi-global.test.ts deleted file mode 100644 index bcbe7aecb..000000000 --- a/packages/opencode/test/server/httpapi-global.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NodeHttpServer } from "@effect/platform-node" -import { describe, expect } from "bun:test" -import { Context, Effect, Layer, Option } from "effect" -import { HttpBody, HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http" -import { HttpApiBuilder } from "effect/unstable/httpapi" -import { Auth } from "../../src/auth" -import { Config } from "../../src/config/config" -import { Installation } from "../../src/installation" -import { MoveSession } from "@opencode-ai/core/control-plane/move-session" -import { ServerAuth } from "../../src/server/auth" -import { RootHttpApi } from "../../src/server/routes/instance/httpapi/api" -import { GlobalPaths } from "../../src/server/routes/instance/httpapi/groups/global" -import { controlHandlers } from "../../src/server/routes/instance/httpapi/handlers/control" -import { controlPlaneHandlers } from "../../src/server/routes/instance/httpapi/handlers/control-plane" -import { globalHandlers } from "../../src/server/routes/instance/httpapi/handlers/global" -import { authorizationLayer } from "../../src/server/routes/instance/httpapi/middleware/authorization" -import { schemaErrorLayer } from "../../src/server/routes/instance/httpapi/middleware/schema-error" -import { testEffect } from "../lib/effect" - -const apiLayer = HttpRouter.serve( - HttpApiBuilder.layer(RootHttpApi).pipe( - Layer.provide([controlHandlers, controlPlaneHandlers, globalHandlers]), - Layer.provide([authorizationLayer, schemaErrorLayer]), - // Raw HttpApi routes expose an opaque handler context at the request boundary. - // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion - HttpRouter.provideRequest(Layer.succeedContext(Context.empty() as Context.Context)), - ), - { disableListenLog: true, disableLogger: true }, -).pipe( - Layer.provideMerge(NodeHttpServer.layerTest), - Layer.provide(Layer.mock(Auth.Service)({})), - Layer.provide(Layer.mock(Config.Service)({})), - Layer.provide(Layer.mock(MoveSession.Service)({})), - Layer.provide( - Layer.mock(Installation.Service)({ - method: () => Effect.succeed("npm"), - latest: () => Effect.succeed("9.9.9"), - upgrade: () => Effect.void, - }), - ), - Layer.provide(ServerAuth.Config.configLayer({ password: Option.none(), username: "opencode" })), -) -const it = testEffect(apiLayer) - -describe("global HttpApi", () => { - it.live("upgrades to latest when the request body is omitted", () => - Effect.gen(function* () { - const response = yield* HttpClient.post(GlobalPaths.upgrade) - - expect(response.status).toBe(200) - expect(yield* response.json).toEqual({ success: true, version: "9.9.9" }) - }), - ) - - it.live("rejects malformed upgrade payloads", () => - Effect.gen(function* () { - const response = yield* HttpClientRequest.post(GlobalPaths.upgrade).pipe( - HttpClientRequest.setBody(HttpBody.text("{", "application/json")), - HttpClient.execute, - ) - - expect(response.status).toBe(400) - expect(yield* response.json).toEqual({ success: false, error: "Invalid request body" }) - }), - ) -}) diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 10032b811..41c197978 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -335,7 +335,7 @@ describe("session.retry.retryable", () => { expect(retryable).toEqual({ message: "Response decompression failed" }) }) - test("maps free limits to Go upsell action", () => { + test("maps free limits to managed-provider guidance", () => { const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)( new SessionV1.APIError({ message: "Free usage exceeded", @@ -354,7 +354,7 @@ describe("session.retry.retryable", () => { reason: "free_tier_limit", provider: "opencode", title: "Free limit reached", - message: "Subscribe to OpenCode Go for reliable access to the best open-source models, starting at $5/month.", + message: "Free limit reached on the managed provider. Configure your own provider API key for unlimited use.", label: "subscribe", link: SessionRetry.GO_UPSELL_URL, },