Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f697b7960b | ||
|
|
a2c66fe968 |
@@ -36,6 +36,8 @@ jobs:
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: ./.github/actions/setup-bun
|
||||
- id: version
|
||||
run: |
|
||||
@@ -56,6 +58,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tags: true
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
@@ -100,6 +103,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-tags: true
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@v2
|
||||
@@ -188,14 +192,12 @@ jobs:
|
||||
needs:
|
||||
- version
|
||||
- build-cli
|
||||
# - build-tauri
|
||||
- build-tauri
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install OpenCode
|
||||
if: inputs.bump || inputs.version
|
||||
@@ -225,6 +227,8 @@ jobs:
|
||||
git config --global user.name "opencode"
|
||||
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: opencode-cli
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-gUWzUsk81miIrjg0fZQmsIQG4pZYmEHgzN6BaXI+lfc=",
|
||||
"aarch64-linux": "sha256-gwEG75ha/ojTO2iAObTmLTtEkXIXJ7BThzfI5CqlJh8=",
|
||||
"aarch64-darwin": "sha256-20RGG2GkUItCzD67gDdoSLfexttM8abS//FKO9bfjoM=",
|
||||
"x86_64-darwin": "sha256-i2VawFuR1UbjPVYoybU6aJDJfFo0tcvtl1aM31Y2mTQ="
|
||||
"x86_64-linux": "sha256-yAtZlh6YR78RwPt0LK/7Pk0qUm0/97+s6ghhZzuoE/0=",
|
||||
"aarch64-linux": "sha256-6j81rdjQ7Wps9bvfw+mmdwW5p01qUOwX40UZltCTe3Y=",
|
||||
"aarch64-darwin": "sha256-pDM8M/QMWR6Go5pz3XXsJqcJDHAlHrx2Faijjkzcngo=",
|
||||
"x86_64-darwin": "sha256-eOAPtMd1n5xYupBOevCLhY1eFy3wzGqFk/EsZocl9Y8="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -130,7 +130,7 @@ export function SessionHeader() {
|
||||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] max-w-full min-w-0 p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
aria-label={language.t("session.header.searchFiles")}
|
||||
>
|
||||
|
||||
@@ -132,14 +132,12 @@ export function Titlebar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<header class="h-10 shrink-0 bg-background-base flex items-center relative" data-tauri-drag-region>
|
||||
<div
|
||||
classList={{
|
||||
"flex items-center min-w-0": true,
|
||||
"flex items-center w-full min-w-0": true,
|
||||
"pl-2": !mac(),
|
||||
"pr-6": !windows(),
|
||||
}}
|
||||
onMouseDown={drag}
|
||||
data-tauri-drag-region
|
||||
@@ -220,29 +218,20 @@ export function Titlebar() {
|
||||
</div>
|
||||
</div>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="min-w-0 flex items-center justify-center pointer-events-none lg:absolute lg:inset-0 lg:flex lg:items-center lg:justify-center"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div id="opencode-titlebar-center" class="pointer-events-auto w-full min-w-0 flex justify-center lg:w-fit" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
classList={{
|
||||
"flex items-center min-w-0 justify-end": true,
|
||||
"pr-6": !windows(),
|
||||
}}
|
||||
onMouseDown={drag}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0 justify-end" data-tauri-drag-region />
|
||||
<div class="flex-1 h-full" data-tauri-drag-region />
|
||||
<div
|
||||
id="opencode-titlebar-right"
|
||||
class="flex items-center gap-3 shrink-0 flex-1 justify-end"
|
||||
data-tauri-drag-region
|
||||
/>
|
||||
<Show when={windows()}>
|
||||
<div class="w-6 shrink-0" />
|
||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||
</Show>
|
||||
</div>
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div id="opencode-titlebar-center" class="pointer-events-auto" />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.1.43"
|
||||
version = "0.0.0-ci-202601291718"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.43/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v0.0.0-ci-202601291718/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -49,7 +49,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.13.0",
|
||||
"@agentclientprotocol/sdk": "0.12.0",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.73",
|
||||
"@ai-sdk/anthropic": "2.0.57",
|
||||
"@ai-sdk/azure": "2.0.91",
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
type ToolCallContent,
|
||||
type ToolKind,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
|
||||
import { Log } from "../util/log"
|
||||
import { ACPSessionManager } from "./session"
|
||||
import type { ACPConfig } from "./types"
|
||||
@@ -41,11 +40,6 @@ import { LoadAPIKeyError } from "ai"
|
||||
import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
|
||||
import { applyPatch } from "diff"
|
||||
|
||||
type ModeOption = { id: string; name: string; description?: string }
|
||||
type ModelOption = { modelId: string; name: string }
|
||||
|
||||
const DEFAULT_VARIANT_VALUE = "default"
|
||||
|
||||
export namespace ACP {
|
||||
const log = Log.create({ service: "acp-agent" })
|
||||
|
||||
@@ -482,7 +476,7 @@ export namespace ACP {
|
||||
sessionId,
|
||||
models: load.models,
|
||||
modes: load.modes,
|
||||
_meta: load._meta,
|
||||
_meta: {},
|
||||
}
|
||||
} catch (e) {
|
||||
const error = MessageV2.fromError(e, {
|
||||
@@ -535,7 +529,7 @@ export namespace ACP {
|
||||
providerID: lastUser.model.providerID,
|
||||
modelID: lastUser.model.modelID,
|
||||
})
|
||||
if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) {
|
||||
if (result.modes.availableModes.some((m) => m.id === lastUser.agent)) {
|
||||
result.modes.currentModeId = lastUser.agent
|
||||
this.sessionManager.setMode(sessionId, lastUser.agent)
|
||||
}
|
||||
@@ -962,7 +956,27 @@ export namespace ACP {
|
||||
}
|
||||
}
|
||||
|
||||
private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
|
||||
private async loadSessionMode(params: LoadSessionRequest) {
|
||||
const directory = params.cwd
|
||||
const model = await defaultModel(this.config, directory)
|
||||
const sessionId = params.sessionId
|
||||
|
||||
const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
|
||||
const entries = providers.sort((a, b) => {
|
||||
const nameA = a.name.toLowerCase()
|
||||
const nameB = b.name.toLowerCase()
|
||||
if (nameA < nameB) return -1
|
||||
if (nameA > nameB) return 1
|
||||
return 0
|
||||
})
|
||||
const availableModels = entries.flatMap((provider) => {
|
||||
const models = Provider.sort(Object.values(provider.models))
|
||||
return models.map((model) => ({
|
||||
modelId: `${provider.id}/${model.id}`,
|
||||
name: `${provider.name}/${model.name}`,
|
||||
}))
|
||||
})
|
||||
|
||||
const agents = await this.config.sdk.app
|
||||
.agents(
|
||||
{
|
||||
@@ -972,56 +986,6 @@ export namespace ACP {
|
||||
)
|
||||
.then((resp) => resp.data!)
|
||||
|
||||
return agents
|
||||
.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
|
||||
.map((agent) => ({
|
||||
id: agent.name,
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
}))
|
||||
}
|
||||
|
||||
private async resolveModeState(
|
||||
directory: string,
|
||||
sessionId: string,
|
||||
): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> {
|
||||
const availableModes = await this.loadAvailableModes(directory)
|
||||
const currentModeId =
|
||||
this.sessionManager.get(sessionId).modeId ||
|
||||
(await (async () => {
|
||||
if (!availableModes.length) return undefined
|
||||
const defaultAgentName = await AgentModule.defaultAgent()
|
||||
const resolvedModeId =
|
||||
availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
|
||||
this.sessionManager.setMode(sessionId, resolvedModeId)
|
||||
return resolvedModeId
|
||||
})())
|
||||
|
||||
return { availableModes, currentModeId }
|
||||
}
|
||||
|
||||
private async loadSessionMode(params: LoadSessionRequest) {
|
||||
const directory = params.cwd
|
||||
const model = await defaultModel(this.config, directory)
|
||||
const sessionId = params.sessionId
|
||||
|
||||
const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
|
||||
const entries = sortProvidersByName(providers)
|
||||
const availableVariants = modelVariantsFromProviders(entries, model)
|
||||
const currentVariant = this.sessionManager.getVariant(sessionId)
|
||||
if (currentVariant && !availableVariants.includes(currentVariant)) {
|
||||
this.sessionManager.setVariant(sessionId, undefined)
|
||||
}
|
||||
const availableModels = buildAvailableModels(entries, { includeVariants: true })
|
||||
const modeState = await this.resolveModeState(directory, sessionId)
|
||||
const currentModeId = modeState.currentModeId
|
||||
const modes = currentModeId
|
||||
? {
|
||||
availableModes: modeState.availableModes,
|
||||
currentModeId,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const commands = await this.config.sdk.command
|
||||
.list(
|
||||
{
|
||||
@@ -1042,6 +1006,20 @@ export namespace ACP {
|
||||
description: "compact the session",
|
||||
})
|
||||
|
||||
const availableModes = agents
|
||||
.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
|
||||
.map((agent) => ({
|
||||
id: agent.name,
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
}))
|
||||
|
||||
const defaultAgentName = await AgentModule.defaultAgent()
|
||||
const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id
|
||||
|
||||
// Persist the default mode so prompt() uses it immediately
|
||||
this.sessionManager.setMode(sessionId, currentModeId)
|
||||
|
||||
const mcpServers: Record<string, Config.Mcp> = {}
|
||||
for (const server of params.mcpServers) {
|
||||
if ("type" in server) {
|
||||
@@ -1095,46 +1073,40 @@ export namespace ACP {
|
||||
return {
|
||||
sessionId,
|
||||
models: {
|
||||
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
|
||||
currentModelId: `${model.providerID}/${model.modelID}`,
|
||||
availableModels,
|
||||
},
|
||||
modes,
|
||||
_meta: buildVariantMeta({
|
||||
model,
|
||||
variant: this.sessionManager.getVariant(sessionId),
|
||||
availableVariants,
|
||||
}),
|
||||
modes: {
|
||||
availableModes,
|
||||
currentModeId,
|
||||
},
|
||||
_meta: {},
|
||||
}
|
||||
}
|
||||
|
||||
async unstable_setSessionModel(params: SetSessionModelRequest) {
|
||||
const session = this.sessionManager.get(params.sessionId)
|
||||
const providers = await this.sdk.config
|
||||
.providers({ directory: session.cwd }, { throwOnError: true })
|
||||
.then((x) => x.data!.providers)
|
||||
|
||||
const selection = parseModelSelection(params.modelId, providers)
|
||||
this.sessionManager.setModel(session.id, selection.model)
|
||||
this.sessionManager.setVariant(session.id, selection.variant)
|
||||
const model = Provider.parseModel(params.modelId)
|
||||
|
||||
const entries = sortProvidersByName(providers)
|
||||
const availableVariants = modelVariantsFromProviders(entries, selection.model)
|
||||
this.sessionManager.setModel(session.id, {
|
||||
providerID: model.providerID,
|
||||
modelID: model.modelID,
|
||||
})
|
||||
|
||||
return {
|
||||
_meta: buildVariantMeta({
|
||||
model: selection.model,
|
||||
variant: selection.variant,
|
||||
availableVariants,
|
||||
}),
|
||||
_meta: {},
|
||||
}
|
||||
}
|
||||
|
||||
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
|
||||
const session = this.sessionManager.get(params.sessionId)
|
||||
const availableModes = await this.loadAvailableModes(session.cwd)
|
||||
if (!availableModes.some((mode) => mode.id === params.modeId)) {
|
||||
throw new Error(`Agent not found: ${params.modeId}`)
|
||||
}
|
||||
this.sessionManager.get(params.sessionId)
|
||||
await this.config.sdk.app
|
||||
.agents({}, { throwOnError: true })
|
||||
.then((x) => x.data)
|
||||
.then((agent) => {
|
||||
if (!agent) throw new Error(`Agent not found: ${params.modeId}`)
|
||||
})
|
||||
this.sessionManager.setMode(params.sessionId, params.modeId)
|
||||
}
|
||||
|
||||
@@ -1251,7 +1223,6 @@ export namespace ACP {
|
||||
providerID: model.providerID,
|
||||
modelID: model.modelID,
|
||||
},
|
||||
variant: this.sessionManager.getVariant(sessionID),
|
||||
parts,
|
||||
agent,
|
||||
directory,
|
||||
@@ -1463,105 +1434,4 @@ export namespace ACP {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] {
|
||||
return [...providers].sort((a, b) => {
|
||||
const nameA = a.name.toLowerCase()
|
||||
const nameB = b.name.toLowerCase()
|
||||
if (nameA < nameB) return -1
|
||||
if (nameA > nameB) return 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
function modelVariantsFromProviders(
|
||||
providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
|
||||
model: { providerID: string; modelID: string },
|
||||
): string[] {
|
||||
const provider = providers.find((entry) => entry.id === model.providerID)
|
||||
if (!provider) return []
|
||||
const modelInfo = provider.models[model.modelID]
|
||||
if (!modelInfo?.variants) return []
|
||||
return Object.keys(modelInfo.variants)
|
||||
}
|
||||
|
||||
function buildAvailableModels(
|
||||
providers: Array<{ id: string; name: string; models: Record<string, any> }>,
|
||||
options: { includeVariants?: boolean } = {},
|
||||
): ModelOption[] {
|
||||
const includeVariants = options.includeVariants ?? false
|
||||
return providers.flatMap((provider) => {
|
||||
const models = Provider.sort(Object.values(provider.models) as any)
|
||||
return models.flatMap((model) => {
|
||||
const base: ModelOption = {
|
||||
modelId: `${provider.id}/${model.id}`,
|
||||
name: `${provider.name}/${model.name}`,
|
||||
}
|
||||
if (!includeVariants || !model.variants) return [base]
|
||||
const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE)
|
||||
const variantOptions = variants.map((variant) => ({
|
||||
modelId: `${provider.id}/${model.id}/${variant}`,
|
||||
name: `${provider.name}/${model.name} (${variant})`,
|
||||
}))
|
||||
return [base, ...variantOptions]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function formatModelIdWithVariant(
|
||||
model: { providerID: string; modelID: string },
|
||||
variant: string | undefined,
|
||||
availableVariants: string[],
|
||||
includeVariant: boolean,
|
||||
) {
|
||||
const base = `${model.providerID}/${model.modelID}`
|
||||
if (!includeVariant || !variant || !availableVariants.includes(variant)) return base
|
||||
return `${base}/${variant}`
|
||||
}
|
||||
|
||||
function buildVariantMeta(input: {
|
||||
model: { providerID: string; modelID: string }
|
||||
variant?: string
|
||||
availableVariants: string[]
|
||||
}) {
|
||||
return {
|
||||
opencode: {
|
||||
modelId: `${input.model.providerID}/${input.model.modelID}`,
|
||||
variant: input.variant ?? null,
|
||||
availableVariants: input.availableVariants,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function parseModelSelection(
|
||||
modelId: string,
|
||||
providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
|
||||
): { model: { providerID: string; modelID: string }; variant?: string } {
|
||||
const parsed = Provider.parseModel(modelId)
|
||||
const provider = providers.find((p) => p.id === parsed.providerID)
|
||||
if (!provider) {
|
||||
return { model: parsed, variant: undefined }
|
||||
}
|
||||
|
||||
// Check if modelID exists directly
|
||||
if (provider.models[parsed.modelID]) {
|
||||
return { model: parsed, variant: undefined }
|
||||
}
|
||||
|
||||
// Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high")
|
||||
const segments = parsed.modelID.split("/")
|
||||
if (segments.length > 1) {
|
||||
const candidateVariant = segments[segments.length - 1]
|
||||
const baseModelId = segments.slice(0, -1).join("/")
|
||||
const baseModelInfo = provider.models[baseModelId]
|
||||
if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) {
|
||||
return {
|
||||
model: { providerID: parsed.providerID, modelID: baseModelId },
|
||||
variant: candidateVariant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { model: parsed, variant: undefined }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,18 +96,6 @@ export class ACPSessionManager {
|
||||
return session
|
||||
}
|
||||
|
||||
getVariant(sessionId: string) {
|
||||
const session = this.get(sessionId)
|
||||
return session.variant
|
||||
}
|
||||
|
||||
setVariant(sessionId: string, variant?: string) {
|
||||
const session = this.get(sessionId)
|
||||
session.variant = variant
|
||||
this.sessions.set(sessionId, session)
|
||||
return session
|
||||
}
|
||||
|
||||
setMode(sessionId: string, modeId: string) {
|
||||
const session = this.get(sessionId)
|
||||
session.modeId = modeId
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface ACPSessionState {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
variant?: string
|
||||
modeId?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
if (inFavorites) return false
|
||||
const inRecents = recents.some(
|
||||
const inRecents = recentList.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
if (inRecents) return false
|
||||
|
||||
@@ -32,21 +32,6 @@ import { Event } from "../server/event"
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
// Managed settings directory for enterprise deployments (highest priority, admin-controlled)
|
||||
// These settings override all user and project settings
|
||||
function getManagedConfigDir(): string {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
return "/Library/Application Support/opencode"
|
||||
case "win32":
|
||||
return path.join(process.env.ProgramData || "C:\\ProgramData", "opencode")
|
||||
default:
|
||||
return "/etc/opencode"
|
||||
}
|
||||
}
|
||||
|
||||
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || getManagedConfigDir()
|
||||
|
||||
// Custom merge function that concatenates array fields instead of replacing them
|
||||
function mergeConfigConcatArrays(target: Info, source: Info): Info {
|
||||
const merged = mergeDeep(target, source)
|
||||
@@ -163,18 +148,8 @@ export namespace Config {
|
||||
result.plugin.push(...(await loadPlugin(dir)))
|
||||
}
|
||||
|
||||
// Load managed config files last (highest priority) - enterprise admin-controlled
|
||||
// Kept separate from directories array to avoid write operations when installing plugins
|
||||
// which would fail on system directories requiring elevated permissions
|
||||
// This way it only loads config file and not skills/plugins/commands
|
||||
if (existsSync(managedConfigDir)) {
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
result = mergeConfigConcatArrays(result, await loadFile(path.join(managedConfigDir, file)))
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate deprecated mode field to agent field
|
||||
for (const [name, mode] of Object.entries(result.mode ?? {})) {
|
||||
for (const [name, mode] of Object.entries(result.mode)) {
|
||||
result.agent = mergeDeep(result.agent ?? {}, {
|
||||
[name]: {
|
||||
...mode,
|
||||
|
||||
@@ -135,7 +135,7 @@ export namespace MCP {
|
||||
return client.callTool(
|
||||
{
|
||||
name: mcpTool.name,
|
||||
arguments: (args || {}) as Record<string, unknown>,
|
||||
arguments: args as Record<string, unknown>,
|
||||
},
|
||||
CallToolResultSchema,
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect, describe, mock, afterEach } from "bun:test"
|
||||
import { test, expect, describe, mock } from "bun:test"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Auth } from "../../src/auth"
|
||||
@@ -6,23 +6,6 @@ import { tmpdir } from "../fixture/fixture"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Global } from "../../src/global"
|
||||
|
||||
// Get managed config directory from environment (set in preload.ts)
|
||||
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {})
|
||||
})
|
||||
|
||||
async function writeManagedSettings(settings: object, filename = "opencode.json") {
|
||||
await fs.mkdir(managedConfigDir, { recursive: true })
|
||||
await Bun.write(path.join(managedConfigDir, filename), JSON.stringify(settings))
|
||||
}
|
||||
|
||||
async function writeConfig(dir: string, config: object, name = "opencode.json") {
|
||||
await Bun.write(path.join(dir, name), JSON.stringify(config))
|
||||
}
|
||||
|
||||
test("loads config with defaults when no files exist", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
@@ -38,11 +21,14 @@ test("loads config with defaults when no files exist", async () => {
|
||||
test("loads JSON config file", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await writeConfig(dir, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
model: "test/model",
|
||||
username: "testuser",
|
||||
})
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
model: "test/model",
|
||||
username: "testuser",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
@@ -82,19 +68,21 @@ test("loads JSONC config file", async () => {
|
||||
test("merges multiple config files with correct precedence", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await writeConfig(
|
||||
dir,
|
||||
{
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.jsonc"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
model: "base",
|
||||
username: "base",
|
||||
},
|
||||
"opencode.jsonc",
|
||||
}),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
model: "override",
|
||||
}),
|
||||
)
|
||||
await writeConfig(dir, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
model: "override",
|
||||
})
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
@@ -114,10 +102,13 @@ test("handles environment variable substitution", async () => {
|
||||
try {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await writeConfig(dir, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
theme: "{env:TEST_VAR}",
|
||||
})
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
theme: "{env:TEST_VAR}",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
@@ -178,10 +169,13 @@ test("handles file inclusion substitution", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "included.txt"), "test_theme")
|
||||
await writeConfig(dir, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
theme: "{file:included.txt}",
|
||||
})
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
theme: "{file:included.txt}",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
@@ -196,10 +190,13 @@ test("handles file inclusion substitution", async () => {
|
||||
test("validates config schema and throws on invalid fields", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await writeConfig(dir, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
invalid_field: "should cause error",
|
||||
})
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
invalid_field: "should cause error",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
@@ -228,16 +225,19 @@ test("throws error for invalid JSON", async () => {
|
||||
test("handles agent configuration", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await writeConfig(dir, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
agent: {
|
||||
test_agent: {
|
||||
model: "test/model",
|
||||
temperature: 0.7,
|
||||
description: "test agent",
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
agent: {
|
||||
test_agent: {
|
||||
model: "test/model",
|
||||
temperature: 0.7,
|
||||
description: "test agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
@@ -258,16 +258,19 @@ test("handles agent configuration", async () => {
|
||||
test("handles command configuration", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await writeConfig(dir, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
command: {
|
||||
test_command: {
|
||||
template: "test template",
|
||||
description: "test command",
|
||||
agent: "test_agent",
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
command: {
|
||||
test_command: {
|
||||
template: "test template",
|
||||
description: "test command",
|
||||
agent: "test_agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
@@ -891,86 +894,6 @@ test("migrates legacy write tool to edit permission", async () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Managed settings tests
|
||||
// Note: preload.ts sets OPENCODE_TEST_MANAGED_CONFIG which Global.Path.managedConfig uses
|
||||
|
||||
test("managed settings override user settings", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await writeConfig(dir, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
model: "user/model",
|
||||
share: "auto",
|
||||
username: "testuser",
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
await writeManagedSettings({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
model: "managed/model",
|
||||
share: "disabled",
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
expect(config.model).toBe("managed/model")
|
||||
expect(config.share).toBe("disabled")
|
||||
expect(config.username).toBe("testuser")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("managed settings override project settings", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await writeConfig(dir, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
autoupdate: true,
|
||||
disabled_providers: [],
|
||||
theme: "dark",
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
await writeManagedSettings({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
autoupdate: false,
|
||||
disabled_providers: ["openai"],
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
expect(config.autoupdate).toBe(false)
|
||||
expect(config.disabled_providers).toEqual(["openai"])
|
||||
expect(config.theme).toBe("dark")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("missing managed settings file is not an error", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await writeConfig(dir, {
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
model: "user/model",
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
expect(config.model).toBe("user/model")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("migrates legacy edit tool to edit permission", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
|
||||
@@ -17,10 +17,6 @@ const testHome = path.join(dir, "home")
|
||||
await fs.mkdir(testHome, { recursive: true })
|
||||
process.env["OPENCODE_TEST_HOME"] = testHome
|
||||
|
||||
// Set test managed config directory to isolate tests from system managed settings
|
||||
const testManagedConfigDir = path.join(dir, "managed")
|
||||
process.env["OPENCODE_TEST_MANAGED_CONFIG_DIR"] = testManagedConfigDir
|
||||
|
||||
process.env["XDG_DATA_HOME"] = path.join(dir, "share")
|
||||
process.env["XDG_CACHE_HOME"] = path.join(dir, "cache")
|
||||
process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -87,7 +87,6 @@ You can also access our models through the following API endpoints.
|
||||
| GLM 4.7 Free | glm-4.7-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 Free | kimi-k2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -121,8 +120,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
| GLM 4.7 Free | Free | Free | Free | - |
|
||||
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
|
||||
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
|
||||
| Kimi K2.5 Free | Free | Free | Free | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
|
||||
| Kimi K2 | $0.40 | $2.50 | - | - |
|
||||
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
|
||||
@@ -155,9 +153,8 @@ Credit card fees are passed along at cost (4.4% + $0.30 per transaction); we don
|
||||
|
||||
The free models:
|
||||
|
||||
- GLM 4.7 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
- Kimi M2.5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
- MiniMax M2.1 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
- GLM 4.7 is currently free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
- MiniMax M2.1 is currently free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
- Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
|
||||
|
||||
<a href={email}>Contact us</a> if you have any questions.
|
||||
@@ -187,10 +184,9 @@ charging you more than $20 if your balance goes below $5.
|
||||
|
||||
All our models are hosted in the US. Our providers follow a zero-retention policy and do not use your data for model training, with the following exceptions:
|
||||
|
||||
- GLM 4.7: During its free period, collected data may be used to improve the model.
|
||||
- MiniMax M2.1: During its free period, collected data may be used to improve the model.
|
||||
- Big Pickle: During its free period, collected data may be used to improve the model.
|
||||
- GLM 4.7 Free: During its free period, collected data may be used to improve the model.
|
||||
- Kimi K2.5 Free: During its free period, collected data may be used to improve the model.
|
||||
- MiniMax M2.1 Free: During its free period, collected data may be used to improve the model.
|
||||
- OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
|
||||
- Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.1.43",
|
||||
"version": "0.0.0-ci-202601291718",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user