diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 71a7d22b8f2..0e1b4c0d9a1 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -116,12 +116,12 @@ export function DialogModel(props: { providerID?: string }) { (provider) => provider.id !== "opencode", (provider) => provider.name, ), + filter((provider) => (props.providerID ? provider.id === props.providerID : true)), flatMap((provider) => pipe( provider.models, entries(), filter(([_, info]) => info.status !== "deprecated"), - filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)), map(([model, info]) => { const value = { providerID: provider.id, diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index cb33c6301bd..c973aa20764 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -8,7 +8,7 @@ import { DialogPrompt } from "../ui/dialog-prompt" import { Link } from "../ui/link" import { useTheme } from "../context/theme" import { TextAttributes } from "@opentui/core" -import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2" +import type { ProviderAuthAuthorization, ProviderAuthMethodPrompt } from "@opencode-ai/sdk/v2" import { DialogModel } from "./dialog-model" import { useKeyboard } from "@opentui/solid" import { Clipboard } from "@tui/util/clipboard" @@ -26,6 +26,7 @@ export function createDialogProviderOptions() { const sync = useSync() const dialog = useDialog() const sdk = useSDK() + const toast = useToast() const options = createMemo(() => { return pipe( sync.data.provider_next.all, @@ -67,10 +68,58 @@ export function createDialogProviderOptions() { if (index == null) return const method = methods[index] if (method.type === "oauth") { + const inputs: Record = {} + + for (const prompt of method.prompts ?? []) { + if (prompt.conditional) { + // Format: "key:value" - checks if inputs[key] === value + const [key, value] = prompt.conditional.split(":") + if (!key || !value || inputs[key] !== value) continue + } + + if (prompt.type === "select") { + if (!prompt.options?.length) continue + + const selectedValue = await new Promise((resolve) => { + dialog.replace( + () => ( + ({ + title: opt.label, + value: opt.value, + description: opt.hint, + }))} + onSelect={(option) => resolve(option.value)} + /> + ), + () => resolve(null), + ) + }) + if (selectedValue === null) return + inputs[prompt.key] = selectedValue + continue + } + + const textValue = await DialogPrompt.show(dialog, prompt.message, { + placeholder: prompt.placeholder ?? "Enter value", + }) + if (textValue === null) return + inputs[prompt.key] = textValue + } + const result = await sdk.client.provider.oauth.authorize({ providerID: provider.id, method: index, + inputs, }) + if (result.error) { + toast.show({ + variant: "error", + message: "Connection failed. Check the URL or domain.", + }) + return + } if (result.data?.method === "code") { dialog.replace(() => ( @@ -130,7 +179,8 @@ function AutoMethod(props: AutoMethodProps) { } await sdk.client.instance.dispose() await sync.bootstrap() - dialog.replace(() => ) + const actualProvider = result.data?.provider ?? props.providerID + dialog.replace(() => ) }) return ( @@ -171,7 +221,7 @@ function CodeMethod(props: CodeMethodProps) { title={props.title} placeholder="Authorization code" onConfirm={async (value) => { - const { error } = await sdk.client.provider.oauth.callback({ + const { error, data } = await sdk.client.provider.oauth.callback({ providerID: props.providerID, method: props.index, code: value, @@ -179,7 +229,8 @@ function CodeMethod(props: CodeMethodProps) { if (!error) { await sdk.client.instance.dispose() await sync.bootstrap() - dialog.replace(() => ) + const actualProvider = data?.provider ?? props.providerID + dialog.replace(() => ) return } setError(true) diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index e6681ff0891..724c7f1cfb4 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -18,16 +18,56 @@ export namespace ProviderAuth { return { methods, pending: {} as Record } }) + export const MethodPromptOption = z + .object({ + label: z.string(), + value: z.string(), + hint: z.string().optional(), + }) + .meta({ + ref: "ProviderAuthMethodPromptOption", + }) + export type MethodPromptOption = z.infer + + export const MethodPrompt = z + .object({ + type: z.union([z.literal("select"), z.literal("text")]), + key: z.string(), + message: z.string(), + placeholder: z.string().optional(), + options: MethodPromptOption.array().optional(), + conditional: z.string().optional(), // Serialized condition: "key:value" + }) + .meta({ + ref: "ProviderAuthMethodPrompt", + }) + export type MethodPrompt = z.infer + export const Method = z .object({ type: z.union([z.literal("oauth"), z.literal("api")]), label: z.string(), + prompts: MethodPrompt.array().optional(), }) .meta({ ref: "ProviderAuthMethod", }) export type Method = z.infer + function serializeCondition(condition: unknown): string | undefined { + if (typeof condition === "string") return condition + if (typeof condition !== "function") return undefined + const source = condition.toString() + const match = source.match(/inputs\.(\w+)\s*===?\s*["'`]([^"'`]+)["'`]/) + + if (!match) { + console.warn(`[ProviderAuth] Failed to serialize condition: ${source.slice(0, 100)}`) + return undefined + } + + return `${match[1]}:${match[2]}` + } + export async function methods() { const s = await state().then((x) => x.methods) return mapValues(s, (x) => @@ -35,6 +75,23 @@ export namespace ProviderAuth { (y): Method => ({ type: y.type, label: y.label, + prompts: y.prompts?.map( + (p: { + type: string + key: string + message: string + placeholder?: string + options?: MethodPromptOption[] + condition?: unknown + }): MethodPrompt => ({ + type: p.type as "select" | "text", + key: p.key, + message: p.message, + placeholder: p.placeholder, + options: p.options, + conditional: serializeCondition(p.condition), + }), + ), }), ), ) @@ -55,12 +112,13 @@ export namespace ProviderAuth { z.object({ providerID: z.string(), method: z.number(), + inputs: z.record(z.string(), z.string()).optional(), }), async (input): Promise => { const auth = await state().then((s) => s.methods[input.providerID]) const method = auth.methods[input.method] if (method.type === "oauth") { - const result = await method.authorize() + const result = await method.authorize(input.inputs ?? {}) await state().then((s) => (s.pending[input.providerID] = result)) return { url: result.url, @@ -92,8 +150,10 @@ export namespace ProviderAuth { } if (result?.type === "success") { + const saveProvider = result.provider ?? input.providerID + if ("key" in result) { - await Auth.set(input.providerID, { + await Auth.set(saveProvider, { type: "api", key: result.key, }) @@ -108,12 +168,12 @@ export namespace ProviderAuth { if (result.accountId) { info.accountId = result.accountId } - await Auth.set(input.providerID, info) + await Auth.set(saveProvider, info) } - return + return { provider: saveProvider } } - throw new OauthCallbackFailed({}) + throw new OauthCallbackFailed({ providerID: input.providerID }) }, ) @@ -143,5 +203,10 @@ export namespace ProviderAuth { }), ) - export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({})) + export const OauthCallbackFailed = NamedError.create( + "ProviderAuthOauthCallbackFailed", + z.object({ + providerID: z.string(), + }), + ) } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 52457515b8e..0d4569ef81e 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1855,14 +1855,19 @@ export namespace Server { "json", z.object({ method: z.number().meta({ description: "Auth method index" }), + inputs: z + .record(z.string(), z.string()) + .optional() + .meta({ description: "Prompt inputs collected from the user" }), }), ), async (c) => { const providerID = c.req.valid("param").providerID - const { method } = c.req.valid("json") + const body = c.req.valid("json") const result = await ProviderAuth.authorize({ providerID, - method, + method: body.method, + inputs: body.inputs, }) return c.json(result) }, @@ -1878,7 +1883,11 @@ export namespace Server { description: "OAuth callback processed successfully", content: { "application/json": { - schema: resolver(z.boolean()), + schema: resolver( + z.object({ + provider: z.string().optional(), + }), + ), }, }, }, @@ -1901,12 +1910,12 @@ export namespace Server { async (c) => { const providerID = c.req.valid("param").providerID const { method, code } = c.req.valid("json") - await ProviderAuth.callback({ + const result = await ProviderAuth.callback({ providerID, method, code, }) - return c.json(true) + return c.json(result ?? {}) }, ) .get( diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index f83913ea5e1..356815a46e6 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -1907,6 +1907,9 @@ export class Oauth extends HeyApiClient { providerID: string directory?: string method?: number + inputs?: { + [key: string]: string + } }, options?: Options, ) { @@ -1918,6 +1921,7 @@ export class Oauth extends HeyApiClient { { in: "path", key: "providerID" }, { in: "query", key: "directory" }, { in: "body", key: "method" }, + { in: "body", key: "inputs" }, ], }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e423fecea42..fcc0a32a543 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1929,9 +1929,25 @@ export type Provider = { } } +export type ProviderAuthMethodPromptOption = { + label: string + value: string + hint?: string +} + +export type ProviderAuthMethodPrompt = { + type: "select" | "text" + key: string + message: string + placeholder?: string + options?: Array + conditional?: string +} + export type ProviderAuthMethod = { type: "oauth" | "api" label: string + prompts?: Array } export type ProviderAuthAuthorization = { @@ -3856,6 +3872,12 @@ export type ProviderOauthAuthorizeData = { * Auth method index */ method: number + /** + * Prompt inputs collected from the user + */ + inputs?: { + [key: string]: string + } } path: { /** @@ -3923,7 +3945,9 @@ export type ProviderOauthCallbackResponses = { /** * OAuth callback processed successfully */ - 200: boolean + 200: { + provider?: string + } } export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 367985e5d29..45261db0338 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -3779,6 +3779,16 @@ "method": { "description": "Auth method index", "type": "number" + }, + "inputs": { + "description": "Prompt inputs collected from the user", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } } }, "required": ["method"] @@ -10060,6 +10070,57 @@ }, "required": ["id", "name", "source", "env", "options", "models"] }, + "ProviderAuthMethodPromptOption": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "hint": { + "type": "string" + } + }, + "required": ["label", "value"] + }, + "ProviderAuthMethodPrompt": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "select" + }, + { + "type": "string", + "const": "text" + } + ] + }, + "key": { + "type": "string" + }, + "message": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProviderAuthMethodPromptOption" + } + }, + "conditional": { + "type": "string" + } + }, + "required": ["type", "key", "message"] + }, "ProviderAuthMethod": { "type": "object", "properties": { @@ -10077,6 +10138,12 @@ }, "label": { "type": "string" + }, + "prompts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProviderAuthMethodPrompt" + } } }, "required": ["type", "label"] diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 80c6f89e150..28d03655246 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -574,7 +574,17 @@ Some models need to be manually enabled in your [GitHub Copilot settings](https: /connect ``` -2. Navigate to [github.com/login/device](https://github.com/login/device) and enter the code. +2. Select your GitHub deployment type. + + ```txt + ┌ Select GitHub deployment type + │ + │ GitHub.com Public + │ GitHub Enterprise + └ + ``` + +3. Navigate to [github.com/login/device](https://github.com/login/device) and enter the code. ```txt ┌ Login with GitHub Copilot @@ -586,12 +596,44 @@ Some models need to be manually enabled in your [GitHub Copilot settings](https: └ Waiting for authorization... ``` -3. Now run the `/models` command to select the model you want. +4. Now run the `/models` command to select the model you want. ```txt /models ``` +##### GitHub Enterprise + +For organizations using GitHub Enterprise (data residency or self-hosted): + +1. Run `/connect` and select **GitHub Copilot**. + +2. Select **GitHub Enterprise** as the deployment type. + +3. Enter your GitHub Enterprise URL or domain. + + ```txt + ┌ Enter your GitHub Enterprise URL or domain + │ + │ company.ghe.com or https://company.ghe.com + │ + └ enter submit + ``` + +4. Navigate to your Enterprise device login page and enter the code. + + ```txt + ┌ Login with GitHub Copilot + │ + │ https://company.ghe.com/login/device + │ + │ Enter code: C30A-4C07 + │ + └ Waiting for authorization... + ``` + +5. Run `/models` to select the model you want. + --- ### Google Vertex AI