From c4cba53f67eedcb463f87dedede70c0cbe2627cb Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 4 Aug 2026 20:00:21 +0000 Subject: [PATCH] fix(core): refresh Console auth before catalog load --- packages/core/src/plugin/provider/opencode.ts | 13 ++- .../test/plugin/provider-opencode.test.ts | 91 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 75704a1c7b..5c04ae44c4 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -89,14 +89,24 @@ export const OpencodePlugin = define Effect.succeed(undefined))) : undefined + // Plugin activation batches transforms, so the first resolve can precede this plugin's OAuth registration. + const credential = + connection && resolved?.type === "oauth" && resolved.expires <= Date.now() + Duration.toMillis(Duration.minutes(5)) + ? yield* ctx.integration.reload().pipe( + Effect.andThen(ctx.integration.connection.resolve(connection)), + Effect.catch(() => Effect.succeed(undefined)), + ) + : resolved connected = connection !== undefined + savedConnection = connection?.type === "credential" providers = credential ? yield* fetchProviders(http, credential).pipe( Effect.catch((cause) => @@ -116,6 +126,7 @@ export const OpencodePlugin = define { + if (savedConnection && providers === undefined) catalog.provider.remove(Provider.ID.opencode) for (const [providerID, item] of Object.entries(providers ?? {})) { catalog.provider.update(providerID, (provider) => { provider.integrationID = Integration.ID.make("opencode") diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index e8f8323dbf..30019d2ec1 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -10,6 +10,7 @@ import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode" import { Provider } from "@opencode-ai/core/provider" +import { State } from "@opencode-ai/core/state" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -283,6 +284,96 @@ describe("OpencodePlugin", () => { ), ) + it.live("refreshes saved OAuth before loading the Console catalog on cold startup", () => + Effect.acquireUseRelease( + Effect.sync(() => { + const requests: string[] = [] + return { + requests, + server: Bun.serve({ + port: 0, + fetch: (request) => { + const url = new URL(request.url) + if (url.pathname === "/auth/device/token") { + requests.push("refresh") + return Response.json({ access_token: "fresh", refresh_token: "next", expires_in: 600 }) + } + if (url.pathname === "/api/config") { + requests.push(`config:${request.headers.get("authorization")}`) + return Response.json({ + config: { + provider: { + console: { + name: "Console", + npm: "@ai-sdk/openai-compatible", + models: { current: { name: "Current" } }, + }, + }, + }, + }) + } + return new Response("Not found", { status: 404 }) + }, + }), + } + }), + ({ requests, server }) => + Effect.gen(function* () { + yield* (yield* Credential.Service).create({ + integrationID: Integration.ID.make("opencode"), + value: Credential.OAuth.make({ + type: "oauth", + methodID: Integration.MethodID.make("device"), + access: "expired", + refresh: "refresh", + expires: 1, + metadata: { server: server.url.origin }, + }), + }) + + yield* State.batch(addPlugin()) + + expect(requests).toEqual(["refresh", "config:Bearer fresh"]) + expect( + yield* (yield* Catalog.Service).model.get(Provider.ID.make("console"), Model.ID.make("current")), + ).toBeDefined() + }), + ({ server }) => Effect.promise(() => server.stop(true)), + ), + ) + + it.live("hides legacy fallback models when Console configuration fails", () => + Effect.acquireUseRelease( + Effect.sync(() => + Bun.serve({ + port: 0, + fetch: () => new Response("Unauthorized", { status: 401 }), + }), + ), + (server) => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + yield* catalog.transform((draft) => { + draft.provider.update(Provider.ID.opencode, () => {}) + draft.model.update(Provider.ID.opencode, Model.ID.make("legacy"), () => {}) + }) + yield* (yield* Credential.Service).create({ + integrationID: Integration.ID.make("opencode"), + value: Credential.Key.make({ + type: "key", + key: "console-key", + metadata: { server: server.url.origin }, + }), + }) + + yield* State.batch(addPlugin()) + + expect(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("legacy"))).toBeUndefined() + }), + (server) => Effect.promise(() => server.stop(true)), + ), + ) + it.effect("uses a public key and disables paid models without credentials", () => withEnv({ OPENCODE_API_KEY: undefined }, () => Effect.gen(function* () {