Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure model list #1123

Merged
merged 7 commits into from
Feb 12, 2025
Merged
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
46 changes: 46 additions & 0 deletions docs/src/content/docs/getting-started/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,52 @@ script({

</Steps>

### Listing models

In order to allow GenAIScript to list deployments in your Azure OpenAI service,
you need to provide the Subscription ID **and you need to use Microsoft Entra!**.

<Steps>

<ol>

<li>

Open the Azure OpenAI resource in the [Azure Portal](https://portal.azure.com), open the **Overview** tab and copy the **Subscription ID**.

</li>

<li>

Update the `.env` file with the subscription id.

```txt title=".env"
AZURE_OPENAI_SUBSCRIPTION_ID="..."
```

</li>

<li>

Test your configuration by running

```sh
npx genaiscript models azure
```

:::note

This feature will probably not work with `AZURE_OPENAI_API_KEY`
as the token does not have the proper scope to query the list of deployments.

:::

</li>

</ol>

</Steps>

### Custom credentials

In some situations, the default credentials chain lookup may not work.
Expand Down
27 changes: 19 additions & 8 deletions docs/src/content/docs/reference/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,6 @@ Commands:
help Show help for all commands
system Show system information
env [options] [provider] Show .env information
models
```

### `info help`
Expand Down Expand Up @@ -673,23 +672,35 @@ Options:
-h, --help display help for command
```

### `info models`
## `models`

```
Usage: genaiscript info models [options] [command]
Usage: genaiscript models [options] [command]

Options:
-h, --help display help for command
-h, --help display help for command

Commands:
alias Show model alias mapping
help [command] display help for command
list [provider] List all available models
alias Show model alias mapping
help [command] display help for command
```

### `models list`

```
Usage: genaiscript models list [options] [provider]

List all available models

Options:
-h, --help display help for command
```

#### `models alias`
### `models alias`

```
Usage: genaiscript info models alias [options]
Usage: genaiscript models alias [options]

Show model alias mapping

Expand Down
15 changes: 13 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ import {
} from "./parse" // Parsing functions
import { compileScript, createScript, fixScripts, listScripts } from "./scripts" // Script utilities
import { codeQuery } from "./codequery" // Code parsing and query execution
import { envInfo, modelAliasesInfo, scriptModelInfo, systemInfo } from "./info" // Information utilities
import {
envInfo,
modelAliasesInfo,
modelList,
scriptModelInfo,
systemInfo,
} from "./info" // Information utilities
import { scriptTestList, scriptTestsView, scriptsTest } from "./test" // Test functions
import { cacheClear } from "./cache" // Cache management
import "node:console" // Importing console for side effects
Expand Down Expand Up @@ -528,7 +534,12 @@ export async function cli() {
.option("-e, --error", "show errors")
.option("-m, --models", "show models if possible")
.action(envInfo) // Action to show environment information
const models = info.command("models")
const models = program.command("models")
models
.command("list", { isDefault: true })
.description("List all available models")
.arguments("[provider]")
.action(modelList)
models
.command("alias")
.description("Show model alias mapping")
Expand Down
28 changes: 27 additions & 1 deletion packages/cli/src/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { CORE_VERSION } from "../../core/src/version"
import { YAMLStringify } from "../../core/src/yaml"
import { buildProject } from "./build"
import { kMaxLength } from "buffer"
import { deleteUndefinedValues } from "../../core/src/cleaners"

/**
* Outputs basic system information including node version, platform, architecture, and process ID.
Expand Down Expand Up @@ -106,3 +106,29 @@ export async function modelAliasesInfo() {
)
console.log(YAMLStringify(res))
}

/**
* Outputs environment information for model providers.
* @param provider - The specific provider to filter by (optional).
* @param options - Configuration options, including whether to show tokens.
*/
export async function modelList(
provider: string,
options?: { error?: boolean }
) {
await runtimeHost.readConfig()
const providers = await resolveLanguageModelConfigurations(provider, {
...(options || {}),
models: true,
error: true,
})
console.log(
YAMLStringify(
deleteUndefinedValues(
Object.fromEntries(
providers.map((p) => [p.provider, p.error || p.models])
)
)
)
)
}
11 changes: 9 additions & 2 deletions packages/cli/src/nodehost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
AZURE_AI_INFERENCE_TOKEN_SCOPES,
MODEL_PROVIDER_AZURE_SERVERLESS_OPENAI,
DOT_ENV_FILENAME,
AZURE_MANAGEMENT_TOKEN_SCOPES,
} from "../../core/src/constants"
import { tryReadText } from "../../core/src/fs"
import {
Expand Down Expand Up @@ -53,7 +54,7 @@ import {
Project,
ResponseStatus,
} from "../../core/src/server/messages"
import { createAzureTokenResolver } from "./azuretoken"
import { createAzureTokenResolver } from "../../core/src/azuretoken"
import {
createAzureContentSafetyClient,
isAzureContentSafetyClientConfigured,
Expand Down Expand Up @@ -98,13 +99,14 @@ export class NodeHost extends EventTarget implements RuntimeHost {
readonly userInputQueue = new PLimitPromiseQueue(1)
readonly azureToken: AzureTokenResolver
readonly azureServerlessToken: AzureTokenResolver
readonly azureManagementToken: AzureTokenResolver
readonly microsoftGraphToken: AzureTokenResolver

constructor(dotEnvPath: string) {
super()
this.dotEnvPath = dotEnvPath
this.azureToken = createAzureTokenResolver(
"Azure",
"Azure OpenAI",
"AZURE_OPENAI_TOKEN_SCOPES",
AZURE_COGNITIVE_SERVICES_TOKEN_SCOPES
)
Expand All @@ -113,6 +115,11 @@ export class NodeHost extends EventTarget implements RuntimeHost {
"AZURE_SERVERLESS_OPENAI_TOKEN_SCOPES",
AZURE_AI_INFERENCE_TOKEN_SCOPES
)
this.azureManagementToken = createAzureTokenResolver(
"Azure Management",
"AZURE_MANAGEMENT_TOKEN_SCOPES",
AZURE_MANAGEMENT_TOKEN_SCOPES
)
this.microsoftGraphToken = createAzureTokenResolver(
"Microsoft Graph",
"MICROSOFT_GRAPH_TOKEN_SCOPES",
Expand Down
98 changes: 98 additions & 0 deletions packages/core/src/azureopenai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { LanguageModel, ListModelsFunction } from "./chat"
import {
AZURE_MANAGEMENT_API_VERSION,
MODEL_PROVIDER_AZURE_OPENAI,
} from "./constants"
import { errorMessage, serializeError } from "./error"
import { createFetch } from "./fetch"
import { OpenAIChatCompletion } from "./openai"
import { runtimeHost } from "./host"

const listModels: ListModelsFunction = async (cfg, options) => {
try {
// Create a fetch instance to make HTTP requests
const { base } = cfg
const subscriptionId = process.env.AZURE_OPENAI_SUBSCRIPTION_ID
let resourceGroupName = process.env.AZURE_OPENAI_RESOURCE_GROUP
const accountName = /^https:\/\/([^\.]+)\./.exec(base)[1]

if (!subscriptionId || !accountName)
throw new Error("Missing subscriptionId, or accountName")
const token = await runtimeHost.azureManagementToken.token(
"default",
options
)
if (token.error) throw new Error(errorMessage(token.error))

const fetch = await createFetch({ retries: 0, ...options })
const get = async (url: string) => {
const res = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${token.token.token}`,
},
})
if (res.status !== 200)
return {
ok: false,
status: res.status,
error: serializeError(res.statusText),
}
return await res.json()
}

if (!resourceGroupName) {
const resources: {
value: {
id: string
name: string
type: "OpenAI"
}[]
} = await get(
`https://management.azure.com/subscriptions/${subscriptionId}/resources?api-version=2021-04-01`
)
const resource = resources.value.find((r) => r.name === accountName)
resourceGroupName = /\/resourceGroups\/([^\/]+)\/providers\//.exec(
resource?.id
)[1]
if (!resourceGroupName) throw new Error("Resource group not found")
}

// https://learn.microsoft.com/en-us/rest/api/aiservices/accountmanagement/deployments/list-skus?view=rest-aiservices-accountmanagement-2024-10-01&tabs=HTTP
const deployments: {
value: {
id: string
name: string
properties: {
model: {
format: string
name: string
version: string
}
}
}[]
} = await get(
`https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.CognitiveServices/accounts/${accountName}/deployments/?api-version=${AZURE_MANAGEMENT_API_VERSION}`
)
return {
ok: true,
models: deployments.value.map((model) => ({
id: model.name,
family: model.properties.model.name,
details: `${model.properties.model.format} ${model.properties.model.name}`,
url: `https://ai.azure.com/resource/deployments/${encodeURIComponent(model.id)}`,
version: model.properties.model.version,
})),
}
} catch (e) {
return { ok: false, error: serializeError(e) }
}
}

// Define the Ollama model with its completion handler and model listing function
export const AzureOpenAIModel = Object.freeze<LanguageModel>({
id: MODEL_PROVIDER_AZURE_OPENAI,
completer: OpenAIChatCompletion,
listModels,
})
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ class AzureTokenResolverImpl implements AzureTokenResolver {
if (this._token || this._error)
return { token: this._token, error: this._error }
if (!this._resolver) {
logVerbose(`${this.name}: creating token`)
const scope = await runtimeHost.readSecret(this.envName)
const scopes = scope ? scope.split(",") : this.scopes
this._resolver = createAzureToken(
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ export const MAX_TOOL_CALLS = 10000
// https://github.com/Azure/azure-rest-api-specs/blob/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference/stable/2024-02-01/inference.yaml
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
export const AZURE_OPENAI_API_VERSION = "2024-06-01"
export const AZURE_MANAGEMENT_API_VERSION = "2024-10-01"
export const AZURE_COGNITIVE_SERVICES_TOKEN_SCOPES = Object.freeze([
"https://cognitiveservices.azure.com/.default",
])
export const AZURE_AI_INFERENCE_VERSION = "2024-08-01-preview"
export const AZURE_AI_INFERENCE_TOKEN_SCOPES = Object.freeze([
"https://ml.azure.com/.default",
])
export const AZURE_MANAGEMENT_TOKEN_SCOPES = Object.freeze([
"https://management.azure.com/.default",
])
export const AZURE_TOKEN_EXPIRATION = 59 * 60_000 // 59 minutes

export const TOOL_URL = "https://microsoft.github.io/genaiscript"
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,10 @@ export interface RuntimeHost extends Host {
project: Project
workspace: Omit<WorkspaceFileSystem, "grep" | "writeCached">

azureToken: AzureTokenResolver
azureServerlessToken: AzureTokenResolver
microsoftGraphToken: AzureTokenResolver
azureToken?: AzureTokenResolver
azureServerlessToken?: AzureTokenResolver
azureManagementToken?: AzureTokenResolver
microsoftGraphToken?: AzureTokenResolver

modelAliases: Readonly<ModelConfigurations>
clientLanguageModel?: LanguageModel
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/llms.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
"description": "Azure OpenAI API key. **You do NOT need this if you are using Microsoft Entra ID.",
"secret": true
},
"AZURE_OPENAI_SUBSCRIPTION_ID": {
"description": "Azure OpenAI subscription ID to list available deployments (Microsoft Entra only)."
},
"AZURE_OPENAI_API_VERSION": {
"description": "Azure OpenAI API version."
},
"AZURE_OPENAI_API_CREDENTIALS": {
"description": "Azure OpenAI API credentials type. Leave as 'default' unless you have a special Azure setup.",
"enum": [
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/lm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
MODEL_PROVIDER_TRANSFORMERS,
MODEL_PROVIDERS,
MODEL_WHISPERASR_PROVIDER,
MODEL_PROVIDER_AZURE_OPENAI,
} from "./constants"
import { runtimeHost } from "./host"
import { OllamaModel } from "./ollama"
Expand All @@ -20,13 +21,15 @@ import { TransformersModel } from "./transformers"
import { GitHubModel } from "./github"
import { LMStudioModel } from "./lmstudio"
import { WhiserAsrModel } from "./whisperasr"
import { AzureOpenAIModel } from "./azureopenai"

export function resolveLanguageModel(provider: string): LanguageModel {
if (provider === MODEL_PROVIDER_GITHUB_COPILOT_CHAT) {
const m = runtimeHost.clientLanguageModel
if (!m) throw new Error("Github Copilot Chat Models not available")
return m
}
if (provider === MODEL_PROVIDER_AZURE_OPENAI) return AzureOpenAIModel
if (provider === MODEL_PROVIDER_GITHUB) return GitHubModel
if (provider === MODEL_PROVIDER_OLLAMA) return OllamaModel
if (provider === MODEL_PROVIDER_AICI) return AICIModel
Expand Down
Loading
Loading