From 22007f90ccb7663a854d4475da2b7aaf8e7303a6 Mon Sep 17 00:00:00 2001 From: jogesr <5592869+jogesr@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:04:59 +0200 Subject: [PATCH] fix(v2): show default aliases and support local plugin loading --- CHANGELOG.md | 8 ++++++++ README.md | 5 +++++ server.js | 1 + src/v2/context.ts | 1 + src/v2/model.ts | 5 ++++- test/v2/entry.test.ts | 8 ++++++++ test/v2/model.test.ts | 21 ++++++++++++++++++++- 7 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 server.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8d10c..d1081ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Fixed: loading the plugin from a local directory + +Added `server.js` at the repository root so OpenCode can load the built V2 adapter from a local directory. + +### Fixed: recommended aliases were hidden in OpenCode Desktop 2 + +The exact aliases `coding`, `thinking` and `fast` receive the current timestamp and separate families to appear by default in Desktop 2.0.29. This is a visibility workaround, not their actual release date. Explicit user visibility choices, other models and V1 are unchanged. + ### Fixed: setup could not store the API key for OpenCode 2 on Windows With OpenCode 2 installed through npm, `opencode` on Windows is a `.cmd` shim. Node refuses to pass arguments containing JSON quotes to a batch file (CVE-2024-27980 hardening), so `credential.create` failed with "contains a cmd.exe special character", and the error message echoed the request body including the API key. Setup now resolves the real `opencode.exe` from PATH, looking through the npm shim, and synchronous spawn errors no longer leak argv. Replacing an existing credential also failed: `credential.remove` takes `credentialID` as a path parameter (`--param`), not as a request body. diff --git a/README.md b/README.md index e7768a6..7c969bc 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,7 @@ The layer depends on the `config` hook running before OpenCode resolves provider ## Behavior +- On OpenCode Desktop 2, `coding`, `thinking` and `fast` receive the current timestamp and separate families to appear by default. This is a visibility workaround, not their actual release date. Explicit user visibility choices still take precedence; other models may need enabling under **Manage models**. - Discovery prefers `GET /v1/model_group/info`, which reports one entry per alias with mode, context limits, costs and capability flags. It falls back to `GET /v1/models` for older LiteLLM versions and restricted keys. - Costs are converted from LiteLLM's per-token prices to the per-million unit OpenCode expects. Cache-read and cache-write prices come from `GET /v1/model/info` (`cache_read_input_token_cost`, `cache_creation_input_token_cost`), since `/model_group/info` reports only input and output. OpenCode prices cached tokens at zero unless told otherwise, and in an agentic session those are nearly all the prompt tokens, so without them its cost display sits 10–50× below what the proxy bills. If that lookup fails, cached tokens are priced like input: too high beats invisible. - Wildcard entries are LiteLLM access rules rather than callable models and are dropped, as is anything whose mode is not `chat` or `responses`. An entry without a mode is kept. @@ -256,6 +257,10 @@ npm install npm run check ``` +### Testing locally in OpenCode Desktop 2 + +Run `npm run build`, then set the Neuron entry's `package` to the absolute repository directory, for example `"C:/workspaces/opencode-plugin-neuron"`. Use the same source globally and in projects to avoid duplicate plugin IDs; keep profile options unchanged. Rebuild after source changes and restore the npm package source when finished. + To inspect the package that will be distributed: ```sh diff --git a/server.js b/server.js new file mode 100644 index 0000000..c912f23 --- /dev/null +++ b/server.js @@ -0,0 +1 @@ +export { default } from "./dist/v2.js" diff --git a/src/v2/context.ts b/src/v2/context.ts index 38e4901..6142892 100644 --- a/src/v2/context.ts +++ b/src/v2/context.ts @@ -25,6 +25,7 @@ export interface V2ModelInfo { modelID: string providerID: string name: string + family?: string /** Overrides the provider's adapter for this one model. */ package?: string capabilities: { tools: boolean; input: string[]; output: string[] } diff --git a/src/v2/model.ts b/src/v2/model.ts index 42ce4b6..51b9430 100644 --- a/src/v2/model.ts +++ b/src/v2/model.ts @@ -39,16 +39,19 @@ export function toModelInfo(model: LiteLLMModel, providerID: string): V2ModelInf : [] const isResponsesAPI = model.mode?.toLowerCase() === "responses" + // Recent dates and separate families keep all three aliases visible in Desktop 2. + const isDefaultAlias = model.id === "coding" || model.id === "thinking" || model.id === "fast" return { id: model.id, modelID: model.id, providerID, name: model.id, + ...(isDefaultAlias ? { family: model.id } : {}), ...(isResponsesAPI ? { package: RESPONSES_API_PACKAGE_V2 } : {}), capabilities: { tools: model.supports_function_calling ?? true, input, output: ["text"] }, variants: [], - time: { released: 0 }, + time: { released: isDefaultAlias ? Date.now() : 0 }, cost, status: "active", enabled: true, diff --git a/test/v2/entry.test.ts b/test/v2/entry.test.ts index f9ccf68..068d8e3 100644 --- a/test/v2/entry.test.ts +++ b/test/v2/entry.test.ts @@ -1,8 +1,16 @@ import { describe, expect, it } from "vitest" +import { Host } from "@opencode/plugin/host" +import { fileURLToPath } from "node:url" import { NeuronPlugin } from "../../src/plugin.js" import entry from "../../src/v2.js" describe("the ./server entry", () => { + it("resolves a local plugin directory to the server entry", () => { + const directory = fileURLToPath(new URL("../../", import.meta.url)) + const resolved = Host.resolve({ directory }) + expect(resolved.server).toBe(new URL("../../server.js", import.meta.url).href) + }) + it("serves OpenCode 2 through setup and OpenCode 1 through server", () => { // OpenCode 1.16 to 1.18 resolve exports["./server"] for npm packages too // and insist on a server() function; 0.5.0 and 0.5.1 lacked it. diff --git a/test/v2/model.test.ts b/test/v2/model.test.ts index 55ae597..61af71f 100644 --- a/test/v2/model.test.ts +++ b/test/v2/model.test.ts @@ -1,8 +1,27 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it, vi } from "vitest" import { DEFAULT_CONTEXT_LIMIT, DEFAULT_OUTPUT_LIMIT, RESPONSES_API_PACKAGE_V2 } from "../../src/constants.js" import { toModelInfo } from "../../src/v2/model.js" describe("toModelInfo", () => { + it.each(["coding", "thinking", "fast"])("makes the %s alias recent and independently selectable in Desktop", (id) => { + const now = Date.UTC(2026, 9, 14) + const clock = vi.spyOn(Date, "now").mockReturnValue(now) + try { + const info = toModelInfo({ id }, "neuron-2") + expect(info.time.released).toBe(now) + expect(info).toHaveProperty("family", id) + expect(info.enabled).toBe(true) + } finally { + clock.mockRestore() + } + }) + + it.each(["Coding", "fast-model", "provider/thinking", "claude-sonnet"])("does not change release metadata for %s", (id) => { + const info = toModelInfo({ id }, "neuron-2") + expect(info.time.released).toBe(0) + expect(info).not.toHaveProperty("family") + }) + it("maps ids, limits, capabilities and prices", () => { const info = toModelInfo( {