feat: prune cloud providers and console API, complete Neuron branding sweep

This commit is contained in:
2026-08-21 14:59:43 -05:00
parent f37714c9fc
commit e30d80389c
43 changed files with 324 additions and 2700 deletions
-2
View File
@@ -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",
+4 -4
View File
@@ -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 {
@@ -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}`),
]
}
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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<string | null> {
+1 -1
View File
@@ -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",
@@ -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.
@@ -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<string, string | undefined>) {
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
+1 -1
View File
@@ -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"],
-26
View File
@@ -1,26 +0,0 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
export async function AzureAuthPlugin(_input: PluginInput): Promise<Hooks> {
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,
},
],
},
}
}
@@ -1,65 +0,0 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
export async function CloudflareWorkersAuthPlugin(_input: PluginInput): Promise<Hooks> {
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<Hooks> {
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,
},
],
},
}
}
+1 -1
View File
@@ -283,7 +283,7 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise<Hooks>
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 {
-9
View File
@@ -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,
]
@@ -1,17 +0,0 @@
import type { Hooks } from "@opencode-ai/plugin"
import { ModalModels } from "./models"
export async function ModalPlugin(): Promise<Hooks> {
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(() => ({}))
},
},
}
}
@@ -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<string, Model>) {
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"
@@ -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<typeof createServer> | 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<PkceCodes> {
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<void>((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<TokenResponse> {
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<Hooks> {
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<string, string | undefined>)
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"),
},
],
},
}
}
+1 -495
View File
@@ -111,14 +111,9 @@ type BundledSDK = {
}
const BUNDLED_PROVIDERS: Record<string, () => 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<string, CustomLoader> {
},
}
}),
"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<string, any>) {
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<string, any> = {
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<string, any>) {
return { AWS_REGION: options.region ?? defaultRegion }
},
async getModel(sdk: any, modelID: string, options?: Record<string, any>, 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<string, CustomLoader> {
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<string, CustomLoader> {
},
},
}),
"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<string, any>) {
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<string, CustomLoader> {
},
}
}),
"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=<your-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}=<value>`).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<string, any>) {
// 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<string, CustomLoader> {
},
},
}),
"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<string, any> = { 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,
}
}),
}
}
+1 -1
View File
@@ -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")) {
@@ -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)
@@ -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`, {
@@ -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<ApiMoveSessionError>("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." })),
)
@@ -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, {
@@ -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, {
@@ -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, {
@@ -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`, {
@@ -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, {
@@ -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, {
@@ -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
}
@@ -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)),
+3 -3
View File
@@ -7,8 +7,8 @@ import { isRecord } from "@/util/record"
export type Err = ReturnType<NamedError["toObject"]>
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,
},
@@ -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,
)
@@ -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: "<HOME>"]"
--cwd working directory [string] [default: "<HOME>"]
"
`;
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 <name> 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 <name> 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 <url>
"
█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀
neuron attach <url>
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 <name> 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 <name> 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 <file>
"
█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀
neuron import <file>
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 <number>
"
█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀
neuron pr <number>
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 <sessionID> delete a session
neuron session list list sessions
neuron session delete <sessionID> 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 <module>
"
█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀
neuron plugin <module>
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 <sessionID>
"
█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█ █ █ █ █▀▀▀ █ █ █ █ █ █ █ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀
neuron session delete <sessionID>
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]
"
`;
@@ -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",
])
@@ -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(
@@ -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()
})
@@ -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({})
})
@@ -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<Record<string, unknown>> = []
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
}
})
})
@@ -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<string, string | undefined>()
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)
})
})
@@ -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* () {
@@ -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<MoveSession.Input | undefined>(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<unknown>)),
),
{ 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)
}),
)
})
@@ -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<unknown>)),
),
{ 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" })
}),
)
})
+2 -2
View File
@@ -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,
},