Skip to content
This repository was archived by the owner on Feb 19, 2026. It is now read-only.

Commit fe94bb8

Browse files
OlaHullebergjkorsvikactions-userrekram1-node
authored
feat(provider): add GitHub Enterprise support for Copilot (anomalyco#2522)
Co-authored-by: Jon-Mikkel Korsvik <48263282+jkorsvik@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
1 parent ba8bc1b commit fe94bb8

File tree

7 files changed

+312
-159
lines changed

7 files changed

+312
-159
lines changed

packages/opencode/src/auth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export namespace Auth {
1010
refresh: z.string(),
1111
access: z.string(),
1212
expires: z.number(),
13+
enterpriseUrl: z.string().optional(),
1314
})
1415
.meta({ ref: "OAuth" })
1516

packages/opencode/src/cli/cmd/auth.ts

Lines changed: 193 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -102,178 +102,223 @@ export const AuthLoginCommand = cmd({
102102
prompts.outro("Done")
103103
return
104104
}
105-
await ModelsDev.refresh().catch(() => {})
106-
const providers = await ModelsDev.get()
107-
const priority: Record<string, number> = {
108-
opencode: 0,
109-
anthropic: 1,
110-
"github-copilot": 2,
111-
openai: 3,
112-
google: 4,
113-
openrouter: 5,
114-
vercel: 6,
115-
}
116-
let provider = await prompts.autocomplete({
117-
message: "Select provider",
118-
maxItems: 8,
119-
options: [
120-
...pipe(
121-
providers,
122-
values(),
123-
sortBy(
124-
(x) => priority[x.id] ?? 99,
125-
(x) => x.name ?? x.id,
126-
),
127-
map((x) => ({
128-
label: x.name,
129-
value: x.id,
130-
hint: priority[x.id] <= 1 ? "recommended" : undefined,
131-
})),
105+
await ModelsDev.refresh().catch(() => {})
106+
const providers = await ModelsDev.get()
107+
const priority: Record<string, number> = {
108+
opencode: 0,
109+
anthropic: 1,
110+
"github-copilot": 2,
111+
openai: 3,
112+
google: 4,
113+
openrouter: 5,
114+
vercel: 6,
115+
}
116+
let provider = await prompts.autocomplete({
117+
message: "Select provider",
118+
maxItems: 8,
119+
options: [
120+
...pipe(
121+
providers,
122+
values(),
123+
sortBy(
124+
(x) => priority[x.id] ?? 99,
125+
(x) => x.name ?? x.id,
132126
),
133-
{
134-
value: "other",
135-
label: "Other",
136-
},
137-
],
138-
})
127+
map((x) => ({
128+
label: x.name,
129+
value: x.id,
130+
hint: priority[x.id] <= 1 ? "recommended" : undefined,
131+
})),
132+
),
133+
{
134+
value: "other",
135+
label: "Other",
136+
},
137+
],
138+
})
139139

140-
if (prompts.isCancel(provider)) throw new UI.CancelledError()
140+
if (prompts.isCancel(provider)) throw new UI.CancelledError()
141141

142-
const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
143-
if (plugin && plugin.auth) {
144-
let index = 0
145-
if (plugin.auth.methods.length > 1) {
146-
const method = await prompts.select({
147-
message: "Login method",
148-
options: [
149-
...plugin.auth.methods.map((x, index) => ({
150-
label: x.label,
151-
value: index.toString(),
152-
})),
153-
],
154-
})
155-
if (prompts.isCancel(method)) throw new UI.CancelledError()
156-
index = parseInt(method)
157-
}
158-
const method = plugin.auth.methods[index]
159-
if (method.type === "oauth") {
160-
await new Promise((resolve) => setTimeout(resolve, 10))
161-
const authorize = await method.authorize()
142+
const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
143+
if (plugin && plugin.auth) {
144+
let index = 0
145+
if (plugin.auth.methods.length > 1) {
146+
const method = await prompts.select({
147+
message: "Login method",
148+
options: [
149+
...plugin.auth.methods.map((x, index) => ({
150+
label: x.label,
151+
value: index.toString(),
152+
})),
153+
],
154+
})
155+
if (prompts.isCancel(method)) throw new UI.CancelledError()
156+
index = parseInt(method)
157+
}
158+
const method = plugin.auth.methods[index]
162159

163-
if (authorize.url) {
164-
prompts.log.info("Go to: " + authorize.url)
160+
// Handle prompts for all auth types
161+
await new Promise((resolve) => setTimeout(resolve, 10))
162+
const inputs: Record<string, string> = {}
163+
if (method.prompts) {
164+
for (const prompt of method.prompts) {
165+
if (prompt.condition && !prompt.condition(inputs)) {
166+
continue
165167
}
168+
if (prompt.type === "select") {
169+
const value = await prompts.select({
170+
message: prompt.message,
171+
options: prompt.options,
172+
})
173+
if (prompts.isCancel(value)) throw new UI.CancelledError()
174+
inputs[prompt.key] = value
175+
} else {
176+
const value = await prompts.text({
177+
message: prompt.message,
178+
placeholder: prompt.placeholder,
179+
validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined,
180+
})
181+
if (prompts.isCancel(value)) throw new UI.CancelledError()
182+
inputs[prompt.key] = value
183+
}
184+
}
185+
}
166186

167-
if (authorize.method === "auto") {
168-
if (authorize.instructions) {
169-
prompts.log.info(authorize.instructions)
170-
}
171-
const spinner = prompts.spinner()
172-
spinner.start("Waiting for authorization...")
173-
const result = await authorize.callback()
174-
if (result.type === "failed") {
175-
spinner.stop("Failed to authorize", 1)
187+
if (method.type === "oauth") {
188+
const authorize = await method.authorize(inputs)
189+
190+
if (authorize.url) {
191+
prompts.log.info("Go to: " + authorize.url)
192+
}
193+
194+
if (authorize.method === "auto") {
195+
if (authorize.instructions) {
196+
prompts.log.info(authorize.instructions)
197+
}
198+
const spinner = prompts.spinner()
199+
spinner.start("Waiting for authorization...")
200+
const result = await authorize.callback()
201+
if (result.type === "failed") {
202+
spinner.stop("Failed to authorize", 1)
203+
}
204+
if (result.type === "success") {
205+
const saveProvider = result.provider ?? provider
206+
if ("refresh" in result) {
207+
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
208+
await Auth.set(saveProvider, {
209+
type: "oauth",
210+
refresh,
211+
access,
212+
expires,
213+
...extraFields,
214+
})
176215
}
177-
if (result.type === "success") {
178-
if ("refresh" in result) {
179-
await Auth.set(provider, {
180-
type: "oauth",
181-
refresh: result.refresh,
182-
access: result.access,
183-
expires: result.expires,
184-
})
185-
}
186-
if ("key" in result) {
187-
await Auth.set(provider, {
188-
type: "api",
189-
key: result.key,
190-
})
191-
}
192-
spinner.stop("Login successful")
216+
if ("key" in result) {
217+
await Auth.set(saveProvider, {
218+
type: "api",
219+
key: result.key,
220+
})
193221
}
222+
spinner.stop("Login successful")
194223
}
224+
}
195225

196-
if (authorize.method === "code") {
197-
const code = await prompts.text({
198-
message: "Paste the authorization code here: ",
199-
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
200-
})
201-
if (prompts.isCancel(code)) throw new UI.CancelledError()
202-
const result = await authorize.callback(code)
203-
if (result.type === "failed") {
204-
prompts.log.error("Failed to authorize")
226+
if (authorize.method === "code") {
227+
const code = await prompts.text({
228+
message: "Paste the authorization code here: ",
229+
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
230+
})
231+
if (prompts.isCancel(code)) throw new UI.CancelledError()
232+
const result = await authorize.callback(code)
233+
if (result.type === "failed") {
234+
prompts.log.error("Failed to authorize")
235+
}
236+
if (result.type === "success") {
237+
const saveProvider = result.provider ?? provider
238+
if ("refresh" in result) {
239+
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
240+
await Auth.set(saveProvider, {
241+
type: "oauth",
242+
refresh,
243+
access,
244+
expires,
245+
...extraFields,
246+
})
205247
}
206-
if (result.type === "success") {
207-
if ("refresh" in result) {
208-
await Auth.set(provider, {
209-
type: "oauth",
210-
refresh: result.refresh,
211-
access: result.access,
212-
expires: result.expires,
213-
})
214-
}
215-
if ("key" in result) {
216-
await Auth.set(provider, {
217-
type: "api",
218-
key: result.key,
219-
})
220-
}
221-
prompts.log.success("Login successful")
248+
if ("key" in result) {
249+
await Auth.set(saveProvider, {
250+
type: "api",
251+
key: result.key,
252+
})
222253
}
254+
prompts.log.success("Login successful")
223255
}
224-
prompts.outro("Done")
225-
return
226256
}
227-
}
228257

229-
if (provider === "other") {
230-
provider = await prompts.text({
231-
message: "Enter provider id",
232-
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
233-
})
234-
if (prompts.isCancel(provider)) throw new UI.CancelledError()
235-
provider = provider.replace(/^@ai-sdk\//, "")
236-
if (prompts.isCancel(provider)) throw new UI.CancelledError()
237-
prompts.log.warn(
238-
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
239-
)
240-
}
241-
242-
if (provider === "amazon-bedrock") {
243-
prompts.log.info(
244-
"Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
245-
)
246-
prompts.outro("Done")
247-
return
248-
}
249-
250-
if (provider === "google-vertex") {
251-
prompts.log.info(
252-
"Google Cloud Vertex AI uses Application Default Credentials. Set GOOGLE_APPLICATION_CREDENTIALS or run 'gcloud auth application-default login'. Optionally set GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION (or VERTEX_LOCATION)",
253-
)
254258
prompts.outro("Done")
255259
return
256260
}
257261

258-
if (provider === "opencode") {
259-
prompts.log.info("Create an api key at https://opencode.ai/auth")
260-
}
261-
262-
if (provider === "vercel") {
263-
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
262+
if (method.type === "api") {
263+
if (method.authorize) {
264+
const result = await method.authorize(inputs)
265+
if (result.type === "failed") {
266+
prompts.log.error("Failed to authorize")
267+
}
268+
if (result.type === "success") {
269+
const saveProvider = result.provider ?? provider
270+
await Auth.set(saveProvider, {
271+
type: "api",
272+
key: result.key,
273+
})
274+
prompts.log.success("Login successful")
275+
}
276+
prompts.outro("Done")
277+
return
278+
}
264279
}
280+
}
265281

266-
const key = await prompts.password({
267-
message: "Enter your API key",
268-
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
269-
})
270-
if (prompts.isCancel(key)) throw new UI.CancelledError()
271-
await Auth.set(provider, {
272-
type: "api",
273-
key,
282+
if (provider === "other") {
283+
provider = await prompts.text({
284+
message: "Enter provider id",
285+
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
274286
})
287+
if (prompts.isCancel(provider)) throw new UI.CancelledError()
288+
provider = provider.replace(/^@ai-sdk\//, "")
289+
if (prompts.isCancel(provider)) throw new UI.CancelledError()
290+
prompts.log.warn(
291+
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
292+
)
293+
}
275294

295+
if (provider === "amazon-bedrock") {
296+
prompts.log.info(
297+
"Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
298+
)
276299
prompts.outro("Done")
300+
return
301+
}
302+
303+
if (provider === "opencode") {
304+
prompts.log.info("Create an api key at https://opencode.ai/auth")
305+
}
306+
307+
if (provider === "vercel") {
308+
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
309+
}
310+
311+
const key = await prompts.password({
312+
message: "Enter your API key",
313+
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
314+
})
315+
if (prompts.isCancel(key)) throw new UI.CancelledError()
316+
await Auth.set(provider, {
317+
type: "api",
318+
key,
319+
})
320+
321+
prompts.outro("Done")
277322
},
278323
})
279324
},

packages/opencode/src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,7 @@ export namespace Config {
574574
.object({
575575
apiKey: z.string().optional(),
576576
baseURL: z.string().optional(),
577+
enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
577578
timeout: z
578579
.union([
579580
z

packages/opencode/src/plugin/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export namespace Plugin {
2828
}
2929
const plugins = [...(config.plugin ?? [])]
3030
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
31-
plugins.push("opencode-copilot-auth@0.0.3")
31+
plugins.push("opencode-copilot-auth@0.0.4")
3232
plugins.push("opencode-anthropic-auth@0.0.2")
3333
}
3434
for (let plugin of plugins) {

0 commit comments

Comments
 (0)