Skip to content

Commit a76680b

Browse files
committed
feat: support OpenCode 2 alongside OpenCode 1
One default export now serves both majors: OpenCode 1 calls server() (the existing implementation, moved to src/v1.ts) and OpenCode 2 calls setup() (new src/v2.ts). Imports from @opencode/plugin are type-only because OpenCode 1 does not provide that package at runtime. On OpenCode 2 the plugin registers the coreinfra integration (API key or COREINFRA_API_KEY), fetches the Hub inventory for the active connection and adds the coreinfra provider with native protocol packages. Model metadata and thinking variants come from OpenCode's built-in catalog. The inventory refreshes when the active credential switches. setup() does nothing in the preview V2 host that OpenCode 1.18+ also runs, since server() already covers it there. Chat-route models (DeepSeek, ZAI, Moonshot) get reasoning effort variants from the upstream effort list on both majors. OpenCode 1 otherwise guesses them from the model ID and offered no "max" for the Hub's deepseek-flash alias; OpenCode 2 offered none at all. Moonshot models no longer advertise temperature support: Moonshot only accepts 1, and OpenCode's title agent sent another value.
1 parent eb73cdc commit a76680b

11 files changed

Lines changed: 1946 additions & 124 deletions

File tree

‎README.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,21 @@ Plugin for [OpenCode](https://opencode.ai) that adds [CoreInfra AI Hub](https://
44

55
## Installation
66

7-
**Important:** requires OpenCode version 1.4.0 or newer
7+
Works with OpenCode 1.x (1.4.0 or newer) and OpenCode 2.x (2.0.28 or newer).
8+
9+
### OpenCode 2
10+
11+
Install the plugin
12+
```bash
13+
opencode plugin add '@coreinfra/opencode-plugin@latest'
14+
```
15+
16+
Log in by entering the API token
17+
```bash
18+
opencode auth login coreinfra
19+
```
20+
21+
### OpenCode 1
822

923
Install the plugin
1024
```bash
@@ -55,6 +69,7 @@ https://hub.coreinfra.ai/pricing
5569

5670
| Variable | Default | Description |
5771
|---|---|---|
72+
| `COREINFRA_API_KEY` | — | On OpenCode 2, an API token to use instead of logging in (a saved login takes precedence). On OpenCode 1 it only authenticates loading the model list; logging in is still required. |
5873
| `COREINFRA_HUB_BASE_URL` | `https://hub.coreinfra.ai` | Base URL of the CoreInfra Hub instance. Overrides the default endpoint for model listings and API proxying. |
5974

6075
## Development

‎README.ru.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,21 @@
44

55
## Установка
66

7-
**Важно:** требуется версия OpenCode не старше 1.4.0
7+
Работает с OpenCode 1.x (1.4.0 или новее) и OpenCode 2.x (2.0.28 или новее).
8+
9+
### OpenCode 2
10+
11+
Установка плагина
12+
```bash
13+
opencode plugin add '@coreinfra/opencode-plugin@latest'
14+
```
15+
16+
Авторизация с вводом API-токена
17+
```bash
18+
opencode auth login coreinfra
19+
```
20+
21+
### OpenCode 1
822

923
Установка плагина
1024
```bash
@@ -55,6 +69,7 @@ https://hub.coreinfra.ai/pricing
5569

5670
| Переменная | По умолчанию | Описание |
5771
|---|---|---|
72+
| `COREINFRA_API_KEY` | — | В OpenCode 2 — API-токен, используемый вместо авторизации (сохранённая авторизация имеет приоритет). В OpenCode 1 используется только для загрузки списка моделей; авторизация всё равно обязательна. |
5873
| `COREINFRA_HUB_BASE_URL` | `https://hub.coreinfra.ai` | Базовый URL экземпляра CoreInfra Hub. Переопределяет стандартный эндпоинт для получения списка моделей и проксирования API. |
5974

6075
## Разработка

‎bun.lock‎

Lines changed: 533 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎docs/plans/opencode-v2-support.md‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# OpenCode 2 support (dual 1.x / 2.x plugin)
2+
3+
Status: implemented and verified (packed package on OpenCode 1.4.0, 1.19.0, 2.0.28); reviewed by Codex. Target release: **1.3.0** (no breaking change for V1).
4+
5+
## Feature
6+
7+
One npm package that works on **OpenCode 1.x (≥ 1.4.0)** and **OpenCode 2.x**:
8+
9+
- On V1, the plugin behaves exactly as it does today.
10+
- On V2, it registers the `coreinfra` login (API key or `COREINFRA_API_KEY`) and the `coreinfra` provider with every model the Hub lists. Each model gets its correct protocol and endpoint and keeps its thinking/effort variants.
11+
- After login, logout or an account switch, the model list refreshes without restarting OpenCode.
12+
13+
## Why the V1 plugin can't just run on V2
14+
15+
| Area | V1 (today) | V2 |
16+
|---|---|---|
17+
| Entry | `default { id, server() → hooks }` | `default { id, setup(ctx) }`. V1 plugins don't load. |
18+
| Models | `config` hook edits `config.provider.coreinfra.models` | `ctx.provider.transform(editor => editor.add(...))`. Must be sync and replayable. |
19+
| Login | `auth` hook + `loader → { apiKey }` | `ctx.integration.transform` registers `key` and `env` methods. Core adds `apiKey` itself. |
20+
| Key storage | `auth.json` | Database. `auth.json` is imported once, so new logins never reach it. |
21+
| Model shape | `provider{npm,api}`, `tool_call`, `modalities`, `cost{cache_read}`, `interleaved` | `package`, `settings.baseURL`, `capabilities`, `cost[{cache{read,write}}]`, `compatibility`, `variants[]`, `time`, `status`, `enabled` |
22+
| Logging | `client.app.log` | none; use `console` |
23+
| CLI | `opencode plugin -g`, `opencode providers login` | `opencode plugin add`, `opencode auth login coreinfra` |
24+
25+
Compatibility is checked in the source: the V2 loader ignores the extra `server` key (`core/src/plugin/module.ts:78`). The V1 1.4.0 loader ignores the extra `setup` key (`readV1Plugin`).
26+
27+
## Plan
28+
29+
1. **Entry:** `export default { id, server: v1, setup: v2 }`. Import V2 types only (`import type`), because `@opencode/plugin` doesn't exist at runtime under V1.
30+
2. **`src/models.ts` (shared):** Hub fetch takes an explicit `apiKey` and has no `auth.json` fallback. Also holds `PROVIDERS` (protocol → base URL, headers), lookups and helpers.
31+
3. **`src/v1.ts`:** today's `config`/`auth` hooks moved here unchanged, including the `auth.json`/env key lookup.
32+
4. **`src/v2.ts` `setup(ctx)`:**
33+
- Integration `coreinfra` ("CoreInfra AI Hub"), with methods `{type:"key"}` and `{type:"env", names:["COREINFRA_API_KEY"]}`. Always registered, even if discovery fails.
34+
- Key comes from `integration.connection.active("coreinfra")` → `resolve()` only. V2 ranks a saved key above env.
35+
- `refresh()` fetches the Hub list and stores a snapshot bound to the connection. Refreshes are serialized, results for a stale connection are dropped, and the last good data is kept only for the same connection.
36+
- `provider.transform` (sync) does `editor.add({ info: { id:"coreinfra", integrationID:"coreinfra", activation:"auto", package:"" }, models, sourceConnection })`.
37+
- `event.subscribe` reacts to `credential.switched` for `coreinfra` by calling `refresh()` and then `ctx.provider.reload()`. Cleanup aborts it.
38+
5. **V2 model mapping:** the Hub list decides which models exist. For each Hub model, look up the built-in upstream record (`openai`/`anthropic`/`deepseek`/…) inside each transform replay, never cached in `setup`.
39+
- Package: `@opencode/ai/providers/{openai|anthropic|openai-compatible}`, explicit native packages, not `aisdk:`.
40+
- Copy only allowlisted fields from upstream: capabilities, limit, cost, family, time, status. Replace ids, providerID, package, `settings.baseURL`, headers and `enabled`.
41+
- Variants: deep-clone only the reviewed thinking/effort fields, and only when `upstream.package ?? record.provider.package` equals our package. Otherwise use `variants: []`. This is an accepted limitation for DeepSeek, ZAI and Moonshot.
42+
- `compatibility.reasoningField` comes from the upstream model on chat routes only. No synthetic `reasoning_content` for Anthropic.
43+
- If the upstream record is missing, fall back to the Hub name plus defaults and log a warning. V2 no longer fetches models.dev itself.
44+
6. **Packaging:** devDependencies `@opencode-ai/plugin`, `@opencode-ai/sdk`, `@opencode/plugin`. All host peers marked optional.
45+
7. **Docs:** README install and login steps for both majors; note that a saved key takes priority over env on V2.
46+
47+
## Tests
48+
49+
- Unit: V1 behaviour unchanged. V2 mapper covers inherited package, matching and mismatched package, missing record and variant field isolation.
50+
- V2 lifecycle with a fake `ctx`:
51+
- discovery fails at first, then login succeeds
52+
- env only, and env + saved key together
53+
- switch account and logout
54+
- a stale refresh finishes late
55+
- transform replay after a catalog reload
56+
- cleanup
57+
- `auth.json` is never read
58+
- Manual release gate: install the **packed** package (no devDependencies) on OpenCode 1.4.0, 1.19.0 and 2.0.28. Log in, list models, then do a reasoning + tool-call round trip on the openai, anthropic and deepseek routes.
59+
60+
## Out of scope / known limits
61+
62+
- Thinking variants for models whose upstream uses a different package (DeepSeek/ZAI/Moonshot).
63+
- A key changed without a switch event doesn't refresh until restart.
64+
- Hub prices: OpenCode still supports USD only.
65+
- Hub models that models.dev marks deprecated are missing from OpenCode 2's built-in catalog, so on V2 they get default metadata (and a warning).
66+
- 2.x support is verified on 2.0.28. Older 2.x releases are unverified.

‎package.json‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,28 @@
3434
},
3535
"peerDependencies": {
3636
"@opencode-ai/plugin": ">=1.4.0",
37-
"@opencode-ai/sdk": ">=1.4.0"
37+
"@opencode-ai/sdk": ">=1.4.0",
38+
"@opencode/plugin": ">=2.0.28"
39+
},
40+
"peerDependenciesMeta": {
41+
"@opencode-ai/plugin": {
42+
"optional": true
43+
},
44+
"@opencode-ai/sdk": {
45+
"optional": true
46+
},
47+
"@opencode/plugin": {
48+
"optional": true
49+
}
3850
},
3951
"devDependencies": {
52+
"@biomejs/biome": "^2.4.11",
4053
"@opencode-ai/plugin": "1.4.0",
4154
"@opencode-ai/sdk": "1.4.0",
55+
"@opencode/plugin": "2.0.28",
4256
"@types/node": "^24.9.0",
43-
"@biomejs/biome": "^2.4.11",
44-
"vitest": "^4.1.4",
45-
"typescript": "^5.8.0"
57+
"typescript": "^5.8.0",
58+
"vitest": "^4.1.4"
4659
},
4760
"dependencies": {
4861
"xdg-basedir": "^5.1.0"

‎src/index.ts‎

Lines changed: 8 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,12 @@
1-
import type { Config, Hooks, PluginInput } from "@opencode-ai/plugin";
2-
3-
import {
4-
buildConfigModels,
5-
fetchModelsDevData,
6-
fetchHubModels,
7-
} from "./models.ts";
8-
9-
const PROVIDER_NAME = "CoreInfra AI Hub";
10-
11-
function ensureCoreInfraProvider(config: Config) {
12-
config.provider ??= {};
13-
config.provider.coreinfra = {
14-
...(config.provider.coreinfra ?? {}),
15-
name: PROVIDER_NAME,
16-
};
17-
}
18-
19-
async function log(
20-
input: PluginInput,
21-
message: string,
22-
extra?: Record<string, unknown>,
23-
level: "debug" | "warn" | "error" = "debug",
24-
) {
25-
await input.client.app
26-
.log({
27-
body: {
28-
service: "plugin.coreinfra",
29-
level,
30-
message,
31-
extra,
32-
},
33-
})
34-
.catch(() => {});
35-
}
36-
37-
async function plugin(input: PluginInput): Promise<Hooks> {
38-
const t0 = performance.now();
39-
await log(input, "plugin load started");
40-
await log(input, "plugin hooks registered", {
41-
duration: `${Math.round(performance.now() - t0)}ms`,
42-
});
43-
44-
return {
45-
config: async (config) => {
46-
const t = performance.now();
47-
ensureCoreInfraProvider(config);
48-
try {
49-
const [modelsDevData, hubData] = await Promise.all([
50-
fetchModelsDevData(),
51-
fetchHubModels(),
52-
]);
53-
const { models: configModels, warnings } = buildConfigModels(
54-
modelsDevData,
55-
hubData,
56-
);
57-
for (const w of warnings) {
58-
await log(input, w);
59-
}
60-
if (!config.provider) config.provider = {};
61-
if (!config.provider.coreinfra) config.provider.coreinfra = {};
62-
config.provider.coreinfra.models =
63-
configModels as typeof config.provider.coreinfra.models;
64-
} catch (err) {
65-
await log(
66-
input,
67-
"fetchModels failed",
68-
{
69-
error: err instanceof Error ? err.message : String(err),
70-
},
71-
"error",
72-
);
73-
}
74-
await log(input, "config hook completed", {
75-
duration: `${Math.round(performance.now() - t)}ms`,
76-
});
77-
},
78-
auth: {
79-
provider: "coreinfra",
80-
methods: [
81-
{
82-
type: "api",
83-
label: "CoreInfra API Key",
84-
},
85-
],
86-
loader: async (getAuth) => {
87-
const t = performance.now();
88-
const auth = await getAuth();
89-
if (!auth || auth.type !== "api") {
90-
await log(input, "auth loader completed (no auth)", {
91-
duration: `${Math.round(performance.now() - t)}ms`,
92-
});
93-
return {};
94-
}
95-
await log(input, "auth loader completed", {
96-
duration: `${Math.round(performance.now() - t)}ms`,
97-
});
98-
return { apiKey: auth.key };
99-
},
100-
},
101-
};
102-
}
1+
import { server } from "./v1.ts";
2+
import { setup } from "./v2.ts";
1033

4+
// One default export serves both OpenCode majors: OpenCode 1 calls server()
5+
// and ignores setup, OpenCode 2 calls setup() and ignores server. Everything
6+
// imported from @opencode/plugin is type-only because OpenCode 1 does not
7+
// provide that package at runtime.
1048
export default {
1059
id: "coreinfra-opencode-plugin",
106-
server: plugin,
10+
server,
11+
setup,
10712
};

0 commit comments

Comments
 (0)