summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/auth/anthropic.ts84
-rw-r--r--packages/opencode/src/auth/copilot.ts19
-rw-r--r--packages/opencode/src/auth/index.ts44
-rw-r--r--packages/opencode/src/cli/cmd/auth.ts363
-rw-r--r--packages/opencode/src/flag/flag.ts1
-rw-r--r--packages/opencode/src/plugin/index.ts26
-rw-r--r--packages/opencode/src/provider/provider.ts114
-rw-r--r--packages/opencode/src/server/server.ts32
-rw-r--r--packages/plugin/src/index.ts55
-rw-r--r--packages/sdk/js/src/gen/sdk.gen.ts20
-rw-r--r--packages/sdk/js/src/gen/types.gen.ts56
11 files changed, 374 insertions, 440 deletions
diff --git a/packages/opencode/src/auth/anthropic.ts b/packages/opencode/src/auth/anthropic.ts
deleted file mode 100644
index d3228cb88..000000000
--- a/packages/opencode/src/auth/anthropic.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { generatePKCE } from "@openauthjs/openauth/pkce"
-import { Auth } from "./index"
-
-export namespace AuthAnthropic {
- const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
-
- export async function authorize(mode: "max" | "console") {
- const pkce = await generatePKCE()
-
- const url = new URL(
- `https://${mode === "console" ? "console.anthropic.com" : "claude.ai"}/oauth/authorize`,
- import.meta.url,
- )
- url.searchParams.set("code", "true")
- url.searchParams.set("client_id", CLIENT_ID)
- url.searchParams.set("response_type", "code")
- url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback")
- url.searchParams.set("scope", "org:create_api_key user:profile user:inference")
- url.searchParams.set("code_challenge", pkce.challenge)
- url.searchParams.set("code_challenge_method", "S256")
- url.searchParams.set("state", pkce.verifier)
- return {
- url: url.toString(),
- verifier: pkce.verifier,
- }
- }
-
- export async function exchange(code: string, verifier: string) {
- const splits = code.split("#")
- const result = await fetch("https://console.anthropic.com/v1/oauth/token", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- code: splits[0],
- state: splits[1],
- grant_type: "authorization_code",
- client_id: CLIENT_ID,
- redirect_uri: "https://console.anthropic.com/oauth/code/callback",
- code_verifier: verifier,
- }),
- })
- if (!result.ok) throw new ExchangeFailed()
- const json = await result.json()
- return {
- refresh: json.refresh_token as string,
- access: json.access_token as string,
- expires: Date.now() + json.expires_in * 1000,
- }
- }
-
- export async function access() {
- const info = await Auth.get("anthropic")
- if (!info || info.type !== "oauth") return
- if (info.access && info.expires > Date.now()) return info.access
- const response = await fetch("https://console.anthropic.com/v1/oauth/token", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- grant_type: "refresh_token",
- refresh_token: info.refresh,
- client_id: CLIENT_ID,
- }),
- })
- if (!response.ok) return
- const json = await response.json()
- await Auth.set("anthropic", {
- type: "oauth",
- refresh: json.refresh_token as string,
- access: json.access_token as string,
- expires: Date.now() + json.expires_in * 1000,
- })
- return json.access_token as string
- }
-
- export class ExchangeFailed extends Error {
- constructor() {
- super("Exchange failed")
- }
- }
-}
diff --git a/packages/opencode/src/auth/copilot.ts b/packages/opencode/src/auth/copilot.ts
deleted file mode 100644
index 7a9b70f09..000000000
--- a/packages/opencode/src/auth/copilot.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Global } from "../global"
-import { lazy } from "../util/lazy"
-import path from "path"
-
-export const AuthCopilot = lazy(async () => {
- const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
- const exists = await file.exists()
- const response = fetch("https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts")
- .then((x) => Bun.write(file, x))
- .catch(() => {})
-
- if (!exists) {
- const worked = await response
- if (!worked) return
- }
- const result = await import(file.name!).catch(() => {})
- if (!result) return
- return result.AuthCopilot
-})
diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts
index ace51b26f..a09143438 100644
--- a/packages/opencode/src/auth/index.ts
+++ b/packages/opencode/src/auth/index.ts
@@ -4,25 +4,31 @@ import fs from "fs/promises"
import { z } from "zod"
export namespace Auth {
- export const Oauth = z.object({
- type: z.literal("oauth"),
- refresh: z.string(),
- access: z.string(),
- expires: z.number(),
- })
-
- export const Api = z.object({
- type: z.literal("api"),
- key: z.string(),
- })
-
- export const WellKnown = z.object({
- type: z.literal("wellknown"),
- key: z.string(),
- token: z.string(),
- })
-
- export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown])
+ export const Oauth = z
+ .object({
+ type: z.literal("oauth"),
+ refresh: z.string(),
+ access: z.string(),
+ expires: z.number(),
+ })
+ .openapi({ ref: "OAuth" })
+
+ export const Api = z
+ .object({
+ type: z.literal("api"),
+ key: z.string(),
+ })
+ .openapi({ ref: "ApiAuth" })
+
+ export const WellKnown = z
+ .object({
+ type: z.literal("wellknown"),
+ key: z.string(),
+ token: z.string(),
+ })
+ .openapi({ ref: "WellKnownAuth" })
+
+ export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).openapi({ ref: "Auth" })
export type Info = z.infer<typeof Info>
const filepath = path.join(Global.Path.data, "auth.json")
diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts
index dab0bfd50..ab06d5bf3 100644
--- a/packages/opencode/src/cli/cmd/auth.ts
+++ b/packages/opencode/src/cli/cmd/auth.ts
@@ -1,5 +1,3 @@
-import { AuthAnthropic } from "../../auth/anthropic"
-import { AuthCopilot } from "../../auth/copilot"
import { Auth } from "../../auth"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
@@ -10,6 +8,8 @@ import { map, pipe, sortBy, values } from "remeda"
import path from "path"
import os from "os"
import { Global } from "../../global"
+import { Plugin } from "../../plugin"
+import { App } from "../../app/app"
export const AuthCommand = cmd({
command: "auth",
@@ -75,242 +75,179 @@ export const AuthLoginCommand = cmd({
type: "string",
}),
async handler(args) {
- UI.empty()
- prompts.intro("Add credential")
- if (args.url) {
- const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
- prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
- const proc = Bun.spawn({
- cmd: wellknown.auth.command,
- stdout: "pipe",
- })
- const exit = await proc.exited
- if (exit !== 0) {
- prompts.log.error("Failed")
+ await App.provide({ cwd: process.cwd() }, async () => {
+ UI.empty()
+ prompts.intro("Add credential")
+ if (args.url) {
+ const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
+ prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
+ const proc = Bun.spawn({
+ cmd: wellknown.auth.command,
+ stdout: "pipe",
+ })
+ const exit = await proc.exited
+ if (exit !== 0) {
+ prompts.log.error("Failed")
+ prompts.outro("Done")
+ return
+ }
+ const token = await new Response(proc.stdout).text()
+ await Auth.set(args.url, {
+ type: "wellknown",
+ key: wellknown.auth.env,
+ token: token.trim(),
+ })
+ prompts.log.success("Logged into " + args.url)
prompts.outro("Done")
return
}
- const token = await new Response(proc.stdout).text()
- await Auth.set(args.url, {
- type: "wellknown",
- key: wellknown.auth.env,
- token: token.trim(),
- })
- prompts.log.success("Logged into " + args.url)
- prompts.outro("Done")
- return
- }
- await ModelsDev.refresh().catch(() => {})
- const providers = await ModelsDev.get()
- const priority: Record<string, number> = {
- anthropic: 0,
- "github-copilot": 1,
- openai: 2,
- google: 3,
- openrouter: 4,
- vercel: 5,
- }
- let provider = await prompts.autocomplete({
- message: "Select provider",
- maxItems: 8,
- options: [
- ...pipe(
- providers,
- values(),
- sortBy(
- (x) => priority[x.id] ?? 99,
- (x) => x.name ?? x.id,
- ),
- map((x) => ({
- label: x.name,
- value: x.id,
- hint: priority[x.id] === 0 ? "recommended" : undefined,
- })),
- ),
- {
- value: "other",
- label: "Other",
- },
- ],
- })
-
- if (prompts.isCancel(provider)) throw new UI.CancelledError()
-
- if (provider === "other") {
- provider = await prompts.text({
- message: "Enter provider id",
- validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
- })
- if (prompts.isCancel(provider)) throw new UI.CancelledError()
- provider = provider.replace(/^@ai-sdk\//, "")
- if (prompts.isCancel(provider)) throw new UI.CancelledError()
- prompts.log.warn(
- `This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
- )
- }
-
- if (provider === "amazon-bedrock") {
- prompts.log.info(
- "Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
- )
- prompts.outro("Done")
- return
- }
-
- if (provider === "anthropic") {
- const method = await prompts.select({
- message: "Login method",
+ await ModelsDev.refresh().catch(() => {})
+ const providers = await ModelsDev.get()
+ const priority: Record<string, number> = {
+ anthropic: 0,
+ "github-copilot": 1,
+ openai: 2,
+ google: 3,
+ openrouter: 4,
+ vercel: 5,
+ }
+ let provider = await prompts.autocomplete({
+ message: "Select provider",
+ maxItems: 8,
options: [
+ ...pipe(
+ providers,
+ values(),
+ sortBy(
+ (x) => priority[x.id] ?? 99,
+ (x) => x.name ?? x.id,
+ ),
+ map((x) => ({
+ label: x.name,
+ value: x.id,
+ hint: priority[x.id] === 0 ? "recommended" : undefined,
+ })),
+ ),
{
- label: "Claude Pro/Max",
- value: "max",
- },
- {
- label: "Create API Key",
- value: "console",
- },
- {
- label: "Manually enter API Key",
- value: "api",
+ value: "other",
+ label: "Other",
},
],
})
- if (prompts.isCancel(method)) throw new UI.CancelledError()
-
- if (method === "max") {
- // some weird bug where program exits without this
- await new Promise((resolve) => setTimeout(resolve, 10))
- const { url, verifier } = await AuthAnthropic.authorize("max")
- prompts.note("Trying to open browser...")
- try {
- await open(url)
- } catch (e) {
- prompts.log.error(
- "Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:",
- )
- }
- prompts.log.info(url)
- const code = await prompts.text({
- message: "Paste the authorization code here: ",
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
- })
- if (prompts.isCancel(code)) throw new UI.CancelledError()
+ if (prompts.isCancel(provider)) throw new UI.CancelledError()
- try {
- const credentials = await AuthAnthropic.exchange(code, verifier)
- await Auth.set("anthropic", {
- type: "oauth",
- refresh: credentials.refresh,
- access: credentials.access,
- expires: credentials.expires,
+ const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider))
+ if (plugin && plugin.auth) {
+ let index = 0
+ if (plugin.auth.methods.length > 1) {
+ const method = await prompts.select({
+ message: "Login method",
+ options: [
+ ...plugin.auth.methods.map((x, index) => ({
+ label: x.label,
+ value: index.toString(),
+ })),
+ ],
})
- prompts.log.success("Login successful")
- } catch {
- prompts.log.error("Invalid code")
+ if (prompts.isCancel(method)) throw new UI.CancelledError()
+ index = parseInt(method)
}
- prompts.outro("Done")
- return
- }
+ const method = plugin.auth.methods[index]
+ if (method.type === "oauth") {
+ await new Promise((resolve) => setTimeout(resolve, 10))
+ const authorize = await method.authorize()
- if (method === "console") {
- // some weird bug where program exits without this
- await new Promise((resolve) => setTimeout(resolve, 10))
- const { url, verifier } = await AuthAnthropic.authorize("console")
- prompts.note("Trying to open browser...")
- try {
- await open(url)
- } catch (e) {
- prompts.log.error(
- "Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:",
- )
- }
- prompts.log.info(url)
-
- const code = await prompts.text({
- message: "Paste the authorization code here: ",
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
- })
- if (prompts.isCancel(code)) throw new UI.CancelledError()
+ if (authorize.url) {
+ try {
+ await open(authorize.url)
+ } catch (e) {}
+ prompts.log.info("Go to: " + authorize.url)
+ }
- try {
- const credentials = await AuthAnthropic.exchange(code, verifier)
- const accessToken = credentials.access
- const response = await fetch("https://api.anthropic.com/api/oauth/claude_cli/create_api_key", {
- method: "POST",
- headers: {
- Authorization: `Bearer ${accessToken}`,
- "Content-Type": "application/x-www-form-urlencoded",
- Accept: "application/json, text/plain, */*",
- },
- })
- if (!response.ok) {
- throw new Error("Failed to create API key")
+ if (authorize.method === "auto") {
+ if (authorize.instructions) {
+ prompts.log.info(authorize.instructions)
+ }
+ const spinner = prompts.spinner()
+ spinner.start("Waiting for authorization...")
+ const result = await authorize.callback()
+ if (result.type === "failed") {
+ spinner.stop("Failed to authorize", 1)
+ }
+ if (result.type === "success") {
+ await Auth.set(provider, {
+ type: "oauth",
+ refresh: result.refresh,
+ access: result.access,
+ expires: result.expires,
+ })
+ spinner.stop("Login successful")
+ }
}
- const json = await response.json()
- await Auth.set("anthropic", {
- type: "api",
- key: json.raw_key,
- })
- prompts.log.success("Login successful - API key created and saved")
- } catch (error) {
- prompts.log.error("Invalid code or failed to create API key")
+ if (authorize.method === "code") {
+ const code = await prompts.text({
+ message: "Paste the authorization code here: ",
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(code)) throw new UI.CancelledError()
+ const result = await authorize.callback(code)
+ if (result.type === "failed") {
+ prompts.log.error("Failed to authorize")
+ }
+ if (result.type === "success") {
+ await Auth.set(provider, {
+ type: "oauth",
+ refresh: result.refresh,
+ access: result.access,
+ expires: result.expires,
+ })
+ prompts.log.success("Login successful")
+ }
+ }
+ prompts.outro("Done")
+ return
}
- prompts.outro("Done")
- return
}
- }
-
- const copilot = await AuthCopilot()
- if (provider === "github-copilot" && copilot) {
- await new Promise((resolve) => setTimeout(resolve, 10))
- const deviceInfo = await copilot.authorize()
- prompts.note(`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`)
-
- const spinner = prompts.spinner()
- spinner.start("Waiting for authorization...")
+ if (provider === "other") {
+ provider = await prompts.text({
+ message: "Enter provider id",
+ validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
+ })
+ if (prompts.isCancel(provider)) throw new UI.CancelledError()
+ provider = provider.replace(/^@ai-sdk\//, "")
+ if (prompts.isCancel(provider)) throw new UI.CancelledError()
+ prompts.log.warn(
+ `This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
+ )
+ }
- while (true) {
- await new Promise((resolve) => setTimeout(resolve, deviceInfo.interval * 1000))
- const response = await copilot.poll(deviceInfo.device)
- if (response.status === "pending") continue
- if (response.status === "success") {
- await Auth.set("github-copilot", {
- type: "oauth",
- refresh: response.refresh,
- access: response.access,
- expires: response.expires,
- })
- spinner.stop("Login successful")
- break
- }
- if (response.status === "failed") {
- spinner.stop("Failed to authorize", 1)
- break
- }
+ if (provider === "amazon-bedrock") {
+ prompts.log.info(
+ "Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID",
+ )
+ prompts.outro("Done")
+ return
}
- prompts.outro("Done")
- return
- }
+ if (provider === "vercel") {
+ prompts.log.info("You can create an api key in the dashboard")
+ }
- if (provider === "vercel") {
- prompts.log.info("You can create an api key in the dashboard")
- }
+ const key = await prompts.password({
+ message: "Enter your API key",
+ validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+ })
+ if (prompts.isCancel(key)) throw new UI.CancelledError()
+ await Auth.set(provider, {
+ type: "api",
+ key,
+ })
- const key = await prompts.password({
- message: "Enter your API key",
- validate: (x) => (x && x.length > 0 ? undefined : "Required"),
- })
- if (prompts.isCancel(key)) throw new UI.CancelledError()
- await Auth.set(provider, {
- type: "api",
- key,
+ prompts.outro("Done")
})
-
- prompts.outro("Done")
},
})
diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts
index 38253d729..0d8bffa9e 100644
--- a/packages/opencode/src/flag/flag.ts
+++ b/packages/opencode/src/flag/flag.ts
@@ -4,6 +4,7 @@ export namespace Flag {
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"]
+ export const OPENCODE_DISABLE_DEFAULT_PLUGINS = truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts
index 3ffa30191..8fbd38c67 100644
--- a/packages/opencode/src/plugin/index.ts
+++ b/packages/opencode/src/plugin/index.ts
@@ -6,6 +6,7 @@ import { Log } from "../util/log"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { Server } from "../server/server"
import { BunProc } from "../bun"
+import { Flag } from "../flag/flag"
export namespace Plugin {
const log = Log.create({ service: "plugin" })
@@ -17,7 +18,17 @@ export namespace Plugin {
})
const config = await Config.get()
const hooks = []
- for (let plugin of config.plugin ?? []) {
+ const input = {
+ client,
+ app,
+ $: Bun.$,
+ }
+ const plugins = [...(config.plugin ?? [])]
+ if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
+ plugins.push("opencode-copilot-auth")
+ plugins.push("opencode-anthropic-auth")
+ }
+ for (let plugin of plugins) {
log.info("loading plugin", { path: plugin })
if (!plugin.startsWith("file://")) {
const [pkg, version] = plugin.split("@")
@@ -25,22 +36,19 @@ export namespace Plugin {
}
const mod = await import(plugin)
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
- const init = await fn({
- client,
- app,
- $: Bun.$,
- })
+ const init = await fn(input)
hooks.push(init)
}
}
return {
hooks,
+ input,
}
})
export async function trigger<
- Name extends keyof Required<Hooks>,
+ Name extends Exclude<keyof Required<Hooks>, "auth" | "event">,
Input = Parameters<Required<Hooks>[Name]>[0],
Output = Parameters<Required<Hooks>[Name]>[1],
>(name: Name, input: Input, output: Output): Promise<Output> {
@@ -56,6 +64,10 @@ export namespace Plugin {
return output
}
+ export async function list() {
+ return state().then((x) => x.hooks)
+ }
+
export function init() {
Bus.subscribeAll(async (input) => {
const hooks = await state().then((x) => x.hooks)
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index f32231c53..42bb18042 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -5,8 +5,7 @@ import { mergeDeep, sortBy } from "remeda"
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import { BunProc } from "../bun"
-import { AuthAnthropic } from "../auth/anthropic"
-import { AuthCopilot } from "../auth/copilot"
+import { Plugin } from "../plugin"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
@@ -26,103 +25,13 @@ export namespace Provider {
type Source = "env" | "config" | "custom" | "api"
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
- async anthropic(provider) {
- const access = await AuthAnthropic.access()
- if (!access)
- return {
- autoload: false,
- options: {
- headers: {
- "anthropic-beta":
- "claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
- },
- },
- }
- for (const model of Object.values(provider.models)) {
- model.cost = {
- input: 0,
- output: 0,
- }
- }
- return {
- autoload: true,
- options: {
- apiKey: "",
- async fetch(input: any, init: any) {
- const access = await AuthAnthropic.access()
- const headers = {
- ...init.headers,
- authorization: `Bearer ${access}`,
- "anthropic-beta":
- "oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
- }
- delete headers["x-api-key"]
- return fetch(input, {
- ...init,
- headers,
- })
- },
- },
- }
- },
- "github-copilot": async (provider) => {
- const copilot = await AuthCopilot()
- if (!copilot) return { autoload: false }
- let info = await Auth.get("github-copilot")
- if (!info || info.type !== "oauth") return { autoload: false }
-
- if (provider && provider.models) {
- for (const model of Object.values(provider.models)) {
- model.cost = {
- input: 0,
- output: 0,
- }
- }
- }
-
+ async anthropic() {
return {
- autoload: true,
+ autoload: false,
options: {
- apiKey: "",
- async fetch(input: any, init: any) {
- const info = await Auth.get("github-copilot")
- if (!info || info.type !== "oauth") return
- if (!info.access || info.expires < Date.now()) {
- const tokens = await copilot.access(info.refresh)
- if (!tokens) throw new Error("GitHub Copilot authentication expired")
- await Auth.set("github-copilot", {
- type: "oauth",
- ...tokens,
- })
- info.access = tokens.access
- }
- let isAgentCall = false
- let isVisionRequest = false
- try {
- const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body
- if (body?.messages) {
- isAgentCall = body.messages.some((msg: any) => msg.role && ["tool", "assistant"].includes(msg.role))
- isVisionRequest = body.messages.some(
- (msg: any) =>
- Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
- )
- }
- } catch {}
- const headers: Record<string, string> = {
- ...init.headers,
- ...copilot.HEADERS,
- Authorization: `Bearer ${info.access}`,
- "Openai-Intent": "conversation-edits",
- "X-Initiator": isAgentCall ? "agent" : "user",
- }
- if (isVisionRequest) {
- headers["Copilot-Vision-Request"] = "true"
- }
- delete headers["x-api-key"]
- return fetch(input, {
- ...init,
- headers,
- })
+ headers: {
+ "anthropic-beta":
+ "claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
},
},
}
@@ -350,6 +259,17 @@ export namespace Provider {
}
}
+ for (const plugin of await Plugin.list()) {
+ if (!plugin.auth) continue
+ const providerID = plugin.auth.provider
+ if (disabled.has(providerID)) continue
+ const auth = await Auth.get(providerID)
+ if (!auth) continue
+ if (!plugin.auth.loader) continue
+ const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
+ mergeProvider(plugin.auth.provider, options ?? {}, "custom")
+ }
+
// load config
for (const [providerID, provider] of configProviders) {
mergeProvider(providerID, provider.options ?? {}, "config")
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 63d63b0aa..2ed65cbb4 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -20,6 +20,7 @@ import { callTui, TuiRoute } from "./tui"
import { Permission } from "../permission"
import { lazy } from "../util/lazy"
import { Agent } from "../agent/agent"
+import { Auth } from "../auth"
const ERRORS = {
400: {
@@ -1120,6 +1121,37 @@ export namespace Server {
async (c) => c.json(await callTui(c)),
)
.route("/tui/control", TuiRoute)
+ .put(
+ "/auth/:id",
+ describeRoute({
+ description: "Set authentication credentials",
+ operationId: "auth.set",
+ responses: {
+ 200: {
+ description: "Successfully set authentication credentials",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...ERRORS,
+ },
+ }),
+ zValidator(
+ "param",
+ z.object({
+ id: z.string(),
+ }),
+ ),
+ zValidator("json", Auth.Info),
+ async (c) => {
+ const id = c.req.valid("param").id
+ const info = c.req.valid("json")
+ await Auth.set(id, info)
+ return c.json(true)
+ },
+ )
return result
})
diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts
index 7ea82d2aa..1a6cbf124 100644
--- a/packages/plugin/src/index.ts
+++ b/packages/plugin/src/index.ts
@@ -1,4 +1,14 @@
-import type { Event, createOpencodeClient, App, Model, Provider, Permission, UserMessage, Part } from "@opencode-ai/sdk"
+import type {
+ Event,
+ createOpencodeClient,
+ App,
+ Model,
+ Provider,
+ Permission,
+ UserMessage,
+ Part,
+ Auth,
+} from "@opencode-ai/sdk"
import type { BunShell } from "./shell"
export type PluginInput = {
@@ -10,6 +20,49 @@ export type Plugin = (input: PluginInput) => Promise<Hooks>
export interface Hooks {
event?: (input: { event: Event }) => Promise<void>
+ auth?: {
+ provider: string
+ loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
+ methods: (
+ | {
+ type: "oauth"
+ label: string
+ authorize(): Promise<
+ { url: string; instructions: string } & (
+ | {
+ method: "auto"
+ callback(): Promise<
+ | {
+ type: "success"
+ refresh: string
+ access: string
+ expires: number
+ }
+ | {
+ type: "failed"
+ }
+ >
+ }
+ | {
+ method: "code"
+ callback(code: string): Promise<
+ | {
+ type: "success"
+ refresh: string
+ access: string
+ expires: number
+ }
+ | {
+ type: "failed"
+ }
+ >
+ }
+ )
+ >
+ }
+ | { type: "api"; label: string }
+ )[]
+ }
/**
* Called when a new message is received
*/
diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts
index 8a26dfa70..5dd8552d5 100644
--- a/packages/sdk/js/src/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/gen/sdk.gen.ts
@@ -77,6 +77,9 @@ import type {
TuiClearPromptResponses,
TuiExecuteCommandData,
TuiExecuteCommandResponses,
+ AuthSetData,
+ AuthSetResponses,
+ AuthSetErrors,
} from "./types.gen.js"
import { client as _heyApiClient } from "./client.gen.js"
@@ -517,6 +520,22 @@ class Tui extends _HeyApiClient {
}
}
+class Auth extends _HeyApiClient {
+ /**
+ * Set authentication credentials
+ */
+ public set<ThrowOnError extends boolean = false>(options: Options<AuthSetData, ThrowOnError>) {
+ return (options.client ?? this._client).put<AuthSetResponses, AuthSetErrors, ThrowOnError>({
+ url: "/auth/{id}",
+ ...options,
+ headers: {
+ "Content-Type": "application/json",
+ ...options.headers,
+ },
+ })
+ }
+}
+
export class OpencodeClient extends _HeyApiClient {
/**
* Respond to a permission request
@@ -544,4 +563,5 @@ export class OpencodeClient extends _HeyApiClient {
find = new Find({ client: this._client })
file = new File({ client: this._client })
tui = new Tui({ client: this._client })
+ auth = new Auth({ client: this._client })
}
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index 3652ea584..d2b990469 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -1105,6 +1105,35 @@ export type Agent = {
}
}
+export type Auth =
+ | ({
+ type: "oauth"
+ } & OAuth)
+ | ({
+ type: "api"
+ } & ApiAuth)
+ | ({
+ type: "wellknown"
+ } & WellKnownAuth)
+
+export type OAuth = {
+ type: "oauth"
+ refresh: string
+ access: string
+ expires: number
+}
+
+export type ApiAuth = {
+ type: "api"
+ key: string
+}
+
+export type WellKnownAuth = {
+ type: "wellknown"
+ key: string
+ token: string
+}
+
export type EventSubscribeData = {
body?: never
path?: never
@@ -1858,6 +1887,33 @@ export type TuiExecuteCommandResponses = {
export type TuiExecuteCommandResponse = TuiExecuteCommandResponses[keyof TuiExecuteCommandResponses]
+export type AuthSetData = {
+ body?: Auth
+ path: {
+ id: string
+ }
+ query?: never
+ url: "/auth/{id}"
+}
+
+export type AuthSetErrors = {
+ /**
+ * Bad request
+ */
+ 400: _Error
+}
+
+export type AuthSetError = AuthSetErrors[keyof AuthSetErrors]
+
+export type AuthSetResponses = {
+ /**
+ * Successfully set authentication credentials
+ */
+ 200: boolean
+}
+
+export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses]
+
export type ClientOptions = {
baseUrl: `${string}://${string}` | (string & {})
}