@opeoginni/opencode-copilot-auto 0.1.8 → 1.0.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 +42 -12
- package/dist/auto.js +52 -0
- package/dist/auto.js.map +1 -0
- package/dist/cache.js +29 -0
- package/dist/cache.js.map +1 -0
- package/dist/index.js +94 -400
- package/dist/index.js.map +1 -0
- package/dist/package.json +14 -0
- package/dist/prompt.js +35 -0
- package/dist/prompt.js.map +1 -0
- package/dist/router.js +69 -0
- package/dist/router.js.map +1 -0
- package/dist/rpc.js +23 -0
- package/dist/rpc.js.map +1 -0
- package/dist/tui.js +21 -0
- package/dist/tui.js.map +1 -0
- package/package.json +23 -22
- package/dist/index.d.ts +0 -3
package/README.md
CHANGED
|
@@ -1,33 +1,63 @@
|
|
|
1
1
|
# @opeoginni/opencode-copilot-auto
|
|
2
2
|
|
|
3
|
-
Adds GitHub Copilot's **Auto** model to OpenCode. Copilot
|
|
3
|
+
Adds GitHub Copilot's **Auto** model to OpenCode V2. Pick `github-copilot/auto` and Copilot chooses which of your available models handles each prompt, the same way Auto works in VS Code.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Requires OpenCode `2.x`. For OpenCode 1, use `0.1.x`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Setup
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Connect GitHub Copilot in OpenCode (`/connect`), then add the plugin to `opencode.jsonc`:
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
10
12
|
{
|
|
11
13
|
"$schema": "https://opencode.ai/config.json",
|
|
12
|
-
"
|
|
14
|
+
"plugins": ["@opeoginni/opencode-copilot-auto"]
|
|
13
15
|
}
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
Restart OpenCode
|
|
18
|
+
Restart OpenCode and select **Auto** under GitHub Copilot in the model picker. It only appears once Copilot is connected.
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
Each request to `auto` is sent to Copilot's routing endpoint, which picks a model from those available on your plan. The plugin then makes the real request with that model over its native protocol (Responses for GPT-5 and newer, Chat Completions otherwise), so tools, images and reasoning behave exactly as they do when you select the model directly.
|
|
23
|
+
|
|
24
|
+
Routing happens once per user prompt; tool calls within the same turn reuse the choice.
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
## Options
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
{
|
|
30
|
+
"plugins": [
|
|
31
|
+
{
|
|
32
|
+
"package": "@opeoginni/opencode-copilot-auto",
|
|
33
|
+
"options": { "sticky": true, "notifications": true }
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
19
38
|
|
|
20
|
-
|
|
39
|
+
| Option | Default | Description |
|
|
40
|
+
| --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
41
|
+
| `sticky` | `false` | `false`: Copilot picks a model for every prompt. `true`: the first model Copilot picks is kept for the whole session. |
|
|
42
|
+
| `notifications` | `false` | Show a toast naming the model Copilot picked. Fires once per prompt, or once per session when `sticky` is on. Mostly useful while developing. |
|
|
21
43
|
|
|
22
|
-
|
|
23
|
-
- `/copilot-autorefresh`: Toggles fresh model selection for every prompt. Run it again to resume using the cached routing session.
|
|
24
|
-
- `/copilot-notify`: Toggles notifications between a toast and the projection bus.
|
|
44
|
+
OpenCode does not record which model answered on the message itself, so the toast is the only place the choice is visible.
|
|
25
45
|
|
|
26
46
|
## Development
|
|
27
47
|
|
|
28
48
|
```sh
|
|
29
49
|
bun install
|
|
30
50
|
bun run check
|
|
31
|
-
bun
|
|
51
|
+
bun test
|
|
32
52
|
bun run build
|
|
33
53
|
```
|
|
54
|
+
|
|
55
|
+
To try the plugin locally without publishing, copy the example config, build, and start an isolated OpenCode in this directory:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
cp opencode.example.jsonc opencode.jsonc
|
|
59
|
+
bun run build
|
|
60
|
+
opencode --standalone
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`opencode.jsonc` points at `./dist`, so rebuild after changes. Running with `--standalone` keeps the test server separate from your regular OpenCode service.
|
package/dist/auto.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { lastUserPrompt } from "./prompt.js";
|
|
2
|
+
const PROVIDER_ID = "github-copilot";
|
|
3
|
+
const MODEL_ID = "auto";
|
|
4
|
+
const COPILOT_PACKAGE = "aisdk:@ai-sdk/github-copilot";
|
|
5
|
+
function isCopilotSDK(sdk) {
|
|
6
|
+
return (typeof sdk === "object" || typeof sdk === "function") && sdk !== null && typeof sdk.chat === "function" && typeof sdk.responses === "function";
|
|
7
|
+
}
|
|
8
|
+
function endpointFor(model, endpoints) {
|
|
9
|
+
const known = endpoints.get(model);
|
|
10
|
+
if (known === "responses") return "responses";
|
|
11
|
+
if (known === "chat" || known === "messages") return "chat";
|
|
12
|
+
if (model.startsWith("mai-")) return "responses";
|
|
13
|
+
const match = /^gpt-(\d+)/.exec(model);
|
|
14
|
+
return match && Number(match[1]) >= 5 && !model.startsWith("gpt-5-mini") ? "responses" : "chat";
|
|
15
|
+
}
|
|
16
|
+
function autoModel(input) {
|
|
17
|
+
const resolve = async (options) => {
|
|
18
|
+
const decision = await input.decide(lastUserPrompt(options.prompt));
|
|
19
|
+
const endpoint = endpointFor(decision.model, input.endpoints);
|
|
20
|
+
const model = endpoint === "responses" ? input.sdk.responses(decision.model) : input.sdk.chat(decision.model);
|
|
21
|
+
return {
|
|
22
|
+
model,
|
|
23
|
+
options: {
|
|
24
|
+
...options,
|
|
25
|
+
headers: { ...options.headers, "copilot-session-token": decision.token }
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
specificationVersion: "v3",
|
|
31
|
+
provider: PROVIDER_ID,
|
|
32
|
+
modelId: MODEL_ID,
|
|
33
|
+
supportedUrls: {},
|
|
34
|
+
async doGenerate(options) {
|
|
35
|
+
const next = await resolve(options);
|
|
36
|
+
return next.model.doGenerate(next.options);
|
|
37
|
+
},
|
|
38
|
+
async doStream(options) {
|
|
39
|
+
const next = await resolve(options);
|
|
40
|
+
return next.model.doStream(next.options);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
COPILOT_PACKAGE,
|
|
46
|
+
MODEL_ID,
|
|
47
|
+
PROVIDER_ID,
|
|
48
|
+
autoModel,
|
|
49
|
+
endpointFor,
|
|
50
|
+
isCopilotSDK
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=auto.js.map
|
package/dist/auto.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auto.ts"],"sourcesContent":["import type { LanguageModelV3, LanguageModelV3CallOptions } from \"@ai-sdk/provider\"\nimport { lastUserPrompt, type Prompt } from \"./prompt.js\"\nimport type { Decision } from \"./router.js\"\n\nexport const PROVIDER_ID = \"github-copilot\"\nexport const MODEL_ID = \"auto\"\nexport const COPILOT_PACKAGE = \"aisdk:@ai-sdk/github-copilot\"\n\n/** The SDK OpenCode's built-in Copilot plugin creates for `@ai-sdk/github-copilot` models. */\nexport type CopilotSDK = {\n chat: (modelID: string) => LanguageModelV3\n responses: (modelID: string) => LanguageModelV3\n}\n\n// AI SDK providers are callable functions with model factories attached as properties.\nexport function isCopilotSDK(sdk: unknown): sdk is CopilotSDK {\n return (\n (typeof sdk === \"object\" || typeof sdk === \"function\") &&\n sdk !== null &&\n typeof (sdk as CopilotSDK).chat === \"function\" &&\n typeof (sdk as CopilotSDK).responses === \"function\"\n )\n}\n\n/**\n * Mirrors the built-in Copilot plugin: use the endpoint Copilot advertises for\n * the model, otherwise GPT-5 and newer speak Responses. `/v1/messages` models\n * fall back to chat because the OpenAI-compatible SDK cannot speak it.\n */\nexport function endpointFor(model: string, endpoints: ReadonlyMap<string, string>): \"chat\" | \"responses\" {\n const known = endpoints.get(model)\n if (known === \"responses\") return \"responses\"\n if (known === \"chat\" || known === \"messages\") return \"chat\"\n if (model.startsWith(\"mai-\")) return \"responses\"\n const match = /^gpt-(\\d+)/.exec(model)\n return match && Number(match[1]) >= 5 && !model.startsWith(\"gpt-5-mini\") ? \"responses\" : \"chat\"\n}\n\nexport type AutoModelInput = {\n sdk: CopilotSDK\n endpoints: ReadonlyMap<string, string>\n decide: (prompt: Prompt) => Promise<Decision>\n}\n\n/**\n * A language model that asks Copilot which model to use, then delegates the\n * call to that model's native protocol.\n */\nexport function autoModel(input: AutoModelInput): LanguageModelV3 {\n const resolve = async (options: LanguageModelV3CallOptions) => {\n const decision = await input.decide(lastUserPrompt(options.prompt))\n const endpoint = endpointFor(decision.model, input.endpoints)\n const model = endpoint === \"responses\" ? input.sdk.responses(decision.model) : input.sdk.chat(decision.model)\n return {\n model,\n options: {\n ...options,\n headers: { ...options.headers, \"copilot-session-token\": decision.token },\n },\n }\n }\n\n return {\n specificationVersion: \"v3\",\n provider: PROVIDER_ID,\n modelId: MODEL_ID,\n supportedUrls: {},\n async doGenerate(options) {\n const next = await resolve(options)\n return next.model.doGenerate(next.options)\n },\n async doStream(options) {\n const next = await resolve(options)\n return next.model.doStream(next.options)\n },\n }\n}\n"],"mappings":"AACA,SAAS,sBAAmC;AAGrC,MAAM,cAAc;AACpB,MAAM,WAAW;AACjB,MAAM,kBAAkB;AASxB,SAAS,aAAa,KAAiC;AAC5D,UACG,OAAO,QAAQ,YAAY,OAAO,QAAQ,eAC3C,QAAQ,QACR,OAAQ,IAAmB,SAAS,cACpC,OAAQ,IAAmB,cAAc;AAE7C;AAOO,SAAS,YAAY,OAAe,WAA8D;AACvG,QAAM,QAAQ,UAAU,IAAI,KAAK;AACjC,MAAI,UAAU,YAAa,QAAO;AAClC,MAAI,UAAU,UAAU,UAAU,WAAY,QAAO;AACrD,MAAI,MAAM,WAAW,MAAM,EAAG,QAAO;AACrC,QAAM,QAAQ,aAAa,KAAK,KAAK;AACrC,SAAO,SAAS,OAAO,MAAM,CAAC,CAAC,KAAK,KAAK,CAAC,MAAM,WAAW,YAAY,IAAI,cAAc;AAC3F;AAYO,SAAS,UAAU,OAAwC;AAChE,QAAM,UAAU,OAAO,YAAwC;AAC7D,UAAM,WAAW,MAAM,MAAM,OAAO,eAAe,QAAQ,MAAM,CAAC;AAClE,UAAM,WAAW,YAAY,SAAS,OAAO,MAAM,SAAS;AAC5D,UAAM,QAAQ,aAAa,cAAc,MAAM,IAAI,UAAU,SAAS,KAAK,IAAI,MAAM,IAAI,KAAK,SAAS,KAAK;AAC5G,WAAO;AAAA,MACL;AAAA,MACA,SAAS;AAAA,QACP,GAAG;AAAA,QACH,SAAS,EAAE,GAAG,QAAQ,SAAS,yBAAyB,SAAS,MAAM;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,sBAAsB;AAAA,IACtB,UAAU;AAAA,IACV,SAAS;AAAA,IACT,eAAe,CAAC;AAAA,IAChB,MAAM,WAAW,SAAS;AACxB,YAAM,OAAO,MAAM,QAAQ,OAAO;AAClC,aAAO,KAAK,MAAM,WAAW,KAAK,OAAO;AAAA,IAC3C;AAAA,IACA,MAAM,SAAS,SAAS;AACtB,YAAM,OAAO,MAAM,QAAQ,OAAO;AAClC,aAAO,KAAK,MAAM,SAAS,KAAK,OAAO;AAAA,IACzC;AAAA,EACF;AACF;","names":[]}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class Cache {
|
|
2
|
+
constructor(limit) {
|
|
3
|
+
this.limit = limit;
|
|
4
|
+
}
|
|
5
|
+
limit;
|
|
6
|
+
entries = /* @__PURE__ */ new Map();
|
|
7
|
+
get(key) {
|
|
8
|
+
return this.entries.get(key);
|
|
9
|
+
}
|
|
10
|
+
set(key, value) {
|
|
11
|
+
this.entries.delete(key);
|
|
12
|
+
this.entries.set(key, value);
|
|
13
|
+
if (this.entries.size > this.limit) {
|
|
14
|
+
const oldest = this.entries.keys().next();
|
|
15
|
+
if (!oldest.done) this.entries.delete(oldest.value);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
delete(key) {
|
|
20
|
+
this.entries.delete(key);
|
|
21
|
+
}
|
|
22
|
+
get size() {
|
|
23
|
+
return this.entries.size;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
Cache
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts"],"sourcesContent":["/** Small insertion-ordered cache that drops the oldest entry past `limit`. */\nexport class Cache<K, V> {\n private readonly entries = new Map<K, V>()\n\n constructor(private readonly limit: number) {}\n\n get(key: K): V | undefined {\n return this.entries.get(key)\n }\n\n set(key: K, value: V): V {\n this.entries.delete(key)\n this.entries.set(key, value)\n if (this.entries.size > this.limit) {\n const oldest = this.entries.keys().next()\n if (!oldest.done) this.entries.delete(oldest.value)\n }\n return value\n }\n\n delete(key: K) {\n this.entries.delete(key)\n }\n\n get size() {\n return this.entries.size\n }\n}\n"],"mappings":"AACO,MAAM,MAAY;AAAA,EAGvB,YAA6B,OAAe;AAAf;AAAA,EAAgB;AAAA,EAAhB;AAAA,EAFZ,UAAU,oBAAI,IAAU;AAAA,EAIzC,IAAI,KAAuB;AACzB,WAAO,KAAK,QAAQ,IAAI,GAAG;AAAA,EAC7B;AAAA,EAEA,IAAI,KAAQ,OAAa;AACvB,SAAK,QAAQ,OAAO,GAAG;AACvB,SAAK,QAAQ,IAAI,KAAK,KAAK;AAC3B,QAAI,KAAK,QAAQ,OAAO,KAAK,OAAO;AAClC,YAAM,SAAS,KAAK,QAAQ,KAAK,EAAE,KAAK;AACxC,UAAI,CAAC,OAAO,KAAM,MAAK,QAAQ,OAAO,OAAO,KAAK;AAAA,IACpD;AACA,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,KAAQ;AACb,SAAK,QAAQ,OAAO,GAAG;AAAA,EACzB;AAAA,EAEA,IAAI,OAAO;AACT,WAAO,KAAK,QAAQ;AAAA,EACtB;AACF;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,407 +1,101 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}).catch(() => {});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function makeTextPart(sessionID, text) {
|
|
32
|
-
return {
|
|
33
|
-
id: crypto.randomUUID(),
|
|
34
|
-
sessionID,
|
|
35
|
-
messageID: crypto.randomUUID(),
|
|
36
|
-
type: "text",
|
|
37
|
-
text
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
var CopilotAutoPlugin = async (input) => {
|
|
41
|
-
const client = input.client;
|
|
42
|
-
installFetchAdapter(client);
|
|
43
|
-
const notifyClient = (message) => notify(client, message);
|
|
44
|
-
return {
|
|
45
|
-
provider: {
|
|
46
|
-
id: "github-copilot",
|
|
47
|
-
models: async (provider) => ({ ...provider.models, auto: autoModel() })
|
|
48
|
-
},
|
|
49
|
-
config: async (input2) => {
|
|
50
|
-
input2.command ??= {};
|
|
51
|
-
input2.command["copilot-refresh"] ??= {
|
|
52
|
-
template: "/copilot-refresh",
|
|
53
|
-
description: "Clear Copilot Auto routing cache so the next prompt re-selects a model"
|
|
54
|
-
};
|
|
55
|
-
input2.command["copilot-autorefresh"] ??= {
|
|
56
|
-
template: "/copilot-autorefresh",
|
|
57
|
-
description: "Toggle automatic model re-selection on every prompt"
|
|
58
|
-
};
|
|
59
|
-
input2.command["copilot-notify"] ??= {
|
|
60
|
-
template: "/copilot-notify",
|
|
61
|
-
description: "Toggle between toast and projection bus notifications"
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
"command.execute.before": async (input2, output) => {
|
|
65
|
-
if (input2.command === "copilot-refresh") {
|
|
66
|
-
sessions.clear();
|
|
67
|
-
await notifyClient("Routing cache cleared. Next prompt will select a fresh model.");
|
|
68
|
-
output.parts.length = 0;
|
|
69
|
-
output.parts.push(makeTextPart(input2.sessionID, "Copilot Auto routing cache cleared. The next prompt will select a fresh model."));
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (input2.command === "copilot-autorefresh") {
|
|
73
|
-
autoRefresh = !autoRefresh;
|
|
74
|
-
await notifyClient(autoRefresh ? "Refresh enabled. Every prompt will select a fresh model." : "Refresh disabled. Reusing cached routing session.");
|
|
75
|
-
output.parts.length = 0;
|
|
76
|
-
output.parts.push(makeTextPart(input2.sessionID, autoRefresh ? "Copilot Auto refresh enabled. Every prompt will select a fresh model." : "Copilot Auto refresh disabled. Reusing cached routing session."));
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (input2.command === "copilot-notify") {
|
|
80
|
-
notifyMode = notifyMode === "toast" ? "projection" : "toast";
|
|
81
|
-
await notifyClient(`Notification mode: ${notifyMode}`);
|
|
82
|
-
output.parts.length = 0;
|
|
83
|
-
output.parts.push(makeTextPart(input2.sessionID, `Copilot Auto notification mode: ${notifyMode}`));
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
|
-
function autoModel() {
|
|
90
|
-
return {
|
|
91
|
-
id: "auto",
|
|
92
|
-
providerID: "github-copilot",
|
|
93
|
-
name: "Auto",
|
|
94
|
-
family: "gpt",
|
|
95
|
-
api: {
|
|
96
|
-
id: "auto",
|
|
97
|
-
url: COPILOT_BASE_URL,
|
|
98
|
-
npm: "@ai-sdk/github-copilot"
|
|
99
|
-
},
|
|
100
|
-
status: "active",
|
|
101
|
-
headers: {},
|
|
102
|
-
options: {},
|
|
103
|
-
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
|
104
|
-
limit: { context: 128000, input: 128000, output: 16384 },
|
|
105
|
-
capabilities: {
|
|
106
|
-
temperature: true,
|
|
107
|
-
reasoning: false,
|
|
108
|
-
attachment: true,
|
|
109
|
-
toolcall: true,
|
|
110
|
-
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
111
|
-
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
112
|
-
interleaved: false
|
|
113
|
-
},
|
|
114
|
-
release_date: "",
|
|
115
|
-
variants: {}
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function installFetchAdapter(client) {
|
|
119
|
-
const marker = Symbol.for("opeoginni.opencode-copilot-auto.fetch-adapter");
|
|
120
|
-
const runtime = globalThis;
|
|
121
|
-
if (runtime[marker])
|
|
122
|
-
return;
|
|
123
|
-
runtime[marker] = true;
|
|
124
|
-
const originalFetch = globalThis.fetch.bind(globalThis);
|
|
125
|
-
const adapter = async (input, init) => {
|
|
126
|
-
const request = new Request(input, init);
|
|
127
|
-
if (!isAutoRequest(request))
|
|
128
|
-
return originalFetch(input, init);
|
|
129
|
-
const body = await request.clone().text();
|
|
130
|
-
const payload = parseJson(body);
|
|
131
|
-
if (!payload || payload.model !== "auto")
|
|
132
|
-
return originalFetch(input, init);
|
|
133
|
-
if (autoRefresh)
|
|
134
|
-
sessions.clear();
|
|
135
|
-
const session = await getSession(originalFetch, request.headers);
|
|
136
|
-
const model = await route(originalFetch, request.headers, session, payload);
|
|
137
|
-
await notify(client, `Routed to ${model}`);
|
|
138
|
-
const useResponses = usesResponses(model);
|
|
139
|
-
const headers = new Headers(request.headers);
|
|
140
|
-
headers.set("copilot-session-token", session.token);
|
|
141
|
-
headers.set("X-GitHub-Api-Version", COPILOT_API_VERSION);
|
|
142
|
-
const next = useResponses ? toResponsesRequest(payload, model) : { ...payload, model };
|
|
143
|
-
const url = useResponses ? toResponsesUrl(request.url) : request.url;
|
|
144
|
-
const response = await originalFetch(new Request(url, {
|
|
145
|
-
method: request.method,
|
|
146
|
-
headers,
|
|
147
|
-
body: JSON.stringify(next),
|
|
148
|
-
signal: request.signal
|
|
149
|
-
}));
|
|
150
|
-
return useResponses ? wrapResponsesResponse(response) : response;
|
|
151
|
-
};
|
|
152
|
-
globalThis.fetch = Object.assign(adapter, originalFetch);
|
|
153
|
-
}
|
|
154
|
-
function isAutoRequest(request) {
|
|
155
|
-
const url = new URL(request.url);
|
|
156
|
-
return url.origin === COPILOT_BASE_URL && request.method === "POST" && (url.pathname.endsWith("/chat/completions") || url.pathname.endsWith("/responses"));
|
|
157
|
-
}
|
|
158
|
-
function usesResponses(modelID) {
|
|
159
|
-
const match = /^gpt-(\d+)/.exec(modelID);
|
|
160
|
-
return Boolean(match && Number(match[1]) >= 5);
|
|
161
|
-
}
|
|
162
|
-
function toResponsesUrl(url) {
|
|
163
|
-
return url.replace(/\/chat\/completions\/?$/, "/responses");
|
|
164
|
-
}
|
|
165
|
-
function toResponsesRequest(payload, model) {
|
|
166
|
-
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
167
|
-
const instructions = messages.filter((m) => isRecord(m) && m.role === "system").map((m) => isRecord(m) && typeof m.content === "string" ? m.content : "").filter(Boolean).join(`
|
|
168
|
-
`);
|
|
169
|
-
const input = messages.filter((m) => isRecord(m) && m.role !== "system").flatMap((m) => {
|
|
170
|
-
const msg = m;
|
|
171
|
-
const role = msg.role;
|
|
172
|
-
const content = msg.content;
|
|
173
|
-
if (role === "tool") {
|
|
174
|
-
return [{
|
|
175
|
-
type: "function_call_output",
|
|
176
|
-
call_id: msg.tool_call_id,
|
|
177
|
-
output: typeof content === "string" ? content : JSON.stringify(content)
|
|
178
|
-
}];
|
|
179
|
-
}
|
|
180
|
-
if (role === "assistant" && Array.isArray(msg.tool_calls)) {
|
|
181
|
-
const items = msg.tool_calls.map((tc) => {
|
|
182
|
-
if (!isRecord(tc) || !isRecord(tc.function))
|
|
183
|
-
return null;
|
|
184
|
-
return {
|
|
185
|
-
type: "function_call",
|
|
186
|
-
call_id: tc.id,
|
|
187
|
-
name: tc.function.name,
|
|
188
|
-
arguments: tc.function.arguments
|
|
189
|
-
};
|
|
190
|
-
}).filter((x) => x !== null);
|
|
191
|
-
if (typeof content === "string" && content) {
|
|
192
|
-
items.unshift({
|
|
193
|
-
role: "assistant",
|
|
194
|
-
content: [{ type: "output_text", text: content }]
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
return items;
|
|
198
|
-
}
|
|
199
|
-
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => isRecord(part) && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
200
|
-
`) : "";
|
|
201
|
-
return [{
|
|
202
|
-
role,
|
|
203
|
-
content: [{ type: role === "user" ? "input_text" : "output_text", text }]
|
|
204
|
-
}];
|
|
205
|
-
});
|
|
206
|
-
return {
|
|
207
|
-
model,
|
|
208
|
-
input,
|
|
209
|
-
stream: payload.stream === true,
|
|
210
|
-
...instructions ? { instructions } : {},
|
|
211
|
-
...typeof payload.temperature === "number" ? { temperature: payload.temperature } : {},
|
|
212
|
-
...typeof payload.top_p === "number" ? { top_p: payload.top_p } : {},
|
|
213
|
-
...typeof payload.max_tokens === "number" ? { max_output_tokens: payload.max_tokens } : typeof payload.max_completion_tokens === "number" ? { max_output_tokens: payload.max_completion_tokens } : {},
|
|
214
|
-
...Array.isArray(payload.tools) ? { tools: payload.tools.map(unwrapFunction) } : {},
|
|
215
|
-
...payload.tool_choice !== undefined ? { tool_choice: unwrapFunction(payload.tool_choice) } : {}
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
function unwrapFunction(value) {
|
|
219
|
-
if (!isRecord(value) || !isRecord(value.function))
|
|
220
|
-
return value;
|
|
221
|
-
const { function: fn, ...rest } = value;
|
|
222
|
-
return { ...fn, ...rest };
|
|
223
|
-
}
|
|
224
|
-
function wrapResponsesResponse(response) {
|
|
225
|
-
const chunkId = `chatcmpl-auto-${Date.now()}`;
|
|
226
|
-
const decoder = new TextDecoder;
|
|
227
|
-
const encoder = new TextEncoder;
|
|
228
|
-
let buffer = "";
|
|
229
|
-
let toolCallIndex = -1;
|
|
230
|
-
const transformed = new ReadableStream({
|
|
231
|
-
start(controller) {
|
|
232
|
-
const reader = response.body?.getReader();
|
|
233
|
-
if (!reader) {
|
|
234
|
-
controller.close();
|
|
235
|
-
return;
|
|
1
|
+
import { Plugin } from "@opencode/plugin";
|
|
2
|
+
import { autoModel, COPILOT_PACKAGE, isCopilotSDK, MODEL_ID, PROVIDER_ID } from "./auto.js";
|
|
3
|
+
import { Cache } from "./cache.js";
|
|
4
|
+
import { fingerprint, lastUserPrompt } from "./prompt.js";
|
|
5
|
+
import { Router } from "./router.js";
|
|
6
|
+
import { CopilotAuto } from "./rpc.js";
|
|
7
|
+
const DEFAULT_BASE_URL = "https://api.githubcopilot.com";
|
|
8
|
+
var index_default = Plugin.define({
|
|
9
|
+
id: "opencode-copilot-auto",
|
|
10
|
+
async setup(ctx) {
|
|
11
|
+
const sticky = ctx.options.sticky === true;
|
|
12
|
+
const notifications = ctx.options.notifications === true;
|
|
13
|
+
const rpc = notifications ? await ctx.rpc.register(CopilotAuto, {}) : void 0;
|
|
14
|
+
const endpoints = /* @__PURE__ */ new Map();
|
|
15
|
+
const sessions = new Cache(500);
|
|
16
|
+
const models = new Cache(500);
|
|
17
|
+
const routers = /* @__PURE__ */ new Map();
|
|
18
|
+
await ctx.provider.transform((editor) => {
|
|
19
|
+
endpoints.clear();
|
|
20
|
+
const record = editor.get(PROVIDER_ID);
|
|
21
|
+
if (!record?.sourceConnection) return;
|
|
22
|
+
let template;
|
|
23
|
+
for (const model of record.models.values()) {
|
|
24
|
+
if (model.id === MODEL_ID) continue;
|
|
25
|
+
const endpoint = model.settings?.endpoint;
|
|
26
|
+
if (typeof endpoint === "string") endpoints.set(model.id, endpoint);
|
|
27
|
+
if (!template && model.package === COPILOT_PACKAGE) template = model;
|
|
236
28
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
29
|
+
if (!template) return;
|
|
30
|
+
const baseURL = template.settings?.baseURL;
|
|
31
|
+
editor.models.update(PROVIDER_ID, MODEL_ID, (model) => {
|
|
32
|
+
model.name = "Auto";
|
|
33
|
+
model.package = COPILOT_PACKAGE;
|
|
34
|
+
model.settings = { ...typeof baseURL === "string" ? { baseURL } : {}, endpoint: "chat" };
|
|
35
|
+
model.capabilities = { tools: true, input: ["text", "image"], output: ["text"] };
|
|
36
|
+
model.limit = { context: 128e3, output: 16384 };
|
|
37
|
+
model.enabled = true;
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const remember = (event) => {
|
|
41
|
+
if (event.model.id !== MODEL_ID) return;
|
|
42
|
+
sessions.set(fingerprint(lastUserPrompt(event.messages)), event.sessionID);
|
|
43
|
+
};
|
|
44
|
+
await ctx.session.hook("context", remember, { providerID: PROVIDER_ID });
|
|
45
|
+
await ctx.session.hook("compaction", remember, { providerID: PROVIDER_ID });
|
|
46
|
+
await ctx.session.hook("generate", remember, { providerID: PROVIDER_ID });
|
|
47
|
+
await ctx.session.hook("title", remember, { providerID: PROVIDER_ID });
|
|
48
|
+
const decide = async (router, prompt) => {
|
|
49
|
+
const prompted = fingerprint(prompt);
|
|
50
|
+
const sessionID = sessions.get(prompted);
|
|
51
|
+
const key = sticky ? sessionID ?? prompted : prompted;
|
|
52
|
+
const pending = models.get(key) ?? models.set(
|
|
53
|
+
key,
|
|
54
|
+
router.route(prompt).then((model) => {
|
|
55
|
+
void rpc?.events.emit("routed", { model, ...sessionID ? { sessionID } : {} }).catch(() => {
|
|
56
|
+
});
|
|
57
|
+
return model;
|
|
58
|
+
}).catch((error) => {
|
|
59
|
+
models.delete(key);
|
|
60
|
+
throw error;
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
return { model: await pending, token: await router.token() };
|
|
64
|
+
};
|
|
65
|
+
await ctx.aisdk.hook(
|
|
66
|
+
"language",
|
|
67
|
+
(event) => {
|
|
68
|
+
if (event.model.providerID !== PROVIDER_ID || event.model.id !== MODEL_ID) return;
|
|
69
|
+
if (!isCopilotSDK(event.sdk)) {
|
|
70
|
+
console.error(`[copilot-auto] unexpected Copilot SDK shape, leaving model untouched`);
|
|
255
71
|
return;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
id: event.item.call_id,
|
|
267
|
-
type: "function",
|
|
268
|
-
function: { name: event.item.name, arguments: "" }
|
|
269
|
-
}]
|
|
270
|
-
});
|
|
271
|
-
} else if (type === "response.function_call_arguments.delta") {
|
|
272
|
-
emitChunk({
|
|
273
|
-
tool_calls: [{
|
|
274
|
-
index: toolCallIndex,
|
|
275
|
-
function: { arguments: event.delta }
|
|
276
|
-
}]
|
|
277
|
-
});
|
|
278
|
-
} else if (type === "response.completed") {
|
|
279
|
-
emitChunk({}, "stop");
|
|
280
|
-
controller.enqueue(encoder.encode(`data: [DONE]
|
|
281
|
-
|
|
282
|
-
`));
|
|
283
|
-
}
|
|
284
|
-
} catch {}
|
|
285
|
-
}
|
|
286
|
-
function pump() {
|
|
287
|
-
return stream.read().then(({ done, value }) => {
|
|
288
|
-
if (done) {
|
|
289
|
-
if (buffer) {
|
|
290
|
-
for (const line of buffer.split(`
|
|
291
|
-
`))
|
|
292
|
-
processLine(line);
|
|
293
|
-
}
|
|
294
|
-
controller.close();
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
buffer += decoder.decode(value, { stream: true });
|
|
298
|
-
const lines = buffer.split(`
|
|
299
|
-
`);
|
|
300
|
-
buffer = lines.pop() ?? "";
|
|
301
|
-
for (const line of lines)
|
|
302
|
-
processLine(line);
|
|
303
|
-
return pump();
|
|
72
|
+
}
|
|
73
|
+
const baseURL = typeof event.options.baseURL === "string" ? event.options.baseURL : DEFAULT_BASE_URL;
|
|
74
|
+
const apiKey = typeof event.options.apiKey === "string" ? event.options.apiKey : "";
|
|
75
|
+
const key = `${baseURL} ${apiKey}`;
|
|
76
|
+
const router = routers.get(key) ?? new Router(baseURL, fetcher(event.options));
|
|
77
|
+
routers.set(key, router);
|
|
78
|
+
event.language = autoModel({
|
|
79
|
+
sdk: event.sdk,
|
|
80
|
+
endpoints,
|
|
81
|
+
decide: (prompt) => decide(router, prompt)
|
|
304
82
|
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
});
|
|
309
|
-
return new Response(transformed, {
|
|
310
|
-
status: response.status,
|
|
311
|
-
statusText: response.statusText,
|
|
312
|
-
headers: new Headers({
|
|
313
|
-
"content-type": "text/event-stream",
|
|
314
|
-
"cache-control": "no-cache"
|
|
315
|
-
})
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
async function getSession(fetcher, requestHeaders) {
|
|
319
|
-
const key = requestHeaders.get("authorization") ?? "anonymous";
|
|
320
|
-
const cached = sessions.get(key);
|
|
321
|
-
if (cached && cached.expiresAt > Math.floor(Date.now() / 1000) + SESSION_REFRESH_BUFFER_SECONDS)
|
|
322
|
-
return cached;
|
|
323
|
-
const response = await fetcher(`${COPILOT_BASE_URL}/models/session`, {
|
|
324
|
-
method: "POST",
|
|
325
|
-
headers: copilotHeaders(requestHeaders),
|
|
326
|
-
body: JSON.stringify({ auto_mode: { model_hints: ["auto"] } }),
|
|
327
|
-
signal: AbortSignal.timeout(5000)
|
|
328
|
-
});
|
|
329
|
-
if (!response.ok)
|
|
330
|
-
throw new Error(`Copilot Auto could not create a routing session: ${response.status}`);
|
|
331
|
-
const data = await response.json();
|
|
332
|
-
const session = {
|
|
333
|
-
availableModels: data.available_models,
|
|
334
|
-
selectedModel: data.selected_model,
|
|
335
|
-
token: data.session_token,
|
|
336
|
-
expiresAt: data.expires_at
|
|
337
|
-
};
|
|
338
|
-
sessions.set(key, session);
|
|
339
|
-
return session;
|
|
340
|
-
}
|
|
341
|
-
async function route(fetcher, requestHeaders, session, payload) {
|
|
342
|
-
const messages = payload.messages ?? payload.input;
|
|
343
|
-
const prompt = promptText(messages);
|
|
344
|
-
const headers = copilotHeaders(requestHeaders);
|
|
345
|
-
headers.set("copilot-session-token", session.token);
|
|
346
|
-
const response = await fetcher(`${COPILOT_BASE_URL}/models/session/intent`, {
|
|
347
|
-
method: "POST",
|
|
348
|
-
headers,
|
|
349
|
-
body: JSON.stringify({
|
|
350
|
-
prompt,
|
|
351
|
-
available_models: session.availableModels,
|
|
352
|
-
has_image: false,
|
|
353
|
-
...HYDRA_ROUTING ? {
|
|
354
|
-
session_id: "opencode-session://auto",
|
|
355
|
-
reference_count: 0,
|
|
356
|
-
prompt_char_count: prompt.length,
|
|
357
|
-
turn_number: userTurns(messages),
|
|
358
|
-
routing_method: "hydra",
|
|
359
|
-
copilot_plan: "individual"
|
|
360
|
-
} : {}
|
|
361
|
-
}),
|
|
362
|
-
signal: AbortSignal.timeout(5000)
|
|
363
|
-
});
|
|
364
|
-
if (!response.ok)
|
|
365
|
-
throw new Error(`Copilot Auto could not select a model: ${response.status}`);
|
|
366
|
-
const intent = await response.json();
|
|
367
|
-
return intent.chosen_model ?? session.selectedModel;
|
|
368
|
-
}
|
|
369
|
-
function copilotHeaders(requestHeaders) {
|
|
370
|
-
const headers = new Headers(requestHeaders);
|
|
371
|
-
headers.set("Content-Type", "application/json");
|
|
372
|
-
headers.set("X-GitHub-Api-Version", COPILOT_API_VERSION);
|
|
373
|
-
return headers;
|
|
374
|
-
}
|
|
375
|
-
function parseJson(value) {
|
|
376
|
-
try {
|
|
377
|
-
const parsed = JSON.parse(value);
|
|
378
|
-
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : undefined;
|
|
379
|
-
} catch {
|
|
380
|
-
return;
|
|
83
|
+
},
|
|
84
|
+
{ providerID: PROVIDER_ID }
|
|
85
|
+
);
|
|
381
86
|
}
|
|
87
|
+
});
|
|
88
|
+
function fetcher(options) {
|
|
89
|
+
if (typeof options.fetch === "function") return options.fetch;
|
|
90
|
+
const apiKey = typeof options.apiKey === "string" ? options.apiKey : void 0;
|
|
91
|
+
if (!apiKey) return fetch;
|
|
92
|
+
return (input, init) => {
|
|
93
|
+
const headers = new Headers(init?.headers);
|
|
94
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
95
|
+
return fetch(input, { ...init, headers });
|
|
96
|
+
};
|
|
382
97
|
}
|
|
383
|
-
function promptText(messages) {
|
|
384
|
-
if (!Array.isArray(messages))
|
|
385
|
-
return "";
|
|
386
|
-
const message = [...messages].reverse().find((item) => isRecord(item) && item.role === "user");
|
|
387
|
-
if (!message)
|
|
388
|
-
return "";
|
|
389
|
-
const content = message.content;
|
|
390
|
-
if (typeof content === "string")
|
|
391
|
-
return content;
|
|
392
|
-
if (!Array.isArray(content))
|
|
393
|
-
return "";
|
|
394
|
-
return content.map((part) => isRecord(part) && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
395
|
-
`);
|
|
396
|
-
}
|
|
397
|
-
function userTurns(messages) {
|
|
398
|
-
return Array.isArray(messages) ? messages.filter((item) => isRecord(item) && item.role === "user").length : 0;
|
|
399
|
-
}
|
|
400
|
-
function isRecord(value) {
|
|
401
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
402
|
-
}
|
|
403
|
-
var src_default = CopilotAutoPlugin;
|
|
404
98
|
export {
|
|
405
|
-
|
|
406
|
-
CopilotAutoPlugin
|
|
99
|
+
index_default as default
|
|
407
100
|
};
|
|
101
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { Plugin, type Model } from \"@opencode/plugin\"\nimport { autoModel, COPILOT_PACKAGE, isCopilotSDK, MODEL_ID, PROVIDER_ID } from \"./auto.js\"\nimport { Cache } from \"./cache.js\"\nimport { fingerprint, lastUserPrompt, type Prompt } from \"./prompt.js\"\nimport { Router, type Decision, type Fetch } from \"./router.js\"\nimport { CopilotAuto } from \"./rpc.js\"\n\nconst DEFAULT_BASE_URL = \"https://api.githubcopilot.com\"\n\nexport default Plugin.define({\n id: \"opencode-copilot-auto\",\n async setup(ctx) {\n const sticky = ctx.options.sticky === true\n const notifications = ctx.options.notifications === true\n // The TUI half of this package listens for `routed` and shows a toast.\n const rpc = notifications ? await ctx.rpc.register(CopilotAuto, {}) : undefined\n\n // Endpoint Copilot advertises per model, captured from the provider inventory.\n const endpoints = new Map<string, string>()\n // Last user prompt -> session, so the model wrapper can tell sessions apart.\n const sessions = new Cache<string, string>(500)\n // Routing decisions keyed by session (sticky) or by prompt.\n const models = new Cache<string, Promise<string>>(500)\n const routers = new Map<string, Router>()\n\n await ctx.provider.transform((editor) => {\n endpoints.clear()\n const record = editor.get(PROVIDER_ID)\n // The built-in Copilot plugin binds the inventory to a connection only\n // after a successful login, so this is the \"user has authed\" signal.\n if (!record?.sourceConnection) return\n\n let template: Model.Info | undefined\n for (const model of record.models.values()) {\n if (model.id === MODEL_ID) continue\n const endpoint = model.settings?.endpoint\n if (typeof endpoint === \"string\") endpoints.set(model.id, endpoint)\n if (!template && model.package === COPILOT_PACKAGE) template = model\n }\n if (!template) return\n\n const baseURL = template.settings?.baseURL\n editor.models.update(PROVIDER_ID, MODEL_ID, (model) => {\n model.name = \"Auto\"\n model.package = COPILOT_PACKAGE\n model.settings = { ...(typeof baseURL === \"string\" ? { baseURL } : {}), endpoint: \"chat\" }\n model.capabilities = { tools: true, input: [\"text\", \"image\"], output: [\"text\"] }\n model.limit = { context: 128_000, output: 16_384 }\n model.enabled = true\n })\n })\n\n const remember = (event: { sessionID: string; model: Model.Ref; messages: unknown }) => {\n if (event.model.id !== MODEL_ID) return\n sessions.set(fingerprint(lastUserPrompt(event.messages)), event.sessionID)\n }\n await ctx.session.hook(\"context\", remember, { providerID: PROVIDER_ID })\n await ctx.session.hook(\"compaction\", remember, { providerID: PROVIDER_ID })\n await ctx.session.hook(\"generate\", remember, { providerID: PROVIDER_ID })\n await ctx.session.hook(\"title\", remember, { providerID: PROVIDER_ID })\n\n const decide = async (router: Router, prompt: Prompt): Promise<Decision> => {\n const prompted = fingerprint(prompt)\n const sessionID = sessions.get(prompted)\n const key = sticky ? (sessionID ?? prompted) : prompted\n const pending =\n models.get(key) ??\n models.set(\n key,\n router\n .route(prompt)\n .then((model) => {\n // Fresh decision: once per session when sticky, once per prompt otherwise.\n // Fire-and-forget so the model request never waits on the UI.\n void rpc?.events.emit(\"routed\", { model, ...(sessionID ? { sessionID } : {}) }).catch(() => {})\n return model\n })\n .catch((error: unknown) => {\n models.delete(key)\n throw error\n }),\n )\n return { model: await pending, token: await router.token() }\n }\n\n await ctx.aisdk.hook(\n \"language\",\n (event) => {\n if (event.model.providerID !== PROVIDER_ID || event.model.id !== MODEL_ID) return\n if (!isCopilotSDK(event.sdk)) {\n console.error(`[copilot-auto] unexpected Copilot SDK shape, leaving model untouched`)\n return\n }\n\n const baseURL = typeof event.options.baseURL === \"string\" ? event.options.baseURL : DEFAULT_BASE_URL\n const apiKey = typeof event.options.apiKey === \"string\" ? event.options.apiKey : \"\"\n const key = `${baseURL} ${apiKey}`\n const router = routers.get(key) ?? new Router(baseURL, fetcher(event.options))\n routers.set(key, router)\n\n event.language = autoModel({\n sdk: event.sdk,\n endpoints,\n decide: (prompt) => decide(router, prompt),\n })\n },\n { providerID: PROVIDER_ID },\n )\n },\n})\n\n/**\n * The built-in Copilot plugin installs an authenticated fetch on the model\n * options. Fall back to a bearer token when it is not there.\n */\nfunction fetcher(options: Record<string, unknown>): Fetch {\n if (typeof options.fetch === \"function\") return options.fetch as Fetch\n const apiKey = typeof options.apiKey === \"string\" ? options.apiKey : undefined\n if (!apiKey) return fetch\n return (input, init) => {\n const headers = new Headers(init?.headers)\n headers.set(\"Authorization\", `Bearer ${apiKey}`)\n return fetch(input, { ...init, headers })\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opeoginni/opencode-copilot-auto",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js",
|
|
8
|
+
"./rpc": "./rpc.js",
|
|
9
|
+
"./tui": "./tui.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@opencode/plugin": "^2.0.12"
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
function lastUserPrompt(messages) {
|
|
3
|
+
const message = lastUser(messages);
|
|
4
|
+
if (!message) return { text: "", image: false };
|
|
5
|
+
const content = message.content;
|
|
6
|
+
if (typeof content === "string") return { text: content, image: false };
|
|
7
|
+
if (!Array.isArray(content)) return { text: "", image: false };
|
|
8
|
+
const parts = content.filter(isRecord);
|
|
9
|
+
return {
|
|
10
|
+
text: parts.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n"),
|
|
11
|
+
image: parts.some(
|
|
12
|
+
(part) => (part.type === "file" || part.type === "media" || part.type === "image") && typeof part.mediaType === "string" && part.mediaType.startsWith("image/")
|
|
13
|
+
)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function fingerprint(prompt) {
|
|
17
|
+
return createHash("sha1").update(prompt.text).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
function lastUser(messages) {
|
|
20
|
+
if (!Array.isArray(messages)) return void 0;
|
|
21
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
22
|
+
const message = messages[i];
|
|
23
|
+
if (isRecord(message) && message.role === "user") return message;
|
|
24
|
+
}
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
fingerprint,
|
|
32
|
+
isRecord,
|
|
33
|
+
lastUserPrompt
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/prompt.ts"],"sourcesContent":["import { createHash } from \"node:crypto\"\n\n// Works for both OpenCode's internal messages (parts: text | media) and the\n// AI SDK LanguageModelV3 prompt (parts: text | file). Only the fields shared\n// by both shapes are read.\nexport type Prompt = {\n text: string\n image: boolean\n}\n\nexport function lastUserPrompt(messages: unknown): Prompt {\n const message = lastUser(messages)\n if (!message) return { text: \"\", image: false }\n const content = message.content\n if (typeof content === \"string\") return { text: content, image: false }\n if (!Array.isArray(content)) return { text: \"\", image: false }\n const parts = content.filter(isRecord)\n return {\n text: parts\n .filter((part) => part.type === \"text\" && typeof part.text === \"string\")\n .map((part) => part.text as string)\n .join(\"\\n\"),\n image: parts.some(\n (part) =>\n (part.type === \"file\" || part.type === \"media\" || part.type === \"image\") &&\n typeof part.mediaType === \"string\" &&\n part.mediaType.startsWith(\"image/\"),\n ),\n }\n}\n\nexport function fingerprint(prompt: Prompt): string {\n return createHash(\"sha1\").update(prompt.text).digest(\"hex\")\n}\n\nfunction lastUser(messages: unknown): Record<string, unknown> | undefined {\n if (!Array.isArray(messages)) return undefined\n for (let i = messages.length - 1; i >= 0; i--) {\n const message: unknown = messages[i]\n if (isRecord(message) && message.role === \"user\") return message\n }\n return undefined\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n"],"mappings":"AAAA,SAAS,kBAAkB;AAUpB,SAAS,eAAe,UAA2B;AACxD,QAAM,UAAU,SAAS,QAAQ;AACjC,MAAI,CAAC,QAAS,QAAO,EAAE,MAAM,IAAI,OAAO,MAAM;AAC9C,QAAM,UAAU,QAAQ;AACxB,MAAI,OAAO,YAAY,SAAU,QAAO,EAAE,MAAM,SAAS,OAAO,MAAM;AACtE,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO,EAAE,MAAM,IAAI,OAAO,MAAM;AAC7D,QAAM,QAAQ,QAAQ,OAAO,QAAQ;AACrC,SAAO;AAAA,IACL,MAAM,MACH,OAAO,CAAC,SAAS,KAAK,SAAS,UAAU,OAAO,KAAK,SAAS,QAAQ,EACtE,IAAI,CAAC,SAAS,KAAK,IAAc,EACjC,KAAK,IAAI;AAAA,IACZ,OAAO,MAAM;AAAA,MACX,CAAC,UACE,KAAK,SAAS,UAAU,KAAK,SAAS,WAAW,KAAK,SAAS,YAChE,OAAO,KAAK,cAAc,YAC1B,KAAK,UAAU,WAAW,QAAQ;AAAA,IACtC;AAAA,EACF;AACF;AAEO,SAAS,YAAY,QAAwB;AAClD,SAAO,WAAW,MAAM,EAAE,OAAO,OAAO,IAAI,EAAE,OAAO,KAAK;AAC5D;AAEA,SAAS,SAAS,UAAwD;AACxE,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO;AACrC,WAAS,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;AAC7C,UAAM,UAAmB,SAAS,CAAC;AACnC,QAAI,SAAS,OAAO,KAAK,QAAQ,SAAS,OAAQ,QAAO;AAAA,EAC3D;AACA,SAAO;AACT;AAEO,SAAS,SAAS,OAAkD;AACzE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;","names":[]}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const API_VERSION = "2026-08-01";
|
|
2
|
+
const REFRESH_BUFFER_SECONDS = 30;
|
|
3
|
+
const TIMEOUT_MS = 5e3;
|
|
4
|
+
class Router {
|
|
5
|
+
constructor(baseURL, call) {
|
|
6
|
+
this.baseURL = baseURL;
|
|
7
|
+
this.call = call;
|
|
8
|
+
}
|
|
9
|
+
baseURL;
|
|
10
|
+
call;
|
|
11
|
+
session;
|
|
12
|
+
/** Current routing session token. Refreshed when close to expiry. */
|
|
13
|
+
async token() {
|
|
14
|
+
return (await this.getSession()).token;
|
|
15
|
+
}
|
|
16
|
+
/** Ask Copilot which model should handle the prompt. */
|
|
17
|
+
async route(prompt) {
|
|
18
|
+
const session = await this.getSession();
|
|
19
|
+
const response = await this.call(`${this.baseURL}/models/session/intent`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: headers({ "copilot-session-token": session.token }),
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
prompt: prompt.text,
|
|
24
|
+
available_models: session.availableModels,
|
|
25
|
+
has_image: prompt.image
|
|
26
|
+
}),
|
|
27
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) throw new Error(`Copilot Auto could not select a model: ${response.status}`);
|
|
30
|
+
const intent = await response.json();
|
|
31
|
+
debug("intent", intent);
|
|
32
|
+
return intent.chosen_model ?? session.selectedModel;
|
|
33
|
+
}
|
|
34
|
+
async getSession() {
|
|
35
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
36
|
+
if (this.session && this.session.expiresAt > now + REFRESH_BUFFER_SECONDS) return this.session;
|
|
37
|
+
const response = await this.call(`${this.baseURL}/models/session`, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: headers(),
|
|
40
|
+
body: JSON.stringify({ auto_mode: { model_hints: ["auto"] } }),
|
|
41
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) throw new Error(`Copilot Auto could not create a routing session: ${response.status}`);
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
debug("session", { ...data, session_token: "<redacted>" });
|
|
46
|
+
this.session = {
|
|
47
|
+
availableModels: data.available_models,
|
|
48
|
+
selectedModel: data.selected_model,
|
|
49
|
+
token: data.session_token,
|
|
50
|
+
expiresAt: data.expires_at
|
|
51
|
+
};
|
|
52
|
+
return this.session;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function debug(label, value) {
|
|
56
|
+
if (!process.env.OPENCODE_COPILOT_AUTO_DEBUG) return;
|
|
57
|
+
console.error(`[copilot-auto] ${label} ${JSON.stringify(value)}`);
|
|
58
|
+
}
|
|
59
|
+
function headers(extra = {}) {
|
|
60
|
+
return {
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
"X-GitHub-Api-Version": API_VERSION,
|
|
63
|
+
...extra
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
Router
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/router.ts"],"sourcesContent":["import type { Prompt } from \"./prompt.js\"\n\nconst API_VERSION = \"2026-08-01\"\nconst REFRESH_BUFFER_SECONDS = 30\nconst TIMEOUT_MS = 5_000\n\nexport type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>\n\nexport type Decision = {\n model: string\n token: string\n}\n\ntype CopilotSession = {\n availableModels: string[]\n selectedModel: string\n token: string\n expiresAt: number\n}\n\n/**\n * Talks to Copilot's routing endpoints. The fetch passed in must already\n * carry Copilot authentication; OpenCode's built-in Copilot plugin provides\n * one on the model options.\n */\nexport class Router {\n private session?: CopilotSession\n\n constructor(\n private readonly baseURL: string,\n private readonly call: Fetch,\n ) {}\n\n /** Current routing session token. Refreshed when close to expiry. */\n async token(): Promise<string> {\n return (await this.getSession()).token\n }\n\n /** Ask Copilot which model should handle the prompt. */\n async route(prompt: Prompt): Promise<string> {\n const session = await this.getSession()\n const response = await this.call(`${this.baseURL}/models/session/intent`, {\n method: \"POST\",\n headers: headers({ \"copilot-session-token\": session.token }),\n body: JSON.stringify({\n prompt: prompt.text,\n available_models: session.availableModels,\n has_image: prompt.image,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n })\n if (!response.ok) throw new Error(`Copilot Auto could not select a model: ${response.status}`)\n const intent = (await response.json()) as { chosen_model?: string }\n debug(\"intent\", intent)\n return intent.chosen_model ?? session.selectedModel\n }\n\n private async getSession(): Promise<CopilotSession> {\n const now = Math.floor(Date.now() / 1000)\n if (this.session && this.session.expiresAt > now + REFRESH_BUFFER_SECONDS) return this.session\n\n const response = await this.call(`${this.baseURL}/models/session`, {\n method: \"POST\",\n headers: headers(),\n body: JSON.stringify({ auto_mode: { model_hints: [\"auto\"] } }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n })\n if (!response.ok) throw new Error(`Copilot Auto could not create a routing session: ${response.status}`)\n const data = (await response.json()) as {\n available_models: string[]\n selected_model: string\n session_token: string\n expires_at: number\n }\n debug(\"session\", { ...data, session_token: \"<redacted>\" })\n this.session = {\n availableModels: data.available_models,\n selectedModel: data.selected_model,\n token: data.session_token,\n expiresAt: data.expires_at,\n }\n return this.session\n }\n}\n\n// Set OPENCODE_COPILOT_AUTO_DEBUG=1 on the server to print Copilot's raw routing responses.\nfunction debug(label: string, value: unknown) {\n if (!process.env.OPENCODE_COPILOT_AUTO_DEBUG) return\n console.error(`[copilot-auto] ${label} ${JSON.stringify(value)}`)\n}\n\nfunction headers(extra: Record<string, string> = {}) {\n return {\n \"Content-Type\": \"application/json\",\n \"X-GitHub-Api-Version\": API_VERSION,\n ...extra,\n }\n}\n"],"mappings":"AAEA,MAAM,cAAc;AACpB,MAAM,yBAAyB;AAC/B,MAAM,aAAa;AAqBZ,MAAM,OAAO;AAAA,EAGlB,YACmB,SACA,MACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA,EAJX;AAAA;AAAA,EAQR,MAAM,QAAyB;AAC7B,YAAQ,MAAM,KAAK,WAAW,GAAG;AAAA,EACnC;AAAA;AAAA,EAGA,MAAM,MAAM,QAAiC;AAC3C,UAAM,UAAU,MAAM,KAAK,WAAW;AACtC,UAAM,WAAW,MAAM,KAAK,KAAK,GAAG,KAAK,OAAO,0BAA0B;AAAA,MACxE,QAAQ;AAAA,MACR,SAAS,QAAQ,EAAE,yBAAyB,QAAQ,MAAM,CAAC;AAAA,MAC3D,MAAM,KAAK,UAAU;AAAA,QACnB,QAAQ,OAAO;AAAA,QACf,kBAAkB,QAAQ;AAAA,QAC1B,WAAW,OAAO;AAAA,MACpB,CAAC;AAAA,MACD,QAAQ,YAAY,QAAQ,UAAU;AAAA,IACxC,CAAC;AACD,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,0CAA0C,SAAS,MAAM,EAAE;AAC7F,UAAM,SAAU,MAAM,SAAS,KAAK;AACpC,UAAM,UAAU,MAAM;AACtB,WAAO,OAAO,gBAAgB,QAAQ;AAAA,EACxC;AAAA,EAEA,MAAc,aAAsC;AAClD,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAI,KAAK,WAAW,KAAK,QAAQ,YAAY,MAAM,uBAAwB,QAAO,KAAK;AAEvF,UAAM,WAAW,MAAM,KAAK,KAAK,GAAG,KAAK,OAAO,mBAAmB;AAAA,MACjE,QAAQ;AAAA,MACR,SAAS,QAAQ;AAAA,MACjB,MAAM,KAAK,UAAU,EAAE,WAAW,EAAE,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC;AAAA,MAC7D,QAAQ,YAAY,QAAQ,UAAU;AAAA,IACxC,CAAC;AACD,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,oDAAoD,SAAS,MAAM,EAAE;AACvG,UAAM,OAAQ,MAAM,SAAS,KAAK;AAMlC,UAAM,WAAW,EAAE,GAAG,MAAM,eAAe,aAAa,CAAC;AACzD,SAAK,UAAU;AAAA,MACb,iBAAiB,KAAK;AAAA,MACtB,eAAe,KAAK;AAAA,MACpB,OAAO,KAAK;AAAA,MACZ,WAAW,KAAK;AAAA,IAClB;AACA,WAAO,KAAK;AAAA,EACd;AACF;AAGA,SAAS,MAAM,OAAe,OAAgB;AAC5C,MAAI,CAAC,QAAQ,IAAI,4BAA6B;AAC9C,UAAQ,MAAM,kBAAkB,KAAK,IAAI,KAAK,UAAU,KAAK,CAAC,EAAE;AAClE;AAEA,SAAS,QAAQ,QAAgC,CAAC,GAAG;AACnD,SAAO;AAAA,IACL,gBAAgB;AAAA,IAChB,wBAAwB;AAAA,IACxB,GAAG;AAAA,EACL;AACF;","names":[]}
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Rpc } from "@opencode/plugin/rpc";
|
|
2
|
+
const CopilotAuto = Rpc.define({
|
|
3
|
+
id: "copilot-auto",
|
|
4
|
+
methods: {},
|
|
5
|
+
events: {
|
|
6
|
+
/** Emitted when Copilot picks a model for a prompt. Only sent when `notifications` is enabled. */
|
|
7
|
+
routed: {
|
|
8
|
+
schema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
model: { type: "string" },
|
|
12
|
+
sessionID: { type: "string" }
|
|
13
|
+
},
|
|
14
|
+
required: ["model"],
|
|
15
|
+
additionalProperties: false
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
export {
|
|
21
|
+
CopilotAuto
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=rpc.js.map
|
package/dist/rpc.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rpc.ts"],"sourcesContent":["import { Rpc } from \"@opencode/plugin/rpc\"\n\nexport const CopilotAuto = Rpc.define({\n id: \"copilot-auto\",\n methods: {},\n events: {\n /** Emitted when Copilot picks a model for a prompt. Only sent when `notifications` is enabled. */\n routed: {\n schema: {\n type: \"object\",\n properties: {\n model: { type: \"string\" },\n sessionID: { type: \"string\" },\n },\n required: [\"model\"],\n additionalProperties: false,\n },\n },\n },\n})\n"],"mappings":"AAAA,SAAS,WAAW;AAEb,MAAM,cAAc,IAAI,OAAO;AAAA,EACpC,IAAI;AAAA,EACJ,SAAS,CAAC;AAAA,EACV,QAAQ;AAAA;AAAA,IAEN,QAAQ;AAAA,MACN,QAAQ;AAAA,QACN,MAAM;AAAA,QACN,YAAY;AAAA,UACV,OAAO,EAAE,MAAM,SAAS;AAAA,UACxB,WAAW,EAAE,MAAM,SAAS;AAAA,QAC9B;AAAA,QACA,UAAU,CAAC,OAAO;AAAA,QAClB,sBAAsB;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AACF,CAAC;","names":[]}
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
2
|
+
import { CopilotAuto } from "./rpc.js";
|
|
3
|
+
var tui_default = Plugin.define({
|
|
4
|
+
id: "opencode-copilot-auto.tui",
|
|
5
|
+
setup(ctx) {
|
|
6
|
+
const api = ctx.client.rpc(CopilotAuto);
|
|
7
|
+
return api.events.on("routed", (event) => {
|
|
8
|
+
const data = event.data;
|
|
9
|
+
ctx.ui.toast.show({
|
|
10
|
+
title: "Copilot Auto",
|
|
11
|
+
message: `Answering with ${data.model}`,
|
|
12
|
+
variant: "info",
|
|
13
|
+
sessionID: data.sessionID
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
export {
|
|
19
|
+
tui_default as default
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=tui.js.map
|
package/dist/tui.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tui.ts"],"sourcesContent":["import { Plugin } from \"@opencode/plugin/tui\"\nimport { CopilotAuto } from \"./rpc.js\"\n\nexport default Plugin.define({\n id: \"opencode-copilot-auto.tui\",\n setup(ctx) {\n const api = ctx.client.rpc(CopilotAuto)\n return api.events.on(\"routed\", (event) => {\n const data = event.data as { model: string; sessionID?: string }\n ctx.ui.toast.show({\n title: \"Copilot Auto\",\n message: `Answering with ${data.model}`,\n variant: \"info\",\n sessionID: data.sessionID,\n })\n })\n },\n})\n"],"mappings":"AAAA,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAE5B,IAAO,cAAQ,OAAO,OAAO;AAAA,EAC3B,IAAI;AAAA,EACJ,MAAM,KAAK;AACT,UAAM,MAAM,IAAI,OAAO,IAAI,WAAW;AACtC,WAAO,IAAI,OAAO,GAAG,UAAU,CAAC,UAAU;AACxC,YAAM,OAAO,MAAM;AACnB,UAAI,GAAG,MAAM,KAAK;AAAA,QAChB,OAAO;AAAA,QACP,SAAS,kBAAkB,KAAK,KAAK;AAAA,QACrC,SAAS;AAAA,QACT,WAAW,KAAK;AAAA,MAClB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF,CAAC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opeoginni/opencode-copilot-auto",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "GitHub Copilot Auto model selection for OpenCode V2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
7
7
|
"opencode-plugin",
|
|
@@ -15,40 +15,41 @@
|
|
|
15
15
|
"type": "git",
|
|
16
16
|
"url": "git+https://github.com/OpeOginni/opencode-copilot-auto.git"
|
|
17
17
|
},
|
|
18
|
+
"homepage": "https://github.com/OpeOginni/opencode-copilot-auto#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/OpeOginni/opencode-copilot-auto/issues"
|
|
21
|
+
},
|
|
18
22
|
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./dist/index.js",
|
|
25
|
+
"./rpc": "./dist/rpc.js",
|
|
26
|
+
"./tui": "./dist/tui.js"
|
|
27
|
+
},
|
|
19
28
|
"files": [
|
|
20
29
|
"dist",
|
|
21
30
|
"README.md",
|
|
22
31
|
"LICENSE"
|
|
23
32
|
],
|
|
24
|
-
"main": "./dist/index.js",
|
|
25
|
-
"types": "./dist/index.d.ts",
|
|
26
|
-
"exports": {
|
|
27
|
-
".": {
|
|
28
|
-
"types": "./dist/index.d.ts",
|
|
29
|
-
"import": "./dist/index.js",
|
|
30
|
-
"default": "./dist/index.js"
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
33
|
"publishConfig": {
|
|
34
|
-
"access": "public"
|
|
34
|
+
"access": "public",
|
|
35
|
+
"registry": "https://registry.npmjs.org/"
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
|
-
"build": "
|
|
38
|
+
"build": "tsup",
|
|
38
39
|
"check": "tsc --noEmit",
|
|
39
40
|
"test": "bun test",
|
|
40
|
-
"prepack": "bun run
|
|
41
|
+
"prepack": "bun run build",
|
|
42
|
+
"prepublishOnly": "bun run check && bun test",
|
|
43
|
+
"publish:dry-run": "bun publish --dry-run",
|
|
44
|
+
"release": "bun publish"
|
|
41
45
|
},
|
|
42
|
-
"
|
|
43
|
-
"@opencode
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@opencode/plugin": "^2.0.12"
|
|
44
48
|
},
|
|
45
49
|
"devDependencies": {
|
|
46
|
-
"@
|
|
47
|
-
"@opencode-ai/sdk": "^1.17.20",
|
|
50
|
+
"@ai-sdk/provider": "^3.0.8",
|
|
48
51
|
"@types/bun": "^1.2.6",
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
"engines": {
|
|
52
|
-
"bun": ">=1.2.0"
|
|
52
|
+
"tsup": "^8.5.1",
|
|
53
|
+
"typescript": "^5.9.0"
|
|
53
54
|
}
|
|
54
55
|
}
|
package/dist/index.d.ts
DELETED