From 216ba8f05f72ad502f3a807c5513ac2e93d02586 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:39:45 -0500 Subject: [PATCH] fix(opencode): stop Azure model discovery from logging to stdout (#46646) --- packages/opencode/src/plugin/azure.ts | 22 ++--- packages/opencode/test/plugin/azure.test.ts | 102 +++++++++++++++----- 2 files changed, 88 insertions(+), 36 deletions(-) diff --git a/packages/opencode/src/plugin/azure.ts b/packages/opencode/src/plugin/azure.ts index 33a164f372d..7a663093fbc 100644 --- a/packages/opencode/src/plugin/azure.ts +++ b/packages/opencode/src/plugin/azure.ts @@ -5,7 +5,7 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version" import { which } from "@opencode-ai/core/util/which" import type { Hooks } from "@opencode-ai/plugin" import type { Provider } from "@opencode-ai/sdk/v2" -import { Effect, Schema } from "effect" +import { Schema } from "effect" import { OAUTH_DUMMY_KEY } from "../auth" import { Process } from "../util/process" @@ -69,9 +69,9 @@ export async function AzureAuthPlugin(): Promise { export function createAzureAuthHooks( run: AzureCommand, - request: (input: RequestInfo | URL, init?: RequestInit) => Promise = fetch, - accounts: readonly AzureAccount[] = [], - available = true, + request: (input: RequestInfo | URL, init?: RequestInit) => Promise, + accounts: readonly AzureAccount[], + available: boolean, ): Hooks { const tokens = new Map() async function token(scope: string) { @@ -128,17 +128,13 @@ export function createAzureAuthHooks( id: "azure", async models(provider, context) { if (context.auth?.type !== "oauth") return provider.models + // Discovery shells out to the Azure CLI, so skip it when the CLI is missing. + if (!available) return provider.models const resource = context.auth.accountId if (!resource) return {} - return discoverAzureModels(provider.models, resource, run).catch((error: unknown) => { - Effect.runSync( - Effect.logWarning("Azure model discovery failed", { - resource, - error: error instanceof Error ? error.message : String(error), - }), - ) - return provider.models - }) + // This hook runs outside the app's Effect runtime, so logging here would go to the + // console. Fall back to the configured models silently. + return discoverAzureModels(provider.models, resource, run).catch(() => provider.models) }, }, auth: { diff --git a/packages/opencode/test/plugin/azure.test.ts b/packages/opencode/test/plugin/azure.test.ts index 11e7f3a2c3e..66444965d8e 100644 --- a/packages/opencode/test/plugin/azure.test.ts +++ b/packages/opencode/test/plugin/azure.test.ts @@ -241,7 +241,7 @@ describe("plugin.azure", () => { test("keeps the existing API-key method and adds Entra ID", () => { delete process.env.AZURE_RESOURCE_NAME - const hooks = createAzureAuthHooks(azureShell([])) + const hooks = createAzureAuthHooks(azureShell([]), fetch, [], true) expect(hooks.auth?.provider).toBe("azure") expect(hooks.provider?.id).toBe("azure") @@ -272,10 +272,15 @@ describe("plugin.azure", () => { test("lists Azure CLI resources and allows entering another resource", () => { delete process.env.AZURE_RESOURCE_NAME - const hooks = createAzureAuthHooks(azureShell([]), fetch, [ - { name: "first-resource", resourceGroup: "first-group" }, - { name: "second-resource", resourceGroup: "second-group" }, - ]) + const hooks = createAzureAuthHooks( + azureShell([]), + fetch, + [ + { name: "first-resource", resourceGroup: "first-group" }, + { name: "second-resource", resourceGroup: "second-group" }, + ], + true, + ) expect(oauthMethod(hooks).prompts).toEqual([ { @@ -299,9 +304,12 @@ describe("plugin.azure", () => { }) test("uses the selected Azure CLI resource", async () => { - const hooks = createAzureAuthHooks(azureShell([]), fetch, [ - { name: "selected-resource", resourceGroup: "selected-group" }, - ]) + const hooks = createAzureAuthHooks( + azureShell([]), + fetch, + [{ name: "selected-resource", resourceGroup: "selected-group" }], + true, + ) const authorization = await oauthMethod(hooks).authorize({ resourceSelection: "selected-resource" }) if (authorization.method !== "auto") throw new Error("Unexpected Azure authorization method") @@ -309,7 +317,12 @@ describe("plugin.azure", () => { }) test("uses a manually entered Azure resource that was not listed", async () => { - const hooks = createAzureAuthHooks(azureShell([]), fetch, [{ name: "listed-resource", resourceGroup: "group" }]) + const hooks = createAzureAuthHooks( + azureShell([]), + fetch, + [{ name: "listed-resource", resourceGroup: "group" }], + true, + ) const authorization = await oauthMethod(hooks).authorize({ resourceSelection: "__manual__", resourceName: "unlisted-resource", @@ -321,7 +334,7 @@ describe("plugin.azure", () => { test("checks Azure CLI and stores the resource name", async () => { const scopes: string[] = [] - const hooks = createAzureAuthHooks(azureShell(scopes)) + const hooks = createAzureAuthHooks(azureShell(scopes), fetch, [], true) const authorization = await oauthMethod(hooks).authorize({ resourceName: "test-resource" }) if (authorization.method !== "auto") throw new Error("Unexpected Azure authorization method") @@ -335,10 +348,15 @@ describe("plugin.azure", () => { }) test("supports Azure CLI versions that only provide expiresOn", async () => { - const hooks = createAzureAuthHooks(async () => ({ - accessToken: "legacy-token", - expiresOn: new Date(Date.now() + 60 * 60 * 1000).toISOString(), - })) + const hooks = createAzureAuthHooks( + async () => ({ + accessToken: "legacy-token", + expiresOn: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + }), + fetch, + [], + true, + ) const authorization = await oauthMethod(hooks).authorize({ resourceName: "test-resource" }) if (authorization.method !== "auto") throw new Error("Unexpected Azure authorization method") @@ -346,7 +364,7 @@ describe("plugin.azure", () => { }) test("rejects Azure CLI tokens without a usable expiration", async () => { - const hooks = createAzureAuthHooks(async () => ({ accessToken: "invalid-token" })) + const hooks = createAzureAuthHooks(async () => ({ accessToken: "invalid-token" }), fetch, [], true) const authorization = await oauthMethod(hooks).authorize({ resourceName: "test-resource" }) if (authorization.method !== "auto") throw new Error("Unexpected Azure authorization method") @@ -379,6 +397,9 @@ describe("plugin.azure", () => { ], commands, ), + fetch, + [], + true, ) const list = hooks.provider?.models if (!list) throw new Error("Azure provider model hook is missing") @@ -410,6 +431,9 @@ describe("plugin.azure", () => { [{ name: "gpt-production", properties: { model: { name: "gpt-5-mini" }, provisioningState: "Succeeded" } }], commands, ), + fetch, + [], + true, ) const list = hooks.provider?.models if (!list) throw new Error("Azure provider model hook is missing") @@ -433,6 +457,9 @@ describe("plugin.azure", () => { ], [], ), + fetch, + [], + true, ) const list = hooks.provider?.models if (!list) throw new Error("Azure provider model hook is missing") @@ -446,9 +473,14 @@ describe("plugin.azure", () => { }) test("keeps configured models available when Azure discovery fails", async () => { - const hooks = createAzureAuthHooks(async () => { - throw new Error("Azure CLI failed") - }) + const hooks = createAzureAuthHooks( + async () => { + throw new Error("Azure CLI failed") + }, + fetch, + [], + true, + ) const list = hooks.provider?.models if (!list) throw new Error("Azure provider model hook is missing") @@ -456,9 +488,28 @@ describe("plugin.azure", () => { expect(await list({ ...provider, models: catalog }, { auth: oauth })).toBe(catalog) }) + test("skips model discovery when the Azure CLI is unavailable", async () => { + const calls: string[][] = [] + const hooks = createAzureAuthHooks( + async (args) => { + calls.push(args) + throw new Error("spawn az ENOENT") + }, + fetch, + [], + false, + ) + const list = hooks.provider?.models + if (!list) throw new Error("Azure provider model hook is missing") + + const catalog = models("gpt-5-mini") + expect(await list({ ...provider, models: catalog }, { auth: oauth })).toBe(catalog) + expect(calls).toEqual([]) + }) + test("does not change API-key loading", async () => { const scopes: string[] = [] - const hooks = createAzureAuthHooks(azureShell(scopes)) + const hooks = createAzureAuthHooks(azureShell(scopes), fetch, [], true) const catalog = models("gpt-5-mini") const list = hooks.provider?.models if (!list) throw new Error("Azure provider model hook is missing") @@ -471,10 +522,15 @@ describe("plugin.azure", () => { test("uses Azure CLI bearer tokens for Azure inference endpoints", async () => { const scopes: string[] = [] const requests: Headers[] = [] - const hooks = createAzureAuthHooks(azureShell(scopes), async (_input, init) => { - requests.push(new Headers(init?.headers)) - return new Response(null, { status: 200 }) - }) + const hooks = createAzureAuthHooks( + azureShell(scopes), + async (_input, init) => { + requests.push(new Headers(init?.headers)) + return new Response(null, { status: 200 }) + }, + [], + true, + ) const options = await loader(hooks)(async () => oauth, provider) const request = customFetch(options)