@vheins/opencode-9router 0.4.5 → 0.5.0
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 +40 -42
- package/dist/plugin.js +44 -109
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,29 +2,34 @@
|
|
|
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@latest"]
|
|
12
|
+
"plugin": ["@vheins/opencode-9router@latest"],
|
|
13
|
+
"provider": {
|
|
14
|
+
"9router": {
|
|
15
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
16
|
+
"name": "9Router",
|
|
17
|
+
"options": {
|
|
18
|
+
"baseURL": "https://model.idsolutions.id/v1"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
13
22
|
}
|
|
14
23
|
```
|
|
15
24
|
|
|
16
|
-
1. Tambahkan plugin ke `opencode.json`
|
|
25
|
+
1. Tambahkan plugin dan provider ke `opencode.json`
|
|
17
26
|
2. Restart OpenCode
|
|
18
|
-
3. `/
|
|
19
|
-
4. Masukkan Base URL (default: `http://localhost:20128`) dan API Key
|
|
20
|
-
5. `/models` → pilih model 9Router
|
|
27
|
+
3. `/models` → pilih model 9Router
|
|
21
28
|
|
|
22
29
|
## Features
|
|
23
30
|
|
|
24
31
|
- **Auto-discover models** — Models dari 9Router otomatis terdeteksi saat startup
|
|
25
|
-
- **Configurable baseURL** — Atur Base URL via `/connect`, plugin options, atau environment variable
|
|
26
32
|
- **Dynamic model list** — Semua model dari 9Router tersedia, termasuk combo kustom
|
|
27
|
-
- **27+ fallback models** — Well-known models tersedia jika 9Router belum running
|
|
28
33
|
- **OpenAI-compatible** — Menggunakan `@ai-sdk/openai-compatible`
|
|
29
34
|
- **Type-safe** — Menggunakan `config` hook untuk registrasi provider yang sesuai dengan OpenCode config schema
|
|
30
35
|
|
|
@@ -35,11 +40,20 @@ Mendaftarkan 9Router sebagai custom provider di OpenCode dengan auto-discovery m
|
|
|
35
40
|
```json
|
|
36
41
|
{
|
|
37
42
|
"$schema": "https://opencode.ai/config.json",
|
|
38
|
-
"plugin": ["@vheins/opencode-9router@latest"]
|
|
43
|
+
"plugin": ["@vheins/opencode-9router@latest"],
|
|
44
|
+
"provider": {
|
|
45
|
+
"9router": {
|
|
46
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
47
|
+
"name": "9Router",
|
|
48
|
+
"options": {
|
|
49
|
+
"baseURL": "https://model.idsolutions.id/v1"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
39
53
|
}
|
|
40
54
|
```
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
Plugin akan auto-discover models dari baseURL yang dikonfigurasi.
|
|
43
57
|
|
|
44
58
|
### Local file
|
|
45
59
|
|
|
@@ -50,14 +64,9 @@ cp src/constants.ts .opencode/plugins/constants.ts
|
|
|
50
64
|
|
|
51
65
|
## Usage
|
|
52
66
|
|
|
53
|
-
### 1.
|
|
67
|
+
### 1. Configure provider
|
|
54
68
|
|
|
55
|
-
|
|
56
|
-
/connect
|
|
57
|
-
→ Select: Connect to 9Router
|
|
58
|
-
→ Base URL: http://localhost:20128 (customize if needed)
|
|
59
|
-
→ API Key: [paste from 9Router Dashboard → Endpoints]
|
|
60
|
-
```
|
|
69
|
+
Tambahkan provider `9router` ke `opencode.json` dengan `baseURL` yang sesuai.
|
|
61
70
|
|
|
62
71
|
### 2. Select model
|
|
63
72
|
|
|
@@ -69,29 +78,22 @@ cp src/constants.ts .opencode/plugins/constants.ts
|
|
|
69
78
|
|
|
70
79
|
### Custom Base URL
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
#### Via `/connect` (recommended)
|
|
75
|
-
|
|
76
|
-
Masukkan URL custom saat diminta Base URL.
|
|
77
|
-
|
|
78
|
-
#### Via plugin options
|
|
81
|
+
Ubah `baseURL` di provider config sesuai kebutuhan:
|
|
79
82
|
|
|
80
83
|
```json
|
|
81
84
|
{
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
"provider": {
|
|
86
|
+
"9router": {
|
|
87
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
88
|
+
"name": "9Router",
|
|
89
|
+
"options": {
|
|
90
|
+
"baseURL": "http://localhost:20128/v1"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
86
94
|
}
|
|
87
95
|
```
|
|
88
96
|
|
|
89
|
-
#### Via environment variable
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
export ROUTER_BASE_URL=http://192.168.1.100:20128
|
|
93
|
-
```
|
|
94
|
-
|
|
95
97
|
## Model Prefixes
|
|
96
98
|
|
|
97
99
|
| Prefix | Provider | Tier |
|
|
@@ -118,18 +120,14 @@ opencode.json "plugin": ["@vheins/opencode-9router@latest"]
|
|
|
118
120
|
Bun installs package from npm
|
|
119
121
|
↓
|
|
120
122
|
Plugin loads at startup:
|
|
121
|
-
1.
|
|
122
|
-
2. Try GET /v1/models from
|
|
123
|
+
1. Read provider config from opencode.json
|
|
124
|
+
2. Try GET /v1/models from baseURL (3s timeout)
|
|
123
125
|
3. If OK → register live models
|
|
124
|
-
4. If fail →
|
|
126
|
+
4. If fail → log error, no models registered
|
|
125
127
|
↓
|
|
126
|
-
config hook
|
|
128
|
+
config hook updates provider with discovered models
|
|
127
129
|
↓
|
|
128
130
|
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
131
|
```
|
|
134
132
|
|
|
135
133
|
## 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,52 @@ 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) => {
|
|
93
|
-
config.provider
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
],
|
|
55
|
+
if (!config.provider) {
|
|
56
|
+
await log("info", "9Router provider not configured. Add it to opencode.json provider section.");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const existingProvider = config.provider[PLUGIN_NAME];
|
|
60
|
+
if (!existingProvider) {
|
|
61
|
+
await log("info", "9Router provider not configured. Add it to opencode.json provider section.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const options = existingProvider.options;
|
|
65
|
+
const baseURL = options?.baseURL;
|
|
66
|
+
if (!baseURL) {
|
|
67
|
+
await log("warn", "9Router provider missing baseURL option.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const normalizedURL = normalizeBaseURL(baseURL);
|
|
71
|
+
const apiURL = ensureAPIPath(normalizedURL);
|
|
72
|
+
const discovered = await discoverModels(normalizedURL);
|
|
73
|
+
if (discovered) {
|
|
74
|
+
config.provider[PLUGIN_NAME] = {
|
|
75
|
+
...existingProvider,
|
|
76
|
+
options: { ...options, baseURL: apiURL },
|
|
77
|
+
models: discovered,
|
|
78
|
+
};
|
|
79
|
+
await log("info", `Discovered ${Object.keys(discovered).length} models from ${apiURL}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
await log("error", `Failed to discover models from ${apiURL}. Check if 9Router is running and accessible.`);
|
|
83
|
+
}
|
|
149
84
|
},
|
|
150
85
|
};
|
|
151
86
|
};
|