From af1f9e626989cdfc79fc5f230912425b5a3a3aa4 Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:53:43 +0200 Subject: [PATCH] remove azure discovery stuff (#46666) --- packages/opencode/src/plugin/azure.ts | 141 +-------- packages/opencode/test/plugin/azure.test.ts | 316 ++------------------ packages/web/src/content/docs/providers.mdx | 6 +- 3 files changed, 24 insertions(+), 439 deletions(-) diff --git a/packages/opencode/src/plugin/azure.ts b/packages/opencode/src/plugin/azure.ts index 7a663093fb..6916aaaf3a 100644 --- a/packages/opencode/src/plugin/azure.ts +++ b/packages/opencode/src/plugin/azure.ts @@ -1,10 +1,6 @@ -import { readFile } from "node:fs/promises" -import { homedir } from "node:os" -import { join } from "node:path" 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 { Schema } from "effect" import { OAUTH_DUMMY_KEY } from "../auth" import { Process } from "../util/process" @@ -19,58 +15,16 @@ const AzureCliToken = Schema.Struct({ expiresOn: Schema.optional(Schema.NonEmptyString), }) const decodeAzureCliToken = Schema.decodeUnknownPromise(AzureCliToken) -const decodeAzureProfile = Schema.decodeUnknownPromise( - Schema.fromJsonString(Schema.Struct({ subscriptions: Schema.Array(Schema.Unknown) })), -) - -const decodeAzureAccounts = Schema.decodeUnknownPromise( - Schema.Array( - Schema.Struct({ - name: Schema.NonEmptyString, - resourceGroup: Schema.NonEmptyString, - }), - ), -) - -const decodeAzureDeployments = Schema.decodeUnknownPromise( - Schema.Array( - Schema.Struct({ - name: Schema.NonEmptyString, - properties: Schema.Struct({ - model: Schema.Struct({ - name: Schema.NonEmptyString, - }), - provisioningState: Schema.NonEmptyString, - }), - }), - ), -) - type AzureCommand = (args: string[]) => Promise -type AzureAccount = { readonly name: string; readonly resourceGroup: string } export async function AzureAuthPlugin(): Promise { const available = Boolean(which("az")) - // Avoid launching Azure CLI on unrelated commands just because the executable is installed. - const signedIn = available - ? await readFile(join(process.env.AZURE_CONFIG_DIR ?? join(homedir(), ".azure"), "azureProfile.json"), "utf8") - .then((text) => decodeAzureProfile(text.replace(/^\uFEFF/, ""))) - .then((profile) => profile.subscriptions.length > 0) - .catch(() => false) - : false - const accounts = - !process.env.AZURE_RESOURCE_NAME && !process.env.AZURE_RESOURCE_GROUP && signedIn - ? await runAzure(["cognitiveservices", "account", "list", "--output", "json", "--only-show-errors"]) - .then(decodeAzureAccounts) - .catch(() => []) - : [] - return createAzureAuthHooks(runAzure, fetch, accounts, available) + return createAzureAuthHooks(runAzure, fetch, available) } export function createAzureAuthHooks( run: AzureCommand, request: (input: RequestInfo | URL, init?: RequestInit) => Promise, - accounts: readonly AzureAccount[], available: boolean, ): Hooks { const tokens = new Map() @@ -97,46 +51,7 @@ export function createAzureAuthHooks( placeholder: "e.g. my-models", }) } - const oauthPrompts = - accounts.length > 0 && !process.env.AZURE_RESOURCE_NAME - ? [ - { - type: "select" as const, - key: "resourceSelection", - message: "Select Azure resource", - options: [ - ...accounts.map((account) => ({ - label: account.name, - value: account.name, - hint: account.resourceGroup, - })), - { label: "Enter another resource name", value: "__manual__" }, - ], - }, - { - type: "text" as const, - key: "resourceName", - message: "Enter Azure Resource Name", - placeholder: "e.g. my-models", - when: { key: "resourceSelection", op: "eq" as const, value: "__manual__" }, - }, - ] - : prompts - const hooks: Hooks = { - provider: { - 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 {} - // 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: { provider: "azure", async loader(getAuth) { @@ -164,17 +79,14 @@ export function createAzureAuthHooks( { type: "oauth", label: "Microsoft Entra ID (Azure CLI)", - prompts: oauthPrompts, + prompts, async authorize(inputs) { return { url: "", instructions: "Sign in with `az login` before continuing.", method: "auto", callback: async () => { - const resourceName = - inputs?.resourceName ?? - (inputs?.resourceSelection === "__manual__" ? undefined : inputs?.resourceSelection) ?? - process.env.AZURE_RESOURCE_NAME + const resourceName = inputs?.resourceName ?? process.env.AZURE_RESOURCE_NAME if (!resourceName) throw new Error("Azure Resource Name is required") await token(AZURE_COGNITIVE_SERVICES_SCOPE) @@ -201,53 +113,6 @@ async function runAzure(args: string[]): Promise { return JSON.parse(result.stdout.toString()) } -async function discoverAzureModels(models: Provider["models"], resourceName: string, run: AzureCommand) { - const resourceGroup = process.env.AZURE_RESOURCE_GROUP - const account = resourceGroup - ? { name: resourceName, resourceGroup } - : ( - await decodeAzureAccounts( - await run(["cognitiveservices", "account", "list", "--output", "json", "--only-show-errors"]), - ) - ).find((account) => account.name.toLowerCase() === resourceName.toLowerCase()) - if (!account) throw new Error(`Azure resource "${resourceName}" was not found in the active subscription`) - - const deployments = await decodeAzureDeployments( - await run([ - "cognitiveservices", - "account", - "deployment", - "list", - "--name", - account.name, - "--resource-group", - account.resourceGroup, - "--output", - "json", - "--only-show-errors", - ]), - ) - const found = new Map() - deployments.forEach((deployment) => { - if (deployment.properties.provisioningState !== "Succeeded") return - const modelID = Object.keys(models).find( - (modelID) => modelID.toLowerCase() === deployment.properties.model.name.toLowerCase(), - ) - if (!modelID) return - const id = found.has(modelID) ? deployment.name : modelID - found.set(id, { - ...models[modelID], - id, - name: id === modelID ? models[modelID].name : `${models[modelID].name} (${deployment.name})`, - api: { - ...models[modelID].api, - id: deployment.name, - }, - }) - }) - return Object.fromEntries(found) -} - function scopeForRequest(input: RequestInfo | URL) { const url = new URL(input instanceof Request ? input.url : input) if (url.hostname.endsWith(".services.ai.azure.com") && !url.pathname.startsWith("/models")) { diff --git a/packages/opencode/test/plugin/azure.test.ts b/packages/opencode/test/plugin/azure.test.ts index 66444965d8..2dcdfb0399 100644 --- a/packages/opencode/test/plugin/azure.test.ts +++ b/packages/opencode/test/plugin/azure.test.ts @@ -11,17 +11,11 @@ import { Process } from "../../src/util/process" import { which } from "@opencode-ai/core/util/which" const resourceName = process.env.AZURE_RESOURCE_NAME -const resourceGroup = process.env.AZURE_RESOURCE_GROUP -const azureConfig = process.env.AZURE_CONFIG_DIR const originalPath = process.env.PATH afterEach(() => { if (resourceName === undefined) delete process.env.AZURE_RESOURCE_NAME else process.env.AZURE_RESOURCE_NAME = resourceName - if (resourceGroup === undefined) delete process.env.AZURE_RESOURCE_GROUP - else process.env.AZURE_RESOURCE_GROUP = resourceGroup - if (azureConfig === undefined) delete process.env.AZURE_CONFIG_DIR - else process.env.AZURE_CONFIG_DIR = azureConfig if (originalPath === undefined) delete process.env.PATH else process.env.PATH = originalPath }) @@ -64,37 +58,6 @@ function customFetch(options: Record) { } } -function models(...ids: string[]): Provider["models"] { - return Object.fromEntries( - ids.map((id) => [ - id, - { - id, - providerID: "azure", - name: id, - family: "", - api: { id, url: "", npm: "@ai-sdk/azure" }, - status: "active", - headers: {}, - options: {}, - cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, - limit: { context: 0, output: 0 }, - capabilities: { - temperature: true, - reasoning: false, - attachment: false, - toolcall: true, - input: { text: true, audio: false, image: false, video: false, pdf: false }, - output: { text: true, audio: false, image: false, video: false, pdf: false }, - interleaved: false, - }, - release_date: "", - variants: {}, - }, - ]), - ) -} - function azureShell(scopes: string[]) { return async (args: string[]) => { const scope = args[args.indexOf("--scope") + 1] @@ -106,14 +69,6 @@ function azureShell(scopes: string[]) { } } -function discoveryShell(accounts: unknown, deployments: unknown, commands: string[]) { - return async (args: string[]) => { - const command = ["az", ...args].join(" ") - commands.push(command) - return command.includes("deployment list") ? deployments : accounts - } -} - async function azureCli(dir: string) { const bin = path.join(dir, "azure cli") const calls = path.join(dir, "calls.jsonl") @@ -127,7 +82,7 @@ async function azureCli(dir: string) { fs.appendFileSync(${JSON.stringify(calls)}, JSON.stringify(args) + "\\n") console.log(JSON.stringify(args.includes("get-access-token") ? { accessToken: "test-token", expires_on: Math.floor(Date.now() / 1000) + 3600 } - : args.includes("deployment") ? [] : [{ name: "test-resource", resourceGroup: "test group & value" }])) + : [])) `, ) const executable = path.join(bin, process.platform === "win32" ? "az.cmd" : "az") @@ -162,7 +117,6 @@ describe("plugin.azure", () => { const entry = path.join(tmp.path, "azure.mjs") await Bun.write(entry, bundle.outputs[0]) const cli = await azureCli(tmp.path) - await Bun.write(path.join(tmp.path, "azureProfile.json"), '\uFEFF{"subscriptions":[{}]}') for (const installed of [false, true]) { const result = await Process.run( [ @@ -174,23 +128,21 @@ describe("plugin.azure", () => { import { AzureAuthPlugin } from ${JSON.stringify(pathToFileURL(entry).href)} assert.equal(typeof Bun, "undefined") delete process.env.AZURE_RESOURCE_NAME - delete process.env.AZURE_RESOURCE_GROUP const hooks = await AzureAuthPlugin({ $: undefined }) assert.equal(hooks.auth.provider, "azure") assert.deepEqual(hooks.auth.methods.map((method) => method.type), ${JSON.stringify(installed ? ["api", "oauth"] : ["api"])}) if (${installed}) { const method = hooks.auth.methods.find((method) => method.type === "oauth") - assert.equal(method.prompts[0].type, "select") - const authorization = await method.authorize({ resourceSelection: "test-resource" }) + assert.equal(method.prompts[0].type, "text") + const authorization = await method.authorize({ resourceName: "test-resource" }) const auth = await authorization.callback() assert.equal(auth.type, "success") assert.equal(auth.accountId, "test-resource") - assert.deepEqual(await hooks.provider.models({ models: {} }, { auth: { ...auth, type: "oauth" } }), {}) } `, ], { - env: { PATH: installed ? cli.bin : tmp.path, XDG_DATA_HOME: tmp.path, AZURE_CONFIG_DIR: tmp.path }, + env: { PATH: installed ? cli.bin : tmp.path, XDG_DATA_HOME: tmp.path }, nothrow: true, }, ) @@ -198,53 +150,27 @@ describe("plugin.azure", () => { expect(result.code).toBe(0) } expect(await cli.calls()).toEqual([ - ["cognitiveservices", "account", "list", "--output", "json", "--only-show-errors"], ["account", "get-access-token", "--scope", "https://cognitiveservices.azure.com/.default", "--output", "json"], - ["cognitiveservices", "account", "list", "--output", "json", "--only-show-errors"], - [ - "cognitiveservices", - "account", - "deployment", - "list", - "--name", - "test-resource", - "--resource-group", - "test group & value", - "--output", - "json", - "--only-show-errors", - ], ]) }) - for (const profile of [ - { name: "missing", content: undefined, signedIn: false }, - { name: "logged out", content: '{"subscriptions":[]}', signedIn: false }, - { name: "signed in with BOM", content: '\uFEFF{"subscriptions":[{}]}', signedIn: true }, - ]) { - test(`only lists resources for a cached Azure login (${profile.name})`, async () => { - await using tmp = await tmpdir() - const cli = await azureCli(tmp.path) - process.env.PATH = cli.bin - process.env.AZURE_CONFIG_DIR = path.join(tmp.path, "azure-cli") - if (profile.content) - await Bun.write(path.join(process.env.AZURE_CONFIG_DIR, "azureProfile.json"), profile.content) - delete process.env.AZURE_RESOURCE_NAME - delete process.env.AZURE_RESOURCE_GROUP - const hooks = await AzureAuthPlugin() + test("does not invoke Azure CLI during initialization", async () => { + await using tmp = await tmpdir() + const cli = await azureCli(tmp.path) + process.env.PATH = cli.bin + delete process.env.AZURE_RESOURCE_NAME - expect(await cli.calls()).toHaveLength(profile.signedIn ? 1 : 0) - expect(hooks.auth?.methods.some((method) => method.type === "oauth")).toBe(true) - if (profile.signedIn) expect(oauthMethod(hooks).prompts?.[0].type).toBe("select") - }) - } + const hooks = await AzureAuthPlugin() + + expect(await cli.calls()).toEqual([]) + expect(oauthMethod(hooks).prompts?.[0].type).toBe("text") + }) test("keeps the existing API-key method and adds Entra ID", () => { delete process.env.AZURE_RESOURCE_NAME - const hooks = createAzureAuthHooks(azureShell([]), fetch, [], true) + const hooks = createAzureAuthHooks(azureShell([]), fetch, true) expect(hooks.auth?.provider).toBe("azure") - expect(hooks.provider?.id).toBe("azure") expect(hooks.auth?.methods.map((method) => [method.type, method.label])).toEqual([ ["api", "API key"], ["oauth", "Microsoft Entra ID (Azure CLI)"], @@ -265,76 +191,14 @@ describe("plugin.azure", () => { }) test("hides Azure CLI authentication when the Azure CLI is not installed", () => { - const hooks = createAzureAuthHooks(azureShell([]), fetch, [], false) + const hooks = createAzureAuthHooks(azureShell([]), fetch, false) expect(hooks.auth?.methods.map((method) => method.type)).toEqual(["api"]) }) - 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" }, - ], - true, - ) - - expect(oauthMethod(hooks).prompts).toEqual([ - { - type: "select", - key: "resourceSelection", - message: "Select Azure resource", - options: [ - { label: "first-resource", value: "first-resource", hint: "first-group" }, - { label: "second-resource", value: "second-resource", hint: "second-group" }, - { label: "Enter another resource name", value: "__manual__" }, - ], - }, - { - type: "text", - key: "resourceName", - message: "Enter Azure Resource Name", - placeholder: "e.g. my-models", - when: { key: "resourceSelection", op: "eq", value: "__manual__" }, - }, - ]) - }) - - test("uses the selected Azure CLI resource", async () => { - 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") - - expect(await authorization.callback()).toMatchObject({ type: "success", accountId: "selected-resource" }) - }) - - test("uses a manually entered Azure resource that was not listed", async () => { - const hooks = createAzureAuthHooks( - azureShell([]), - fetch, - [{ name: "listed-resource", resourceGroup: "group" }], - true, - ) - const authorization = await oauthMethod(hooks).authorize({ - resourceSelection: "__manual__", - resourceName: "unlisted-resource", - }) - if (authorization.method !== "auto") throw new Error("Unexpected Azure authorization method") - - expect(await authorization.callback()).toMatchObject({ type: "success", accountId: "unlisted-resource" }) - }) - test("checks Azure CLI and stores the resource name", async () => { const scopes: string[] = [] - const hooks = createAzureAuthHooks(azureShell(scopes), fetch, [], true) + 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") @@ -354,7 +218,6 @@ describe("plugin.azure", () => { expiresOn: new Date(Date.now() + 60 * 60 * 1000).toISOString(), }), fetch, - [], true, ) const authorization = await oauthMethod(hooks).authorize({ resourceName: "test-resource" }) @@ -364,158 +227,18 @@ describe("plugin.azure", () => { }) test("rejects Azure CLI tokens without a usable expiration", async () => { - const hooks = createAzureAuthHooks(async () => ({ accessToken: "invalid-token" }), fetch, [], true) + 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") await expect(authorization.callback()).rejects.toThrow("Azure CLI returned an invalid token expiration") }) - test("discovers deployed models through Azure CLI", async () => { - delete process.env.AZURE_RESOURCE_GROUP - const commands: string[] = [] - const hooks = createAzureAuthHooks( - discoveryShell( - [{ name: "test-resource", resourceGroup: "test-group" }], - [ - { - name: "gpt-production", - properties: { model: { name: "gpt-5-mini" }, provisioningState: "Succeeded" }, - }, - { - name: "DeepSeek-V4-Flash", - properties: { model: { name: "DeepSeek-V4-Flash" }, provisioningState: "Succeeded" }, - }, - { - name: "phi-production", - properties: { model: { name: "Phi-4-mini-instruct" }, provisioningState: "Succeeded" }, - }, - { - name: "gpt-5-nano", - properties: { model: { name: "gpt-5-nano" }, provisioningState: "Creating" }, - }, - ], - commands, - ), - fetch, - [], - true, - ) - const list = hooks.provider?.models - if (!list) throw new Error("Azure provider model hook is missing") - - const result = await list( - { - ...provider, - models: models("gpt-5-mini", "deepseek-v4-flash", "phi-4-mini", "phi-4-mini-instruct", "gpt-5-nano"), - }, - { auth: oauth }, - ) - - expect(Object.keys(result)).toEqual(["gpt-5-mini", "deepseek-v4-flash", "phi-4-mini-instruct"]) - expect(result["gpt-5-mini"].api.id).toBe("gpt-production") - expect(result["deepseek-v4-flash"].api.id).toBe("DeepSeek-V4-Flash") - expect(result["phi-4-mini-instruct"].api.id).toBe("phi-production") - expect(commands).toEqual([ - "az cognitiveservices account list --output json --only-show-errors", - "az cognitiveservices account deployment list --name test-resource --resource-group test-group --output json --only-show-errors", - ]) - }) - - test("discovers models directly when the resource group is configured", async () => { - process.env.AZURE_RESOURCE_GROUP = "restricted-group" - const commands: string[] = [] - const hooks = createAzureAuthHooks( - discoveryShell( - [], - [{ 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") - - const result = await list({ ...provider, models: models("gpt-5-mini") }, { auth: oauth }) - - expect(result["gpt-5-mini"].api.id).toBe("gpt-production") - expect(commands).toEqual([ - "az cognitiveservices account deployment list --name test-resource --resource-group restricted-group --output json --only-show-errors", - ]) - }) - - test("preserves multiple deployments of the same model", async () => { - delete process.env.AZURE_RESOURCE_GROUP - const hooks = createAzureAuthHooks( - discoveryShell( - [{ name: "test-resource", resourceGroup: "test-group" }], - [ - { name: "gpt-production", properties: { model: { name: "gpt-5-mini" }, provisioningState: "Succeeded" } }, - { name: "gpt-staging", properties: { model: { name: "gpt-5-mini" }, provisioningState: "Succeeded" } }, - ], - [], - ), - fetch, - [], - true, - ) - const list = hooks.provider?.models - if (!list) throw new Error("Azure provider model hook is missing") - - const result = await list({ ...provider, models: models("gpt-5-mini") }, { auth: oauth }) - - expect(Object.keys(result)).toEqual(["gpt-5-mini", "gpt-staging"]) - expect(result["gpt-5-mini"].api.id).toBe("gpt-production") - expect(result["gpt-staging"].api.id).toBe("gpt-staging") - expect(result["gpt-staging"].name).toBe("gpt-5-mini (gpt-staging)") - }) - - test("keeps configured models available when Azure discovery fails", async () => { - 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") - - const catalog = models("gpt-5-mini") - 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), fetch, [], true) - const catalog = models("gpt-5-mini") - const list = hooks.provider?.models - if (!list) throw new Error("Azure provider model hook is missing") + const hooks = createAzureAuthHooks(azureShell(scopes), fetch, true) expect(await loader(hooks)(async () => ({ type: "api", key: "test-key" }), provider)).toEqual({}) - expect(await list({ ...provider, models: catalog }, { auth: { type: "api", key: "test-key" } })).toBe(catalog) expect(scopes).toEqual([]) }) @@ -528,7 +251,6 @@ describe("plugin.azure", () => { requests.push(new Headers(init?.headers)) return new Response(null, { status: 200 }) }, - [], true, ) const options = await loader(hooks)(async () => oauth, provider) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 877f91c4e2..a1d01079f1 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -459,7 +459,7 @@ If you encounter "I'm sorry, but I cannot assist with that request" errors, try #### Microsoft Entra ID (Azure CLI) -You can use your Azure CLI session instead of an API key. [Install the Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli), run `az login`, then run `/connect`, select **Azure**, and choose **Microsoft Entra ID (Azure CLI)**. OpenCode lists the Resources visible to your Azure CLI session and their Resource groups. Select a Resource, or choose **Enter another resource name** to enter one manually. If resource listing is unavailable, OpenCode asks for the name directly. Use `az login --tenant TENANT_ID` if the Resource belongs to a different tenant. +You can use your Azure CLI session instead of an API key. [Install the Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli), run `az login`, then run `/connect`, select **Azure**, and choose **Microsoft Entra ID (Azure CLI)**. Enter the Azure Resource name when prompted. Use `az login --tenant TENANT_ID` if the Resource belongs to a different tenant. Find the Resource name by opening your Azure OpenAI or Foundry Resource in the [Azure portal](https://portal.azure.com/) or [Microsoft Foundry](https://ai.azure.com/). It is also the first part of the endpoint: `my-models` in `https://my-models.openai.azure.com/` or `https://my-models.services.ai.azure.com/`. If your identity can list Resources, you can also find their names and Resource groups with: @@ -469,9 +469,7 @@ az cognitiveservices account list \ --output table ``` -OpenCode finds the Resource group and discovers its deployed models from the active Azure CLI subscription. Run `az account set --subscription NAME_OR_ID` first if the Resource is in a different subscription. Set `AZURE_RESOURCE_GROUP` to skip listing the subscription and query a known Resource directly. - -Model discovery requires Azure control-plane permissions, which are separate from inference permissions. If your identity cannot list deployments, OpenCode keeps the Azure model catalog available instead. Select a model whose name matches your deployment, or configure its deployment name explicitly: +OpenCode does not query Azure management APIs or discover deployments. Select a model whose catalog name matches your deployment, or configure its deployment name explicitly: ```json title="opencode.json" {