opencode-copilot-responses 0.0.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/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/auth/entitlement.d.ts +9 -0
- package/dist/auth/entitlement.d.ts.map +1 -0
- package/dist/auth/entitlement.js +23 -0
- package/dist/auth/headers.d.ts +5 -0
- package/dist/auth/headers.d.ts.map +1 -0
- package/dist/auth/headers.js +5 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/oauth.d.ts +28 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +68 -0
- package/dist/auth/types.d.ts +8 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/models/registry.d.ts +37 -0
- package/dist/models/registry.d.ts.map +1 -0
- package/dist/models/registry.js +86 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +114 -0
- package/dist/provider/fetch.d.ts +5 -0
- package/dist/provider/fetch.d.ts.map +1 -0
- package/dist/provider/fetch.js +107 -0
- package/dist/provider/headers.d.ts +7 -0
- package/dist/provider/headers.d.ts.map +1 -0
- package/dist/provider/headers.js +18 -0
- package/dist/provider/initiator.d.ts +3 -0
- package/dist/provider/initiator.d.ts.map +1 -0
- package/dist/provider/initiator.js +37 -0
- package/dist/provider/normalize.d.ts +2 -0
- package/dist/provider/normalize.d.ts.map +1 -0
- package/dist/provider/normalize.js +80 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nate Smyth
|
|
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,21 @@
|
|
|
1
|
+
# Copilot Responses API Provider for OpenCode
|
|
2
|
+
|
|
3
|
+
This plugin provides access to `api.githubcopilot.com/responses` for accessing OpenAI models (GPT-5.x) via your Copilot subscription.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Add to the `plugin` array in `opencode.json` or `opencode.jsonc`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
"plugin": [
|
|
11
|
+
"opencode-copilot-responses@latest"
|
|
12
|
+
]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
2. Run `opencode auth login`
|
|
16
|
+
3. Search or scroll to "other"
|
|
17
|
+
4. Enter "copilot-responses"
|
|
18
|
+
5. Finish OAuth flow in browser
|
|
19
|
+
6. Launch opencode
|
|
20
|
+
|
|
21
|
+
You will now see a new "copilot-responses" provider populated with all available models that support `/responses`, obtained from Copilot's `/models` endpoint.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitlement.d.ts","sourceRoot":"","sources":["../../src/auth/entitlement.ts"],"names":[],"mappings":"AAEA,wBAAsB,gBAAgB,CAAC,KAAK,EAAE;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,CAAA;CACZ;;WAyB4C,MAAM;GAClD"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AUTH_AGENT } from "./headers";
|
|
2
|
+
export async function fetchEntitlement(input) {
|
|
3
|
+
const base = input.url ?? "https://api.github.com";
|
|
4
|
+
const run = input.fetch ?? fetch;
|
|
5
|
+
const endpoint = new URL("/copilot_internal/user", base);
|
|
6
|
+
const res = await run(endpoint, {
|
|
7
|
+
headers: {
|
|
8
|
+
Authorization: `Bearer ${input.token}`,
|
|
9
|
+
Accept: "application/json",
|
|
10
|
+
"User-Agent": AUTH_AGENT,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
throw new Error(`entitlement check failed (${res.status}) ${endpoint.pathname}`);
|
|
15
|
+
}
|
|
16
|
+
const data = (await res.json());
|
|
17
|
+
const endpoints = data.endpoints;
|
|
18
|
+
const api = endpoints?.api;
|
|
19
|
+
if (typeof api !== "string" || api.length === 0) {
|
|
20
|
+
throw new Error("entitlement response missing endpoints.api");
|
|
21
|
+
}
|
|
22
|
+
return { baseUrl: api, login: data.login };
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/auth/headers.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,YAAY,CAAA;AAI5C,eAAO,MAAM,YAAY,QAAyF,CAAA;AAClH,eAAO,MAAM,eAAe,QAAoG,CAAA;AAChI,eAAO,MAAM,UAAU,WAAW,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const COPILOT_CLI_VERSION = "0.0.411";
|
|
2
|
+
const term = process.env.TERM_PROGRAM ?? "xterm-256color";
|
|
3
|
+
export const MODELS_AGENT = `copilot/${COPILOT_CLI_VERSION} (${process.platform} ${process.version}) term/${term}`;
|
|
4
|
+
export const RESPONSES_AGENT = `copilot/${COPILOT_CLI_VERSION} (client/cli ${process.platform} ${process.version}) term/${term}`;
|
|
5
|
+
export const AUTH_AGENT = "undici";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAChD,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const CLIENT_ID = "Ov23ctDVkRmgkPke0Mmm";
|
|
2
|
+
export declare function authorizeDeviceCode(input?: {
|
|
3
|
+
fetch?: typeof fetch;
|
|
4
|
+
url?: string;
|
|
5
|
+
clientId?: string;
|
|
6
|
+
scope?: string;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
device_code: string;
|
|
9
|
+
user_code: string;
|
|
10
|
+
verification_uri: string;
|
|
11
|
+
expires_in: number;
|
|
12
|
+
interval: number;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function pollForToken(input: {
|
|
15
|
+
deviceCode: string;
|
|
16
|
+
interval: number;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
fetch?: typeof fetch;
|
|
19
|
+
url?: string;
|
|
20
|
+
clientId?: string;
|
|
21
|
+
sleep?: (ms: number) => Promise<void>;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
access_token: string;
|
|
25
|
+
token_type: string;
|
|
26
|
+
scope: string;
|
|
27
|
+
}>;
|
|
28
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS,yBAAyB,CAAA;AAE/C,wBAAsB,mBAAmB,CAAC,KAAK,CAAC,EAAE;IACjD,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;CACd;iBAsBc,MAAM;eACR,MAAM;sBACC,MAAM;gBACZ,MAAM;cACR,MAAM;GAEjB;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE;IACzC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAClB;kBAWe,MAAM;gBACR,MAAM;WACX,MAAM;GAiDd"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AUTH_AGENT } from "./headers";
|
|
2
|
+
export const CLIENT_ID = "Ov23ctDVkRmgkPke0Mmm";
|
|
3
|
+
export async function authorizeDeviceCode(input) {
|
|
4
|
+
const base = input?.url ?? "https://github.com";
|
|
5
|
+
const id = input?.clientId ?? CLIENT_ID;
|
|
6
|
+
const scope = input?.scope ?? "read:user";
|
|
7
|
+
const run = input?.fetch ?? fetch;
|
|
8
|
+
const endpoint = new URL("/login/device/code", base);
|
|
9
|
+
const res = await run(endpoint, {
|
|
10
|
+
method: "POST",
|
|
11
|
+
headers: {
|
|
12
|
+
Accept: "application/json",
|
|
13
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
14
|
+
"User-Agent": AUTH_AGENT,
|
|
15
|
+
},
|
|
16
|
+
body: new URLSearchParams({ client_id: id, scope }).toString(),
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
throw new Error(`device code request failed (${res.status}) ${endpoint.pathname}`);
|
|
20
|
+
}
|
|
21
|
+
return (await res.json());
|
|
22
|
+
}
|
|
23
|
+
export async function pollForToken(input) {
|
|
24
|
+
const base = input.url ?? "https://github.com";
|
|
25
|
+
const id = input.clientId ?? CLIENT_ID;
|
|
26
|
+
const run = input.fetch ?? fetch;
|
|
27
|
+
const sleep = input.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
28
|
+
const now = input.now ?? (() => Date.now());
|
|
29
|
+
const endpoint = new URL("/login/oauth/access_token", base);
|
|
30
|
+
const step = async (interval) => {
|
|
31
|
+
if (Math.floor(now() / 1000) >= input.expiresAt) {
|
|
32
|
+
throw new Error("expired_token");
|
|
33
|
+
}
|
|
34
|
+
const res = await run(endpoint, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
Accept: "application/json",
|
|
38
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
39
|
+
"User-Agent": AUTH_AGENT,
|
|
40
|
+
},
|
|
41
|
+
body: new URLSearchParams({
|
|
42
|
+
client_id: id,
|
|
43
|
+
device_code: input.deviceCode,
|
|
44
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
45
|
+
}).toString(),
|
|
46
|
+
});
|
|
47
|
+
const data = (await res.json());
|
|
48
|
+
if ("access_token" in data) {
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
if (data.error === "authorization_pending") {
|
|
52
|
+
await sleep(interval * 1000);
|
|
53
|
+
return step(interval);
|
|
54
|
+
}
|
|
55
|
+
if (data.error === "slow_down") {
|
|
56
|
+
const next = typeof data.interval === "number" ? data.interval : interval + 5;
|
|
57
|
+
await sleep(next * 1000);
|
|
58
|
+
return step(next);
|
|
59
|
+
}
|
|
60
|
+
if (data.error === "expired_token")
|
|
61
|
+
throw new Error("expired_token");
|
|
62
|
+
if (data.error === "access_denied")
|
|
63
|
+
throw new Error("access_denied");
|
|
64
|
+
const detail = data.error_description ? ` ${data.error_description}` : "";
|
|
65
|
+
throw new Error(`${String(data.error)}${detail}`);
|
|
66
|
+
};
|
|
67
|
+
return step(input.interval);
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,CAAC,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;CACf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CopilotResponsesPlugin } from "./plugin";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Model } from "@opencode-ai/sdk";
|
|
2
|
+
export interface CopilotModel {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
vendor: string;
|
|
6
|
+
preview?: boolean;
|
|
7
|
+
capabilities?: {
|
|
8
|
+
family?: string;
|
|
9
|
+
limits?: {
|
|
10
|
+
max_context_window_tokens?: number;
|
|
11
|
+
max_output_tokens?: number;
|
|
12
|
+
max_prompt_tokens?: number;
|
|
13
|
+
vision?: {
|
|
14
|
+
max_prompt_image_size?: number;
|
|
15
|
+
max_prompt_images?: number;
|
|
16
|
+
supported_media_types?: string[];
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
supports?: {
|
|
20
|
+
structured_outputs?: boolean;
|
|
21
|
+
max_thinking_budget?: number;
|
|
22
|
+
min_thinking_budget?: number;
|
|
23
|
+
streaming?: boolean;
|
|
24
|
+
tool_calls?: boolean;
|
|
25
|
+
vision?: boolean;
|
|
26
|
+
parallel_tool_calls?: boolean;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
supported_endpoints?: string[];
|
|
30
|
+
}
|
|
31
|
+
export declare function mapToOpencodeModel(model: CopilotModel, baseUrl: string): Model;
|
|
32
|
+
export declare function fetchModels(input: {
|
|
33
|
+
token: string;
|
|
34
|
+
baseUrl: string;
|
|
35
|
+
fetch?: typeof fetch;
|
|
36
|
+
}): Promise<Model[]>;
|
|
37
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/models/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAG7C,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,YAAY,CAAC,EAAE;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE;YACR,yBAAyB,CAAC,EAAE,MAAM,CAAA;YAClC,iBAAiB,CAAC,EAAE,MAAM,CAAA;YAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAA;YAC1B,MAAM,CAAC,EAAE;gBACR,qBAAqB,CAAC,EAAE,MAAM,CAAA;gBAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAA;gBAC1B,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAA;aAChC,CAAA;SACD,CAAA;QACD,QAAQ,CAAC,EAAE;YACV,kBAAkB,CAAC,EAAE,OAAO,CAAA;YAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAA;YAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAA;YAC5B,SAAS,CAAC,EAAE,OAAO,CAAA;YACnB,UAAU,CAAC,EAAE,OAAO,CAAA;YACpB,MAAM,CAAC,EAAE,OAAO,CAAA;YAChB,mBAAmB,CAAC,EAAE,OAAO,CAAA;SAC7B,CAAA;KACD,CAAA;IACD,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC9B;AAaD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAmD9E;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACpB,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAsBnB"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { MODELS_AGENT } from "../auth/headers";
|
|
2
|
+
const RESPONSES_ENDPOINT = "/responses";
|
|
3
|
+
function parseModels(value) {
|
|
4
|
+
if (Array.isArray(value))
|
|
5
|
+
return value;
|
|
6
|
+
if (!value || typeof value !== "object")
|
|
7
|
+
return [];
|
|
8
|
+
const record = value;
|
|
9
|
+
if (Array.isArray(record.data))
|
|
10
|
+
return record.data;
|
|
11
|
+
if (Array.isArray(record.models))
|
|
12
|
+
return record.models;
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
export function mapToOpencodeModel(model, baseUrl) {
|
|
16
|
+
const caps = model.capabilities ?? {};
|
|
17
|
+
const limits = caps.limits ?? {};
|
|
18
|
+
const supports = caps.supports ?? {};
|
|
19
|
+
const vision = !!supports.vision;
|
|
20
|
+
return {
|
|
21
|
+
id: model.id,
|
|
22
|
+
providerID: "copilot-responses",
|
|
23
|
+
name: model.name,
|
|
24
|
+
api: {
|
|
25
|
+
id: model.id,
|
|
26
|
+
url: baseUrl,
|
|
27
|
+
npm: "@ai-sdk/openai",
|
|
28
|
+
},
|
|
29
|
+
capabilities: {
|
|
30
|
+
temperature: true,
|
|
31
|
+
reasoning: true,
|
|
32
|
+
attachment: vision,
|
|
33
|
+
toolcall: !!supports.tool_calls,
|
|
34
|
+
input: {
|
|
35
|
+
text: true,
|
|
36
|
+
audio: false,
|
|
37
|
+
image: vision,
|
|
38
|
+
video: false,
|
|
39
|
+
pdf: false,
|
|
40
|
+
},
|
|
41
|
+
output: {
|
|
42
|
+
text: true,
|
|
43
|
+
audio: false,
|
|
44
|
+
image: false,
|
|
45
|
+
video: false,
|
|
46
|
+
pdf: false,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
cost: {
|
|
50
|
+
input: 0,
|
|
51
|
+
output: 0,
|
|
52
|
+
cache: {
|
|
53
|
+
read: 0,
|
|
54
|
+
write: 0,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
limit: {
|
|
58
|
+
context: limits.max_context_window_tokens ?? 200000,
|
|
59
|
+
output: limits.max_output_tokens ?? 64000,
|
|
60
|
+
},
|
|
61
|
+
status: model.preview ? "beta" : "active",
|
|
62
|
+
options: {},
|
|
63
|
+
headers: {},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function fetchModels(input) {
|
|
67
|
+
const run = input.fetch ?? fetch;
|
|
68
|
+
const url = new URL("/models", input.baseUrl);
|
|
69
|
+
const headers = {
|
|
70
|
+
authorization: `Bearer ${input.token}`,
|
|
71
|
+
"user-agent": MODELS_AGENT,
|
|
72
|
+
"copilot-integration-id": "copilot-developer-cli",
|
|
73
|
+
"x-github-api-version": "2025-05-01",
|
|
74
|
+
"x-interaction-type": "model-access",
|
|
75
|
+
"openai-intent": "model-access",
|
|
76
|
+
"x-request-id": crypto.randomUUID(),
|
|
77
|
+
};
|
|
78
|
+
const res = await run(url, { method: "GET", headers });
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
return [];
|
|
81
|
+
const data = (await res.json());
|
|
82
|
+
const models = parseModels(data);
|
|
83
|
+
return models
|
|
84
|
+
.filter((m) => Array.isArray(m.supported_endpoints) && m.supported_endpoints.includes(RESPONSES_ENDPOINT))
|
|
85
|
+
.map((m) => mapToOpencodeModel(m, input.baseUrl));
|
|
86
|
+
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAiBxD,eAAO,MAAM,sBAAsB,EAAE,MA4FpC,CAAA"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { authorizeDeviceCode, fetchEntitlement, pollForToken } from "./auth";
|
|
2
|
+
import { fetchModels } from "./models/registry";
|
|
3
|
+
import { copilotResponsesFetch } from "./provider/fetch";
|
|
4
|
+
export const CopilotResponsesPlugin = async (input) => {
|
|
5
|
+
return {
|
|
6
|
+
config: async (config) => {
|
|
7
|
+
if (!config.provider)
|
|
8
|
+
config.provider = {};
|
|
9
|
+
if (!config.provider["copilot-responses"]) {
|
|
10
|
+
config.provider["copilot-responses"] = {
|
|
11
|
+
npm: "@ai-sdk/openai",
|
|
12
|
+
name: "Copilot Responses",
|
|
13
|
+
models: {},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"chat.headers": async (data, output) => {
|
|
18
|
+
if (data.model.providerID !== "copilot-responses")
|
|
19
|
+
return;
|
|
20
|
+
const session = await input.client.session
|
|
21
|
+
.get({ path: { id: data.sessionID }, throwOnError: true })
|
|
22
|
+
.catch(() => undefined);
|
|
23
|
+
if (!session?.data?.parentID)
|
|
24
|
+
return;
|
|
25
|
+
output.headers["x-initiator"] = "agent";
|
|
26
|
+
},
|
|
27
|
+
auth: {
|
|
28
|
+
provider: "copilot-responses",
|
|
29
|
+
methods: [
|
|
30
|
+
{
|
|
31
|
+
type: "oauth",
|
|
32
|
+
label: "Login with GitHub (Copilot CLI)",
|
|
33
|
+
authorize: async () => {
|
|
34
|
+
const device = await authorizeDeviceCode();
|
|
35
|
+
return {
|
|
36
|
+
url: device.verification_uri,
|
|
37
|
+
instructions: `Enter code: ${device.user_code}`,
|
|
38
|
+
method: "auto",
|
|
39
|
+
callback: async () => {
|
|
40
|
+
const expiresAt = Math.floor(Date.now() / 1000) + device.expires_in;
|
|
41
|
+
const token = await pollForToken({
|
|
42
|
+
deviceCode: device.device_code,
|
|
43
|
+
interval: device.interval,
|
|
44
|
+
expiresAt,
|
|
45
|
+
});
|
|
46
|
+
const entitlement = await fetchEntitlement({
|
|
47
|
+
token: token.access_token,
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
type: "success",
|
|
51
|
+
refresh: token.access_token,
|
|
52
|
+
access: token.access_token,
|
|
53
|
+
expires: 0,
|
|
54
|
+
baseUrl: entitlement.baseUrl,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
loader: async (getAuth, provider) => {
|
|
62
|
+
const stored = (await getAuth());
|
|
63
|
+
if (!stored || stored.type !== "oauth")
|
|
64
|
+
return {};
|
|
65
|
+
const token = typeof stored.access === "string" && stored.access.startsWith("gho_")
|
|
66
|
+
? stored.access
|
|
67
|
+
: typeof stored.refresh === "string" && stored.refresh.startsWith("gho_")
|
|
68
|
+
? stored.refresh
|
|
69
|
+
: null;
|
|
70
|
+
if (!token)
|
|
71
|
+
return {};
|
|
72
|
+
const base = await resolveBaseUrl(stored, token, input);
|
|
73
|
+
const models = await fetchModels({ token, baseUrl: base });
|
|
74
|
+
const target = provider;
|
|
75
|
+
if (!target.models)
|
|
76
|
+
target.models = {};
|
|
77
|
+
for (const model of models) {
|
|
78
|
+
const existing = target.models[model.id];
|
|
79
|
+
if (!existing) {
|
|
80
|
+
target.models[model.id] = model;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
target.models[model.id] = mergeModel(model, existing);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
name: "openai",
|
|
87
|
+
apiKey: "",
|
|
88
|
+
baseURL: base,
|
|
89
|
+
fetch: (req, init) => copilotResponsesFetch(req, init, { token }),
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
async function resolveBaseUrl(stored, token, input) {
|
|
96
|
+
if (typeof stored.baseUrl === "string" && stored.baseUrl.length > 0)
|
|
97
|
+
return stored.baseUrl;
|
|
98
|
+
const entitlement = await fetchEntitlement({ token });
|
|
99
|
+
// auth.set body type is opaque to the plugin; cast is unavoidable here
|
|
100
|
+
await input.client.auth.set({
|
|
101
|
+
path: { id: "copilot-responses" },
|
|
102
|
+
body: { ...stored, baseUrl: entitlement.baseUrl },
|
|
103
|
+
});
|
|
104
|
+
return entitlement.baseUrl;
|
|
105
|
+
}
|
|
106
|
+
function mergeModel(model, existing) {
|
|
107
|
+
return {
|
|
108
|
+
...model,
|
|
109
|
+
limit: { ...model.limit, ...existing.limit },
|
|
110
|
+
options: { ...model.options, ...existing.options },
|
|
111
|
+
headers: { ...model.headers, ...existing.headers },
|
|
112
|
+
variants: { ...model.variants, ...existing.variants },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/provider/fetch.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAA;CACb;AAED,wBAAsB,qBAAqB,CAC1C,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,EAC7B,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,OAAO,EAAE,YAAY,GACnB,OAAO,CAAC,QAAQ,CAAC,CA2BnB"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { buildHeaders } from "./headers";
|
|
2
|
+
import { determineInitiator, hasImageContent } from "./initiator";
|
|
3
|
+
import { normalizeReasoningIds } from "./normalize";
|
|
4
|
+
export async function copilotResponsesFetch(input, init, context) {
|
|
5
|
+
const headers = merge(input, init);
|
|
6
|
+
headers.delete("x-api-key");
|
|
7
|
+
const body = readBody(init?.body);
|
|
8
|
+
const initiator = forced(headers.get("x-initiator")) ??
|
|
9
|
+
(isInternalAgent(body) ? "agent" : determineInitiator(body.input));
|
|
10
|
+
const images = hasImageContent(body.input);
|
|
11
|
+
const copilot = buildHeaders({
|
|
12
|
+
token: context.token,
|
|
13
|
+
initiator,
|
|
14
|
+
hasImages: images,
|
|
15
|
+
});
|
|
16
|
+
for (const [key, value] of Object.entries(copilot)) {
|
|
17
|
+
headers.set(key, value);
|
|
18
|
+
}
|
|
19
|
+
const response = await fetch(input, {
|
|
20
|
+
...init,
|
|
21
|
+
headers,
|
|
22
|
+
body: stripIds(init?.body),
|
|
23
|
+
});
|
|
24
|
+
if (!response.body || !isSSE(response))
|
|
25
|
+
return response;
|
|
26
|
+
return new Response(normalizeReasoningIds(response.body), {
|
|
27
|
+
status: response.status,
|
|
28
|
+
statusText: response.statusText,
|
|
29
|
+
headers: response.headers,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
function merge(input, init) {
|
|
34
|
+
const headers = new Headers(input instanceof Request ? input.headers : undefined);
|
|
35
|
+
if (!init?.headers)
|
|
36
|
+
return headers;
|
|
37
|
+
const incoming = new Headers(init.headers);
|
|
38
|
+
for (const [key, value] of incoming.entries()) {
|
|
39
|
+
headers.set(key, value);
|
|
40
|
+
}
|
|
41
|
+
return headers;
|
|
42
|
+
}
|
|
43
|
+
function readBody(body) {
|
|
44
|
+
if (!body)
|
|
45
|
+
return { input: [] };
|
|
46
|
+
if (typeof body === "string")
|
|
47
|
+
return parse(body);
|
|
48
|
+
if (body instanceof ArrayBuffer)
|
|
49
|
+
return parse(decoder.decode(body));
|
|
50
|
+
if (ArrayBuffer.isView(body))
|
|
51
|
+
return parse(decoder.decode(body));
|
|
52
|
+
return { input: [] };
|
|
53
|
+
}
|
|
54
|
+
function parse(text) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(text);
|
|
57
|
+
return {
|
|
58
|
+
input: Array.isArray(parsed.input) ? parsed.input : [],
|
|
59
|
+
instructions: parsed.instructions,
|
|
60
|
+
system: parsed.system,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { input: [] };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isSSE(response) {
|
|
68
|
+
return (response.headers.get("content-type") ?? "").includes("text/event-stream");
|
|
69
|
+
}
|
|
70
|
+
// Strip stale IDs from input items; item_reference keeps its id.
|
|
71
|
+
function stripIds(body) {
|
|
72
|
+
if (typeof body !== "string")
|
|
73
|
+
return body;
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(body);
|
|
76
|
+
if (!Array.isArray(parsed.input))
|
|
77
|
+
return body;
|
|
78
|
+
for (const item of parsed.input) {
|
|
79
|
+
if (item.type === "item_reference")
|
|
80
|
+
continue;
|
|
81
|
+
delete item.id;
|
|
82
|
+
}
|
|
83
|
+
return JSON.stringify(parsed);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return body;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function isInternalAgent(body) {
|
|
90
|
+
const prompt = body.instructions ?? body.system;
|
|
91
|
+
if (!prompt)
|
|
92
|
+
return false;
|
|
93
|
+
if (typeof prompt === "string")
|
|
94
|
+
return prompt.startsWith("You are a title generator");
|
|
95
|
+
if (Array.isArray(prompt) && prompt.length > 0) {
|
|
96
|
+
const first = prompt[0];
|
|
97
|
+
if (typeof first === "object" && typeof first.text === "string") {
|
|
98
|
+
return first.text.startsWith("You are a title generator");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
function forced(value) {
|
|
104
|
+
if (value === "user" || value === "agent")
|
|
105
|
+
return value;
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/provider/headers.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgB3E"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RESPONSES_AGENT } from "../auth/headers";
|
|
2
|
+
export function buildHeaders(context) {
|
|
3
|
+
const headers = {
|
|
4
|
+
authorization: `Bearer ${context.token}`,
|
|
5
|
+
"user-agent": RESPONSES_AGENT,
|
|
6
|
+
"copilot-integration-id": "copilot-developer-cli",
|
|
7
|
+
"x-github-api-version": "2025-05-01",
|
|
8
|
+
"x-interaction-type": "conversation-agent",
|
|
9
|
+
"openai-intent": "conversation-agent",
|
|
10
|
+
"x-interaction-id": crypto.randomUUID(),
|
|
11
|
+
"x-request-id": crypto.randomUUID(),
|
|
12
|
+
"x-initiator": context.initiator,
|
|
13
|
+
};
|
|
14
|
+
if (context.hasImages) {
|
|
15
|
+
headers["Copilot-Vision-Request"] = "true";
|
|
16
|
+
}
|
|
17
|
+
return headers;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"initiator.d.ts","sourceRoot":"","sources":["../../src/provider/initiator.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CASnE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAWvD"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function determineInitiator(input) {
|
|
2
|
+
if (!Array.isArray(input) || input.length === 0)
|
|
3
|
+
return "agent";
|
|
4
|
+
const last = input[input.length - 1];
|
|
5
|
+
if (typed(last, "function_call_output"))
|
|
6
|
+
return "agent";
|
|
7
|
+
if (!has(last, "role", "user"))
|
|
8
|
+
return "agent";
|
|
9
|
+
const content = last.content;
|
|
10
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
11
|
+
return "agent";
|
|
12
|
+
if (content.some((p) => typed(p, "input_text")))
|
|
13
|
+
return "user";
|
|
14
|
+
return "agent";
|
|
15
|
+
}
|
|
16
|
+
export function hasImageContent(input) {
|
|
17
|
+
if (!Array.isArray(input))
|
|
18
|
+
return false;
|
|
19
|
+
for (const item of input) {
|
|
20
|
+
const content = item !== null && typeof item === "object"
|
|
21
|
+
? item.content
|
|
22
|
+
: undefined;
|
|
23
|
+
if (!Array.isArray(content))
|
|
24
|
+
continue;
|
|
25
|
+
if (content.some((p) => typed(p, "input_image")))
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function typed(value, kind) {
|
|
31
|
+
return (value !== null && typeof value === "object" && value.type === kind);
|
|
32
|
+
}
|
|
33
|
+
function has(value, key, expected) {
|
|
34
|
+
return (value !== null &&
|
|
35
|
+
typeof value === "object" &&
|
|
36
|
+
value[key] === expected);
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../src/provider/normalize.ts"],"names":[],"mappings":"AAmBA,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAChC,cAAc,CAAC,UAAU,CAAC,CAsB5B"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Normalizes rotated item IDs from the Copilot proxy.
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
const decoder = new TextDecoder();
|
|
4
|
+
// SSE event types whose top-level `item_id` should be rewritten.
|
|
5
|
+
const ITEM_ID_EVENTS = new Set([
|
|
6
|
+
"response.reasoning_summary_text.delta",
|
|
7
|
+
"response.reasoning_summary_text.done",
|
|
8
|
+
"response.reasoning_summary_part.added",
|
|
9
|
+
"response.reasoning_summary_part.done",
|
|
10
|
+
"response.output_text.delta",
|
|
11
|
+
"response.output_text.done",
|
|
12
|
+
"response.content_part.added",
|
|
13
|
+
"response.content_part.done",
|
|
14
|
+
"response.function_call_arguments.delta",
|
|
15
|
+
"response.function_call_arguments.done",
|
|
16
|
+
]);
|
|
17
|
+
export function normalizeReasoningIds(stream) {
|
|
18
|
+
const canonical = {};
|
|
19
|
+
let buffer = "";
|
|
20
|
+
return stream.pipeThrough(new TransformStream({
|
|
21
|
+
transform(chunk, controller) {
|
|
22
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
23
|
+
const parts = buffer.split("\n\n");
|
|
24
|
+
// keep incomplete trailing block in buffer
|
|
25
|
+
buffer = parts.pop() ?? "";
|
|
26
|
+
for (const block of parts) {
|
|
27
|
+
controller.enqueue(encoder.encode(`${rewrite(block, canonical)}\n\n`));
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
flush(controller) {
|
|
31
|
+
if (buffer.trim()) {
|
|
32
|
+
controller.enqueue(encoder.encode(`${rewrite(buffer, canonical)}\n\n`));
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
function rewrite(block, canonical) {
|
|
38
|
+
const dataMatch = block.match(/^data: (.+)$/m);
|
|
39
|
+
if (!dataMatch)
|
|
40
|
+
return block;
|
|
41
|
+
try {
|
|
42
|
+
const data = JSON.parse(dataMatch[1]);
|
|
43
|
+
const type = data.type;
|
|
44
|
+
if (!type)
|
|
45
|
+
return block;
|
|
46
|
+
let changed = false;
|
|
47
|
+
// Record canonical id from output_item.added
|
|
48
|
+
if (type === "response.output_item.added") {
|
|
49
|
+
const item = data.item;
|
|
50
|
+
const idx = data.output_index;
|
|
51
|
+
if (item && idx !== undefined && typeof item.id === "string") {
|
|
52
|
+
canonical[idx] = item.id;
|
|
53
|
+
}
|
|
54
|
+
return block;
|
|
55
|
+
}
|
|
56
|
+
// Rewrite item.id in output_item.done
|
|
57
|
+
if (type === "response.output_item.done") {
|
|
58
|
+
const item = data.item;
|
|
59
|
+
const idx = data.output_index;
|
|
60
|
+
if (item && idx !== undefined && canonical[idx] && item.id !== canonical[idx]) {
|
|
61
|
+
item.id = canonical[idx];
|
|
62
|
+
changed = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Rewrite top-level item_id
|
|
66
|
+
if (ITEM_ID_EVENTS.has(type)) {
|
|
67
|
+
const idx = data.output_index;
|
|
68
|
+
if (idx !== undefined && canonical[idx] && data.item_id !== canonical[idx]) {
|
|
69
|
+
data.item_id = canonical[idx];
|
|
70
|
+
changed = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!changed)
|
|
74
|
+
return block;
|
|
75
|
+
return block.replace(/^data: .+$/m, `data: ${JSON.stringify(data)}`);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return block;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-copilot-responses",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "OpenCode plugin for Copilot via the OpenAI Responses API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "bun run clean:dist && tsc -p tsconfig.build.json",
|
|
19
|
+
"clean:dist": "bun -e \"import { rm } from 'node:fs/promises'; await rm('dist', { recursive: true, force: true })\"",
|
|
20
|
+
"check": "tsc --noEmit",
|
|
21
|
+
"lint": "biome check .",
|
|
22
|
+
"lint:fix": "biome check --write .",
|
|
23
|
+
"format": "biome format --write .",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"preflight": "bun test && bun run check && bun run lint",
|
|
26
|
+
"release:patch": "bun run preflight && bun pm version patch && bun publish && git push --follow-tags",
|
|
27
|
+
"release:minor": "bun run preflight && bun pm version minor && bun publish && git push --follow-tags",
|
|
28
|
+
"release:major": "bun run preflight && bun pm version major && bun publish && git push --follow-tags",
|
|
29
|
+
"prepublishOnly": "bun run build"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@opencode-ai/plugin": "^1.1.48"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@opencode-ai/sdk": "1.1.48",
|
|
36
|
+
"@types/bun": "latest",
|
|
37
|
+
"typescript": "^5.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"typescript": "^5"
|
|
41
|
+
}
|
|
42
|
+
}
|