oc-agent-router 0.1.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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +113 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# oc-agent-router
|
|
2
|
+
|
|
3
|
+
An OpenCode plugin that routes each new `task` subagent to one of your configured models. It asks [Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev) for a structured choice immediately before OpenCode creates the child session.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
opencode plugin add oc-agent-router
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Set `TYPESAFE_API_KEY` in the environment that starts OpenCode, then configure the plugin in `~/.config/opencode/opencode.json` or your project `opencode.json`:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"$schema": "https://opencode.ai/config.json",
|
|
16
|
+
"plugin": [
|
|
17
|
+
[
|
|
18
|
+
"oc-agent-router",
|
|
19
|
+
{
|
|
20
|
+
"models": [
|
|
21
|
+
"openai/gpt-5.4",
|
|
22
|
+
"anthropic/claude-sonnet-4-6",
|
|
23
|
+
"google/gemini-3-pro"
|
|
24
|
+
],
|
|
25
|
+
"fallbackModel": "anthropic/claude-sonnet-4-6"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
All configured models must already be available in OpenCode. Restart OpenCode after installing or changing its configuration.
|
|
33
|
+
|
|
34
|
+
`TYPESAFE_API_KEY` is read only from the environment of the OpenCode process. Do not put it in `opencode.json`.
|
|
35
|
+
|
|
36
|
+
## Options
|
|
37
|
+
|
|
38
|
+
| Option | Default | Description |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `models` | Required | Non-empty allowed model list in `provider/model` form. |
|
|
41
|
+
| `fallbackModel` | First model | Used when the API key is missing, Jev errors, times out, or returns an invalid choice. Must be in `models`. |
|
|
42
|
+
| `jevModel` | `jev-latest` | TypeSafe model ID. Pin a version if routing behavior must be stable. |
|
|
43
|
+
| `apiKeyEnv` | `TYPESAFE_API_KEY` | Environment variable containing the TypeSafe API key. |
|
|
44
|
+
| `timeoutMs` | `5000` | Jev request deadline, from 100 to 30,000 ms. |
|
|
45
|
+
|
|
46
|
+
## How It Works
|
|
47
|
+
|
|
48
|
+
OpenCode's `task` tool does not accept a per-call model override. The plugin uses the documented `tool.execute.before` hook, calls `POST https://api.typesafe.ai/v1/systemone` with a Choice question whose only choices are your allowed models, then rewrites `subagent_type` to a hidden, model-bound copy of the requested subagent.
|
|
49
|
+
|
|
50
|
+
Task resumes (`task_id`) are deliberately not rerouted, so a resumed session keeps its original model. API failures never expand the configured model allowlist and use `fallbackModel` instead.
|
|
51
|
+
|
|
52
|
+
The routing request sends only the subagent type, short task description, and task prompt. It does not send session history, tool outputs, or API credentials.
|
|
53
|
+
|
|
54
|
+
For example, a task that asks `general` to implement a parser sends the following payload. Its `Authorization` header is `Bearer $TYPESAFE_API_KEY`, which comes from the process environment rather than any config file.
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"model": "jev-latest",
|
|
59
|
+
"state": {
|
|
60
|
+
"task": "Implement the parser for the new configuration format.",
|
|
61
|
+
"description": "Implement parser",
|
|
62
|
+
"requested_agent": "general"
|
|
63
|
+
},
|
|
64
|
+
"questions": {
|
|
65
|
+
"model": {
|
|
66
|
+
"type": "choice",
|
|
67
|
+
"instructions": "Choose the configured model most suitable for completing this OpenCode subagent task. Prefer a capable model for implementation, debugging, and complex reasoning; prefer an efficient model for focused exploration or simple tasks.",
|
|
68
|
+
"criteria": {
|
|
69
|
+
"openai/gpt-5.4": "Use the configured OpenCode model openai/gpt-5.4.",
|
|
70
|
+
"anthropic/claude-sonnet-4-6": "Use the configured OpenCode model anthropic/claude-sonnet-4-6.",
|
|
71
|
+
"google/gemini-3-pro": "Use the configured OpenCode model google/gemini-3-pro."
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The plugin routes the built-in `general` and `explore` agents plus subagents declared in `opencode.json` under `agent`. Markdown-only subagents are left unchanged because OpenCode's plugin hook does not expose their resolved definitions for safe cloning.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Plugin } from '@opencode-ai/plugin';
|
|
2
|
+
|
|
3
|
+
interface RouterOptions {
|
|
4
|
+
/** Models eligible to receive subagent tasks, in OpenCode provider/model form. */
|
|
5
|
+
models: string[];
|
|
6
|
+
/** TypeSafe model ID used for routing. Defaults to jev-latest. */
|
|
7
|
+
jevModel?: string;
|
|
8
|
+
/** Environment variable containing the TypeSafe API key. Defaults to TYPESAFE_API_KEY. */
|
|
9
|
+
apiKeyEnv?: string;
|
|
10
|
+
/** Maximum time to wait for a route decision. Defaults to 5000. */
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
/** Model to use when Jev is unavailable or returns an invalid answer. Defaults to the first model. */
|
|
13
|
+
fallbackModel?: string;
|
|
14
|
+
}
|
|
15
|
+
interface TaskArgs {
|
|
16
|
+
description?: unknown;
|
|
17
|
+
prompt?: unknown;
|
|
18
|
+
subagent_type?: unknown;
|
|
19
|
+
task_id?: unknown;
|
|
20
|
+
}
|
|
21
|
+
interface FetchResponse {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
status: number;
|
|
24
|
+
json(): Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
type Fetcher = (input: string, init: RequestInit) => Promise<FetchResponse>;
|
|
27
|
+
declare function parseOptions(value: unknown): Required<RouterOptions>;
|
|
28
|
+
declare function routedAgentName(agent: string, model: string): string;
|
|
29
|
+
declare function selectModel(options: Required<RouterOptions>, args: TaskArgs, apiKey: string | undefined, fetcher?: Fetcher): Promise<string>;
|
|
30
|
+
|
|
31
|
+
declare const plugin: Plugin;
|
|
32
|
+
|
|
33
|
+
export { type RouterOptions, plugin as default, parseOptions, routedAgentName, selectModel };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/router.ts
|
|
2
|
+
function parseOptions(value) {
|
|
3
|
+
if (!isRecord(value) || !Array.isArray(value.models)) {
|
|
4
|
+
throw new Error("oc-agent-router requires a non-empty models array");
|
|
5
|
+
}
|
|
6
|
+
const models = value.models.filter((model) => typeof model === "string" && isModel(model));
|
|
7
|
+
if (models.length !== value.models.length || models.length === 0) {
|
|
8
|
+
throw new Error("oc-agent-router models must be provider/model strings");
|
|
9
|
+
}
|
|
10
|
+
const fallbackModel = typeof value.fallbackModel === "string" ? value.fallbackModel : models[0];
|
|
11
|
+
if (!models.includes(fallbackModel)) throw new Error("oc-agent-router fallbackModel must appear in models");
|
|
12
|
+
return {
|
|
13
|
+
models,
|
|
14
|
+
jevModel: typeof value.jevModel === "string" ? value.jevModel : "jev-latest",
|
|
15
|
+
apiKeyEnv: typeof value.apiKeyEnv === "string" ? value.apiKeyEnv : "TYPESAFE_API_KEY",
|
|
16
|
+
timeoutMs: typeof value.timeoutMs === "number" && value.timeoutMs >= 100 && value.timeoutMs <= 3e4 ? value.timeoutMs : 5e3,
|
|
17
|
+
fallbackModel
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function routedAgentName(agent, model) {
|
|
21
|
+
return `oc-agent-router-${encode(agent)}-${encode(model)}`;
|
|
22
|
+
}
|
|
23
|
+
async function selectModel(options, args, apiKey, fetcher = fetch) {
|
|
24
|
+
if (!apiKey) return options.fallbackModel;
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetcher("https://api.typesafe.ai/v1/systemone", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
model: options.jevModel,
|
|
33
|
+
state: {
|
|
34
|
+
task: typeof args.prompt === "string" ? args.prompt : "",
|
|
35
|
+
description: typeof args.description === "string" ? args.description : "",
|
|
36
|
+
requested_agent: typeof args.subagent_type === "string" ? args.subagent_type : ""
|
|
37
|
+
},
|
|
38
|
+
questions: {
|
|
39
|
+
model: {
|
|
40
|
+
type: "choice",
|
|
41
|
+
instructions: "Choose the configured model most suitable for completing this OpenCode subagent task. Prefer a capable model for implementation, debugging, and complex reasoning; prefer an efficient model for focused exploration or simple tasks.",
|
|
42
|
+
criteria: Object.fromEntries(options.models.map((model) => [model, `Use the configured OpenCode model ${model}.`]))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
signal: controller.signal
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) return options.fallbackModel;
|
|
49
|
+
const body = await response.json();
|
|
50
|
+
const choice = isRecord(body) && isRecord(body.answers) && isRecord(body.answers.model) ? body.answers.model.choice : void 0;
|
|
51
|
+
return typeof choice === "string" && options.models.includes(choice) ? choice : options.fallbackModel;
|
|
52
|
+
} catch {
|
|
53
|
+
return options.fallbackModel;
|
|
54
|
+
} finally {
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function encode(value) {
|
|
59
|
+
return Array.from(value, (character) => character.codePointAt(0).toString(36)).join("-");
|
|
60
|
+
}
|
|
61
|
+
function isModel(value) {
|
|
62
|
+
const separator = value.indexOf("/");
|
|
63
|
+
return separator > 0 && separator < value.length - 1;
|
|
64
|
+
}
|
|
65
|
+
function isRecord(value) {
|
|
66
|
+
return typeof value === "object" && value !== null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/index.ts
|
|
70
|
+
var plugin = async (_input, rawOptions) => {
|
|
71
|
+
const options = parseOptions(rawOptions);
|
|
72
|
+
const routeableAgents = /* @__PURE__ */ new Set();
|
|
73
|
+
return {
|
|
74
|
+
async config(config) {
|
|
75
|
+
config.agent ??= {};
|
|
76
|
+
const agents = config.agent;
|
|
77
|
+
const sourceAgents = {
|
|
78
|
+
general: agents.general ?? { mode: "subagent" },
|
|
79
|
+
explore: agents.explore ?? { mode: "subagent" },
|
|
80
|
+
...Object.fromEntries(
|
|
81
|
+
Object.entries(agents).filter(
|
|
82
|
+
(entry) => Boolean(entry[1]) && entry[0] !== "build" && entry[0] !== "plan" && entry[1]?.mode !== "primary"
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
};
|
|
86
|
+
for (const [name, agent] of Object.entries(sourceAgents)) {
|
|
87
|
+
if (name.startsWith("oc-agent-router-")) continue;
|
|
88
|
+
routeableAgents.add(name);
|
|
89
|
+
for (const model of options.models) {
|
|
90
|
+
const routeName = routedAgentName(name, model);
|
|
91
|
+
if (agents[routeName]) continue;
|
|
92
|
+
agents[routeName] = { ...agent, model, mode: "subagent", hidden: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"tool.execute.before": async (input, output) => {
|
|
97
|
+
if (input.tool !== "task") return;
|
|
98
|
+
const args = output.args;
|
|
99
|
+
if (typeof args.subagent_type !== "string" || typeof args.prompt !== "string" || args.task_id) return;
|
|
100
|
+
if (args.subagent_type.startsWith("oc-agent-router-")) return;
|
|
101
|
+
if (!routeableAgents.has(args.subagent_type)) return;
|
|
102
|
+
const model = await selectModel(options, args, process.env[options.apiKeyEnv]);
|
|
103
|
+
args.subagent_type = routedAgentName(args.subagent_type, model);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
var index_default = plugin;
|
|
108
|
+
export {
|
|
109
|
+
index_default as default,
|
|
110
|
+
parseOptions,
|
|
111
|
+
routedAgentName,
|
|
112
|
+
selectModel
|
|
113
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oc-agent-router",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Route OpenCode subagent tasks to configured models with Jev.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/maharshi365/oc-agent-router.git"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
26
|
+
"check": "tsc --noEmit",
|
|
27
|
+
"test": "tsx --test test/**/*.test.ts",
|
|
28
|
+
"verify": "npm run check && npm test && npm run build"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@opencode-ai/plugin": ">=1.18.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@opencode-ai/plugin": "1.18.31",
|
|
35
|
+
"@types/node": "^24.0.0",
|
|
36
|
+
"tsup": "^8.5.0",
|
|
37
|
+
"tsx": "^4.20.0",
|
|
38
|
+
"typescript": "^5.9.0"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"opencode",
|
|
42
|
+
"opencode-plugin",
|
|
43
|
+
"subagent",
|
|
44
|
+
"model-routing",
|
|
45
|
+
"jev"
|
|
46
|
+
]
|
|
47
|
+
}
|