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 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,9 @@
1
+ export declare function fetchEntitlement(input: {
2
+ token: string;
3
+ fetch?: typeof fetch;
4
+ url?: string;
5
+ }): Promise<{
6
+ baseUrl: string;
7
+ login: string;
8
+ }>;
9
+ //# sourceMappingURL=entitlement.d.ts.map
@@ -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,5 @@
1
+ export declare const COPILOT_CLI_VERSION = "0.0.411";
2
+ export declare const MODELS_AGENT: string;
3
+ export declare const RESPONSES_AGENT: string;
4
+ export declare const AUTH_AGENT = "undici";
5
+ //# sourceMappingURL=headers.d.ts.map
@@ -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,4 @@
1
+ export { fetchEntitlement } from "./entitlement";
2
+ export { authorizeDeviceCode, CLIENT_ID, pollForToken } from "./oauth";
3
+ export type { StoredAuth } from "./types";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { fetchEntitlement } from "./entitlement";
2
+ export { authorizeDeviceCode, CLIENT_ID, pollForToken } from "./oauth";
@@ -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,8 @@
1
+ export interface StoredAuth {
2
+ type: "oauth";
3
+ refresh: string;
4
+ access: string;
5
+ expires: 0;
6
+ baseUrl: string;
7
+ }
8
+ //# sourceMappingURL=types.d.ts.map
@@ -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 {};
@@ -0,0 +1,2 @@
1
+ export { CopilotResponsesPlugin } from "./plugin";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const CopilotResponsesPlugin: Plugin;
3
+ //# sourceMappingURL=plugin.d.ts.map
@@ -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,5 @@
1
+ export interface FetchContext {
2
+ token: string;
3
+ }
4
+ export declare function copilotResponsesFetch(input: string | URL | Request, init: RequestInit | undefined, context: FetchContext): Promise<Response>;
5
+ //# sourceMappingURL=fetch.d.ts.map
@@ -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,7 @@
1
+ export interface HeaderContext {
2
+ token: string;
3
+ initiator: "user" | "agent";
4
+ hasImages?: boolean;
5
+ }
6
+ export declare function buildHeaders(context: HeaderContext): Record<string, string>;
7
+ //# sourceMappingURL=headers.d.ts.map
@@ -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,3 @@
1
+ export declare function determineInitiator(input: unknown): "user" | "agent";
2
+ export declare function hasImageContent(input: unknown): boolean;
3
+ //# sourceMappingURL=initiator.d.ts.map
@@ -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,2 @@
1
+ export declare function normalizeReasoningIds(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array>;
2
+ //# sourceMappingURL=normalize.d.ts.map
@@ -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
+ }