@kindgi/adapter-model-azure-openai 0.0.0-stage → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/entry.js ADDED
@@ -0,0 +1,195 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+ /**
4
+ * The Azure OpenAI adapter: a registration's models served by its Azure OpenAI deployments,
5
+ * through `@kindgi/adapter-model-shared` (retries, typed errors, the reasoning state across a
6
+ * pause, usage and cost).
7
+ *
8
+ * How it signs in, pinned (each has a test):
9
+ * - Before each attempt (`beforeAttempt`), with the call's abort signal; the attempt's
10
+ * request reads what it got (`attemptPrepared()`), so calls at once never share one.
11
+ * - `auth: entra`: a bearer token from the runtime's own Azure identity
12
+ * (`AdapterFactoryInput.identities.azure`), asked for every attempt (bounded), for the
13
+ * endpoint's cloud's scope; no `api-key` header. Only an Azure OpenAI host gets it
14
+ * (`AZURE_OPENAI_CLOUDS`): any other is refused at registration.
15
+ * - `auth: api-key`: the key `secret_ref` names, read for every attempt, so a rotated key
16
+ * takes effect on the next call.
17
+ * - A failed sign-in (no token, no key, a blank one) is an `auth` error, never retried. So
18
+ * is a key or a token a header can't carry (a line break inside it): fetch's own error
19
+ * would quote it into the call's failure and the logs, so it's refused first, unquoted.
20
+ * - No request follows a redirect: the credential goes to the endpoint, nowhere else.
21
+ * - Never `AZURE_API_KEY` or `AZURE_RESOURCE_NAME` from the environment: the provider is
22
+ * always given its endpoint and its credential.
23
+ */
24
+ import { createAzure } from '@ai-sdk/azure';
25
+ import { ModelProviderError, tokenCostUsd } from '@kindgi/adapter-model-shared';
26
+ import { attemptPrepared, createAiSdkModelProvider } from '@kindgi/adapter-model-shared/ai-sdk';
27
+ import { adapterConfigError, identitiesPresent, } from '@kindgi/capabilities';
28
+ import { bearerValueProblem, headerValueProblem } from '@kindgi/types';
29
+ import { AZURE_OPENAI_ADAPTER_ID, AZURE_OPENAI_CLOUDS, readAzureOpenAIConfig } from './config.js';
30
+ /** The Entra scope Azure OpenAI takes in Azure's public cloud. */
31
+ export const AZURE_OPENAI_SCOPE = AZURE_OPENAI_CLOUDS[0].scope;
32
+ /** How long a token from the runtime's identity may take before the call fails as `auth`. */
33
+ export const IDENTITY_TIMEOUT_MS = 10_000;
34
+ /** What's wrong with a registration, for `POST /v1/providers` and `GET …/check`. */
35
+ export function azureOpenAICheckConfig(input) {
36
+ const read = readAzureOpenAIConfig(input);
37
+ return read.kind === 'ok' ? [] : read.problems;
38
+ }
39
+ export const azureOpenAIAdapterFactory = (input) => {
40
+ const { metadata } = input;
41
+ const read = readAzureOpenAIConfig({
42
+ metadata,
43
+ ...(input.config !== undefined && { config: input.config }),
44
+ hasSecretRef: input.resolveApiKey !== undefined,
45
+ identities: identitiesPresent(input.identities),
46
+ });
47
+ if (read.kind === 'err') {
48
+ throw adapterConfigError(AZURE_OPENAI_ADAPTER_ID, metadata.id, read.problems[0]);
49
+ }
50
+ const { endpoint, api, auth, scope, deployments } = read.config;
51
+ const tokens = input.identities?.azure;
52
+ const resolveApiKey = input.resolveApiKey;
53
+ return createAiSdkModelProvider({
54
+ metadata,
55
+ ...(input.fetch !== undefined && { fetch: input.fetch }),
56
+ beforeAttempt: (signal) => auth === 'entra'
57
+ ? entraToken(tokens, scope, signal).then((token) => ({
58
+ token,
59
+ }))
60
+ : apiKey(resolveApiKey).then((key) => ({ key })),
61
+ languageModel: (name, fetch) => {
62
+ const send = withoutRedirects(fetch);
63
+ const azure = createAzure({
64
+ ...endpoint,
65
+ ...(auth === 'entra'
66
+ ? { tokenProvider: async () => signedIn().token, fetch: send }
67
+ : // An explicit (empty) key keeps the provider from reading AZURE_API_KEY; the real
68
+ // one, this attempt's, is set on its request.
69
+ { apiKey: '', fetch: withApiKey(send) }),
70
+ });
71
+ const deployment = deployments.get(name);
72
+ return api === 'chat-completions' ? azure.chat(deployment) : azure.responses(deployment);
73
+ },
74
+ // Responses keeps nothing on Azure's side: our journal is the record. Whether a model
75
+ // reasons is the registration's to say where it says (the deployment's name, which the
76
+ // library reads as the model's, can say nothing); where it doesn't, the library decides.
77
+ // Responses reads it under `azure`, Chat under `openai`.
78
+ providerOptions: (model) => {
79
+ const forced = reasons(model);
80
+ const reasoning = forced === undefined ? {} : { forceReasoning: forced };
81
+ return api === 'responses'
82
+ ? { azure: { store: false, ...reasoning } }
83
+ : forced === undefined
84
+ ? undefined
85
+ : { openai: reasoning };
86
+ },
87
+ cost: (model, usage) => tokenCostUsd(model, usage),
88
+ });
89
+ };
90
+ /** The entry a runtime registers: the factory, and its static check. */
91
+ export const azureOpenAIAdapterEntry = {
92
+ adapterId: AZURE_OPENAI_ADAPTER_ID,
93
+ capabilityKind: 'llm-inference',
94
+ factory: azureOpenAIAdapterFactory,
95
+ checkConfig: azureOpenAICheckConfig,
96
+ };
97
+ /**
98
+ * Whether the registration says a model reasons: yes when it thinks or takes no sampling (GPT-5
99
+ * and later, o-series), no when it takes sampling, said; undefined when it says neither.
100
+ */
101
+ function reasons(model) {
102
+ if (model.thinking !== undefined || model.sampling === false)
103
+ return true;
104
+ return model.sampling === true ? false : undefined;
105
+ }
106
+ /** This attempt's sign-in, read by its request. */
107
+ function signedIn() {
108
+ const signed = attemptPrepared();
109
+ if (signed === undefined) {
110
+ throw new ModelProviderError('auth', undefined, 'The request has no sign-in of its own.');
111
+ }
112
+ return signed;
113
+ }
114
+ const SIGN_IN_HINT = 'It signs in as its managed identity (KINDGI_AZURE_CLIENT_ID names a user-assigned one), which needs the Cognitive Services OpenAI User role on the resource.';
115
+ /**
116
+ * A bearer for an attempt, from the runtime's Azure identity: bounded, stopped with the call,
117
+ * and `auth` when it fails.
118
+ */
119
+ async function entraToken(tokens, scope, signal) {
120
+ let token;
121
+ try {
122
+ const asked = signal !== undefined
123
+ ? tokens.getToken(scope, { abortSignal: signal })
124
+ : tokens.getToken(scope);
125
+ token = (await bounded(asked, IDENTITY_TIMEOUT_MS, signal))?.token;
126
+ }
127
+ catch (error) {
128
+ // (A call stopped meanwhile ends with its own reason: the retries check the signal first.)
129
+ throw new ModelProviderError('auth', undefined, `The runtime's Azure identity gave no token for ${scope}: ${messageOf(error)}. ${SIGN_IN_HINT}`, { cause: error });
130
+ }
131
+ if (token === undefined || token === '') {
132
+ throw new ModelProviderError('auth', undefined, `The runtime's Azure identity returned no token for ${scope}. ${SIGN_IN_HINT}`);
133
+ }
134
+ // Sent as `Bearer <token>`, so checked as sent, before the request: never quoted.
135
+ const problem = bearerValueProblem(token);
136
+ if (problem !== undefined) {
137
+ throw new ModelProviderError('auth', undefined, `The runtime's Azure identity returned a token for ${scope} with ${problem}, which a header can't carry.`);
138
+ }
139
+ return token;
140
+ }
141
+ /** The current key `secret_ref` names, for an attempt; `auth` when there's none. */
142
+ async function apiKey(resolveApiKey) {
143
+ let key;
144
+ try {
145
+ key = await resolveApiKey();
146
+ }
147
+ catch (error) {
148
+ throw new ModelProviderError('auth', undefined, `The key secret_ref names couldn't be read: ${messageOf(error)}.`, { cause: error });
149
+ }
150
+ // Whitespace around a key (a stored file's newline) is never the key's.
151
+ const trimmed = key.trim();
152
+ if (trimmed === '') {
153
+ throw new ModelProviderError('auth', undefined, 'The key secret_ref names is empty.');
154
+ }
155
+ // Sent as is in `api-key`, so checked before its request: never quoted.
156
+ const problem = headerValueProblem(trimmed);
157
+ if (problem !== undefined) {
158
+ throw new ModelProviderError('auth', undefined, `The key secret_ref names has ${problem}, which a header can't carry: store the secret without it.`);
159
+ }
160
+ return trimmed;
161
+ }
162
+ /** The `api-key` header on the request, from this attempt's key. */
163
+ function withApiKey(fetch) {
164
+ return async (url, init) => {
165
+ const headers = new Headers(init?.headers);
166
+ headers.set('api-key', signedIn().key);
167
+ return fetch(url, { ...init, headers });
168
+ };
169
+ }
170
+ /** No request follows a redirect, so its credential reaches the endpoint and nothing else. */
171
+ function withoutRedirects(fetch) {
172
+ return (url, init) => fetch(url, { ...init, redirect: 'error' });
173
+ }
174
+ /** The promise, or a failure after `ms`, or the signal's reason when the call stops first. */
175
+ function bounded(promise, ms, signal) {
176
+ // An abort that has already happened fires no event.
177
+ if (signal?.aborted === true)
178
+ return Promise.reject(signal.reason);
179
+ let timer;
180
+ let onAbort;
181
+ const stop = new Promise((_, reject) => {
182
+ timer = setTimeout(() => reject(new Error(`no answer in ${ms / 1000} s`)), ms);
183
+ if (signal !== undefined) {
184
+ onAbort = () => reject(signal.reason);
185
+ signal.addEventListener('abort', onAbort, { once: true });
186
+ }
187
+ });
188
+ return Promise.race([Promise.resolve(promise), stop]).finally(() => {
189
+ clearTimeout(timer);
190
+ if (onAbort !== undefined)
191
+ signal?.removeEventListener('abort', onAbort);
192
+ });
193
+ }
194
+ const messageOf = (error) => error instanceof Error ? error.message : String(error);
195
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { AZURE_OPENAI_ADAPTER_ID, AZURE_OPENAI_APIS, AZURE_OPENAI_AUTHS, AZURE_OPENAI_CLOUDS, type AzureOpenAIApi, type AzureOpenAIAuth, type AzureOpenAIConfig, azureCloudOf, readAzureOpenAIConfig, } from './config.js';
2
+ export { AZURE_OPENAI_SCOPE, IDENTITY_TIMEOUT_MS, azureOpenAIAdapterEntry, azureOpenAIAdapterFactory, azureOpenAICheckConfig, } from './entry.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,YAAY,EACZ,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+ export { AZURE_OPENAI_ADAPTER_ID, AZURE_OPENAI_APIS, AZURE_OPENAI_AUTHS, AZURE_OPENAI_CLOUDS, azureCloudOf, readAzureOpenAIConfig, } from './config.js';
4
+ export { AZURE_OPENAI_SCOPE, IDENTITY_TIMEOUT_MS, azureOpenAIAdapterEntry, azureOpenAIAdapterFactory, azureOpenAICheckConfig, } from './entry.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EAInB,YAAY,EACZ,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@kindgi/adapter-model-azure-openai",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.7",
4
+ "description": "Azure OpenAI ModelProvider for @kindgi/capabilities: your Azure OpenAI deployments (Responses or Chat Completions) by API key or the runtime's Entra identity.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/kindgi/kindgi-sdk.git",
9
+ "directory": "packages/adapters/model-azure-openai"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src",
22
+ "README.md"
23
+ ],
24
+ "dependencies": {
25
+ "@ai-sdk/azure": "4.0.97",
26
+ "@kindgi/adapter-model-shared": "0.1.7",
27
+ "@kindgi/capabilities": "0.1.7",
28
+ "@kindgi/types": "0.1.7"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.10.5",
32
+ "typescript": "^5.7.3",
33
+ "vitest": "^2.1.8"
34
+ },
35
+ "engines": {
36
+ "node": ">=22.12.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "provenance": true
41
+ },
42
+ "homepage": "https://github.com/kindgi/kindgi-sdk/tree/main/packages/adapters/model-azure-openai#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/kindgi/kindgi-sdk/issues"
45
+ },
46
+ "main": "./dist/index.js",
47
+ "types": "./dist/index.d.ts",
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "vitest run",
52
+ "clean": "rm -rf dist *.tsbuildinfo"
53
+ }
6
54
  }
package/src/config.ts ADDED
@@ -0,0 +1,319 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * An Azure OpenAI registration's `adapter_config`, read and checked: where the resource is,
6
+ * which API, how to sign in, and which deployment serves each model. Static: no network, no
7
+ * secret read (`checkConfig` runs it at registration, the factory again when it builds).
8
+ */
9
+
10
+ import type {
11
+ AdapterConfigCheckInput,
12
+ AdapterConfigProblem,
13
+ ProviderMetadata,
14
+ } from '@kindgi/capabilities';
15
+
16
+ export const AZURE_OPENAI_ADAPTER_ID = '@kindgi/adapter-model-azure-openai';
17
+
18
+ /** The v1 API's Responses (the default), or Chat Completions. */
19
+ export const AZURE_OPENAI_APIS = ['responses', 'chat-completions'] as const;
20
+ export type AzureOpenAIApi = (typeof AZURE_OPENAI_APIS)[number];
21
+
22
+ /** A key (`secret_ref`, sent as `api-key`), or the runtime's Entra identity (a bearer token). */
23
+ export const AZURE_OPENAI_AUTHS = ['api-key', 'entra'] as const;
24
+ export type AzureOpenAIAuth = (typeof AZURE_OPENAI_AUTHS)[number];
25
+
26
+ export interface AzureOpenAIConfig {
27
+ readonly endpoint: { readonly resourceName: string } | { readonly baseURL: string };
28
+ readonly api: AzureOpenAIApi;
29
+ readonly auth: AzureOpenAIAuth;
30
+ /** With `auth: entra`: the scope the runtime's token is for, the endpoint's cloud's. */
31
+ readonly scope?: string;
32
+ /** The deployment for each registered model. */
33
+ readonly deployments: ReadonlyMap<string, string>;
34
+ }
35
+
36
+ /**
37
+ * The hosts the runtime's Entra token may go to, by cloud, each with the scope its tokens are
38
+ * for. Nothing else gets the token: a registration naming another host could replay it against
39
+ * every Azure OpenAI resource the runtime's identity holds a role on. A custom endpoint (a
40
+ * gateway) takes `auth: api-key`.
41
+ *
42
+ * Sources (read 2026-10-09): the endpoints by cloud,
43
+ * https://learn.microsoft.com/en-us/azure/azure-government/compare-azure-government-global-azure;
44
+ * Azure Government's scope, https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.openai-readme
45
+ * (its Azure Government example). Microsoft's newer pages name the public
46
+ * scope `https://ai.azure.com/.default`; `cognitiveservices.azure.com` is the one proven live
47
+ * (2026-10-09), so it stays until a live run proves the other.
48
+ */
49
+ export const AZURE_OPENAI_CLOUDS = [
50
+ {
51
+ cloud: 'Azure',
52
+ hostSuffixes: ['.openai.azure.com', '.cognitiveservices.azure.com', '.services.ai.azure.com'],
53
+ scope: 'https://cognitiveservices.azure.com/.default',
54
+ },
55
+ {
56
+ cloud: 'Azure Government',
57
+ hostSuffixes: ['.openai.azure.us', '.cognitiveservices.azure.us'],
58
+ scope: 'https://cognitiveservices.azure.us/.default',
59
+ },
60
+ ] as const;
61
+
62
+ type AzureCloud = (typeof AZURE_OPENAI_CLOUDS)[number];
63
+
64
+ /** The cloud an Azure OpenAI host belongs to; undefined for any other host. */
65
+ export function azureCloudOf(hostname: string): AzureCloud | undefined {
66
+ const host = hostname.toLowerCase();
67
+ return AZURE_OPENAI_CLOUDS.find((c) =>
68
+ c.hostSuffixes.some((suffix) => host.endsWith(suffix) && host.length > suffix.length),
69
+ );
70
+ }
71
+
72
+ /** The v1 API's path on an Azure OpenAI host. */
73
+ const V1_PATH = '/openai/v1';
74
+ const AZURE_HOSTS = AZURE_OPENAI_CLOUDS.flatMap((c) => c.hostSuffixes.map((s) => `*${s}`)).join(
75
+ ', ',
76
+ );
77
+
78
+ const KEYS = ['resourceName', 'baseURL', 'deployments', 'api', 'auth'] as const;
79
+ /** A single DNS label, as Azure's resource names are. */
80
+ const RESOURCE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
81
+ /**
82
+ * A deployment name as it can sit in this setting and in the request path: Azure sets no
83
+ * character rule of its own (its ARM reference says only "string"; `gpt-6.1-sol` is a real
84
+ * one), so only what would break the setting (`,`, `=`) or the path (`/`, whitespace) is refused,
85
+ * and Azure answers for the rest.
86
+ */
87
+ const DEPLOYMENT_NAME = /^[^\s,=/]+$/;
88
+
89
+ const at = (key: string) => `/adapter_config/${key}`;
90
+
91
+ /** The registration read, or every problem with it. */
92
+ export function readAzureOpenAIConfig(
93
+ input: AdapterConfigCheckInput,
94
+ ):
95
+ | { readonly kind: 'ok'; readonly config: AzureOpenAIConfig }
96
+ | { readonly kind: 'err'; readonly problems: readonly AdapterConfigProblem[] } {
97
+ const config = input.config ?? {};
98
+ const problems: AdapterConfigProblem[] = [];
99
+
100
+ for (const key of Object.keys(config)) {
101
+ if (!(KEYS as readonly string[]).includes(key)) {
102
+ problems.push({
103
+ path: at(key),
104
+ message: `adapter_config.${key} isn't an Azure OpenAI setting: it takes resourceName or baseURL, deployments, api and auth.`,
105
+ });
106
+ }
107
+ }
108
+
109
+ const auth = config.auth ?? 'api-key';
110
+ const resourceName = config.resourceName;
111
+ const baseURL = config.baseURL;
112
+ let endpoint: AzureOpenAIConfig['endpoint'] | undefined;
113
+ let cloud: AzureCloud | undefined;
114
+ if (resourceName !== undefined && baseURL !== undefined) {
115
+ problems.push({
116
+ path: at('baseURL'),
117
+ message:
118
+ 'adapter_config.resourceName and adapter_config.baseURL exclude each other: the resource, or a custom endpoint.',
119
+ });
120
+ } else if (resourceName !== undefined) {
121
+ if (typeof resourceName !== 'string' || !RESOURCE_NAME.test(resourceName)) {
122
+ problems.push({
123
+ path: at('resourceName'),
124
+ message:
125
+ 'adapter_config.resourceName must be the Azure OpenAI resource name, as in <name>.openai.azure.com (letters, digits, hyphens).',
126
+ });
127
+ } else {
128
+ endpoint = { resourceName };
129
+ cloud = AZURE_OPENAI_CLOUDS[0];
130
+ }
131
+ } else if (baseURL !== undefined) {
132
+ const read = readBaseURL(baseURL, auth, problems);
133
+ if (read !== undefined) {
134
+ endpoint = { baseURL: read.baseURL };
135
+ cloud = read.cloud;
136
+ }
137
+ } else {
138
+ problems.push({
139
+ path: at('resourceName'),
140
+ message:
141
+ 'needs adapter_config.resourceName, the Azure OpenAI resource (as in <name>.openai.azure.com), or adapter_config.baseURL for a custom endpoint.',
142
+ });
143
+ }
144
+
145
+ const api = config.api ?? 'responses';
146
+ if (!(AZURE_OPENAI_APIS as readonly unknown[]).includes(api)) {
147
+ problems.push({
148
+ path: at('api'),
149
+ message: `adapter_config.api must be one of ${AZURE_OPENAI_APIS.join(', ')}.`,
150
+ });
151
+ }
152
+
153
+ if (!(AZURE_OPENAI_AUTHS as readonly unknown[]).includes(auth)) {
154
+ problems.push({
155
+ path: at('auth'),
156
+ message: `adapter_config.auth must be one of ${AZURE_OPENAI_AUTHS.join(', ')}.`,
157
+ });
158
+ } else if (auth === 'api-key' && !input.hasSecretRef) {
159
+ problems.push({
160
+ path: '/secret_ref',
161
+ message:
162
+ "needs secret_ref: the resource's API key (or adapter_config.auth = entra, to sign in as the runtime's Azure identity).",
163
+ });
164
+ } else if (auth === 'entra' && input.hasSecretRef) {
165
+ problems.push({
166
+ path: '/secret_ref',
167
+ message:
168
+ "adapter_config.auth = entra signs in as the runtime's Azure identity: remove secret_ref.",
169
+ });
170
+ }
171
+ if (auth === 'entra' && input.identities?.azure === false) {
172
+ problems.push({
173
+ path: at('auth'),
174
+ message:
175
+ "adapter_config.auth = entra needs the runtime's Azure identity (its managed identity; KINDGI_AZURE_CLIENT_ID names a user-assigned one), and this runtime has none.",
176
+ });
177
+ }
178
+
179
+ const deployments = readDeployments(config.deployments, input.metadata, problems);
180
+
181
+ if (problems.length > 0 || endpoint === undefined || deployments === undefined) {
182
+ return { kind: 'err', problems };
183
+ }
184
+ return {
185
+ kind: 'ok',
186
+ config: {
187
+ endpoint,
188
+ api: api as AzureOpenAIApi,
189
+ auth: auth as AzureOpenAIAuth,
190
+ ...(auth === 'entra' && cloud !== undefined && { scope: cloud.scope }),
191
+ deployments,
192
+ },
193
+ };
194
+ }
195
+
196
+ /**
197
+ * A custom endpoint: https, with no credentials, query or fragment (they'd sit in the stored
198
+ * settings); on an Azure OpenAI host, the v1 path (the portal's endpoint lacks it, and would
199
+ * 404); and with `auth: entra`, an Azure OpenAI host only.
200
+ */
201
+ function readBaseURL(
202
+ value: unknown,
203
+ auth: unknown,
204
+ problems: AdapterConfigProblem[],
205
+ ): { readonly baseURL: string; readonly cloud: AzureCloud | undefined } | undefined {
206
+ const path = at('baseURL');
207
+ const url = typeof value === 'string' ? parseUrl(value) : undefined;
208
+ if (url === undefined || url.protocol !== 'https:') {
209
+ problems.push({
210
+ path,
211
+ message: `adapter_config.baseURL must be an https URL (e.g. https://<name>.openai.azure.com${V1_PATH}).`,
212
+ });
213
+ return undefined;
214
+ }
215
+ const before = problems.length;
216
+ const raw = value as string;
217
+ if (url.username !== '' || url.password !== '') {
218
+ problems.push({
219
+ path,
220
+ message:
221
+ "adapter_config.baseURL can't hold credentials: a key goes in secret_ref, never in the URL.",
222
+ });
223
+ }
224
+ if (url.search !== '' || raw.includes('?') || url.hash !== '' || raw.includes('#')) {
225
+ problems.push({
226
+ path,
227
+ message:
228
+ "adapter_config.baseURL can't hold a query or a fragment: a key goes in secret_ref, and the API version is the adapter's.",
229
+ });
230
+ }
231
+ if (/\/\/+$/.test(url.pathname)) {
232
+ problems.push({
233
+ path,
234
+ message:
235
+ "adapter_config.baseURL can't end in more than one /: the adapter adds the API's path.",
236
+ });
237
+ }
238
+ const cloud = azureCloudOf(url.hostname);
239
+ if (cloud !== undefined && url.pathname.replace(/\/+$/, '') !== V1_PATH) {
240
+ problems.push({
241
+ path,
242
+ message: `adapter_config.baseURL on an Azure OpenAI host must end in ${V1_PATH} (https://${url.hostname}${V1_PATH}), the v1 API.`,
243
+ });
244
+ }
245
+ if (auth === 'entra' && cloud === undefined) {
246
+ problems.push({
247
+ path,
248
+ message: `adapter_config.auth = entra sends the runtime's Azure token, which goes only to an Azure OpenAI host (${AZURE_HOSTS}); ${url.hostname} isn't one. A custom endpoint takes auth = api-key.`,
249
+ });
250
+ }
251
+ return problems.length === before ? { baseURL: raw, cloud } : undefined;
252
+ }
253
+
254
+ /** `"model=deployment,model=deployment"`: every registered model, each a deployment Azure takes. */
255
+ function readDeployments(
256
+ value: unknown,
257
+ metadata: ProviderMetadata,
258
+ problems: AdapterConfigProblem[],
259
+ ): ReadonlyMap<string, string> | undefined {
260
+ const path = at('deployments');
261
+ if (typeof value !== 'string' || value.trim() === '') {
262
+ problems.push({
263
+ path,
264
+ message:
265
+ 'needs adapter_config.deployments, the deployment for each model: "model=deployment,…" (e.g. "gpt-6.1-sol=gpt-6-1-sol").',
266
+ });
267
+ return undefined;
268
+ }
269
+ const models = new Set(metadata.models.map((m) => m.name));
270
+ const map = new Map<string, string>();
271
+ const before = problems.length;
272
+ for (const pair of value.split(',')) {
273
+ const [model, deployment, extra] = pair.split('=').map((part) => part.trim());
274
+ if (
275
+ model === undefined ||
276
+ model === '' ||
277
+ deployment === undefined ||
278
+ deployment === '' ||
279
+ extra !== undefined
280
+ ) {
281
+ problems.push({
282
+ path,
283
+ message: `adapter_config.deployments has "${pair.trim()}": each entry is model=deployment.`,
284
+ });
285
+ } else if (map.has(model)) {
286
+ problems.push({
287
+ path,
288
+ message: `adapter_config.deployments names ${model} twice: one deployment each.`,
289
+ });
290
+ } else if (!models.has(model)) {
291
+ problems.push({
292
+ path,
293
+ message: `adapter_config.deployments names ${model}, which this registration doesn't list in metadata.models.`,
294
+ });
295
+ } else if (!DEPLOYMENT_NAME.test(deployment)) {
296
+ problems.push({
297
+ path,
298
+ message: `adapter_config.deployments gives ${model} the deployment "${deployment}": a deployment name can't hold spaces, ",", "=" or "/".`,
299
+ });
300
+ } else map.set(model, deployment);
301
+ }
302
+ for (const model of models) {
303
+ if (!map.has(model) && problems.length === before) {
304
+ problems.push({
305
+ path,
306
+ message: `adapter_config.deployments has no deployment for ${model}.`,
307
+ });
308
+ }
309
+ }
310
+ return problems.length === before ? map : undefined;
311
+ }
312
+
313
+ function parseUrl(value: string): URL | undefined {
314
+ try {
315
+ return new URL(value);
316
+ } catch {
317
+ return undefined;
318
+ }
319
+ }