From 82ff918c6af0fcc3f3a168d3f6d997670549e9dd Mon Sep 17 00:00:00 2001 From: Hao Xiang Liew Date: Fri, 31 Oct 2025 17:28:13 -0700 Subject: [PATCH 1/3] feat: anthropic oauth w/ 1m context This is available with the Claude Max plans on "sonnet-4+' models Has corresponding changes in haoxiangliew/opencode-anthropic-auth --- packages/opencode/src/auth/index.ts | 1 + packages/opencode/src/cli/cmd/auth.ts | 2 ++ packages/plugin/src/index.ts | 2 ++ 3 files changed, 5 insertions(+) diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 6d90c932570..2a8e9c0f452 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -10,6 +10,7 @@ export namespace Auth { refresh: z.string(), access: z.string(), expires: z.number(), + has1MContext: z.boolean().optional(), }) .meta({ ref: "OAuth" }) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 763d82b3f44..1605e6ddb78 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -181,6 +181,7 @@ export const AuthLoginCommand = cmd({ refresh: result.refresh, access: result.access, expires: result.expires, + ...(result.has1MContext && { has1MContext: result.has1MContext }), }) } if ("key" in result) { @@ -210,6 +211,7 @@ export const AuthLoginCommand = cmd({ refresh: result.refresh, access: result.access, expires: result.expires, + ...(result.has1MContext && { has1MContext: result.has1MContext }), }) } if ("key" in result) { diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 9c2647c6004..c7aafead554 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -51,6 +51,7 @@ export interface Hooks { refresh: string access: string expires: number + has1MContext?: boolean } | { key: string } )) @@ -69,6 +70,7 @@ export interface Hooks { refresh: string access: string expires: number + has1MContext?: boolean } | { key: string } )) From c3e3c249724b173865560b62ce37d59be315b5d4 Mon Sep 17 00:00:00 2001 From: Hao Xiang Liew Date: Wed, 5 Nov 2025 23:01:55 -0800 Subject: [PATCH 2/3] update --- packages/opencode/src/cli/cmd/auth.ts | 403 ++++++++++++-------------- 1 file changed, 189 insertions(+), 214 deletions(-) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index 2d2556af730..5c268386933 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -14,11 +14,7 @@ export const AuthCommand = cmd({ command: "auth", describe: "manage credentials", builder: (yargs) => - yargs - .command(AuthLoginCommand) - .command(AuthLogoutCommand) - .command(AuthListCommand) - .demandCommand(), + yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(), async handler() {}, }) @@ -64,9 +60,7 @@ export const AuthListCommand = cmd({ prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`) } - prompts.outro( - `${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"), - ) + prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s")) } }, }) @@ -86,9 +80,7 @@ export const AuthLoginCommand = cmd({ UI.empty() prompts.intro("Add credential") if (args.url) { - const wellknown = await fetch(`${args.url}/.well-known/opencode`).then( - (x) => x.json() as any, - ) + const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any) prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``) const proc = Bun.spawn({ cmd: wellknown.auth.command, @@ -110,242 +102,225 @@ export const AuthLoginCommand = cmd({ prompts.outro("Done") return } - await ModelsDev.refresh().catch(() => {}) - const providers = await ModelsDev.get() - const priority: Record = { - opencode: 0, - anthropic: 1, - "github-copilot": 2, - openai: 3, - google: 4, - openrouter: 5, - vercel: 6, - } - 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] <= 1 ? "recommended" : undefined, - })), + await ModelsDev.refresh().catch(() => {}) + const providers = await ModelsDev.get() + const priority: Record = { + opencode: 0, + anthropic: 1, + "github-copilot": 2, + openai: 3, + google: 4, + openrouter: 5, + vercel: 6, + } + 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, ), - { - value: "other", - label: "Other", - }, - ], - }) + map((x) => ({ + label: x.name, + value: x.id, + hint: priority[x.id] <= 1 ? "recommended" : undefined, + })), + ), + { + value: "other", + label: "Other", + }, + ], + }) - if (prompts.isCancel(provider)) throw new UI.CancelledError() + if (prompts.isCancel(provider)) throw new UI.CancelledError() - 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(), - })), - ], - }) - if (prompts.isCancel(method)) throw new UI.CancelledError() - index = parseInt(method) - } - const method = plugin.auth.methods[index] + 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(), + })), + ], + }) + if (prompts.isCancel(method)) throw new UI.CancelledError() + index = parseInt(method) + } + const method = plugin.auth.methods[index] - // Handle prompts for all auth types - await new Promise((resolve) => setTimeout(resolve, 10)) - const inputs: Record = {} - if (method.prompts) { - for (const prompt of method.prompts) { - if (prompt.condition && !prompt.condition(inputs)) { - continue - } - if (prompt.type === "select") { - const value = await prompts.select({ - message: prompt.message, - options: prompt.options, - }) - if (prompts.isCancel(value)) throw new UI.CancelledError() - inputs[prompt.key] = value - } else { - const value = await prompts.text({ - message: prompt.message, - placeholder: prompt.placeholder, - validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined, - }) - if (prompts.isCancel(value)) throw new UI.CancelledError() - inputs[prompt.key] = value - } + // Handle prompts for all auth types + await new Promise((resolve) => setTimeout(resolve, 10)) + const inputs: Record = {} + if (method.prompts) { + for (const prompt of method.prompts) { + if (prompt.condition && !prompt.condition(inputs)) { + continue + } + if (prompt.type === "select") { + const value = await prompts.select({ + message: prompt.message, + options: prompt.options, + }) + if (prompts.isCancel(value)) throw new UI.CancelledError() + inputs[prompt.key] = value + } else { + const value = await prompts.text({ + message: prompt.message, + placeholder: prompt.placeholder, + validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined, + }) + if (prompts.isCancel(value)) throw new UI.CancelledError() + inputs[prompt.key] = value } } + } - if (method.type === "oauth") { - const authorize = await method.authorize(inputs) + if (method.type === "oauth") { + const authorize = await method.authorize(inputs) - if (authorize.url) { - prompts.log.info("Go to: " + authorize.url) - } + if (authorize.url) { + prompts.log.info("Go to: " + authorize.url) + } - 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") { - const saveProvider = result.provider ?? provider - if ("refresh" in result) { - const { - type: _, - provider: __, - refresh, - access, - expires, - has1MContext, - ...extraFields - } = result - await Auth.set(saveProvider, { - type: "oauth", - refresh, - access, - expires, - has1MContext, - ...extraFields, - }) - } - if ("key" in result) { - await Auth.set(saveProvider, { - type: "api", - key: result.key, - }) - } - spinner.stop("Login successful") - } + if (authorize.method === "auto") { + if (authorize.instructions) { + prompts.log.info(authorize.instructions) } - - 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") + 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") { + const saveProvider = result.provider ?? provider + if ("refresh" in result) { + const { type: _, provider: __, refresh, access, expires, has1MContext, ...extraFields } = result + await Auth.set(saveProvider, { + type: "oauth", + refresh, + access, + expires, + has1MContext, + ...extraFields, + }) } - if (result.type === "success") { - const saveProvider = result.provider ?? provider - if ("refresh" in result) { - const { - type: _, - provider: __, - refresh, - access, - expires, - has1MContext, - ...extraFields - } = result - await Auth.set(saveProvider, { - type: "oauth", - refresh, - access, - expires, - has1MContext, - ...extraFields, - }) - } - if ("key" in result) { - await Auth.set(saveProvider, { - type: "api", - key: result.key, - }) - } - prompts.log.success("Login successful") + if ("key" in result) { + await Auth.set(saveProvider, { + type: "api", + key: result.key, + }) } + spinner.stop("Login successful") } - - prompts.outro("Done") - return } - if (method.type === "api") { - if (method.authorize) { - const result = await method.authorize(inputs) - if (result.type === "failed") { - prompts.log.error("Failed to authorize") + 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") { + const saveProvider = result.provider ?? provider + if ("refresh" in result) { + const { type: _, provider: __, refresh, access, expires, has1MContext, ...extraFields } = result + await Auth.set(saveProvider, { + type: "oauth", + refresh, + access, + expires, + has1MContext, + ...extraFields, + }) } - if (result.type === "success") { - const saveProvider = result.provider ?? provider + if ("key" in result) { await Auth.set(saveProvider, { type: "api", key: result.key, }) - prompts.log.success("Login successful") } - prompts.outro("Done") - return + prompts.log.success("Login successful") } } - } - 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 === "opencode") { - prompts.log.info("Create an api key at https://opencode.ai/auth") - } - - if (provider === "vercel") { - prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token") + if (method.type === "api") { + if (method.authorize) { + const result = await method.authorize(inputs) + if (result.type === "failed") { + prompts.log.error("Failed to authorize") + } + if (result.type === "success") { + const saveProvider = result.provider ?? provider + await Auth.set(saveProvider, { + type: "api", + key: result.key, + }) + prompts.log.success("Login successful") + } + prompts.outro("Done") + return + } } + } - 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, + 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 === "opencode") { + prompts.log.info("Create an api key at https://opencode.ai/auth") + } + + if (provider === "vercel") { + prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token") + } + + 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") }, }) }, From f3808d0cf148994daab069b94683dc21ce093fc7 Mon Sep 17 00:00:00 2001 From: Hao Xiang Liew Date: Thu, 6 Nov 2025 17:56:50 -0800 Subject: [PATCH 3/3] reapply changes after merge 'dev' --- packages/opencode/src/cli/cmd/auth.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index b4c47f0a466..b331baf0216 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -212,12 +212,13 @@ export const AuthLoginCommand = cmd({ if (result.type === "success") { const saveProvider = result.provider ?? provider if ("refresh" in result) { - const { type: _, provider: __, refresh, access, expires, ...extraFields } = result + const { type: _, provider: __, refresh, access, expires, has1MContext, ...extraFields } = result await Auth.set(saveProvider, { type: "oauth", refresh, access, expires, + has1MContext, ...extraFields, }) } @@ -244,12 +245,13 @@ export const AuthLoginCommand = cmd({ if (result.type === "success") { const saveProvider = result.provider ?? provider if ("refresh" in result) { - const { type: _, provider: __, refresh, access, expires, ...extraFields } = result + const { type: _, provider: __, refresh, access, expires, has1MContext, ...extraFields } = result await Auth.set(saveProvider, { type: "oauth", refresh, access, expires, + has1MContext, ...extraFields, }) }