Skip to content

Commit 0448a30

Browse files
Spherrricalrekram1-nodegithub-actions[bot]
authored
fix(digitalocean): use OAuth token directly for inference instead of creating MAK (#28897)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 848d763 commit 0448a30

3 files changed

Lines changed: 30 additions & 39 deletions

File tree

packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@ function AutoMethod(props: AutoMethodProps) {
262262
method: props.index,
263263
})
264264
if (result.error) {
265+
toast.show({
266+
variant: "error",
267+
message:
268+
"name" in result.error && result.error.name === "ProviderAuthOauthCallbackFailed"
269+
? "OAuth authorization failed. Try /connect again."
270+
: JSON.stringify(result.error),
271+
})
265272
dialog.clear()
266273
return
267274
}

packages/opencode/src/plugin/digitalocean.ts

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@ import type { Model } from "@opencode-ai/sdk/v2"
33
import * as Log from "@opencode-ai/core/util/log"
44
import { InstallationVersion } from "@opencode-ai/core/installation/version"
55
import { createServer } from "http"
6+
import open from "open"
67

78
const log = Log.create({ service: "plugin.digitalocean" })
89

910
const DO_OAUTH_CLIENT_ID = "b1a6c5158156caac821fd1b30253ca8acb52454a48fa744420e41889cb589f82"
1011
const DO_AUTHORIZE_URL = "https://cloud.digitalocean.com/v1/oauth/authorize"
1112
const DO_API_BASE = "https://api.digitalocean.com"
13+
const DO_GENAI_API = `${DO_API_BASE}/v2/gen-ai`
1214
const DO_INFERENCE_BASE = "https://inference.do-ai.run/v1"
1315
const OAUTH_PORT = 1456
1416
const OAUTH_REDIRECT_PATH = "/auth/callback"
1517
const OAUTH_TOKEN_PATH = "/auth/token"
1618
const ROUTER_REFRESH_INTERVAL_MS = 5 * 60 * 1000
17-
const MAK_NAME_PREFIX = "opencode-oauth"
19+
const OAUTH_SCOPES = "genai:read inference:query"
1820

1921
interface ImplicitTokenPayload {
2022
access_token: string
@@ -28,12 +30,6 @@ interface PendingOAuth {
2830
reject: (error: Error) => void
2931
}
3032

31-
interface ApiKeyInfo {
32-
uuid: string
33-
name: string
34-
secret_key: string
35-
}
36-
3733
interface RouterEntry {
3834
name: string
3935
uuid?: string
@@ -59,7 +55,7 @@ function buildAuthorizeUrl(state: string): string {
5955
response_type: "token",
6056
client_id: DO_OAUTH_CLIENT_ID,
6157
redirect_uri: redirectUri(),
62-
scope: "genai:create genai:read",
58+
scope: OAUTH_SCOPES,
6359
state,
6460
})
6561
return `${DO_AUTHORIZE_URL}?${params.toString()}`
@@ -91,15 +87,20 @@ const HTML_CALLBACK = `<!doctype html>
9187
const errorDescription = params.get("error_description") || search.get("error_description")
9288
const titleEl = document.getElementById("title")
9389
const msgEl = document.getElementById("msg")
90+
const tokenUrl = new URL(${JSON.stringify(OAUTH_TOKEN_PATH)}, window.location.origin).href
9491
try {
9592
const body = error
9693
? { error, error_description: errorDescription || "" }
9794
: { access_token: params.get("access_token") || "", expires_in: params.get("expires_in") || "0", state: params.get("state") || "" }
98-
await fetch(${JSON.stringify(OAUTH_TOKEN_PATH)}, {
95+
const res = await fetch(tokenUrl, {
9996
method: "POST",
10097
headers: { "Content-Type": "application/json" },
10198
body: JSON.stringify(body),
10299
})
100+
if (!res.ok) {
101+
const detail = await res.text().catch(function () { return "" })
102+
throw new Error(detail || ("callback failed (" + res.status + ")"))
103+
}
103104
if (error) {
104105
titleEl.textContent = "Authorization Failed"
105106
msgEl.textContent = errorDescription || error
@@ -225,31 +226,10 @@ function waitForOAuthCallback(state: string): Promise<ImplicitTokenPayload> {
225226
})
226227
}
227228

228-
async function createModelAccessKey(bearer: string): Promise<ApiKeyInfo> {
229-
// Suffix-on-collision strategy keeps re-`/connect` non-destructive.
230-
const name = `${MAK_NAME_PREFIX}-${Math.floor(Date.now() / 1000)}`
231-
const res = await fetch(`${DO_API_BASE}/v2/gen-ai/models/api_keys`, {
232-
method: "POST",
233-
headers: {
234-
Authorization: `Bearer ${bearer}`,
235-
"Content-Type": "application/json",
236-
"User-Agent": `opencode/${InstallationVersion}`,
237-
},
238-
body: JSON.stringify({ name }),
239-
})
240-
if (!res.ok) {
241-
const body = await res.text().catch(() => "")
242-
throw new Error(`Failed to create Model Access Key (${res.status}): ${body}`)
243-
}
244-
const data = (await res.json()) as { api_key_info?: ApiKeyInfo }
245-
if (!data.api_key_info?.secret_key) throw new Error("Model Access Key response missing secret_key")
246-
return data.api_key_info
247-
}
248-
249229
async function listRouters(
250230
bearer: string,
251231
): Promise<{ ok: true; routers: RouterEntry[] } | { ok: false; status: number }> {
252-
const res = await fetch(`${DO_API_BASE}/v2/gen-ai/models/routers`, {
232+
const res = await fetch(`${DO_GENAI_API}/models/routers`, {
253233
headers: {
254234
Authorization: `Bearer ${bearer}`,
255235
Accept: "application/json",
@@ -362,15 +342,16 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise<Hooks>
362342
await startOAuthServer()
363343
const state = generateState()
364344
const callbackPromise = waitForOAuthCallback(state)
345+
const url = buildAuthorizeUrl(state)
346+
await open(url).catch(() => undefined)
365347
return {
366-
url: buildAuthorizeUrl(state),
348+
url,
367349
instructions:
368-
"Sign in to DigitalOcean in your browser. OpenCode will create a Model Access Key named opencode-oauth-* and load your Inference Routers. Re-run /connect to refresh routers later.",
350+
"Sign in to DigitalOcean in your browser. OpenCode will use your DigitalOcean API token directly for inference and load your Inference Routers. Re-run /connect to refresh routers later.",
369351
method: "auto" as const,
370352
async callback() {
371353
try {
372354
const tokens = await callbackPromise
373-
const apiKeyInfo = await createModelAccessKey(tokens.access_token)
374355
const routerResult = await listRouters(tokens.access_token)
375356
const routers = routerResult.ok ? routerResult.routers : []
376357
if (!routerResult.ok) {
@@ -379,12 +360,11 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise<Hooks>
379360
return {
380361
type: "success" as const,
381362
provider: "digitalocean",
382-
key: apiKeyInfo.secret_key,
363+
key: tokens.access_token,
383364
metadata: {
384-
mak_uuid: apiKeyInfo.uuid,
385-
mak_name: apiKeyInfo.name,
386365
oauth_access: tokens.access_token,
387366
oauth_expires: String(Date.now() + tokens.expires_in * 1000),
367+
oauth_scopes: OAUTH_SCOPES,
388368
routers: JSON.stringify(
389369
routers.map((r) => ({ name: r.name, uuid: r.uuid, description: r.description })),
390370
),

packages/web/src/content/docs/providers.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ DigitalOcean's [Inference Engine](https://docs.digitalocean.com/products/inferen
727727

728728
OpenCode supports two authentication methods:
729729

730-
- **OAuth (Recommended)** — Sign in to your DigitalOcean account; OpenCode auto-creates a Model Access Key and discovers your available Models & Inference Routers.
730+
- **OAuth (Recommended)** — Sign in to your DigitalOcean account; OpenCode uses your DigitalOcean API token directly for inference and discovers your Inference Routers.
731731
- **Model Access Key** — Paste an existing key from the DigitalOcean console.
732732

733733
#### OAuth (Recommended)
@@ -751,7 +751,11 @@ OpenCode supports two authentication methods:
751751
3. Your browser opens to authorize OpenCode. Sign in and approve.
752752

753753
:::note
754-
OpenCode creates a Model Access Key named `opencode-oauth-<timestamp>` in your DigitalOcean account. You can rotate or revoke it from the **Model Access Keys** page in the "Manage" section of the DigitalOcean console under Inference.
754+
OpenCode requests `genai:read` and `inference:query` OAuth scopes. Your DigitalOcean API token is used directly for inference — no separate Model Access Key is created.
755+
:::
756+
757+
:::note
758+
Inference Routers only appear in the model picker after OAuth. Pasting a Model Access Key manually does not discover routers.
755759
:::
756760

757761
4. Run the `/models` command. Your Inference Routers appear as the format `router:` in the model selection.

0 commit comments

Comments
 (0)