# @kindgi/adapter-model-azure-openai

> Azure OpenAI ModelProvider for @kindgi/capabilities: your Azure OpenAI deployments (Responses or Chat Completions) by API key or the runtime's Entra identity.

`npm install @kindgi/adapter-model-azure-openai` · [source](https://github.com/kindgi/kindgi-sdk/tree/@kindgi/sdk@0.1.9/packages/adapters/model-azure-openai)

Azure OpenAI `ModelProvider` for [`@kindgi/capabilities`](https://docs.kindgi.com/v0.1.9/reference/packages/capabilities.md): a registration's models served by your Azure OpenAI deployments, on the v1 API, signed in with the resource's key or with the runtime's own Entra identity.

It covers **Azure OpenAI deployments**. Other models served by Azure AI Foundry (Llama, Mistral, DeepSeek…) aren't in it.

It's built on [`@kindgi/adapter-model-shared`](https://docs.kindgi.com/v0.1.9/reference/packages/adapter-model-shared.md): the retries, typed errors (`ModelProviderError`), the reasoning state carried across a pause, usage and cost are the same as every adapter built on it. The requests are sent by the AI SDK's `@ai-sdk/azure` provider at the provider-spec level, pinned exactly.

## A registration

`adapter_id: @kindgi/adapter-model-azure-openai`, with these `adapter_config` keys:

| Key            | What                                                                                                                                                                                                                                                                                                              |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resourceName` | The Azure OpenAI resource, as in `<name>.openai.azure.com`.                                                                                                                                                                                                                                                       |
| `baseURL`      | Instead of `resourceName`: a custom endpoint, https, with no credentials, query or fragment. On an Azure OpenAI host (another cloud's, say) it ends in `/openai/v1`; a gateway's is any path, with `auth: api-key`.                                                                                               |
| `deployments`  | Required. The deployment serving each registered model: `"model=deployment,…"`, e.g. `"gpt-6.1-sol=gpt-6-1-sol,gpt-6-luna=luna-prod"`. Azure deployment names are yours to choose (often the model's own name, as `gpt-6.1-sol`), so every model in `metadata.models` needs one.                                  |
| `api`          | `responses` (default) or `chat-completions`. Tools with GPT-6 need `responses`: Azure refuses them on Chat Completions while the model reasons.                                                                                                                                                                   |
| `auth`         | `api-key` (default): the resource's key, through `secret_ref`. `entra`: the runtime's Azure identity; no `secret_ref`; an Azure OpenAI host only (below); refused where the deployment doesn't let tenant configuration sign in as the server (`serverCredentials: false`, `KINDGI_TENANT_HOST_ACCESS=isolated`). |

```json
{
  "adapter_id": "@kindgi/adapter-model-azure-openai",
  "adapter_config": {
    "resourceName": "acme-openai",
    "deployments": "gpt-6.1-sol=gpt-6-1-sol",
    "auth": "entra"
  },
  "metadata": {
    "id": "azure-openai",
    "region": "canadacentral",
    "models": [
      {
        "name": "gpt-6.1-sol",
        "contextWindow": 1050000,
        "sampling": false,
        "features": ["tool-use", "structured-output"],
        "cost": { "promptUsdPer1kTokens": 0.002, "completionUsdPer1kTokens": 0.01, "cachedPromptMultiplier": 0.05 }
      }
    ]
  }
}
```

`checkConfig` reports each problem with a registration at the setting at fault, without the network or the secret:

- `resourceName` and `baseURL` together, or neither;
- a `resourceName` that isn't one DNS label, or a `baseURL` that isn't https;
- an unknown `api`, `auth` or `adapter_config` key;
- `api-key` without `secret_ref`, or `entra` with one;
- `entra` on a host that isn't Azure OpenAI's, naming the host;
- a `baseURL` with credentials, a query or a fragment, ending in more than one `/`, or an Azure OpenAI host's `baseURL` without `/openai/v1`;
- a malformed `deployments` entry, a model named twice or not listed, a deployment name Azure wouldn't take, or a model with no deployment.

## How it signs in

Each of these has a test. It signs in before each attempt, with the call's abort signal, and the attempt's request carries what that sign-in got, so calls running at once never share a credential.

- **`auth: entra`:** a bearer token from the runtime's Azure identity (`AdapterFactoryInput.identities.azure`), asked for every attempt (the identity caches and renews it), within 10 seconds; a call stopped meanwhile ends at once, with its own reason. No `api-key` header is sent. A runtime with no Azure identity refuses the registration, naming `KINDGI_AZURE_CLIENT_ID`. The identity needs the **Cognitive Services OpenAI User** role on the resource.

  - **Only an Azure OpenAI host gets the token**, because whoever receives it could use it against every Azure OpenAI resource the runtime's identity has a role on. Another host is refused at registration (and by the factory), naming it:

    | Cloud            | Hosts                                                                            | Scope                                          |
    | ---------------- | -------------------------------------------------------------------------------- | ---------------------------------------------- |
    | Azure            | `*.openai.azure.com`, `*.cognitiveservices.azure.com`, `*.services.ai.azure.com` | `https://cognitiveservices.azure.com/.default` |
    | Azure Government | `*.openai.azure.us`, `*.cognitiveservices.azure.us`                              | `https://cognitiveservices.azure.us/.default`  |

    Microsoft lists each cloud's endpoints on [Compare Azure Government and global Azure](https://learn.microsoft.com/en-us/azure/azure-government/compare-azure-government-global-azure). Azure Government needs the runtime's identity to be in Azure Government too.

- **`auth: api-key`:** the key `secret_ref` names, read for every attempt and sent as `api-key` (whitespace around it dropped), so a rotated key takes effect on the next call.

- **A failed sign-in** (the identity gives no token, the key can't be read, even with the secret store down, or is empty or blank) ends the call as an `auth` error at once, never retried.

- **No request follows a redirect** (`redirect: 'error'`): the credential reaches the endpoint and nothing else.

- **Never the environment:** `AZURE_API_KEY` and `AZURE_RESOURCE_NAME` are never read. The provider is always given its endpoint and its credential.

## What it sends

- **Responses** (the default): `POST …/openai/v1/responses?api-version=v1`, with the deployment as `model` and `store: false`, so Azure keeps no conversation state for it: Kindgi's journal is the record.
- **Chat Completions:** `POST …/openai/v1/chat/completions`, with the deployment as `model`.
- **Whether a model reasons** is the registration's to say: one registered with `thinking` or `sampling: false` reasons, one with `sampling: true` doesn't, whatever its deployment is called. A reasoning model keeps its encrypted reasoning between tool turns on Responses, and gets `max_completion_tokens` on Chat Completions. When the registration says neither, the AI SDK decides from the deployment's name, so register one or the other when the name doesn't say.
- Every attempt goes through the runtime's `fetch` (`AdapterFactoryInput.fetch`), which refuses the hosts its deployment forbids.

## Cost

`costUsd` is `tokenCostUsd` over the model's registered rates: prompt and completion, cached prompt tokens at `cachedPromptMultiplier`, and the long-context tier when registered. Azure's prices depend on the deployment type (Global, Data Zone, Regional), so register the rates of yours. Costs are estimates from published prices; Azure's invoice is authoritative.

## Exports

- **`azureOpenAIAdapterEntry`**: the `AdapterFactoryEntry` a runtime registers, with `azureOpenAIAdapterFactory` and `azureOpenAICheckConfig`.
- `readAzureOpenAIConfig(input)`: a registration read and checked, or every problem with it.
- `AZURE_OPENAI_CLOUDS` and `azureCloudOf(hostname)`: the hosts Entra may go to, by cloud, with each scope.
- `AZURE_OPENAI_ADAPTER_ID`, `AZURE_OPENAI_APIS`, `AZURE_OPENAI_AUTHS`, `AZURE_OPENAI_SCOPE` (the public cloud's), `IDENTITY_TIMEOUT_MS`, and the `AzureOpenAIConfig`, `AzureOpenAIApi` and `AzureOpenAIAuth` types.

## License

Apache-2.0
