Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 145 additions & 11 deletions packages/opencode/src/plugin/azure.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,160 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { OAUTH_DUMMY_KEY } from "../auth"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Option, Schema } from "effect"

const AZURE_SCOPE = "https://cognitiveservices.azure.com"
const AZURE_TOKEN_REFRESH_BUFFER = 60_000
const AzureCliToken = Schema.Struct({
accessToken: Schema.String,
expires_on: Schema.optional(Schema.Number),
expiresOn: Schema.optional(Schema.String),
})
const decodeAzureCliToken = Schema.decodeUnknownOption(Schema.fromJsonString(AzureCliToken))

export async function AzureAuthPlugin(_input: PluginInput): Promise<Hooks> {
const prompts = []
if (!process.env.AZURE_RESOURCE_NAME) {
prompts.push({
type: "text" as const,
key: "resourceName",
message: "Enter Azure Resource Name",
placeholder: "e.g. my-models",
})
}
return azureAuthPlugin({
provider: "azure",
resourceEnv: "AZURE_RESOURCE_NAME",
oauthInstructions:
"Sign in with `az login`. The signed-in Azure identity must have the Cognitive Services OpenAI User role for this resource.",
prompts: process.env.AZURE_RESOURCE_NAME
? []
: [
{
type: "text" as const,
key: "resourceName",
message: "Enter Azure Resource Name",
placeholder: "e.g. my-models",
},
],
providerOptions: (resourceName) => ({ resourceName }),
})
}

export async function AzureCognitiveServicesAuthPlugin(_input: PluginInput): Promise<Hooks> {
return azureAuthPlugin({
provider: "azure-cognitive-services",
resourceEnv: "AZURE_COGNITIVE_SERVICES_RESOURCE_NAME",
oauthInstructions:
"Sign in with `az login`. The signed-in Azure identity must have the Cognitive Services User or Foundry User role for this resource.",
prompts: process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
? []
: [
{
type: "text" as const,
key: "resourceName",
message: "Enter Azure Cognitive Services Resource Name",
placeholder: "e.g. my-models",
},
],
})
}

function azureAuthPlugin(input: {
provider: string
resourceEnv: string
oauthInstructions: string
prompts: NonNullable<Hooks["auth"]>["methods"][number]["prompts"]
providerOptions?: (resourceName: string) => Record<string, string>
}): Hooks {
return {
auth: {
provider: "azure",
provider: input.provider,
async loader(getAuth) {
const auth = await getAuth()
if (auth.type !== "oauth") return {}

const resourceName = process.env[input.resourceEnv] || auth.accountId
const tokenProvider = azureCliTokenProvider()

return {
...((resourceName && input.providerOptions?.(resourceName)) ?? {}),
apiKey: OAUTH_DUMMY_KEY,
async fetch(requestInput: RequestInfo | URL, init?: RequestInit) {
const currentAuth = await getAuth()
if (currentAuth.type !== "oauth") return fetch(requestInput, init)

const headers = new Headers(requestInput instanceof Request ? requestInput.headers : undefined)
if (init?.headers) {
const entries =
init.headers instanceof Headers
? init.headers.entries()
: Array.isArray(init.headers)
? init.headers
: Object.entries(init.headers as Record<string, string | undefined>)
for (const [key, value] of entries) {
if (value !== undefined) headers.set(key, String(value))
}
}
headers.delete("api-key")
headers.delete("x-api-key")
headers.set("authorization", `Bearer ${await tokenProvider()}`)
headers.set("User-Agent", `opencode/${InstallationVersion}`)

return fetch(requestInput, { ...init, headers })
},
}
},
methods: [
{
type: "api",
label: "API key",
prompts,
prompts: input.prompts,
},
{
type: "oauth",
label: "Microsoft Entra ID (OAuth via az cli)",
prompts: input.prompts,
authorize: async (inputs) => ({
url: "https://learn.microsoft.com/azure/developer/ai/keyless-connections",
instructions: input.oauthInstructions,
method: "auto" as const,
callback: async () => ({
type: "success" as const,
access: OAUTH_DUMMY_KEY,
refresh: OAUTH_DUMMY_KEY,
expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
accountId: inputs?.resourceName || process.env[input.resourceEnv],
}),
}),
},
],
},
}
}

function azureCliTokenProvider() {
let cached: { token: string; expires: number } | undefined
return async () => {
if (cached && cached.expires - Date.now() > AZURE_TOKEN_REFRESH_BUFFER) return cached.token

const proc = Bun.spawn(["az", "account", "get-access-token", "--resource", AZURE_SCOPE, "--output", "json"], {
stdout: "pipe",
stderr: "pipe",
})
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
])
if (exitCode !== 0) {
throw new Error(stderr.trim() || "Failed to get Azure access token. Run `az login` and try again.")
}

const decoded = decodeAzureCliToken(stdout)
if (Option.isNone(decoded)) throw new Error("Azure CLI did not return an access token")

cached = {
token: decoded.value.accessToken,
// Azure CLI's expiresOn is a timezone-less local datetime; expires_on avoids DST ambiguity.
expires:
decoded.value.expires_on !== undefined
? decoded.value.expires_on * 1000
: decoded.value.expiresOn
? new Date(decoded.value.expiresOn).getTime()
: Date.now() + 30 * 60 * 1000,
}
return cached.token
}
}
3 changes: 2 additions & 1 deletion packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { CopilotAuthPlugin } from "./github-copilot/copilot"
import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
import { PoeAuthPlugin } from "opencode-poe-auth"
import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
import { AzureAuthPlugin } from "./azure"
import { AzureAuthPlugin, AzureCognitiveServicesAuthPlugin } from "./azure"
import { DigitalOceanAuthPlugin } from "./digitalocean"
import { XaiAuthPlugin } from "./xai"
import { SnowflakeCortexAuthPlugin } from "./snowflake-cortex"
Expand Down Expand Up @@ -75,6 +75,7 @@ function internalPlugins(flags: RuntimeFlags.Info): PluginInstance[] {
CloudflareWorkersAuthPlugin,
CloudflareAIGatewayAuthPlugin,
AzureAuthPlugin,
AzureCognitiveServicesAuthPlugin,
DigitalOceanAuthPlugin,
SnowflakeCortexAuthPlugin,
XaiAuthPlugin,
Expand Down
25 changes: 22 additions & 3 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
return [
provider.options?.resourceName,
auth?.type === "api" ? auth.metadata?.resourceName : undefined,
auth?.type === "oauth" ? auth.accountId : undefined,
env["AZURE_RESOURCE_NAME"],
].find((name) => typeof name === "string" && name.trim() !== "")
})
Expand Down Expand Up @@ -266,15 +267,33 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
},
}
}),
"azure-cognitive-services": Effect.fnUntraced(function* () {
const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME")
"azure-cognitive-services": Effect.fnUntraced(function* (provider: Info) {
const env = yield* dep.env()
const auth = yield* dep.auth(provider.id)
const resourceName = [
provider.options?.resourceName,
auth?.type === "api" ? auth.metadata?.resourceName : undefined,
auth?.type === "oauth" ? auth.accountId : undefined,
Comment thread
OpeOginni marked this conversation as resolved.
env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"],
].find((name) => typeof name === "string" && name.trim() !== "")
return {
autoload: false,
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"]))
},
options: {
baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
baseURL:
resourceName && auth?.type !== "oauth"
? `https://${resourceName}.cognitiveservices.azure.com/openai`
: undefined,
},
vars(_options): Record<string, string> {
if (resourceName) {
return {
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: resourceName,
}
}
return {}
},
}
}),
Expand Down
44 changes: 32 additions & 12 deletions packages/web/src/content/docs/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,7 @@ If tool calls aren't working well, pick a loaded model with strong tool-calling
If you encounter "I'm sorry, but I cannot assist with that request" errors, try changing the content filter from **DefaultV2** to **Default** in your Azure resource.
:::

1. Head over to the [Azure portal](https://portal.azure.com/) and create an **Azure OpenAI** resource. You'll need:
- **Resource name**: This becomes part of your API endpoint (`https://RESOURCE_NAME.openai.azure.com/`)
- **API key**: Either `KEY 1` or `KEY 2` from your resource
1. Head over to the [Azure portal](https://portal.azure.com/) and create an **Azure OpenAI** resource. You'll need the **Resource name**; this becomes part of your API endpoint (`https://RESOURCE_NAME.openai.azure.com/`).

2. Go to [Azure AI Foundry](https://ai.azure.com/) and deploy a model.

Expand All @@ -394,7 +392,19 @@ If you encounter "I'm sorry, but I cannot assist with that request" errors, try
/connect
```

4. Enter your API key.
4. Choose an auth method.
- **API key**: Paste either `KEY 1` or `KEY 2` from your resource.
- **Microsoft Entra ID (OAuth via az cli)**: Run `az login` first. The signed-in Azure identity must have the `Cognitive Services OpenAI User` role for the resource.

```txt
┌ Select auth method
│ API key
│ Microsoft Entra ID (OAuth via az cli)
```

5. If you chose API key, enter your API key.

```txt
┌ API key
Expand All @@ -403,7 +413,7 @@ If you encounter "I'm sorry, but I cannot assist with that request" errors, try
└ enter
```

5. Set your resource name as an environment variable:
6. Optional: set your resource name as an environment variable to skip the resource name prompt during `/connect`.

```bash
AZURE_RESOURCE_NAME=XXX opencode
Expand All @@ -415,7 +425,7 @@ If you encounter "I'm sorry, but I cannot assist with that request" errors, try
export AZURE_RESOURCE_NAME=XXX
```

6. Run the `/models` command to select your deployed model.
7. Run the `/models` command to select your deployed model.

```txt
/models
Expand All @@ -425,9 +435,7 @@ If you encounter "I'm sorry, but I cannot assist with that request" errors, try

### Azure Cognitive Services

1. Head over to the [Azure portal](https://portal.azure.com/) and create an **Azure OpenAI** resource. You'll need:
- **Resource name**: This becomes part of your API endpoint (`https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/`)
- **API key**: Either `KEY 1` or `KEY 2` from your resource
1. Head over to the [Azure portal](https://portal.azure.com/) and create an **Azure OpenAI** resource. You'll need the **Resource name**; this becomes part of your API endpoint (`https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/`).

2. Go to [Azure AI Foundry](https://ai.azure.com/) and deploy a model.

Expand All @@ -441,7 +449,19 @@ If you encounter "I'm sorry, but I cannot assist with that request" errors, try
/connect
```

4. Enter your API key.
4. Choose an auth method.
- **API key**: Paste either `KEY 1` or `KEY 2` from your resource.
- **Microsoft Entra ID (OAuth via az cli)**: Run `az login` first. The signed-in Azure identity must have the `Cognitive Services User` or `Foundry User` role for the resource.

```txt
┌ Select auth method
│ API key
│ Microsoft Entra ID (OAuth via az cli)
```

5. If you chose API key, enter your API key.

```txt
┌ API key
Expand All @@ -450,7 +470,7 @@ If you encounter "I'm sorry, but I cannot assist with that request" errors, try
└ enter
```

5. Set your resource name as an environment variable:
6. Optional: set your resource name as an environment variable to skip the resource name prompt during `/connect`.

```bash
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME=XXX opencode
Expand All @@ -462,7 +482,7 @@ If you encounter "I'm sorry, but I cannot assist with that request" errors, try
export AZURE_COGNITIVE_SERVICES_RESOURCE_NAME=XXX
```

6. Run the `/models` command to select your deployed model.
7. Run the `/models` command to select your deployed model.

```txt
/models
Expand Down
Loading