@vheins/opencode-9router 0.4.5 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -53
- package/dist/plugin.js +29 -101
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,29 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
OpenCode plugin provider for [9Router](https://github.com/decolua/9router) — FREE AI Router & Token Saver. 40+ providers, 100+ models.
|
|
4
4
|
|
|
5
|
-
Mendaftarkan 9Router sebagai custom provider di OpenCode dengan auto-discovery models
|
|
5
|
+
Mendaftarkan 9Router sebagai custom provider di OpenCode dengan auto-discovery models.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
9
|
```json
|
|
10
10
|
{
|
|
11
11
|
"$schema": "https://opencode.ai/config.json",
|
|
12
|
-
"plugin": ["@vheins/opencode-9router@
|
|
12
|
+
"plugin": ["@vheins/opencode-9router@0.5.0"]
|
|
13
13
|
}
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
1. Tambahkan plugin ke `opencode.json`
|
|
17
17
|
2. Restart OpenCode
|
|
18
|
-
3. `/
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
3. `/models` → pilih model 9Router
|
|
19
|
+
|
|
20
|
+
Plugin akan auto-discover models dari `http://localhost:20128` (default).
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
24
24
|
- **Auto-discover models** — Models dari 9Router otomatis terdeteksi saat startup
|
|
25
|
-
- **Configurable baseURL** — Atur Base URL via `/connect`, plugin options, atau environment variable
|
|
26
25
|
- **Dynamic model list** — Semua model dari 9Router tersedia, termasuk combo kustom
|
|
27
|
-
- **27+ fallback models** — Well-known models tersedia jika 9Router belum running
|
|
28
26
|
- **OpenAI-compatible** — Menggunakan `@ai-sdk/openai-compatible`
|
|
29
27
|
- **Type-safe** — Menggunakan `config` hook untuk registrasi provider yang sesuai dengan OpenCode config schema
|
|
30
28
|
|
|
@@ -35,61 +33,75 @@ Mendaftarkan 9Router sebagai custom provider di OpenCode dengan auto-discovery m
|
|
|
35
33
|
```json
|
|
36
34
|
{
|
|
37
35
|
"$schema": "https://opencode.ai/config.json",
|
|
38
|
-
"plugin": ["@vheins/opencode-9router@
|
|
36
|
+
"plugin": ["@vheins/opencode-9router@0.5.0"]
|
|
39
37
|
}
|
|
40
38
|
```
|
|
41
39
|
|
|
42
|
-
Tidak perlu mendefinisikan
|
|
43
|
-
|
|
44
|
-
### Local file
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
cp src/plugin.ts .opencode/plugins/9router-provider.ts
|
|
48
|
-
cp src/constants.ts .opencode/plugins/constants.ts
|
|
49
|
-
```
|
|
40
|
+
Tidak perlu mendefinisikan provider secara manual — plugin mendaftarkannya otomatis.
|
|
50
41
|
|
|
51
|
-
|
|
42
|
+
### Custom Base URL
|
|
52
43
|
|
|
53
|
-
|
|
44
|
+
Jika 9Router berjalan di host/port berbeda, tambahkan provider config:
|
|
54
45
|
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"$schema": "https://opencode.ai/config.json",
|
|
49
|
+
"plugin": ["@vheins/opencode-9router@0.5.0"],
|
|
50
|
+
"provider": {
|
|
51
|
+
"9router": {
|
|
52
|
+
"options": {
|
|
53
|
+
"baseURL": "https://model.idsolutions.id/v1"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
60
58
|
```
|
|
61
59
|
|
|
62
|
-
###
|
|
60
|
+
### With API Key
|
|
63
61
|
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"$schema": "https://opencode.ai/config.json",
|
|
65
|
+
"plugin": ["@vheins/opencode-9router@0.5.0"],
|
|
66
|
+
"provider": {
|
|
67
|
+
"9router": {
|
|
68
|
+
"options": {
|
|
69
|
+
"baseURL": "https://model.idsolutions.id/v1",
|
|
70
|
+
"apiKey": "your-api-key-here"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
64
75
|
```
|
|
65
|
-
/models
|
|
66
|
-
→ Find 9Router provider
|
|
67
|
-
→ Pick any model (e.g., kr/claude-sonnet-4.5, cc/claude-opus-4-7)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Custom Base URL
|
|
71
|
-
|
|
72
|
-
Jika 9Router berjalan di host/port berbeda, ada 3 cara konfigurasi:
|
|
73
|
-
|
|
74
|
-
#### Via `/connect` (recommended)
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#### Via plugin options
|
|
77
|
+
Atau pakai environment variable:
|
|
79
78
|
|
|
80
79
|
```json
|
|
81
80
|
{
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
"provider": {
|
|
82
|
+
"9router": {
|
|
83
|
+
"options": {
|
|
84
|
+
"baseURL": "https://model.idsolutions.id/v1",
|
|
85
|
+
"apiKey": "{env:ROUTER_API_KEY}"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
86
89
|
}
|
|
87
90
|
```
|
|
88
91
|
|
|
89
|
-
#### Via environment variable
|
|
90
|
-
|
|
91
92
|
```bash
|
|
92
|
-
export
|
|
93
|
+
export ROUTER_API_KEY=your-api-key-here
|
|
94
|
+
opencode
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
### Select model
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
/models
|
|
103
|
+
→ Find 9Router provider
|
|
104
|
+
→ Pick any model (e.g., kr/claude-sonnet-4.5, cc/claude-opus-4-7)
|
|
93
105
|
```
|
|
94
106
|
|
|
95
107
|
## Model Prefixes
|
|
@@ -113,23 +125,19 @@ export ROUTER_BASE_URL=http://192.168.1.100:20128
|
|
|
113
125
|
## How It Works
|
|
114
126
|
|
|
115
127
|
```
|
|
116
|
-
opencode.json "plugin": ["@vheins/opencode-9router@
|
|
128
|
+
opencode.json "plugin": ["@vheins/opencode-9router@0.5.0"]
|
|
117
129
|
↓
|
|
118
130
|
Bun installs package from npm
|
|
119
131
|
↓
|
|
120
132
|
Plugin loads at startup:
|
|
121
|
-
1.
|
|
122
|
-
2. Try GET /v1/models from
|
|
133
|
+
1. Read baseURL from provider config (or use default http://localhost:20128)
|
|
134
|
+
2. Try GET /v1/models from baseURL (3s timeout)
|
|
123
135
|
3. If OK → register live models
|
|
124
|
-
4. If fail →
|
|
136
|
+
4. If fail → log warning, no models registered
|
|
125
137
|
↓
|
|
126
|
-
config hook
|
|
138
|
+
config hook creates/updates provider "9router" with discovered models
|
|
127
139
|
↓
|
|
128
140
|
Provider "9router" appears in /models
|
|
129
|
-
↓
|
|
130
|
-
User runs /connect → enters baseURL + API key
|
|
131
|
-
↓
|
|
132
|
-
Auth loader overrides provider config with user's baseURL
|
|
133
141
|
```
|
|
134
142
|
|
|
135
143
|
## Development
|
package/dist/plugin.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { PLUGIN_NAME, PROVIDER_DISPLAY_NAME, DEFAULT_BASE_URL, DEFAULT_API_PATH, KNOWN_PROVIDER_PREFIXES, } from "./constants.js";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import * as os from "node:os";
|
|
5
2
|
function formatModelName(modelId) {
|
|
6
3
|
for (const [prefix, provider] of Object.entries(KNOWN_PROVIDER_PREFIXES)) {
|
|
7
4
|
if (modelId.startsWith(prefix)) {
|
|
@@ -13,39 +10,9 @@ function formatModelName(modelId) {
|
|
|
13
10
|
function normalizeBaseURL(url) {
|
|
14
11
|
return url.replace(/\/+$/, "");
|
|
15
12
|
}
|
|
16
|
-
function resolveBaseURL(options) {
|
|
17
|
-
if (options?.baseURL && typeof options.baseURL === "string") {
|
|
18
|
-
return { url: normalizeBaseURL(options.baseURL), isDefault: false };
|
|
19
|
-
}
|
|
20
|
-
const envURL = globalThis.process;
|
|
21
|
-
if (envURL?.env?.ROUTER_BASE_URL) {
|
|
22
|
-
return { url: normalizeBaseURL(envURL.env.ROUTER_BASE_URL), isDefault: false };
|
|
23
|
-
}
|
|
24
|
-
return { url: DEFAULT_BASE_URL, isDefault: true };
|
|
25
|
-
}
|
|
26
13
|
function ensureAPIPath(baseURL) {
|
|
27
14
|
return baseURL.endsWith(DEFAULT_API_PATH) ? baseURL : `${baseURL}${DEFAULT_API_PATH}`;
|
|
28
15
|
}
|
|
29
|
-
async function getAuthBaseURL() {
|
|
30
|
-
try {
|
|
31
|
-
const authPath = path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
|
|
32
|
-
const authContent = await fs.promises.readFile(authPath, "utf-8");
|
|
33
|
-
const auth = JSON.parse(authContent);
|
|
34
|
-
// Find 9Router auth entry
|
|
35
|
-
for (const key of Object.keys(auth)) {
|
|
36
|
-
if (key.toLowerCase().includes("9router")) {
|
|
37
|
-
const entry = auth[key];
|
|
38
|
-
if (entry?.baseURL && typeof entry.baseURL === "string") {
|
|
39
|
-
return normalizeBaseURL(entry.baseURL);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
// Auth file doesn't exist or can't be read
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
16
|
async function discoverModels(baseURL) {
|
|
50
17
|
const apiURL = ensureAPIPath(baseURL);
|
|
51
18
|
try {
|
|
@@ -68,84 +35,45 @@ async function discoverModels(baseURL) {
|
|
|
68
35
|
return null;
|
|
69
36
|
}
|
|
70
37
|
}
|
|
71
|
-
export const NineRouterPlugin = async ({ client }
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
level,
|
|
87
|
-
message,
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
}
|
|
38
|
+
export const NineRouterPlugin = async ({ client }) => {
|
|
39
|
+
const log = async (level, message) => {
|
|
40
|
+
try {
|
|
41
|
+
await client.app.log({
|
|
42
|
+
body: {
|
|
43
|
+
service: "9router-provider",
|
|
44
|
+
level,
|
|
45
|
+
message,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Logging is best-effort
|
|
51
|
+
}
|
|
52
|
+
};
|
|
91
53
|
return {
|
|
92
54
|
config: async (config) => {
|
|
55
|
+
const existingProvider = config.provider?.[PLUGIN_NAME];
|
|
56
|
+
const options = existingProvider?.options;
|
|
57
|
+
const baseURL = options?.baseURL ?? DEFAULT_BASE_URL;
|
|
58
|
+
const normalizedURL = normalizeBaseURL(baseURL);
|
|
59
|
+
const apiURL = ensureAPIPath(normalizedURL);
|
|
60
|
+
const discovered = await discoverModels(normalizedURL);
|
|
93
61
|
config.provider ??= {};
|
|
94
62
|
config.provider[PLUGIN_NAME] = {
|
|
95
63
|
npm: "@ai-sdk/openai-compatible",
|
|
96
64
|
name: PROVIDER_DISPLAY_NAME,
|
|
97
65
|
options: {
|
|
98
|
-
|
|
66
|
+
...options,
|
|
67
|
+
baseURL: apiURL,
|
|
99
68
|
},
|
|
100
69
|
models: discovered ?? {},
|
|
101
70
|
};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const userURL = normalizeBaseURL(String(auth.baseURL));
|
|
109
|
-
const apiURL = ensureAPIPath(userURL);
|
|
110
|
-
// Re-discover models with user-provided baseURL
|
|
111
|
-
const authDiscovered = await discoverModels(userURL);
|
|
112
|
-
if (authDiscovered) {
|
|
113
|
-
if (client?.app?.log) {
|
|
114
|
-
await client.app.log({
|
|
115
|
-
body: {
|
|
116
|
-
service: "9router-provider",
|
|
117
|
-
level: "info",
|
|
118
|
-
message: `Discovered ${Object.keys(authDiscovered).length} models from ${apiURL}`,
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
else if (client?.app?.log) {
|
|
124
|
-
await client.app.log({
|
|
125
|
-
body: {
|
|
126
|
-
service: "9router-provider",
|
|
127
|
-
level: "error",
|
|
128
|
-
message: `Failed to discover models from ${apiURL}. Check if 9Router is running and accessible.`,
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
return { baseURL: apiURL };
|
|
133
|
-
}
|
|
134
|
-
return {};
|
|
135
|
-
},
|
|
136
|
-
methods: [
|
|
137
|
-
{
|
|
138
|
-
label: `Connect to ${PROVIDER_DISPLAY_NAME}`,
|
|
139
|
-
type: "api",
|
|
140
|
-
prompts: [
|
|
141
|
-
{
|
|
142
|
-
type: "text",
|
|
143
|
-
message: `${PROVIDER_DISPLAY_NAME} Base URL (default: ${DEFAULT_BASE_URL})`,
|
|
144
|
-
key: "baseURL",
|
|
145
|
-
},
|
|
146
|
-
],
|
|
147
|
-
},
|
|
148
|
-
],
|
|
71
|
+
if (discovered) {
|
|
72
|
+
await log("info", `Discovered ${Object.keys(discovered).length} models from ${apiURL}`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
await log("warn", `Failed to discover models from ${apiURL}. Check if 9Router is running and accessible.`);
|
|
76
|
+
}
|
|
149
77
|
},
|
|
150
78
|
};
|
|
151
79
|
};
|