mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
remove acount discoverability
This commit is contained in:
@@ -55,13 +55,7 @@ const Accounts = Schema.Struct({
|
||||
data: Schema.Array(Schema.Unknown),
|
||||
$skipToken: Schema.optional(Schema.NonEmptyString),
|
||||
})
|
||||
const Account = Schema.Struct({
|
||||
id: Schema.NonEmptyString,
|
||||
resourceName: Schema.NonEmptyString,
|
||||
resourceGroup: Schema.String,
|
||||
location: Schema.String,
|
||||
})
|
||||
const decodeAccount = Schema.decodeUnknownOption(Account)
|
||||
const decodeAccount = Schema.decodeUnknownOption(Schema.Struct({ id: Schema.NonEmptyString }))
|
||||
|
||||
type Deployment = { readonly name: string; readonly model: string }
|
||||
|
||||
@@ -88,7 +82,6 @@ export function make(
|
||||
deployments?: readonly Deployment[]
|
||||
connection?: Effect.Success<ReturnType<typeof ctx.integration.connection.active>>
|
||||
} = {}
|
||||
const listed: { accounts: readonly (typeof Account.Type)[] } = { accounts: [] }
|
||||
|
||||
const command = (args: string[]) =>
|
||||
processes
|
||||
@@ -127,7 +120,7 @@ export function make(
|
||||
)
|
||||
|
||||
// Resource Graph spans every subscription of the Azure CLI session, unlike a per-subscription account list.
|
||||
const accounts = Effect.fn("AzurePlugin.accounts")(function* (resource?: string) {
|
||||
const accounts = Effect.fn("AzurePlugin.accounts")(function* (resource: string) {
|
||||
return yield* Stream.paginate(undefined, (skipToken: string | undefined) =>
|
||||
HttpClientRequest.post(
|
||||
`${endpoints.management}/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01`,
|
||||
@@ -150,45 +143,17 @@ export function make(
|
||||
).pipe(Stream.runCollect)
|
||||
})
|
||||
|
||||
// Runs only while connecting, so listing Azure resources never costs anything at startup.
|
||||
const detect = Effect.fn("AzurePlugin.detect")(function* () {
|
||||
const found = yield* accounts()
|
||||
if (found.length === 1) return found[0].resourceName
|
||||
listed.accounts = found
|
||||
yield* ctx.integration.reload()
|
||||
return yield* Effect.fail(
|
||||
new Error(
|
||||
found.length === 0
|
||||
? "No Azure resources were found for this Azure CLI session. Connect again and enter the resource name."
|
||||
: `Found ${found.length} Azure resources. Connect again to choose one.`,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const available = Boolean(which("az"))
|
||||
const form = (cli: boolean) =>
|
||||
const form = () =>
|
||||
iife(() => {
|
||||
if (resolveResourceName(configured) || typeof configured?.baseURL === "string") return
|
||||
// Only the Azure CLI session can look the resource up, and only until a lookup found several to choose from.
|
||||
const detectable = cli && listed.accounts.length === 0
|
||||
return Form.Fields.make([
|
||||
{
|
||||
type: "string",
|
||||
key: "resourceName",
|
||||
title: "Enter Azure Resource Name",
|
||||
placeholder: "e.g. my-models",
|
||||
required: !detectable,
|
||||
...(detectable ? { description: "Leave empty to use the resource of your Azure CLI session" } : {}),
|
||||
...(cli && listed.accounts.length > 0
|
||||
? {
|
||||
options: listed.accounts.map((account) => ({
|
||||
value: account.resourceName,
|
||||
label: account.resourceName,
|
||||
description: `${account.resourceGroup} · ${account.location}`,
|
||||
})),
|
||||
custom: true,
|
||||
}
|
||||
: {}),
|
||||
required: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -196,7 +161,7 @@ export function make(
|
||||
yield* ctx.integration.transform((editor) => {
|
||||
editor.method.update({
|
||||
integrationID: Provider.ID.azure,
|
||||
method: { type: "key", label: "API key", form: form(false) },
|
||||
method: { type: "key", label: "API key", form: form() },
|
||||
})
|
||||
if (!available) return
|
||||
editor.method.update({
|
||||
@@ -205,7 +170,7 @@ export function make(
|
||||
id: methodID,
|
||||
type: "oauth",
|
||||
label: "Microsoft Entra ID (Azure CLI)",
|
||||
form: form(true),
|
||||
form: form(),
|
||||
},
|
||||
authorize: (answer) =>
|
||||
Effect.succeed({
|
||||
@@ -215,8 +180,7 @@ export function make(
|
||||
callback: Effect.gen(function* () {
|
||||
const resourceName =
|
||||
(typeof answer.resourceName === "string" ? answer.resourceName.trim() : "") ||
|
||||
resolveResourceName(configured) ||
|
||||
(typeof configured?.baseURL === "string" ? undefined : yield* detect())
|
||||
resolveResourceName(configured)
|
||||
if (!resourceName) return yield* Effect.fail(new Error("Azure resource name is required"))
|
||||
const current = yield* token(cognitiveScope)
|
||||
return Credential.OAuth.make({
|
||||
@@ -502,15 +466,15 @@ function credentialResource(credential: Credential.Value | undefined) {
|
||||
return typeof resource === "string" && resource.trim() !== "" ? resource : undefined
|
||||
}
|
||||
|
||||
function accountQuery(resource?: string) {
|
||||
function accountQuery(resource: string) {
|
||||
return [
|
||||
"resources",
|
||||
"| where type =~ 'microsoft.cognitiveservices/accounts' and kind in~ ('AIServices', 'OpenAI')",
|
||||
// The custom subdomain is the resource name of every endpoint, and Entra ID authentication requires one.
|
||||
"| extend resourceName = tostring(properties.customSubDomainName)",
|
||||
resource === undefined ? "| where isnotempty(resourceName)" : `| where resourceName =~ '${resource}'`,
|
||||
"| project id, resourceName, resourceGroup, location",
|
||||
"| order by resourceName asc",
|
||||
`| where resourceName =~ '${resource}'`,
|
||||
"| project id",
|
||||
"| order by id asc",
|
||||
].join(" ")
|
||||
}
|
||||
|
||||
|
||||
@@ -264,9 +264,8 @@ describe("AzurePlugin", () => {
|
||||
type: "string",
|
||||
key: "resourceName",
|
||||
title: "Enter Azure Resource Name",
|
||||
description: "Leave empty to use the resource of your Azure CLI session",
|
||||
placeholder: "e.g. my-models",
|
||||
required: false,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -813,125 +812,6 @@ describe("AzurePlugin", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("uses the only Azure resource of the Azure CLI session while connecting", () => {
|
||||
const commands: string[][] = []
|
||||
return withEnv({ AZURE_RESOURCE_NAME: undefined, AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: undefined }, () =>
|
||||
withAzureCommands(
|
||||
(args) => {
|
||||
commands.push([...args])
|
||||
return cliTokens(args)
|
||||
},
|
||||
() =>
|
||||
withAzure(
|
||||
(request) =>
|
||||
request.method === "POST"
|
||||
? Response.json({ data: [account("detected-resource")] })
|
||||
: Response.json({ data: [] }),
|
||||
({ endpoints, requests }) =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin(endpoints)
|
||||
// Looking the resource up belongs to connecting, never to startup.
|
||||
expect(commands).toEqual([])
|
||||
const integrations = yield* Integration.Service
|
||||
const integrationID = Integration.ID.make("azure")
|
||||
const attempt = yield* integrations.oauth.connect({
|
||||
integrationID,
|
||||
methodID: Integration.MethodID.make("azure-cli"),
|
||||
})
|
||||
yield* eventually(
|
||||
integrations.oauth.status({ integrationID, attemptID: attempt.attemptID }).pipe(Effect.orDie),
|
||||
(status) => status.status !== "pending",
|
||||
)
|
||||
|
||||
expect((yield* (yield* Credential.Service).list(integrationID))[0]?.value).toMatchObject({
|
||||
type: "oauth",
|
||||
access: "https://cognitiveservices.azure.com/.default-token",
|
||||
metadata: { resourceName: "detected-resource" },
|
||||
})
|
||||
expect(commands.map((args) => args[3])).toEqual([
|
||||
"https://management.azure.com/.default",
|
||||
"https://cognitiveservices.azure.com/.default",
|
||||
])
|
||||
expect(requests[0]?.body).toContain("isnotempty(resourceName)")
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it.live("offers resources across all Resource Graph pages instead of choosing the first page's only resource", () =>
|
||||
withEnv({ AZURE_RESOURCE_NAME: undefined, AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: undefined }, () =>
|
||||
withAzureCommands(cliTokens, () =>
|
||||
withAzure(
|
||||
(request) =>
|
||||
Response.json(
|
||||
request.body.includes('"$skipToken":"next"')
|
||||
? { data: [account("second-resource")] }
|
||||
: { data: [account("first-resource")], $skipToken: "next" },
|
||||
),
|
||||
({ endpoints, requests }) =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin(endpoints)
|
||||
const integrations = yield* Integration.Service
|
||||
const integrationID = Integration.ID.make("azure")
|
||||
const attempt = yield* integrations.oauth.connect({
|
||||
integrationID,
|
||||
methodID: Integration.MethodID.make("azure-cli"),
|
||||
})
|
||||
const status = yield* eventually(
|
||||
integrations.oauth.status({ integrationID, attemptID: attempt.attemptID }).pipe(Effect.orDie),
|
||||
(status) => status.status !== "pending",
|
||||
)
|
||||
|
||||
expect(status).toMatchObject({
|
||||
status: "failed",
|
||||
message: "Found 2 Azure resources. Connect again to choose one.",
|
||||
})
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(JSON.parse(requests[1].body)).toEqual({
|
||||
query: JSON.parse(requests[0].body).query,
|
||||
options: { $skipToken: "next" },
|
||||
})
|
||||
expect(yield* (yield* Credential.Service).list(integrationID)).toEqual([])
|
||||
const methods = (yield* integrations.get(integrationID))?.methods ?? []
|
||||
expect(methods.find((method) => method.type === "oauth")?.form).toEqual([
|
||||
{
|
||||
type: "string",
|
||||
key: "resourceName",
|
||||
title: "Enter Azure Resource Name",
|
||||
placeholder: "e.g. my-models",
|
||||
required: true,
|
||||
options: [
|
||||
{
|
||||
value: "first-resource",
|
||||
label: "first-resource",
|
||||
description: "rg-first-resource · swedencentral",
|
||||
},
|
||||
{
|
||||
value: "second-resource",
|
||||
label: "second-resource",
|
||||
description: "rg-second-resource · swedencentral",
|
||||
},
|
||||
],
|
||||
custom: true,
|
||||
},
|
||||
])
|
||||
// An API key cannot look resources up, so its prompt stays a plain required name.
|
||||
expect(methods.find((method) => method.type === "key")?.form).toEqual([
|
||||
{
|
||||
type: "string",
|
||||
key: "resourceName",
|
||||
title: "Enter Azure Resource Name",
|
||||
placeholder: "e.g. my-models",
|
||||
required: true,
|
||||
},
|
||||
])
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("uses the correct bearer token audience for Azure and Foundry requests", () =>
|
||||
withAzureCommands(
|
||||
(args) => {
|
||||
|
||||
@@ -268,9 +268,7 @@ choose **Microsoft Entra ID (Azure CLI)** when connecting Azure.
|
||||
az login
|
||||
```
|
||||
|
||||
With this method you can leave the resource name empty. OpenCode then looks up the Azure OpenAI and Foundry resources
|
||||
of your Azure CLI session while connecting. It uses the resource when there is only one, and otherwise asks you to
|
||||
connect again and choose from the resources it found. The lookup only runs while connecting, never at startup.
|
||||
Enter the resource name when connecting if it is not already supplied by configuration or the environment.
|
||||
|
||||
For a resource in another tenant or subscription, select both explicitly.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user