diff --git a/.github/workflows/sdk_v2/Foundry Local Core SDK Build.yml b/.github/workflows/sdk_v2/Foundry Local Core SDK Build.yml new file mode 100644 index 0000000..d73f3d3 --- /dev/null +++ b/.github/workflows/sdk_v2/Foundry Local Core SDK Build.yml @@ -0,0 +1,61 @@ +################################################################################# +# OneBranch Pipelines # +# This pipeline was created by EasyStart from a sample located at: # +# https://aka.ms/obpipelines/easystart/samples # +# Documentation: https://aka.ms/obpipelines # +# Yaml Schema: https://aka.ms/obpipelines/yaml/schema # +# Retail Tasks: https://aka.ms/obpipelines/tasks # +# Support: https://aka.ms/onebranchsup # +################################################################################# + +trigger: none # https://aka.ms/obpipelines/triggers + +parameters: # parameters are shown up in ADO UI in a build queue time +- name: 'debug' + displayName: 'Enable debug output' + type: boolean + default: false +- name: 'isRelease' + displayName: 'Release build' + type: boolean + default: false + +variables: + CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning + BUILD_ID: $[counter(variables['Build.SourceBranchName'], 0)] # branch-specific counter that resets for each new branch + DEBIAN_FRONTEND: noninteractive + LinuxContainerImage: 'mcr.microsoft.com/onebranch/azurelinux/build:3.0' # https://eng.ms/docs/products/onebranch/infrastructureandimages/containerimages/linuximages/marinerazurelinux/azurelinux + WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' # https://aka.ms/obpipelines/containers + VERSION: '0.9.0.$(BUILD_ID)' + PRERELEASE_IDENTIFIER: 'dev' + +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + +extends: + template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates + parameters: + git: + fetchDepth: 1 + featureFlags: + EnableCDPxPAT: false + WindowsHostVersion: '1ESWindows2022' + globalSdl: + tsa: + enabled: false + stages: + - template: templates/stages-build-cs.yml + parameters: + version: $(VERSION) + isRelease: ${{ parameters.isRelease }} + prereleaseIdentifier: $(PRERELEASE_IDENTIFIER) + + - template: templates/stages-build-js.yml + parameters: + version: $(VERSION) + isRelease: ${{ parameters.isRelease }} + prereleaseIdentifier: $(PRERELEASE_IDENTIFIER) \ No newline at end of file diff --git a/.github/workflows/sdk_v2/Foundry Local Core SDK WinML Build.yml b/.github/workflows/sdk_v2/Foundry Local Core SDK WinML Build.yml new file mode 100644 index 0000000..dbdefed --- /dev/null +++ b/.github/workflows/sdk_v2/Foundry Local Core SDK WinML Build.yml @@ -0,0 +1,63 @@ +################################################################################# +# OneBranch Pipelines # +# This pipeline was created by EasyStart from a sample located at: # +# https://aka.ms/obpipelines/easystart/samples # +# Documentation: https://aka.ms/obpipelines # +# Yaml Schema: https://aka.ms/obpipelines/yaml/schema # +# Retail Tasks: https://aka.ms/obpipelines/tasks # +# Support: https://aka.ms/onebranchsup # +################################################################################# + +trigger: none # https://aka.ms/obpipelines/triggers + +parameters: # parameters are shown up in ADO UI in a build queue time +- name: 'debug' + displayName: 'Enable debug output' + type: boolean + default: false +- name: 'isRelease' + displayName: 'Release build' + type: boolean + default: false + +variables: + CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning + BUILD_ID: $[counter(variables['Build.SourceBranchName'], 0)] # branch-specific counter that resets for each new branch + DEBIAN_FRONTEND: noninteractive + LinuxContainerImage: 'mcr.microsoft.com/onebranch/azurelinux/build:3.0' # https://eng.ms/docs/products/onebranch/infrastructureandimages/containerimages/linuximages/marinerazurelinux/azurelinux + WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' # https://aka.ms/obpipelines/containers + VERSION: '0.9.0.$(BUILD_ID)' + PRERELEASE_IDENTIFIER: 'dev' + +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + +extends: + template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates + parameters: + git: + fetchDepth: 1 + featureFlags: + EnableCDPxPAT: false + WindowsHostVersion: '1ESWindows2022' + globalSdl: + tsa: + enabled: false + stages: + - template: templates/stages-build-cs.yml + parameters: + version: $(VERSION) + isRelease: ${{ parameters.isRelease }} + prereleaseIdentifier: $(PRERELEASE_IDENTIFIER) + isWinML: true + + - template: templates/stages-build-js.yml + parameters: + version: $(VERSION) + isRelease: ${{ parameters.isRelease }} + prereleaseIdentifier: $(PRERELEASE_IDENTIFIER) + isWinML: true \ No newline at end of file diff --git a/.github/workflows/sdk_v2/templates/sdk-version.yml b/.github/workflows/sdk_v2/templates/sdk-version.yml new file mode 100644 index 0000000..15a2f1b --- /dev/null +++ b/.github/workflows/sdk_v2/templates/sdk-version.yml @@ -0,0 +1,41 @@ +parameters: + - name: version + type: string + - name: isRelease + type: boolean + default: false + - name: prereleaseIdentifier + type: string + default: '' + +steps: + - task: PowerShell@2 + displayName: 'Generate Custom Version' + inputs: + targetType: 'inline' + script: | + $baseVersion = "${{ parameters.version }}" + $isRelease = [System.Convert]::ToBoolean("${{ parameters.isRelease }}") + $prereleaseId = "${{ parameters.prereleaseIdentifier }}".Trim() + + if ($isRelease -and [string]::IsNullOrEmpty($prereleaseId)) { + # Official release: 0.0.1 + $customVersion = $baseVersion + Write-Host "Official release version: $customVersion" + } elseif ($isRelease -and -not [string]::IsNullOrEmpty($prereleaseId)) { + # Prerelease: 0.0.1-beta.timestamp.commit + $timestamp = (Get-Date).ToUniversalTime().ToString("yyyyMMddTHHmmss") + $shortCommitId = "$(Build.SourceVersion)".Substring(0, 8) + $customVersion = "$baseVersion-$prereleaseId.$timestamp.$shortCommitId" + Write-Host "Prerelease version ($prereleaseId): $customVersion" + } else { + # Dev build: 0.0.1-timestamp.commit + $timestamp = (Get-Date).ToUniversalTime().ToString("yyyyMMddTHHmmss") + $shortCommitId = "$(Build.SourceVersion)".Substring(0, 8) + $customVersion = "$baseVersion-$timestamp.$shortCommitId" + Write-Host "Development version: $customVersion" + } + + Write-Host "Generated custom version: $customVersion" + Write-Host "##vso[task.setvariable variable=ProjectVersion]$customVersion" + # vso[task.setvariable...] sets ProjectVersion as a pipeline variable to be used in subsequent tasks \ No newline at end of file diff --git a/.github/workflows/sdk_v2/templates/stages-build-cs.yml b/.github/workflows/sdk_v2/templates/stages-build-cs.yml new file mode 100644 index 0000000..6da2ac9 --- /dev/null +++ b/.github/workflows/sdk_v2/templates/stages-build-cs.yml @@ -0,0 +1,172 @@ +parameters: + - name: version + type: string + - name: isRelease + type: boolean + default: false + - name: prereleaseIdentifier + type: string + default: '' + - name: isWinML + type: boolean + default: false + +stages: + - stage: cs + jobs: + - job: cs + pool: + type: windows + vmImage: 'windows-latest' + variables: + ob_outputDirectory: '$(Build.SourcesDirectory)/out' + buildConfiguration: 'Release' + steps: + - checkout: self + clean: true + + - task: UseDotNet@2 + displayName: 'Use .NET 9 SDK' + inputs: + packageType: 'sdk' + version: '9.0.x' + installationPath: '$(Agent.ToolsDirectory)\dotnet' + + - template: sdk-version.yml + parameters: + version: ${{ parameters.version }} + isRelease: ${{ parameters.isRelease }} + prereleaseIdentifier: ${{ parameters.prereleaseIdentifier }} + + - task: DotNetCoreCLI@2 + displayName: 'Restore dependencies' + inputs: + command: 'restore' + projects: $(Build.SourcesDirectory)\foundry-local-sdk\sdk_v2\cs\src\Microsoft.AI.Foundry.Local.csproj + feedsToUse: 'config' + nugetConfigPath: '$(Build.SourcesDirectory)\foundry-local-sdk\sdk_v2\cs\NuGet.config' + restoreArguments: '/p:UseWinML=${{ parameters.isWinML }}' + # No TargetFramework override: we want to restore for all frameworks in the project (net8.0 is the minimum supported) + + - task: DotNetCoreCLI@2 + displayName: 'Build solution' + inputs: + command: 'build' + projects: $(Build.SourcesDirectory)\foundry-local-sdk\sdk_v2\cs\src\Microsoft.AI.Foundry.Local.csproj + arguments: '--no-restore --configuration $(buildConfiguration) /p:UseWinML=${{ parameters.isWinML }}' + # No TargetFramework override: we want to build for all frameworks in the project (net8.0 is the minimum supported) + + - checkout: git://windows.ai.toolkit/test-data-shared + displayName: 'Checkout test-data-shared for Chat/Audio Client Tests' + lfs: true + persistCredentials: true + + - task: PowerShell@2 + displayName: 'Checkout specific commit in test-data-shared' + inputs: + targetType: 'inline' + workingDirectory: '$(Build.SourcesDirectory)/test-data-shared' + script: | + Write-Host "Current directory: $(Get-Location)" + git checkout 231f820fe285145b7ea4a449b112c1228ce66a41 + if ($LASTEXITCODE -ne 0) { + Write-Error "Git checkout failed." + exit 1 + } + Write-Host "`nDirectory contents:" + Get-ChildItem -Recurse -Depth 2 | ForEach-Object { Write-Host " $($_.FullName)" } + + - task: DotNetCoreCLI@2 + displayName: 'Run Foundry Local Core tests' + inputs: + command: 'test' + projects: $(Build.SourcesDirectory)\foundry-local-sdk\sdk_v2\cs\test\FoundryLocal.Tests\Microsoft.AI.Foundry.Local.Tests.csproj + arguments: '--verbosity normal /p:UseWinML=${{ parameters.isWinML }}' + workingDirectory: '$(Build.SourcesDirectory)' + + # Sign DLLs after building but before packing + - task: PowerShell@2 + displayName: 'Find target framework directory' + inputs: + targetType: 'inline' + script: | + $basePath = "$(Build.SourcesDirectory)\foundry-local-sdk\sdk_v2\cs\src\bin\$(buildConfiguration)" + Write-Host "Searching in base path: $basePath" + Write-Host "Directory contents:" + Get-ChildItem -Path $basePath | ForEach-Object { Write-Host " $($_.Name)" } + + $targetDir = Get-ChildItem -Path $basePath -Directory -Filter "net8.0*" | Select-Object -First 1 + Write-Host "Target framework name: $($targetDir.Name)" + Write-Host "##vso[task.setvariable variable=TargetFramework]$($targetDir.Name)" + + # NOTE: Manual pack using PowerShell with --no-build instead of DotNetCoreCLI@2 task + # + # When UseWinML=true, the project's TargetFramework changes from 'net8.0' to 'net8.0-windows10.0.26100.0' + # causing build outputs to be placed in a different directory (e.g., bin/Release/net8.0-windows10.0.26100.0/). + # + # The DotNetCoreCLI@2 pack task with various parameter combinations failed to locate the signed DLLs + # in the WinML target framework directory. Using 'dotnet pack --no-build' directly with /p:UseWinML + # allows the pack operation to correctly resolve the output path based on the project's UseWinML + # condition evaluation, ensuring it finds the signed binaries in the correct location. + # + # This approach works for both standard (net8.0) and WinML (net8.0-windows10.0.26100.0) builds + # by letting the project naturally evaluate UseWinML and determine the correct target framework path. + - task: PowerShell@2 + displayName: 'Pack NuGet package' + inputs: + targetType: 'inline' + script: | + $projectPath = "$(Build.SourcesDirectory)\foundry-local-sdk\sdk_v2\cs\src\Microsoft.AI.Foundry.Local.csproj" + $outputDir = "$(Build.SourcesDirectory)\foundry-local-sdk\sdk_v2\cs\bin" + $version = "$(ProjectVersion)" + $config = "$(buildConfiguration)" + $useWinML = "${{ parameters.isWinML }}" + + Write-Host "Packing project: $projectPath" + Write-Host "Output directory: $outputDir" + Write-Host "Version: $version" + Write-Host "Configuration: $config" + Write-Host "UseWinML: $useWinML" + + & dotnet pack $projectPath --no-build --configuration $config --output $outputDir /p:PackageVersion=$version /p:UseWinML=$useWinML /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg --verbosity normal + + if ($LASTEXITCODE -ne 0) { + Write-Error "dotnet pack failed with exit code $LASTEXITCODE" + exit $LASTEXITCODE + } + + Write-Host "Pack completed successfully" + Write-Host "Generated packages:" + Get-ChildItem -Path $outputDir -Filter "*.nupkg" | ForEach-Object { Write-Host " $($_.Name)" } + Get-ChildItem -Path $outputDir -Filter "*.snupkg" | ForEach-Object { Write-Host " $($_.Name)" } + + - task: CopyFiles@2 + displayName: 'Copy signed NuGet package files' + inputs: + SourceFolder: '$(Build.SourcesDirectory)\foundry-local-sdk\sdk_v2\cs\bin' + Contents: | + *.nupkg + *.snupkg + TargetFolder: '$(ob_outputDirectory)' + + # Optional + - task: PowerShell@2 + displayName: 'Verify NuGet package signatures' + inputs: + targetType: 'inline' + script: | + $packages = Get-ChildItem -Path "$(ob_outputDirectory)" -Filter "*.nupkg" + foreach ($package in $packages) { + Write-Host "Verifying signature for: $($package.FullName)" + try { + nuget verify -signature "$($package.FullName)" + if ($LASTEXITCODE -eq 0) { + Write-Host "✓ Signature verified successfully for: $($package.Name)" -ForegroundColor Green + } else { + Write-Warning "⚠ Signature verification returned non-zero exit code for: $($package.Name)" + } + } + catch { + Write-Warning "⚠ Could not verify signature for: $($package.Name) - Error: $_" + } + } \ No newline at end of file diff --git a/.github/workflows/sdk_v2/templates/stages-build-js.yml b/.github/workflows/sdk_v2/templates/stages-build-js.yml new file mode 100644 index 0000000..e050fdd --- /dev/null +++ b/.github/workflows/sdk_v2/templates/stages-build-js.yml @@ -0,0 +1,165 @@ +parameters: + - name: version + type: string + - name: isRelease + type: boolean + default: false + - name: prereleaseIdentifier + type: string + default: '' + - name: isWinML + type: boolean + default: false + +stages: + - stage: js + jobs: + - job: js + pool: + type: windows + vmImage: 'windows-latest' + variables: + ob_outputDirectory: '$(Build.SourcesDirectory)/out' + steps: + - checkout: self + clean: true + + - task: NodeTool@0 + inputs: + versionSpec: '20.x' + displayName: 'Install Node.js' + + - template: sdk-version.yml + parameters: + version: ${{ parameters.version }} + isRelease: ${{ parameters.isRelease }} + prereleaseIdentifier: ${{ parameters.prereleaseIdentifier }} + + - ${{ if eq(parameters.isRelease, true) }}: + - task: PowerShell@2 + displayName: 'Format version for JS' + inputs: + targetType: 'inline' + script: | + # Release: 0.9.0.41 -> 0.9.0-41 + $version = "$(ProjectVersion)" + $versionParts = $version -split '\.' + $baseVersion = ($versionParts[0..2]) -join '.' + $buildNumber = $versionParts[3] + $version = "$baseVersion-$buildNumber" + Write-Host "Modified version for JS: $version" + Write-Host "##vso[task.setvariable variable=ProjectVersion]$version" + + - ${{ if eq(parameters.isRelease, false) }}: + - task: PowerShell@2 + displayName: 'Format version for JS' + inputs: + targetType: 'inline' + script: | + # Dev build: 0.9.0.43-timestamp.commit -> 0.9.0-43.dev.timestamp.commit + $prereleaseId = "${{ parameters.prereleaseIdentifier }}".Trim() + $version = "$(ProjectVersion)" + $parts = $version -split '-', 2 + $versionParts = $parts[0] -split '\.' + $baseVersion = ($versionParts[0..2]) -join '.' + $buildNumber = $versionParts[3] + $prefix = if ([string]::IsNullOrEmpty($prereleaseId)) { "dev" } else { $prereleaseId } + $version = "$baseVersion-$buildNumber.$prefix.$($parts[1])" + Write-Host "Modified version for JS: $version" + Write-Host "##vso[task.setvariable variable=ProjectVersion]$version" + + - checkout: git://windows.ai.toolkit/test-data-shared + displayName: 'Checkout test-data-shared' + lfs: true + persistCredentials: true + + - task: PowerShell@2 + displayName: 'Checkout specific commit in test-data-shared' + inputs: + targetType: 'inline' + workingDirectory: '$(Build.SourcesDirectory)/test-data-shared' + script: | + git checkout 231f820fe285145b7ea4a449b112c1228ce66a41 + if ($LASTEXITCODE -ne 0) { exit 1 } + + - task: NuGetToolInstaller@1 + displayName: 'Install NuGet tool' + + # resolves network proxy issues when accessing registry.npmjs.org + - task: PowerShell@2 + displayName: 'Create .npmrc for Azure Artifacts' + inputs: + targetType: 'inline' + workingDirectory: '$(Build.SourcesDirectory)/foundry-local-sdk/sdk_v2/js' + script: | + # Create .npmrc dynamically to avoid breaking local development + $npmrcContent = @" + registry=https://pkgs.dev.azure.com/microsoft/windows.ai.toolkit/_packaging/Neutron/npm/registry/ + always-auth=true + "@ + Set-Content -Path ".npmrc" -Value $npmrcContent + Write-Host "Created .npmrc file for Azure Artifacts registry" + + - ${{ if eq(parameters.isWinML, true) }}: + - task: Npm@1 + displayName: 'npm install' + inputs: + command: 'custom' + workingDir: '$(Build.SourcesDirectory)/foundry-local-sdk/sdk_v2/js' + customCommand: 'install --winml' + + - ${{ if ne(parameters.isWinML, true) }}: + - task: Npm@1 + displayName: 'npm install' + inputs: + command: 'install' + workingDir: '$(Build.SourcesDirectory)/foundry-local-sdk/sdk_v2/js' + + - task: Npm@1 + displayName: 'npm version' + inputs: + command: 'custom' + workingDir: '$(Build.SourcesDirectory)/foundry-local-sdk/sdk_v2/js' + customCommand: 'version $(ProjectVersion) --no-git-tag-version --allow-same-version' + + - task: Npm@1 + displayName: 'npm test' + inputs: + command: 'custom' + workingDir: '$(Build.SourcesDirectory)/foundry-local-sdk/sdk_v2/js' + customCommand: 'test' + + - task: Npm@1 + displayName: 'npm build' + inputs: + command: 'custom' + workingDir: '$(Build.SourcesDirectory)/foundry-local-sdk/sdk_v2/js' + customCommand: 'run build' + + - task: Npm@1 + displayName: 'npm pack' + inputs: + command: 'custom' + workingDir: '$(Build.SourcesDirectory)/foundry-local-sdk/sdk_v2/js' + customCommand: 'pack' + + - task: PowerShell@2 + displayName: 'Rename WinML artifact' + condition: and(succeeded(), eq('${{ parameters.isWinML }}', true)) + inputs: + targetType: 'inline' + workingDirectory: '$(Build.SourcesDirectory)/foundry-local-sdk/sdk_v2/js' + script: | + $tgz = Get-ChildItem *.tgz | Select-Object -First 1 + if ($tgz) { + $newName = $tgz.Name -replace '^foundry-local-sdk-', 'foundry-local-sdk-winml-' + Rename-Item -Path $tgz.FullName -NewName $newName + Write-Host "Renamed $($tgz.Name) to $newName" + } + + - task: CopyFiles@2 + displayName: 'Copy JS SDK artifacts' + inputs: + SourceFolder: '$(Build.SourcesDirectory)/foundry-local-sdk/sdk_v2/js' + Contents: '*.tgz' + TargetFolder: '$(ob_outputDirectory)/js-sdk' diff --git a/sdk_v2/cs/.editorconfig b/sdk_v2/cs/.editorconfig new file mode 100644 index 0000000..a14a741 --- /dev/null +++ b/sdk_v2/cs/.editorconfig @@ -0,0 +1,349 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Documentation files +[*.md] +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +# Code files +[*.{cs,csx,vb,vbx,h,cpp}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom +trim_trailing_whitespace = true + +# Adds guidelines for the EditorGuidelines VS extension. See https://github.com/pharring/EditorGuidelines. +guidelines = 80, 120 + +file_header_template = --------------------------------------------------------------------------------------------------------------------\n\n Copyright (c) Microsoft. All rights reserved.\n\n-------------------------------------------------------------------------------------------------------------------- + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# YAML files +[*.{yml,yaml}] +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +# JSON files +[*.json] +indent_size = 2 + +# Shell script files +[*.sh] +end_of_line = lf +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups=true +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:error +dotnet_style_qualification_for_property = false:error +dotnet_style_qualification_for_method = false:error +dotnet_style_qualification_for_event = false:error + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:error +dotnet_style_collection_initializer = true:error +dotnet_style_coalesce_expression = true:error +dotnet_style_null_propagation = true:error +dotnet_style_explicit_tuple_names = true:error + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = error +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = error +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Constants are PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = error +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style + +dotnet_naming_symbols.constants.applicable_kinds = field +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static fields are camelCase and start with s_ +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_field_style.capitalization = camel_case +# dotnet_naming_style.static_field_style.required_prefix = s_ + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = error +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = error +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = error +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = error +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# CSharp code style settings: +# IDE0045: Convert to conditional expression +dotnet_diagnostic.IDE0045.severity = suggestion + +[*.cs] +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Prefer "var" nowhere +csharp_style_var_for_built_in_types = true:error +csharp_style_var_when_type_is_apparent = true:error +csharp_style_var_elsewhere = false:error +csharp_style_implicit_object_creation_when_type_is_apparent = true:error + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Code-block preferences +csharp_style_namespace_declarations = file_scoped:error + +# Unused value expressions +csharp_style_unused_value_expression_statement_preference = discard_variable:warning + +# 'using' directive preferences +csharp_using_directive_placement = inside_namespace:error + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_inlined_variable_declaration = true:error +csharp_style_throw_expression = true:error +csharp_style_conditional_delegate_call = true:error + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Spacing +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_around_binary_operators = before_and_after +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false + +# Blocks are allowed +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +# Build severity configuration +# Everything above essentially configures IDE behavior and will not reflect in the build. +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options + +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = warning + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = suggestion + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = silent + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = silent + +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = silent + +# SA1402: File may only contain a single type +dotnet_diagnostic.SA1402.severity = none + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none + +# SA1649: File name should match first type name +dotnet_diagnostic.SA1649.severity = error + +# SA1309: Field names should not begin with underscore +dotnet_diagnostic.SA1309.severity = none + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = silent + +# CA1707: Identifiers should not contain underscores +dotnet_diagnostic.CA1707.severity = silent + +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = suggestion + +# CA1822: Mark members as static +dotnet_diagnostic.CA1822.severity = suggestion + +# CA1815: Override equals and operator equals on value types +dotnet_diagnostic.CA1815.severity = suggestion + +# SA1201: Elements should appear in the correct order +dotnet_diagnostic.SA1201.severity = silent + +# SA1602: Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = suggestion + +# SA1118: Parameter should not span multiple lines +dotnet_diagnostic.SA1118.severity = suggestion + +# CA2201: Do not raise reserved exception types +dotnet_diagnostic.CA2201.severity = suggestion + +# CA1050: Declare types in namespaces +dotnet_diagnostic.CA1050.severity = suggestion + +# IDE0005: Remove unnecessary import +dotnet_diagnostic.IDE0005.severity = error + +# IDE1006: Naming Styles +dotnet_diagnostic.IDE1006.severity = error + +# IDE0008: Use explicit type +dotnet_diagnostic.IDE0008.severity = silent + +# IDE0090: Use 'new(...)' +dotnet_diagnostic.IDE0090.severity = error + +# IDE0072: Add missing cases +## Suppressing this particular case due to issues in the analyzer's understanding of pattern matching. +dotnet_diagnostic.IDE0072.severity = suggestion + +# CA2000: Dispose objects before losing scope +dotnet_diagnostic.CA2000.severity = warning + +# IDE0046: Convert to conditional expression +dotnet_diagnostic.IDE0046.severity = silent + +# IDE0050: Convert to tuple +dotnet_diagnostic.IDE0050.severity = suggestion + +# IDE0066: Convert switch statement to expression +dotnet_diagnostic.IDE0066.severity = suggestion + +# IDE0130: Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = silent + +# IDE0161: Convert to file-scoped namespace +dotnet_diagnostic.IDE0161.severity = error + +# IDE0058: Expression value is never used +dotnet_diagnostic.IDE0058.severity = none + +# VSTHRD111: Use ConfigureAwait(bool) +dotnet_diagnostic.VSTHRD111.severity = suggestion + +# IDE0042: Deconstruct variable declaration +dotnet_diagnostic.IDE0042.severity = suggestion + +# IDE0039: Use local function +dotnet_diagnostic.IDE0039.severity = suggestion + +# CA1848: Use the LoggerMessage delegates +dotnet_diagnostic.CA1848.severity = suggestion + +# CA2254: Template should be a static expression +dotnet_diagnostic.CA2254.severity = suggestion + +# IDE0290: Use primary constructor +dotnet_diagnostic.IDE0290.severity = suggestion + +# CA1711: Identifiers should not have incorrect suffix +dotnet_diagnostic.CA1711.severity = suggestion + +# IDE0305: Collection initialization can be simplified +dotnet_diagnostic.IDE0305.severity = suggestion + +# Unused value expressions +csharp_style_unused_value_expression_statement_preference = unused_local_variable:none diff --git a/sdk_v2/cs/.gitignore b/sdk_v2/cs/.gitignore new file mode 100644 index 0000000..b3ed4ac --- /dev/null +++ b/sdk_v2/cs/.gitignore @@ -0,0 +1,295 @@ +# Custom +.dotnet/ +artifacts/ +.build/ +.vscode/ + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Perfview trace +*.etl.zip +*.orig +/src/BenchmarksDriver/results.md +*.trace.zip +/src/BenchmarksDriver/*.zip +eventpipe.netperf +*.netperf +*.bench.json +BenchmarkDotNet.Artifacts/ \ No newline at end of file diff --git a/sdk_v2/cs/LICENSE.txt b/sdk_v2/cs/LICENSE.txt new file mode 100644 index 0000000..48bc6bb --- /dev/null +++ b/sdk_v2/cs/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk_v2/cs/Microsoft.AI.Foundry.Local.SDK.sln b/sdk_v2/cs/Microsoft.AI.Foundry.Local.SDK.sln new file mode 100644 index 0000000..2958f0d --- /dev/null +++ b/sdk_v2/cs/Microsoft.AI.Foundry.Local.SDK.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Foundry.Local", "src\Microsoft.AI.Foundry.Local.csproj", "{247537D6-CBBA-C748-B91D-AA7B236563B4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.Foundry.Local.Tests", "test\FoundryLocal.Tests\Microsoft.AI.Foundry.Local.Tests.csproj", "{CD75C56B-0EB9-41F4-BEE0-9D7C674894CC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {247537D6-CBBA-C748-B91D-AA7B236563B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {247537D6-CBBA-C748-B91D-AA7B236563B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {247537D6-CBBA-C748-B91D-AA7B236563B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {247537D6-CBBA-C748-B91D-AA7B236563B4}.Release|Any CPU.Build.0 = Release|Any CPU + {CD75C56B-0EB9-41F4-BEE0-9D7C674894CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD75C56B-0EB9-41F4-BEE0-9D7C674894CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD75C56B-0EB9-41F4-BEE0-9D7C674894CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD75C56B-0EB9-41F4-BEE0-9D7C674894CC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {247537D6-CBBA-C748-B91D-AA7B236563B4} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {CD75C56B-0EB9-41F4-BEE0-9D7C674894CC} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0138DEC3-F200-43EC-A1A2-6FD8F2C609CB} + EndGlobalSection +EndGlobal diff --git a/sdk_v2/cs/NuGet.config b/sdk_v2/cs/NuGet.config new file mode 100644 index 0000000..ef684cc --- /dev/null +++ b/sdk_v2/cs/NuGet.config @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/sdk_v2/cs/README.md b/sdk_v2/cs/README.md new file mode 100644 index 0000000..9c15b24 --- /dev/null +++ b/sdk_v2/cs/README.md @@ -0,0 +1,74 @@ +# Foundry Local C# SDK + +## Installation + +To use the Foundry Local C# SDK, you need to install the NuGet package: + +```bash +dotnet add package Microsoft.AI.Foundry.Local +``` + +### Building from source +To build the SDK, run the following command in your terminal: + +```bash +cd sdk/cs +dotnet build +``` + +You can also load [FoundryLocal.sln](./FoundryLocal.sln) in Visual Studio 2022 or VSCode. Update your +`nuget.config` to include the local path to the generated NuGet package: + +```xml + + + + + +``` + +Then, install the package using the following command: + +```bash +dotnet add package FoundryLocal --source foundry-local +``` + +## Usage + +> [!NOTE] +> For this example, you'll need the OpenAI Nuget package installed as well: +> ```bash +> dotnet add package OpenAI +> ``` + +```csharp +using Microsoft.AI.Foundry.Local; +using OpenAI; +using OpenAI.Chat; +using System.ClientModel; +using System.Diagnostics.Metrics; + +var alias = "phi-3.5-mini"; + +var manager = await FoundryLocalManager.StartModelAsync(aliasOrModelId: alias); + +var model = await manager.GetModelInfoAsync(aliasOrModelId: alias); +ApiKeyCredential key = new ApiKeyCredential(manager.ApiKey); +OpenAIClient client = new OpenAIClient(key, new OpenAIClientOptions +{ + Endpoint = manager.Endpoint +}); + +var chatClient = client.GetChatClient(model?.ModelId); + +var completionUpdates = chatClient.CompleteChatStreaming("Why is the sky blue'"); + +Console.Write($"[ASSISTANT]: "); +foreach (var completionUpdate in completionUpdates) +{ + if (completionUpdate.ContentUpdate.Count > 0) + { + Console.Write(completionUpdate.ContentUpdate[0].Text); + } +} +``` diff --git a/sdk_v2/cs/src/AssemblyInfo.cs b/sdk_v2/cs/src/AssemblyInfo.cs new file mode 100644 index 0000000..9bebe71 --- /dev/null +++ b/sdk_v2/cs/src/AssemblyInfo.cs @@ -0,0 +1,10 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AI.Foundry.Local.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for Mock of ICoreInterop diff --git a/sdk_v2/cs/src/Catalog.cs b/sdk_v2/cs/src/Catalog.cs new file mode 100644 index 0000000..eb9ba0d --- /dev/null +++ b/sdk_v2/cs/src/Catalog.cs @@ -0,0 +1,200 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; + +using Microsoft.AI.Foundry.Local.Detail; +using Microsoft.Extensions.Logging; + +internal sealed class Catalog : ICatalog, IDisposable +{ + private readonly Dictionary _modelAliasToModel = new(); + private readonly Dictionary _modelIdToModelVariant = new(); + private DateTime _lastFetch; + + private readonly IModelLoadManager _modelLoadManager; + private readonly ICoreInterop _coreInterop; + private readonly ILogger _logger; + private readonly AsyncLock _lock = new(); + + public string Name { get; init; } + + private Catalog(IModelLoadManager modelLoadManager, ICoreInterop coreInterop, ILogger logger) + { + _modelLoadManager = modelLoadManager; + _coreInterop = coreInterop; + _logger = logger; + + _lastFetch = DateTime.MinValue; + + CoreInteropRequest? input = null; + var response = coreInterop.ExecuteCommand("get_catalog_name", input); + if (response.Error != null) + { + throw new FoundryLocalException($"Error getting catalog name: {response.Error}", _logger); + } + + Name = response.Data!; + } + + internal static async Task CreateAsync(IModelLoadManager modelManager, ICoreInterop coreInterop, + ILogger logger, CancellationToken? ct = null) + { + var catalog = new Catalog(modelManager, coreInterop, logger); + await catalog.UpdateModels(ct).ConfigureAwait(false); + return catalog; + } + + public async Task> ListModelsAsync(CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => ListModelsImplAsync(ct), + "Error listing models.", _logger).ConfigureAwait(false); + } + + public async Task> GetCachedModelsAsync(CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => GetCachedModelsImplAsync(ct), + "Error getting cached models.", _logger).ConfigureAwait(false); + } + + public async Task> GetLoadedModelsAsync(CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => GetLoadedModelsImplAsync(ct), + "Error getting loaded models.", _logger).ConfigureAwait(false); + } + + public async Task GetModelAsync(string modelAlias, CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => GetModelImplAsync(modelAlias, ct), + $"Error getting model with alias '{modelAlias}'.", _logger) + .ConfigureAwait(false); + } + + public async Task GetModelVariantAsync(string modelId, CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => GetModelVariantImplAsync(modelId, ct), + $"Error getting model variant with ID '{modelId}'.", _logger) + .ConfigureAwait(false); + } + + private async Task> ListModelsImplAsync(CancellationToken? ct = null) + { + await UpdateModels(ct).ConfigureAwait(false); + + using var disposable = await _lock.LockAsync().ConfigureAwait(false); + return _modelAliasToModel.Values.OrderBy(m => m.Alias).ToList(); + } + + private async Task> GetCachedModelsImplAsync(CancellationToken? ct = null) + { + var cachedModelIds = await Utils.GetCachedModelIdsAsync(_coreInterop, ct).ConfigureAwait(false); + + List cachedModels = new(); + foreach (var modelId in cachedModelIds) + { + if (_modelIdToModelVariant.TryGetValue(modelId, out ModelVariant? modelVariant)) + { + cachedModels.Add(modelVariant); + } + } + + return cachedModels; + } + + private async Task> GetLoadedModelsImplAsync(CancellationToken? ct = null) + { + var loadedModelIds = await _modelLoadManager.ListLoadedModelsAsync(ct).ConfigureAwait(false); + List loadedModels = new(); + + foreach (var modelId in loadedModelIds) + { + if (_modelIdToModelVariant.TryGetValue(modelId, out ModelVariant? modelVariant)) + { + loadedModels.Add(modelVariant); + } + } + + return loadedModels; + } + + private async Task GetModelImplAsync(string modelAlias, CancellationToken? ct = null) + { + await UpdateModels(ct).ConfigureAwait(false); + + using var disposable = await _lock.LockAsync().ConfigureAwait(false); + _modelAliasToModel.TryGetValue(modelAlias, out Model? model); + + return model; + } + + private async Task GetModelVariantImplAsync(string modelId, CancellationToken? ct = null) + { + await UpdateModels(ct).ConfigureAwait(false); + + using var disposable = await _lock.LockAsync().ConfigureAwait(false); + _modelIdToModelVariant.TryGetValue(modelId, out ModelVariant? modelVariant); + return modelVariant; + } + + private async Task UpdateModels(CancellationToken? ct) + { + // TODO: make this configurable + if (DateTime.Now - _lastFetch < TimeSpan.FromHours(6)) + { + return; + } + + CoreInteropRequest? input = null; + var result = await _coreInterop.ExecuteCommandAsync("get_model_list", input, ct).ConfigureAwait(false); + + if (result.Error != null) + { + throw new FoundryLocalException($"Error getting models: {result.Error}", _logger); + } + + var models = JsonSerializer.Deserialize(result.Data!, JsonSerializationContext.Default.ListModelInfo); + if (models == null) + { + _logger.LogDebug($"ListModelInfo deserialization error in UpdateModels. Data: {result.Data}"); + throw new FoundryLocalException($"Failed to deserialize models from response.", _logger); + } + + using var disposable = await _lock.LockAsync().ConfigureAwait(false); + + // TODO: Do we need to clear this out, or can we just add new models? + _modelAliasToModel.Clear(); + _modelIdToModelVariant.Clear(); + + foreach (var modelInfo in models) + { + var variant = new ModelVariant(modelInfo, _modelLoadManager, _coreInterop, _logger); + + var existingModel = _modelAliasToModel.TryGetValue(modelInfo.Alias, out Model? value); + if (!existingModel) + { + value = new Model(variant, _logger); + _modelAliasToModel[modelInfo.Alias] = value; + } + else + { + value!.AddVariant(variant); + } + + _modelIdToModelVariant[variant.Id] = variant; + } + + _lastFetch = DateTime.Now; + } + + public void Dispose() + { + _lock.Dispose(); + } +} diff --git a/sdk_v2/cs/src/Configuration.cs b/sdk_v2/cs/src/Configuration.cs new file mode 100644 index 0000000..5b481bf --- /dev/null +++ b/sdk_v2/cs/src/Configuration.cs @@ -0,0 +1,164 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; + +public class Configuration +{ + /// + /// Your application name. MUST be set to a valid name. + /// + public required string AppName { get; set; } + + /// + /// Application data directory. + /// Default: {home}/.{appname}, where {home} is the user's home directory and {appname} is the AppName value. + /// + public string? AppDataDir { get; init; } + + /// + /// Model cache directory. + /// Default: {appdata}/cache/models, where {appdata} is the AppDataDir value. + /// + public string? ModelCacheDir { get; init; } + + /// + /// Log directory. + /// Default: {appdata}/logs + /// + public string? LogsDir { get; init; } + + /// + /// Logging level. + /// Valid values are: Verbose, Debug, Information, Warning, Error, Fatal. + /// Default: LogLevel.Warning + /// + public LogLevel LogLevel { get; init; } = LogLevel.Warning; + + /// + /// Enable manual execution provider download mode. Only meaningful if using WinML. + /// + /// Default: false + /// + /// When false, EPs are downloaded automatically in the background when FoundryLocalManager is created. + /// When true, EPs are downloaded when FoundryLocalManager.EnsureEpsDownloadedAsync or GetCatalogAsync are called. + /// + /// Once an EP is downloaded it will not be re-downloaded unless a new version is available. + /// + // DISABLED: We want to make sure this is required before making it public as supporting this complicates the + // Core implementation. Can be specified via AdditionalSettings if needed for testing. + // public bool ManualEpDownload { get; init; } + + /// + /// Optional configuration for the built-in web service. + /// NOTE: This is not included in all builds. + /// + public WebService? Web { get; init; } + + /// + /// Additional settings that Foundry Local Core can consume. + /// Keys and values are strings. + /// + public IDictionary? AdditionalSettings { get; init; } + + /// + /// Configuration settings if the optional web service is used. + /// + public class WebService + { + /// + /// Url/s to bind to the web service when is called. + /// After startup, will contain the actual URL/s the service is listening on. + /// + /// Default: 127.0.0.1:0, which binds to a random ephemeral port. + /// Multiple URLs can be specified as a semi-colon separated list. + /// + public string? Urls { get; init; } + + /// + /// If the web service is running in a separate process, it will be accessed using this URI. + /// + /// + /// Both processes should be using the same version of the SDK. If a random port is assigned when creating + /// the web service in the external process the actual port must be provided here. + /// + public Uri? ExternalUrl { get; init; } + } + + internal void Validate() + { + if (string.IsNullOrEmpty(AppName)) + { + throw new ArgumentException("Configuration AppName must be set to a valid application name."); + } + + if (AppName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) + { + throw new ArgumentException("Configuration AppName value contains invalid characters."); + } + + + if (Web?.ExternalUrl?.Port == 0) + { + throw new ArgumentException("Configuration Web.ExternalUrl has invalid port of 0."); + } + } + + internal Dictionary AsDictionary() + { + if (string.IsNullOrEmpty(AppName)) + { + throw new FoundryLocalException( + "Configuration AppName must be set to a valid application name."); + } + + var configValues = new Dictionary + { + { "AppName", AppName }, + { "LogLevel", LogLevel.ToString() } + }; + + if (!string.IsNullOrEmpty(AppDataDir)) + { + configValues.Add("AppDataDir", AppDataDir); + } + + if (!string.IsNullOrEmpty(ModelCacheDir)) + { + configValues.Add("ModelCacheDir", ModelCacheDir); + } + + if (!string.IsNullOrEmpty(LogsDir)) + { + configValues.Add("LogsDir", LogsDir); + } + + //configValues.Add("ManualEpDownload", ManualEpDownload.ToString()); + + if (Web != null) + { + if (Web.Urls != null) + { + configValues["WebServiceUrls"] = Web.Urls; + } + } + + // Emit any additional settings. + if (AdditionalSettings != null) + { + foreach (var kvp in AdditionalSettings) + { + if (string.IsNullOrEmpty(kvp.Key)) + { + continue; // skip empty keys + } + configValues[kvp.Key] = kvp.Value ?? string.Empty; + } + } + + return configValues; + } +} diff --git a/sdk_v2/cs/src/Detail/AsyncLock.cs b/sdk_v2/cs/src/Detail/AsyncLock.cs new file mode 100644 index 0000000..921d7f9 --- /dev/null +++ b/sdk_v2/cs/src/Detail/AsyncLock.cs @@ -0,0 +1,62 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Detail; +using System; +using System.Threading.Tasks; + +public sealed class AsyncLock : IDisposable +{ + private readonly Task _releaserTask; + private readonly SemaphoreSlim _semaphore = new(1, 1); + private readonly IDisposable _releaser; + + public AsyncLock() + { + _releaser = new Releaser(_semaphore); + _releaserTask = Task.FromResult(_releaser); + } + + public void Dispose() + { + _semaphore.Dispose(); + } + + public IDisposable Lock() + { + _semaphore.Wait(); + return _releaser; + } + + public Task LockAsync() + { + Task waitTask = _semaphore.WaitAsync(); + + return waitTask.IsCompleted + ? _releaserTask + : waitTask.ContinueWith( + (_, releaser) => (IDisposable)releaser!, + _releaser, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + private sealed class Releaser : IDisposable + { + private readonly SemaphoreSlim _semaphore; + + public Releaser(SemaphoreSlim semaphore) + { + _semaphore = semaphore; + } + + public void Dispose() + { + _semaphore.Release(); + } + } +} diff --git a/sdk_v2/cs/src/Detail/CoreInterop.cs b/sdk_v2/cs/src/Detail/CoreInterop.cs new file mode 100644 index 0000000..8411473 --- /dev/null +++ b/sdk_v2/cs/src/Detail/CoreInterop.cs @@ -0,0 +1,334 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Detail; + +using System.Diagnostics; +using System.Runtime.InteropServices; + +using Microsoft.Extensions.Logging; + +using static Microsoft.AI.Foundry.Local.Detail.ICoreInterop; + +internal partial class CoreInterop : ICoreInterop +{ + // TODO: Android and iOS may need special handling. See ORT C# NativeMethods.shared.cs + internal const string LibraryName = "Microsoft.AI.Foundry.Local.Core"; + private readonly ILogger _logger; + + private static string AddLibraryExtension(string name) => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"{name}.dll" : + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? $"{name}.so" : + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{name}.dylib" : + throw new PlatformNotSupportedException(); + + private static IntPtr genaiLibHandle = IntPtr.Zero; + private static IntPtr ortLibHandle = IntPtr.Zero; + + // we need to manually load ORT and ORT GenAI dlls on Windows to ensure + // a) we're using the libraries we think we are + // b) that dependencies are resolved correctly as the dlls may not be in the default load path. + // it's a 'Try' as we can't do anything else if it fails as the dlls may be available somewhere else. + private static void LoadOrtDllsIfInSameDir(string path) + { + var genaiLibName = AddLibraryExtension("onnxruntime-genai"); + var ortLibName = AddLibraryExtension("onnxruntime"); + var genaiPath = Path.Combine(path, genaiLibName); + var ortPath = Path.Combine(path, ortLibName); + + // need to load ORT first as the winml GenAI library redirects and tries to load a winml onnxruntime.dll, + // which will not have the EPs we expect/require. if/when we don't bundle our own onnxruntime.dll we need to + // revisit this. + var loadedOrt = NativeLibrary.TryLoad(ortPath, out ortLibHandle); + var loadedGenAI = NativeLibrary.TryLoad(genaiPath, out genaiLibHandle); + +#if DEBUG + Console.WriteLine($"Loaded ORT:{loadedOrt} handle={ortLibHandle}"); + Console.WriteLine($"Loaded GenAI: {loadedGenAI} handle={genaiLibHandle}"); +#endif + } + + static CoreInterop() + { + NativeLibrary.SetDllImportResolver(typeof(CoreInterop).Assembly, (libraryName, assembly, searchPath) => + { + if (libraryName == LibraryName) + { +#if DEBUG + Console.WriteLine($"Resolving {libraryName}. BaseDirectory: {AppContext.BaseDirectory}"); +#endif + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + // check if this build is platform specific. in that case all files are flattened in the one directory + // and there's no need to look in runtimes/-/native. + // e.g. `dotnet publish -r win-x64` copies all the dependencies into the publish output folder. + var libraryPath = Path.Combine(AppContext.BaseDirectory, AddLibraryExtension(LibraryName)); + if (File.Exists(libraryPath)) + { + if (NativeLibrary.TryLoad(libraryPath, out var handle)) + { +#if DEBUG + Console.WriteLine($"Loaded native library from: {libraryPath}"); +#endif + if (isWindows) + { + LoadOrtDllsIfInSameDir(AppContext.BaseDirectory); + } + + return handle; + } + } + + // TODO: figure out what is required on Android and iOS + // The nuget has an AAR and xcframework respectively so we need to determine what files are where + // after a build. + var os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win" : + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "linux" : + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx" : + throw new PlatformNotSupportedException(); + + var arch = RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(); + var runtimePath = Path.Combine(AppContext.BaseDirectory, "runtimes", $"{os}-{arch}", "native"); + libraryPath = Path.Combine(runtimePath, AddLibraryExtension(LibraryName)); + +#if DEBUG + Console.WriteLine($"Looking for native library at: {libraryPath}"); +#endif + if (File.Exists(libraryPath)) + { + if (NativeLibrary.TryLoad(libraryPath, out var handle)) + { +#if DEBUG + Console.WriteLine($"Loaded native library from: {libraryPath}"); +#endif + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + LoadOrtDllsIfInSameDir(runtimePath); + } + + return handle; + } + } + } + + return IntPtr.Zero; + }); + } + + internal CoreInterop(Configuration config, ILogger logger) + { + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + var request = new CoreInteropRequest { Params = config.AsDictionary() }; + var response = ExecuteCommand("initialize", request); + + if (response.Error != null) + { + throw new FoundryLocalException($"Error initializing Foundry.Local.Core library: {response.Error}"); + } + else + { + _logger.LogInformation("Foundry.Local.Core initialized successfully: {Response}", response.Data); + } + } + + // For testing. Skips the 'initialize' command so assumes this has been done previously. + internal CoreInterop(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private unsafe delegate void ExecuteCommandDelegate(RequestBuffer* req, ResponseBuffer* resp); + + // Import the function from the AOT-compiled library + [LibraryImport(LibraryName, EntryPoint = "execute_command")] + [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + private static unsafe partial void CoreExecuteCommand(RequestBuffer* request, ResponseBuffer* response); + + [LibraryImport(LibraryName, EntryPoint = "execute_command_with_callback")] + [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] + private static unsafe partial void CoreExecuteCommandWithCallback(RequestBuffer* nativeRequest, + ResponseBuffer* nativeResponse, + nint callbackPtr, // NativeCallbackFn pointer + nint userData); + + // helper to capture exceptions in callbacks + internal class CallbackHelper + { + public CallbackFn Callback { get; } + public Exception? Exception { get; set; } // keep the first only. most likely it will be the same issue in all + public CallbackHelper(CallbackFn callback) + { + Callback = callback ?? throw new ArgumentNullException(nameof(callback)); + } + } + + private static void HandleCallback(nint data, int length, nint callbackHelper) + { + var callbackData = string.Empty; + CallbackHelper? helper = null; + + try + { + if (data != IntPtr.Zero && length > 0) + { + var managedData = new byte[length]; + Marshal.Copy(data, managedData, 0, length); + callbackData = System.Text.Encoding.UTF8.GetString(managedData); + } + + Debug.Assert(callbackHelper != IntPtr.Zero, "Callback helper pointer is required."); + + helper = (CallbackHelper)GCHandle.FromIntPtr(callbackHelper).Target!; + helper.Callback.Invoke(callbackData); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + FoundryLocalManager.Instance.Logger.LogError(ex, $"Error in callback. Callback data: {callbackData}"); + if (helper != null && helper.Exception == null) + { + helper.Exception = ex; + } + } + } + + private static readonly NativeCallbackFn handleCallbackDelegate = HandleCallback; + + + public Response ExecuteCommandImpl(string commandName, string? commandInput, + CallbackFn? callback = null) + { + try + { + byte[] commandBytes = System.Text.Encoding.UTF8.GetBytes(commandName); + // Allocate unmanaged memory for the command bytes + IntPtr commandPtr = Marshal.AllocHGlobal(commandBytes.Length); + Marshal.Copy(commandBytes, 0, commandPtr, commandBytes.Length); + + byte[]? inputBytes = null; + IntPtr? inputPtr = null; + + if (commandInput != null) + { + inputBytes = System.Text.Encoding.UTF8.GetBytes(commandInput); + inputPtr = Marshal.AllocHGlobal(inputBytes.Length); + Marshal.Copy(inputBytes, 0, inputPtr.Value, inputBytes.Length); + } + + // Prepare request + var request = new RequestBuffer + { + Command = commandPtr, + CommandLength = commandBytes.Length, + Data = inputPtr ?? IntPtr.Zero, + DataLength = inputBytes?.Length ?? 0 + }; + + ResponseBuffer response = default; + + if (callback != null) + { + // NOTE: This assumes the command will NOT return until complete, so the lifetime of the + // objects involved in the callback is limited to the duration of the call to + // CoreExecuteCommandWithCallback. + + var helper = new CallbackHelper(callback); + + var funcPtr = Marshal.GetFunctionPointerForDelegate(handleCallbackDelegate); + var helperHandle = GCHandle.Alloc(helper); + var helperPtr = GCHandle.ToIntPtr(helperHandle); + + unsafe + { + CoreExecuteCommandWithCallback(&request, &response, funcPtr, helperPtr); + } + + helperHandle.Free(); + + if (helper.Exception != null) + { + throw new FoundryLocalException("Exception in callback handler. See InnerException for details", + helper.Exception); + } + } + else + { + // Pin request/response on the stack + unsafe + { + CoreExecuteCommand(&request, &response); + } + } + + Response result = new(); + + // Marshal response. Will have either Data or Error populated. Not both. + if (response.Data != IntPtr.Zero && response.DataLength > 0) + { + byte[] managedResponse = new byte[response.DataLength]; + Marshal.Copy(response.Data, managedResponse, 0, response.DataLength); + result.Data = System.Text.Encoding.UTF8.GetString(managedResponse); + _logger.LogDebug($"Command: {commandName} succeeded."); + } + + if (response.Error != IntPtr.Zero && response.ErrorLength > 0) + { + result.Error = Marshal.PtrToStringUTF8(response.Error, response.ErrorLength)!; + _logger.LogDebug($"Input:{commandInput ?? "null"}"); + _logger.LogDebug($"Command: {commandName} Error: {result.Error}"); + } + + // TODO: Validate this works. C# specific. Attempting to avoid calling free_response to do this + Marshal.FreeHGlobal(response.Data); + Marshal.FreeHGlobal(response.Error); + + Marshal.FreeHGlobal(commandPtr); + if (commandInput != null) + { + Marshal.FreeHGlobal(inputPtr!.Value); + } + + return result; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + var msg = $"Error executing command '{commandName}' with input {commandInput ?? "null"}"; + throw new FoundryLocalException(msg, ex, _logger); + } + } + + public Response ExecuteCommand(string commandName, CoreInteropRequest? commandInput = null) + { + var commandInputJson = commandInput?.ToJson(); + return ExecuteCommandImpl(commandName, commandInputJson); + } + + public Response ExecuteCommandWithCallback(string commandName, CoreInteropRequest? commandInput, + CallbackFn callback) + { + var commandInputJson = commandInput?.ToJson(); + return ExecuteCommandImpl(commandName, commandInputJson, callback); + } + + public Task ExecuteCommandAsync(string commandName, CoreInteropRequest? commandInput = null, + CancellationToken? cancellationToken = null) + { + var ct = cancellationToken ?? CancellationToken.None; + return Task.Run(() => ExecuteCommand(commandName, commandInput), ct); + } + + public Task ExecuteCommandWithCallbackAsync(string commandName, CoreInteropRequest? commandInput, + CallbackFn callback, + CancellationToken? cancellationToken = null) + { + var ct = cancellationToken ?? CancellationToken.None; + return Task.Run(() => ExecuteCommandWithCallback(commandName, commandInput, callback), ct); + } + +} diff --git a/sdk_v2/cs/src/Detail/CoreInteropRequest.cs b/sdk_v2/cs/src/Detail/CoreInteropRequest.cs new file mode 100644 index 0000000..50365ad --- /dev/null +++ b/sdk_v2/cs/src/Detail/CoreInteropRequest.cs @@ -0,0 +1,22 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Detail; +using System.Collections.Generic; +using System.Text.Json; + +public class CoreInteropRequest +{ + public Dictionary Params { get; set; } = new(); +} + +internal static class RequestExtensions +{ + public static string ToJson(this CoreInteropRequest request) + { + return JsonSerializer.Serialize(request, JsonSerializationContext.Default.CoreInteropRequest); + } +} diff --git a/sdk_v2/cs/src/Detail/ICoreInterop.cs b/sdk_v2/cs/src/Detail/ICoreInterop.cs new file mode 100644 index 0000000..1fff9dd --- /dev/null +++ b/sdk_v2/cs/src/Detail/ICoreInterop.cs @@ -0,0 +1,54 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Detail; + +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + + +internal interface ICoreInterop +{ + internal record Response + { + internal string? Data; + internal string? Error; + } + + public delegate void CallbackFn(string callbackData); + + [StructLayout(LayoutKind.Sequential)] + protected unsafe struct RequestBuffer + { + public nint Command; + public int CommandLength; + public nint Data; + public int DataLength; + } + + [StructLayout(LayoutKind.Sequential)] + protected unsafe struct ResponseBuffer + { + public nint Data; + public int DataLength; + public nint Error; + public int ErrorLength; + } + + // native callback function signature + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + protected unsafe delegate void NativeCallbackFn(nint data, int length, nint userData); + + Response ExecuteCommand(string commandName, CoreInteropRequest? commandInput = null); + Response ExecuteCommandWithCallback(string commandName, CoreInteropRequest? commandInput, CallbackFn callback); + + Task ExecuteCommandAsync(string commandName, CoreInteropRequest? commandInput = null, + CancellationToken? ct = null); + Task ExecuteCommandWithCallbackAsync(string commandName, CoreInteropRequest? commandInput, + CallbackFn callback, + CancellationToken? ct = null); +} diff --git a/sdk_v2/cs/src/Detail/IModelLoadManager.cs b/sdk_v2/cs/src/Detail/IModelLoadManager.cs new file mode 100644 index 0000000..a96c669 --- /dev/null +++ b/sdk_v2/cs/src/Detail/IModelLoadManager.cs @@ -0,0 +1,19 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Detail; +using System.Threading.Tasks; + +/// +/// Interface for model load management. +/// These operations can be done directly or via the optional web service. +/// +internal interface IModelLoadManager +{ + internal abstract Task LoadAsync(string modelName, CancellationToken? ct = null); + internal abstract Task UnloadAsync(string modelName, CancellationToken? ct = null); + internal abstract Task ListLoadedModelsAsync(CancellationToken? ct = null); +} diff --git a/sdk_v2/cs/src/Detail/JsonSerializationContext.cs b/sdk_v2/cs/src/Detail/JsonSerializationContext.cs new file mode 100644 index 0000000..b903142 --- /dev/null +++ b/sdk_v2/cs/src/Detail/JsonSerializationContext.cs @@ -0,0 +1,28 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Detail; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; + +using Microsoft.AI.Foundry.Local.OpenAI; + +[JsonSerializable(typeof(ModelInfo))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(CoreInteropRequest))] +[JsonSerializable(typeof(ChatCompletionCreateRequestExtended))] +[JsonSerializable(typeof(ChatCompletionCreateResponse))] +[JsonSerializable(typeof(AudioCreateTranscriptionRequest))] +[JsonSerializable(typeof(AudioCreateTranscriptionResponse))] +[JsonSerializable(typeof(string[]))] // list loaded or cached models +[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false)] +internal partial class JsonSerializationContext : JsonSerializerContext +{ +} diff --git a/sdk_v2/cs/src/Detail/ModelLoadManager.cs b/sdk_v2/cs/src/Detail/ModelLoadManager.cs new file mode 100644 index 0000000..f8bdaca --- /dev/null +++ b/sdk_v2/cs/src/Detail/ModelLoadManager.cs @@ -0,0 +1,177 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Detail; + +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; + +using Microsoft.Extensions.Logging; + +internal sealed class ModelLoadManager : IModelLoadManager, IDisposable +{ + private readonly Uri? _externalServiceUrl; + private readonly HttpClient? _httpClient; + private readonly ICoreInterop _coreInterop; + private readonly ILogger _logger; + + internal ModelLoadManager(Uri? externalServiceUrl, ICoreInterop coreInterop, ILogger logger) + { + _externalServiceUrl = externalServiceUrl; + _coreInterop = coreInterop; + _logger = logger; + + if (_externalServiceUrl != null) + { + // We only have a single instance of ModelLoadManager so we don't need HttpClient to be static. +#pragma warning disable IDISP014 // Use a single instance of HttpClient. + _httpClient = new HttpClient + { + BaseAddress = _externalServiceUrl, + }; +#pragma warning restore IDISP014 // Use a single instance of HttpClient + + // TODO: Wire in Config AppName here + var userAgent = $"foundry-local-cs-sdk/{FoundryLocalManager.AssemblyVersion}"; + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent); + } + } + + public async Task LoadAsync(string modelId, CancellationToken? ct = null) + { + if (_externalServiceUrl != null) + { + await WebLoadModelAsync(modelId, ct).ConfigureAwait(false); + return; + } + + var request = new CoreInteropRequest { Params = new() { { "Model", modelId } } }; + var result = await _coreInterop.ExecuteCommandAsync("load_model", request, ct).ConfigureAwait(false); + if (result.Error != null) + { + throw new FoundryLocalException($"Error loading model {modelId}: {result.Error}"); + } + + // currently just a 'model loaded successfully' message + _logger.LogInformation("Model {ModelId} loaded successfully: {Message}", modelId, result.Data); + } + + public async Task UnloadAsync(string modelId, CancellationToken? ct = null) + { + if (_externalServiceUrl != null) + { + await WebUnloadModelAsync(modelId, ct).ConfigureAwait(false); + return; + } + + var request = new CoreInteropRequest { Params = new() { { "Model", modelId } } }; + var result = await _coreInterop.ExecuteCommandAsync("unload_model", request, ct).ConfigureAwait(false); + if (result.Error != null) + { + throw new FoundryLocalException($"Error unloading model {modelId}: {result.Error}"); + } + + _logger.LogInformation("Model {ModelId} unloaded successfully: {Message}", modelId, result.Data); + } + + public async Task ListLoadedModelsAsync(CancellationToken? ct = null) + { + if (_externalServiceUrl != null) + { + return await WebListLoadedModelAsync(ct).ConfigureAwait(false); + } + + var result = await _coreInterop.ExecuteCommandAsync("list_loaded_models", null, ct).ConfigureAwait(false); + if (result.Error != null) + { + throw new FoundryLocalException($"Error listing loaded models: {result.Error}"); + } + + _logger.LogDebug("Loaded models json: {Data}", result.Data); + + var typeInfo = JsonSerializationContext.Default.StringArray; + var modelList = JsonSerializer.Deserialize(result.Data!, typeInfo); + + return modelList ?? []; + } + + private async Task WebListLoadedModelAsync(CancellationToken? ct = null) + { + using var response = await _httpClient!.GetAsync("models/loaded", ct ?? CancellationToken.None) + .ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + throw new FoundryLocalException($"Error listing loaded models from {_externalServiceUrl}: " + + $"{response.ReasonPhrase}"); + } + + var content = await response.Content.ReadAsStringAsync(ct ?? CancellationToken.None).ConfigureAwait(false); + _logger.LogDebug("Loaded models json from {WebService}: {Data}", _externalServiceUrl, content); + var typeInfo = JsonSerializationContext.Default.StringArray; + var modelList = JsonSerializer.Deserialize(content, typeInfo); + return modelList ?? []; + } + + private async Task WebLoadModelAsync(string modelId, CancellationToken? ct = null) + { + var queryParams = new Dictionary + { + // { "timeout", ... } + }; + + // TODO: What do we need around EP override in the latest setup? + // Can we do this in FLC and limit to generic-gpu models only, picking the vendor GPU EP over WebGPU? + // Not sure there's any other valid override. WebGPU will always try and use the discrete GPU, so vendor + // EP will always be better. + //if (!string.IsNullOrEmpty(modelInfo.EpOverride)) + //{ + // queryParams["ep"] = modelInfo.EpOverride!; + //} + + var uriBuilder = new UriBuilder(_externalServiceUrl!) + { + Path = $"models/load/{modelId}", + Query = string.Join("&", queryParams.Select(kvp => + $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}")) + }; + + using var response = await _httpClient!.GetAsync(uriBuilder.Uri, ct ?? CancellationToken.None) + .ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + throw new FoundryLocalException($"Error loading model {modelId} from {_externalServiceUrl}: " + + $"{response.ReasonPhrase}"); + } + + var content = await response.Content.ReadAsStringAsync(ct ?? CancellationToken.None).ConfigureAwait(false); + _logger.LogInformation("Model {ModelId} loaded successfully from {WebService}: {Message}", + modelId, _externalServiceUrl, content); + } + + private async Task WebUnloadModelAsync(string modelId, CancellationToken? ct = null) + { + using var response = await _httpClient!.GetAsync(new Uri($"models/unload/{modelId}"), + ct ?? CancellationToken.None) + .ConfigureAwait(false); + + // TODO: Do we need to handle a 400 (not found) explicitly or does that not provide any real value? + if (!response.IsSuccessStatusCode) + { + throw new FoundryLocalException($"Error unloading model {modelId} from {_externalServiceUrl}: " + + $"{response.ReasonPhrase}"); + } + + var content = await response.Content.ReadAsStringAsync(ct ?? CancellationToken.None).ConfigureAwait(false); + _logger.LogInformation("Model {ModelId} loaded successfully from {WebService}: {Message}", + modelId, _externalServiceUrl, content); + } + + public void Dispose() + { + _httpClient?.Dispose(); + } +} diff --git a/sdk_v2/cs/src/FoundryLocalException.cs b/sdk_v2/cs/src/FoundryLocalException.cs new file mode 100644 index 0000000..d6e606c --- /dev/null +++ b/sdk_v2/cs/src/FoundryLocalException.cs @@ -0,0 +1,35 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; +using System; +using System.Diagnostics; + +using Microsoft.Extensions.Logging; + +public class FoundryLocalException : Exception +{ + public FoundryLocalException(string message) : base(message) + { + } + + public FoundryLocalException(string message, Exception innerException) : base(message, innerException) + { + } + + internal FoundryLocalException(string message, ILogger logger) : base(message) + { + Debug.Assert(logger != null); + logger.LogError(message); + } + + internal FoundryLocalException(string message, Exception innerException, ILogger logger) + : base(message, innerException) + { + Debug.Assert(logger != null); + logger.LogError(innerException, message); + } +} diff --git a/sdk_v2/cs/src/FoundryLocalManager.cs b/sdk_v2/cs/src/FoundryLocalManager.cs new file mode 100644 index 0000000..ce3712c --- /dev/null +++ b/sdk_v2/cs/src/FoundryLocalManager.cs @@ -0,0 +1,309 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- +namespace Microsoft.AI.Foundry.Local; + +using System; +using System.Text.Json; +using System.Threading.Tasks; + +using Microsoft.AI.Foundry.Local.Detail; +using Microsoft.Extensions.Logging; + +public class FoundryLocalManager : IDisposable +{ + private static FoundryLocalManager? instance; + private static readonly AsyncLock asyncLock = new(); + + internal static readonly string AssemblyVersion = + typeof(FoundryLocalManager).Assembly.GetName().Version?.ToString() ?? "unknown"; + + private readonly Configuration _config; + private CoreInterop _coreInterop = default!; + private Catalog _catalog = default!; + private ModelLoadManager _modelManager = default!; + private readonly AsyncLock _lock = new(); + private bool _disposed; + private readonly ILogger _logger; + + internal Configuration Configuration => _config; + internal ILogger Logger => _logger; + internal ICoreInterop CoreInterop => _coreInterop!; // always valid once the instance is created + + public static bool IsInitialized => instance != null; + public static FoundryLocalManager Instance => instance ?? + throw new FoundryLocalException("FoundryLocalManager has not been created. Call CreateAsync first."); + + /// + /// Bound Urls if the web service has been started. Null otherwise. + /// See . + /// + public string[]? Urls { get; private set; } + + /// + /// Create the singleton instance. + /// + /// Configuration to use. + /// Application logger to use. + /// Use Microsoft.Extensions.Logging.NullLogger.Instance if you wish to ignore log output from the SDK. + /// + /// Optional cancellation token for the initialization. + /// Task creating the instance. + /// + public static async Task CreateAsync(Configuration configuration, ILogger logger, + CancellationToken? ct = null) + { + using var disposable = await asyncLock.LockAsync().ConfigureAwait(false); + + if (instance != null) + { + // throw as we're not going to use the provided configuration in case it differs from the original. + throw new FoundryLocalException("FoundryLocalManager has already been created.", logger); + } + + FoundryLocalManager? manager = null; + try + { + // use a local variable to ensure fully initialized before assigning to static instance. + manager = new FoundryLocalManager(configuration, logger); + await manager.InitializeAsync(ct).ConfigureAwait(false); + + // there is no previous as we only get here if instance is null. + // ownership is transferred to the static instance. +#pragma warning disable IDISP003 // Dispose previous before re-assigning + instance = manager; + manager = null; +#pragma warning restore IDISP003 + } + catch (Exception ex) + { + manager?.Dispose(); + + if (ex is FoundryLocalException or OperationCanceledException) + { + throw; + } + + // log and throw as FoundryLocalException + throw new FoundryLocalException("Error during initialization.", ex, logger); + } + } + + /// + /// Get the model catalog instance. + /// + /// Optional canellation token. + /// The model catalog. + /// + /// The catalog is populated on first use. + /// If you are using a WinML build this will trigger a one-off execution provider download if not already done. + /// It is recommended to call first to separate out the two steps. + /// + public async Task GetCatalogAsync(CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => GetCatalogImplAsync(ct), + "Error getting Catalog.", _logger).ConfigureAwait(false); + } + + /// + /// Start the optional web service. This will provide an OpenAI-compatible REST endpoint that supports + /// /v1/chat_completions + /// /v1/models to list downloaded models + /// /v1/models/{model_id} to get model details + /// + /// is populated with the actual bound Urls after startup. + /// + /// Optional cancellation token. + /// Task starting the web service. + public async Task StartWebServiceAsync(CancellationToken? ct = null) + { + await Utils.CallWithExceptionHandling(() => StartWebServiceImplAsync(ct), + "Error starting web service.", _logger).ConfigureAwait(false); + } + + /// + /// Stops the web service if started. + /// + /// Optional cancellation token. + /// Task stopping the web service. + public async Task StopWebServiceAsync(CancellationToken? ct = null) + { + await Utils.CallWithExceptionHandling(() => StopWebServiceImplAsync(ct), + "Error stopping web service.", _logger).ConfigureAwait(false); + } + + /// + /// Ensure execution providers are downloaded and registered. + /// Only relevant when using WinML. + /// + /// Execution provider download can be time consuming due to the size of the packages. + /// Once downloaded, EPs are not re-downloaded unless a new version is available, so this method will be fast + /// on subsequent calls. + /// + /// Optional cancellation token. + public async Task EnsureEpsDownloadedAsync(CancellationToken? ct = null) + { + await Utils.CallWithExceptionHandling(() => EnsureEpsDownloadedImplAsync(ct), + "Error ensuring execution providers downloaded.", _logger) + .ConfigureAwait(false); + } + + private FoundryLocalManager(Configuration configuration, ILogger logger) + { + _config = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _logger = logger; + } + + private async Task InitializeAsync(CancellationToken? ct = null) + { + _config.Validate(); + _coreInterop = new CoreInterop(_config, _logger); + +#pragma warning disable IDISP003 // Dispose previous before re-assigning. Always null when this is called. + _modelManager = new ModelLoadManager(_config.Web?.ExternalUrl, _coreInterop, _logger); +#pragma warning restore IDISP003 + + if (_config.ModelCacheDir != null) + { + CoreInteropRequest? input = null; + var result = await _coreInterop!.ExecuteCommandAsync("get_cache_directory", input, ct) + .ConfigureAwait(false); + if (result.Error != null) + { + throw new FoundryLocalException($"Error getting current model cache directory: {result.Error}", + _logger); + } + + var curCacheDir = result.Data!; + if (curCacheDir != _config.ModelCacheDir) + { + var request = new CoreInteropRequest + { + Params = new Dictionary { { "Directory", _config.ModelCacheDir } } + }; + + result = await _coreInterop!.ExecuteCommandAsync("set_cache_directory", request, ct) + .ConfigureAwait(false); + if (result.Error != null) + { + throw new FoundryLocalException( + $"Error setting model cache directory to '{_config.ModelCacheDir}': {result.Error}", _logger); + } + } + } + + return; + } + + private async Task GetCatalogImplAsync(CancellationToken? ct = null) + { + // create on first use + if (_catalog == null) + { + using var disposable = await _lock.LockAsync().ConfigureAwait(false); + if (_catalog == null) + { + _catalog = await Catalog.CreateAsync(_modelManager!, _coreInterop!, _logger, ct).ConfigureAwait(false); + } + } + + return _catalog; + } + + private async Task StartWebServiceImplAsync(CancellationToken? ct = null) + { + if (_config?.Web?.Urls == null) + { + throw new FoundryLocalException("Web service configuration was not provided.", _logger); + } + + using var disposable = await asyncLock.LockAsync().ConfigureAwait(false); + + CoreInteropRequest? input = null; + var result = await _coreInterop!.ExecuteCommandAsync("start_service", input, ct).ConfigureAwait(false); + if (result.Error != null) + { + throw new FoundryLocalException($"Error starting web service: {result.Error}", _logger); + } + + var typeInfo = JsonSerializationContext.Default.StringArray; + var boundUrls = JsonSerializer.Deserialize(result.Data!, typeInfo); + if (boundUrls == null || boundUrls.Length == 0) + { + throw new FoundryLocalException("Failed to get bound URLs from web service start response.", _logger); + } + + Urls = boundUrls; + } + + private async Task StopWebServiceImplAsync(CancellationToken? ct = null) + { + if (_config?.Web?.Urls == null) + { + throw new FoundryLocalException("Web service configuration was not provided.", _logger); + } + + using var disposable = await asyncLock.LockAsync().ConfigureAwait(false); + + CoreInteropRequest? input = null; + var result = await _coreInterop!.ExecuteCommandAsync("stop_service", input, ct).ConfigureAwait(false); + if (result.Error != null) + { + throw new FoundryLocalException($"Error stopping web service: {result.Error}", _logger); + } + + // Should we clear these even if there's an error response? + // Service is probably in a bad state or was not running. + Urls = null; + } + + private async Task EnsureEpsDownloadedImplAsync(CancellationToken? ct = null) + { + + using var disposable = await asyncLock.LockAsync().ConfigureAwait(false); + + CoreInteropRequest? input = null; + var result = await _coreInterop!.ExecuteCommandAsync("ensure_eps_downloaded", input, ct); + if (result.Error != null) + { + throw new FoundryLocalException($"Error ensuring execution providers downloaded: {result.Error}", _logger); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (Urls != null) + { + // best effort stop + try + { + StopWebServiceImplAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error stopping web service during Dispose."); + } + } + + _catalog?.Dispose(); + _modelManager?.Dispose(); + _lock.Dispose(); + } + + _disposed = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/sdk_v2/cs/src/FoundryModelInfo.cs b/sdk_v2/cs/src/FoundryModelInfo.cs new file mode 100644 index 0000000..1f795d2 --- /dev/null +++ b/sdk_v2/cs/src/FoundryModelInfo.cs @@ -0,0 +1,122 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; + +using System.Text.Json.Serialization; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum DeviceType +{ + Invalid, + CPU, + GPU, + NPU +} + +public record PromptTemplate +{ + [JsonPropertyName("system")] + public string? System { get; init; } + + [JsonPropertyName("user")] + public string? User { get; init; } + + [JsonPropertyName("assistant")] + public string Assistant { get; init; } = default!; + + [JsonPropertyName("prompt")] + public string Prompt { get; init; } = default!; +} + +public record Runtime +{ + [JsonPropertyName("deviceType")] + public DeviceType DeviceType { get; init; } = default!; + + // there are many different possible values; keep it open‑ended + [JsonPropertyName("executionProvider")] + public string ExecutionProvider { get; init; } = default!; +} + +public record Parameter +{ + public required string Name { get; set; } + public string? Value { get; set; } +} + +public record ModelSettings +{ + [JsonPropertyName("parameters")] + public Parameter[]? Parameters { get; set; } +} + +public record ModelInfo +{ + [JsonPropertyName("id")] + public required string Id { get; init; } + + [JsonPropertyName("name")] + public required string Name { get; init; } + + [JsonPropertyName("version")] + public int Version { get; init; } + + [JsonPropertyName("alias")] + public required string Alias { get; init; } + + [JsonPropertyName("displayName")] + public string? DisplayName { get; init; } + + [JsonPropertyName("providerType")] + public required string ProviderType { get; init; } + + [JsonPropertyName("uri")] + public required string Uri { get; init; } + + [JsonPropertyName("modelType")] + public required string ModelType { get; init; } + + [JsonPropertyName("promptTemplate")] + public PromptTemplate? PromptTemplate { get; init; } + + [JsonPropertyName("publisher")] + public string? Publisher { get; init; } + + [JsonPropertyName("modelSettings")] + public ModelSettings? ModelSettings { get; init; } + + [JsonPropertyName("license")] + public string? License { get; init; } + + [JsonPropertyName("licenseDescription")] + public string? LicenseDescription { get; init; } + + [JsonPropertyName("cached")] + public bool Cached { get; init; } + + + [JsonPropertyName("task")] + public string? Task { get; init; } + + [JsonPropertyName("runtime")] + public Runtime? Runtime { get; init; } + + [JsonPropertyName("fileSizeMb")] + public int? FileSizeMb { get; init; } + + [JsonPropertyName("supportsToolCalling")] + public bool? SupportsToolCalling { get; init; } + + [JsonPropertyName("maxOutputTokens")] + public long? MaxOutputTokens { get; init; } + + [JsonPropertyName("minFLVersion")] + public string? MinFLVersion { get; init; } + + [JsonPropertyName("createdAt")] + public long CreatedAtUnix { get; init; } +} diff --git a/sdk_v2/cs/src/GlobalSuppressions.cs b/sdk_v2/cs/src/GlobalSuppressions.cs new file mode 100644 index 0000000..42d5754 --- /dev/null +++ b/sdk_v2/cs/src/GlobalSuppressions.cs @@ -0,0 +1,10 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +// Neutron code. Appears that the _releaser is deliberately not disposed of because it may be being used elsewhere +// due to being returned from the LockAsync method. +[assembly: SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP002:Dispose member", Justification = "The _releaser is not disposed because it may be used elsewhere after being returned from the LockAsync method.", Scope = "member", Target = "~F:Microsoft.AI.Foundry.Local.Detail.AsyncLock._releaser")] diff --git a/sdk_v2/cs/src/ICatalog.cs b/sdk_v2/cs/src/ICatalog.cs new file mode 100644 index 0000000..1234794 --- /dev/null +++ b/sdk_v2/cs/src/ICatalog.cs @@ -0,0 +1,53 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; +using System.Collections.Generic; + +public interface ICatalog +{ + /// + /// The catalog name. + /// + string Name { get; } + + /// + /// List the available models in the catalog. + /// + /// Optional CancellationToken. + /// List of Model instances. + Task> ListModelsAsync(CancellationToken? ct = null); + + /// + /// Lookup a model by its alias. + /// + /// Model alias. + /// Optional CancellationToken. + /// Model if found. + Task GetModelAsync(string modelAlias, CancellationToken? ct = null); + + /// + /// Lookup a model variant by its unique model id. + /// + /// Model id. + /// Optional CancellationToken. + /// Model variant if found. + Task GetModelVariantAsync(string modelId, CancellationToken? ct = null); + + /// + /// Get a list of currently downloaded models from the model cache. + /// + /// Optional CancellationToken. + /// List of ModelVariant instances. + Task> GetCachedModelsAsync(CancellationToken? ct = null); + + /// + /// Get a list of the currently loaded models. + /// + /// Optional CancellationToken. + /// List of ModelVariant instances. + Task> GetLoadedModelsAsync(CancellationToken? ct = null); +} diff --git a/sdk_v2/cs/src/IModel.cs b/sdk_v2/cs/src/IModel.cs new file mode 100644 index 0000000..c3acba6 --- /dev/null +++ b/sdk_v2/cs/src/IModel.cs @@ -0,0 +1,70 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; + +using System.Threading; +using System.Threading.Tasks; + +public interface IModel +{ + string Id { get; } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", + Justification = "Alias is a suitable name in this context.")] + string Alias { get; } + + Task IsCachedAsync(CancellationToken? ct = null); + Task IsLoadedAsync(CancellationToken? ct = null); + + /// + /// Download the model to local cache if not already present. + /// + /// + /// Optional progress callback for download progress. + /// Percentage download (0 - 100.0) is reported. + /// Optional cancellation token. + Task DownloadAsync(Action? downloadProgress = null, + CancellationToken? ct = null); + + /// + /// Gets the model path if cached. + /// + /// Optional cancellation token. + /// Path of model directory. + Task GetPathAsync(CancellationToken? ct = null); + + /// + /// Load the model into memory if not already loaded. + /// + /// Optional cancellation token. + Task LoadAsync(CancellationToken? ct = null); + + /// + /// Remove the model from the local cache. + /// + /// Optional cancellation token. + Task RemoveFromCacheAsync(CancellationToken? ct = null); + + /// + /// Unload the model if loaded. + /// + /// Optional cancellation token. + Task UnloadAsync(CancellationToken? ct = null); + + /// + /// Get an OpenAI API based ChatClient + /// + /// Optional cancellation token. + /// OpenAI.ChatClient + Task GetChatClientAsync(CancellationToken? ct = null); + + /// + /// Get an OpenAI API based AudioClient + /// + /// Optional cancellation token. + /// OpenAI.AudioClient + Task GetAudioClientAsync(CancellationToken? ct = null); +} diff --git a/sdk_v2/cs/src/LogLevel.cs b/sdk_v2/cs/src/LogLevel.cs new file mode 100644 index 0000000..6362ded --- /dev/null +++ b/sdk_v2/cs/src/LogLevel.cs @@ -0,0 +1,17 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; + +public enum LogLevel +{ + Verbose = 0, + Debug = 1, + Information = 2, + Warning = 3, + Error = 4, + Fatal = 5 +} diff --git a/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj b/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj new file mode 100644 index 0000000..c292ef8 --- /dev/null +++ b/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj @@ -0,0 +1,108 @@ + + + Microsoft AI Foundry Local + Microsoft Foundry Local SDK + Microsoft + Microsoft Corporation + © Microsoft Corporation. All rights reserved. + LICENSE.txt + https://github.com/microsoft/Foundry-Local + Microsoft AI Foundry Local SDK for .NET + Microsoft AI Foundry SDK + README.md + https://github.com/microsoft/Foundry-Local + git + + net8.0 + win-x64;win-arm64;linux-x64;linux-arm64;osx-arm64 + + true + False + enable + True + True + enable + + + true + snupkg + + + false + win-x64;win-arm64 + + + + + $([System.DateTime]::Now.ToString("yyyyMMddHHmmss")) + 0.5.0-dev.local.$(BuildTimestamp) + + + + true + true + true + + + $(DefineConstants);IS_WINDOWS + $(DefineConstants);IS_OSX + $(DefineConstants);IS_LINUX + latest-recommended + + + + + + + + + + + + + + + + + + + + Microsoft AI Foundry Local for WinML + Microsoft Foundry Local SDK for WinML + Microsoft.AI.Foundry.Local.WinML + Microsoft.AI.Foundry.Local.WinML + net8.0-windows10.0.26100.0 + win-x64;win-arm64 + + 10.0.17763.0 + + + $(NoWarn);CsWinRT1028 + + + True + + + True + + + + + + + + + + + + \ No newline at end of file diff --git a/sdk_v2/cs/src/Model.cs b/sdk_v2/cs/src/Model.cs new file mode 100644 index 0000000..83bcef6 --- /dev/null +++ b/sdk_v2/cs/src/Model.cs @@ -0,0 +1,126 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; + +using Microsoft.Extensions.Logging; + +public class Model : IModel +{ + private readonly ILogger _logger; + + public List Variants { get; internal set; } + public ModelVariant SelectedVariant { get; internal set; } = default!; + + public string Alias { get; init; } + public string Id => SelectedVariant.Id; + + /// + /// Is the currently selected variant cached locally? + /// + public Task IsCachedAsync(CancellationToken? ct = null) => SelectedVariant.IsCachedAsync(ct); + + /// + /// Is the currently selected variant loaded in memory? + /// + public Task IsLoadedAsync(CancellationToken? ct = null) => SelectedVariant.IsLoadedAsync(ct); + + internal Model(ModelVariant modelVariant, ILogger logger) + { + _logger = logger; + + Alias = modelVariant.Alias; + Variants = new() { modelVariant }; + + // variants are sorted by Core, so the first one added is the default + SelectedVariant = modelVariant; + } + + internal void AddVariant(ModelVariant variant) + { + if (Alias != variant.Alias) + { + // internal error so log + throw new FoundryLocalException($"Variant alias {variant.Alias} does not match model alias {Alias}", + _logger); + } + + Variants.Add(variant); + + // prefer the highest priority locally cached variant + if (variant.Info.Cached && !SelectedVariant.Info.Cached) + { + SelectedVariant = variant; + } + } + + /// + /// Select a specific model variant by its unique model ID. + /// The selected variant will be used for operations. + /// + /// Model Id of the variant to select. + /// If variant is not valid for this model. + public void SelectVariant(ModelVariant variant) + { + _ = Variants.FirstOrDefault(v => v == variant) ?? + // user error so don't log + throw new FoundryLocalException($"Model {Alias} does not have a {variant.Id} variant."); + + SelectedVariant = variant; + } + + /// + /// Get the latest version of the specified model variant. + /// + /// Model variant. + /// ModelVariant for latest version. Same as `variant` if that is the latest version. + /// If variant is not valid for this model. + public ModelVariant GetLatestVersion(ModelVariant variant) + { + // variants are sorted by version, so the first one matching the name is the latest version for that variant. + var latest = Variants.FirstOrDefault(v => v.Info.Name == variant.Info.Name) ?? + // user error so don't log + throw new FoundryLocalException($"Model {Alias} does not have a {variant.Id} variant."); + + return latest; + } + + public async Task GetPathAsync(CancellationToken? ct = null) + { + return await SelectedVariant.GetPathAsync(ct).ConfigureAwait(false); + } + + public async Task DownloadAsync(Action? downloadProgress = null, + CancellationToken? ct = null) + { + await SelectedVariant.DownloadAsync(downloadProgress, ct).ConfigureAwait(false); + } + + public async Task LoadAsync(CancellationToken? ct = null) + { + await SelectedVariant.LoadAsync(ct).ConfigureAwait(false); + } + + public async Task GetChatClientAsync(CancellationToken? ct = null) + { + return await SelectedVariant.GetChatClientAsync(ct).ConfigureAwait(false); + } + + public async Task GetAudioClientAsync(CancellationToken? ct = null) + { + return await SelectedVariant.GetAudioClientAsync(ct).ConfigureAwait(false); + } + + public async Task UnloadAsync(CancellationToken? ct = null) + { + await SelectedVariant.UnloadAsync(ct).ConfigureAwait(false); + } + + public async Task RemoveFromCacheAsync(CancellationToken? ct = null) + { + await SelectedVariant.RemoveFromCacheAsync(ct).ConfigureAwait(false); + } +} diff --git a/sdk_v2/cs/src/ModelVariant.cs b/sdk_v2/cs/src/ModelVariant.cs new file mode 100644 index 0000000..6ca7cda --- /dev/null +++ b/sdk_v2/cs/src/ModelVariant.cs @@ -0,0 +1,193 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; + +using Microsoft.AI.Foundry.Local.Detail; +using Microsoft.Extensions.Logging; + +public class ModelVariant : IModel +{ + private readonly IModelLoadManager _modelLoadManager; + private readonly ICoreInterop _coreInterop; + private readonly ILogger _logger; + + public ModelInfo Info { get; } // expose the full info record + + // expose a few common properties directly + public string Id => Info.Id; + public string Alias => Info.Alias; + public int Version { get; init; } // parsed from Info.Version if possible, else 0 + + internal ModelVariant(ModelInfo modelInfo, IModelLoadManager modelLoadManager, ICoreInterop coreInterop, + ILogger logger) + { + Info = modelInfo; + Version = modelInfo.Version; + + _modelLoadManager = modelLoadManager; + _coreInterop = coreInterop; + _logger = logger; + + } + + // simpler and always correct to check if loaded from the model load manager + // this allows for multiple instances of ModelVariant to exist + public async Task IsLoadedAsync(CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => IsLoadedImplAsync(ct), + "Error checking if model is loaded", _logger) + .ConfigureAwait(false); + } + + public async Task IsCachedAsync(CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => IsCachedImplAsync(ct), + "Error checking if model is cached", _logger) + .ConfigureAwait(false); + } + + public async Task GetPathAsync(CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => GetPathImplAsync(ct), + "Error getting path for model", _logger) + .ConfigureAwait(false); + } + + public async Task DownloadAsync(Action? downloadProgress = null, + CancellationToken? ct = null) + { + await Utils.CallWithExceptionHandling(() => DownloadImplAsync(downloadProgress, ct), + $"Error downloading model {Id}", _logger) + .ConfigureAwait(false); + } + + public async Task LoadAsync(CancellationToken? ct = null) + { + await Utils.CallWithExceptionHandling(() => _modelLoadManager.LoadAsync(Id, ct), + "Error loading model", _logger) + .ConfigureAwait(false); + } + + public async Task UnloadAsync(CancellationToken? ct = null) + { + await Utils.CallWithExceptionHandling(() => _modelLoadManager.UnloadAsync(Id, ct), + "Error unloading model", _logger) + .ConfigureAwait(false); + } + + public async Task RemoveFromCacheAsync(CancellationToken? ct = null) + { + await Utils.CallWithExceptionHandling(() => RemoveFromCacheImplAsync(ct), + $"Error removing model {Id} from cache", _logger) + .ConfigureAwait(false); + } + + public async Task GetChatClientAsync(CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => GetChatClientImplAsync(ct), + "Error getting chat client for model", _logger) + .ConfigureAwait(false); + } + + public async Task GetAudioClientAsync(CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => GetAudioClientImplAsync(ct), + "Error getting audio client for model", _logger) + .ConfigureAwait(false); + } + + private async Task IsLoadedImplAsync(CancellationToken? ct = null) + { + var loadedModels = await _modelLoadManager.ListLoadedModelsAsync(ct).ConfigureAwait(false); + return loadedModels.Contains(Id); + } + + private async Task IsCachedImplAsync(CancellationToken? ct = null) + { + var cachedModelIds = await Utils.GetCachedModelIdsAsync(_coreInterop, ct).ConfigureAwait(false); + return cachedModelIds.Contains(Id); + } + + private async Task GetPathImplAsync(CancellationToken? ct = null) + { + var request = new CoreInteropRequest { Params = new Dictionary { { "Model", Id } } }; + var result = await _coreInterop.ExecuteCommandAsync("get_model_path", request, ct).ConfigureAwait(false); + if (result.Error != null) + { + throw new FoundryLocalException( + $"Error getting path for model {Id}: {result.Error}. Has it been downloaded?"); + } + + var path = result.Data!; + return path; + } + + private async Task DownloadImplAsync(Action? downloadProgress = null, + CancellationToken? ct = null) + { + var request = new CoreInteropRequest + { + Params = new() { { "Model", Id } } + }; + + ICoreInterop.Response? response; + + if (downloadProgress == null) + { + response = await _coreInterop.ExecuteCommandAsync("download_model", request, ct).ConfigureAwait(false); + } + else + { + var callback = new ICoreInterop.CallbackFn(progressString => + { + if (float.TryParse(progressString, out var progress)) + { + downloadProgress(progress); + } + }); + + response = await _coreInterop.ExecuteCommandWithCallbackAsync("download_model", request, + callback, ct).ConfigureAwait(false); + } + + if (response.Error != null) + { + throw new FoundryLocalException($"Error downloading model {Id}: {response.Error}"); + } + } + + private async Task RemoveFromCacheImplAsync(CancellationToken? ct = null) + { + var request = new CoreInteropRequest { Params = new Dictionary { { "Model", Id } } }; + + var result = await _coreInterop.ExecuteCommandAsync("remove_cached_model", request, ct).ConfigureAwait(false); + if (result.Error != null) + { + throw new FoundryLocalException($"Error removing model {Id} from cache: {result.Error}"); + } + } + + private async Task GetChatClientImplAsync(CancellationToken? ct = null) + { + if (!await IsLoadedAsync(ct)) + { + throw new FoundryLocalException($"Model {Id} is not loaded. Call LoadAsync first."); + } + + return new OpenAIChatClient(Id); + } + + private async Task GetAudioClientImplAsync(CancellationToken? ct = null) + { + if (!await IsLoadedAsync(ct)) + { + throw new FoundryLocalException($"Model {Id} is not loaded. Call LoadAsync first."); + } + + return new OpenAIAudioClient(Id); + } +} diff --git a/sdk_v2/cs/src/OpenAI/AudioClient.cs b/sdk_v2/cs/src/OpenAI/AudioClient.cs new file mode 100644 index 0000000..98f40a6 --- /dev/null +++ b/sdk_v2/cs/src/OpenAI/AudioClient.cs @@ -0,0 +1,182 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; + +using System.Runtime.CompilerServices; +using System.Threading.Channels; + +using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; + +using Microsoft.AI.Foundry.Local.Detail; +using Microsoft.AI.Foundry.Local.OpenAI; +using Microsoft.Extensions.Logging; + +/// +/// Audio Client that uses the OpenAI API. +/// Implemented using Betalgo.Ranul.OpenAI SDK types. +/// +public class OpenAIAudioClient +{ + private readonly string _modelId; + + private readonly ICoreInterop _coreInterop = FoundryLocalManager.Instance.CoreInterop; + private readonly ILogger _logger = FoundryLocalManager.Instance.Logger; + + internal OpenAIAudioClient(string modelId) + { + _modelId = modelId; + } + + /// + /// Transcribe audio from a file. + /// + /// + /// Path to file containing audio recording. + /// Supported formats: ???? + /// + /// Optional cancellation token. + /// Transcription response. + public async Task TranscribeAudioAsync(string audioFilePath, + CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling(() => TranscribeAudioImplAsync(audioFilePath, ct), + "Error during audio transcription.", _logger) + .ConfigureAwait(false); + } + + /// + /// Transcribe audio from a file with streamed output. + /// + /// + /// Path to file containing audio recording. + /// Supported formats: ???? + /// + /// Cancellation token. + /// An asynchronous enumerable of transcription responses. + public async IAsyncEnumerable TranscribeAudioStreamingAsync( + string audioFilePath, [EnumeratorCancellation] CancellationToken ct) + { + var enumerable = Utils.CallWithExceptionHandling( + () => TranscribeAudioStreamingImplAsync(audioFilePath, ct), + "Error during streaming audio transcription.", _logger).ConfigureAwait(false); + + await foreach (var item in enumerable) + { + yield return item; + } + } + + private async Task TranscribeAudioImplAsync(string audioFilePath, + CancellationToken? ct) + { + var openaiRequest = new AudioCreateTranscriptionRequest + { + Model = _modelId, + FileName = audioFilePath + }; + + var request = new CoreInteropRequest + { + Params = new Dictionary + { + { "OpenAICreateRequest", openaiRequest.ToJson() }, + } + }; + + var response = await _coreInterop.ExecuteCommandAsync("audio_transcribe", request, + ct ?? CancellationToken.None).ConfigureAwait(false); + + + var output = response.ToAudioTranscription(_logger); + + return output; + } + + private async IAsyncEnumerable TranscribeAudioStreamingImplAsync( + string audioFilePath, [EnumeratorCancellation] CancellationToken ct) + { + var openaiRequest = new AudioCreateTranscriptionRequest + { + Model = _modelId, + FileName = audioFilePath + }; + + var request = new CoreInteropRequest + { + Params = new Dictionary + { + { "OpenAICreateRequest", openaiRequest.ToJson() }, + } + }; + + var channel = Channel.CreateUnbounded( + new UnboundedChannelOptions + { + SingleWriter = true, + SingleReader = true, + AllowSynchronousContinuations = true + }); + + // The callback will push ChatResponse objects into the channel. + // The channel reader will return the values to the user. + // This setup prevents the user from blocking the thread generating the responses. + _ = Task.Run(async () => + { + try + { + var failed = false; + + await _coreInterop.ExecuteCommandWithCallbackAsync( + "audio_transcribe", + request, + async (callbackData) => + { + try + { + if (!failed) + { + var audioCompletion = callbackData.ToAudioTranscription(_logger); + await channel.Writer.WriteAsync(audioCompletion); + } + } + catch (Exception ex) + { + // propagate exception to reader + channel.Writer.TryComplete( + new FoundryLocalException( + "Error processing streaming audio transcription callback data.", ex, _logger)); + failed = true; + } + }, + ct + ).ConfigureAwait(false); + + // use TryComplete as an exception in the callback may have already closed the channel + _ = channel.Writer.TryComplete(); + } + // Ignore cancellation exceptions so we don't convert them into errors + catch (Exception ex) when (ex is not OperationCanceledException) + { + channel.Writer.TryComplete( + new FoundryLocalException("Error executing streaming chat completion.", ex, _logger)); + } + catch (OperationCanceledException) + { + // Complete the channel on cancellation but don't turn it into an error + channel.Writer.TryComplete(); + } + }, ct); + + // Start reading from the channel as items arrive. + // This will continue until ExecuteCommandWithCallbackAsync completes and closes the channel. + await foreach (var item in channel.Reader.ReadAllAsync(ct)) + { + yield return item; + } + } +} diff --git a/sdk_v2/cs/src/OpenAI/AudioTranscriptionRequestResponseTypes.cs b/sdk_v2/cs/src/OpenAI/AudioTranscriptionRequestResponseTypes.cs new file mode 100644 index 0000000..d2dc729 --- /dev/null +++ b/sdk_v2/cs/src/OpenAI/AudioTranscriptionRequestResponseTypes.cs @@ -0,0 +1,49 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.OpenAI; + +using System.Text.Json; + +using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; + +using Microsoft.AI.Foundry.Local; +using Microsoft.AI.Foundry.Local.Detail; + +using Microsoft.Extensions.Logging; + +internal static class AudioTranscriptionRequestResponseExtensions +{ + internal static string ToJson(this AudioCreateTranscriptionRequest request) + { + return JsonSerializer.Serialize(request, JsonSerializationContext.Default.AudioCreateTranscriptionRequest); + } + internal static AudioCreateTranscriptionResponse ToAudioTranscription(this ICoreInterop.Response response, + ILogger logger) + { + if (response.Error != null) + { + logger.LogError("Error from audio_transcribe: {Error}", response.Error); + throw new FoundryLocalException($"Error from audio_transcribe command: {response.Error}"); + } + + return response.Data!.ToAudioTranscription(logger); + } + + internal static AudioCreateTranscriptionResponse ToAudioTranscription(this string responseData, ILogger logger) + { + var typeInfo = JsonSerializationContext.Default.AudioCreateTranscriptionResponse; + var response = JsonSerializer.Deserialize(responseData, typeInfo); + if (response == null) + { + logger.LogError("Failed to deserialize AudioCreateTranscriptionResponse. Json={Data}", responseData); + throw new FoundryLocalException("Failed to deserialize AudioCreateTranscriptionResponse"); + } + + return response; + } +} diff --git a/sdk_v2/cs/src/OpenAI/ChatClient.cs b/sdk_v2/cs/src/OpenAI/ChatClient.cs new file mode 100644 index 0000000..beab7a5 --- /dev/null +++ b/sdk_v2/cs/src/OpenAI/ChatClient.cs @@ -0,0 +1,185 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Channels; + +using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; + +using Microsoft.AI.Foundry.Local.Detail; +using Microsoft.AI.Foundry.Local.OpenAI; +using Microsoft.Extensions.Logging; + +/// +/// Chat Client that uses the OpenAI API. +/// Implemented using Betalgo.Ranul.OpenAI SDK types. +/// +public class OpenAIChatClient +{ + private readonly string _modelId; + + private readonly ICoreInterop _coreInterop = FoundryLocalManager.Instance.CoreInterop; + private readonly ILogger _logger = FoundryLocalManager.Instance.Logger; + + internal OpenAIChatClient(string modelId) + { + _modelId = modelId; + } + + /// + /// Settings that are supported by Foundry Local + /// + public record ChatSettings + { + public float? FrequencyPenalty { get; set; } + public int? MaxTokens { get; set; } + public int? N { get; set; } + public float? Temperature { get; set; } + public float? PresencePenalty { get; set; } + public int? RandomSeed { get; set; } + internal bool? Stream { get; set; } // this is set internally based on the API used + public int? TopK { get; set; } + public float? TopP { get; set; } + } + + /// + /// Settings to use for chat completions using this client. + /// + public ChatSettings Settings { get; } = new(); + + /// + /// Execute a chat completion request. + /// + /// To continue a conversation, add the ChatMessage from the previous response and new prompt to the messages. + /// + /// Chat messages. The system message is automatically added. + /// Optional cancellation token. + /// Chat completion response. + public async Task CompleteChatAsync(IEnumerable messages, + CancellationToken? ct = null) + { + return await Utils.CallWithExceptionHandling( + () => CompleteChatImplAsync(messages, ct), + "Error during chat completion.", _logger).ConfigureAwait(false); + } + + /// + /// Execute a chat completion request with streamed output. + /// + /// To continue a conversation, add the ChatMessage from the previous response and new prompt to the messages. + /// + /// Chat messages. The system message is automatically added. + /// Optional cancellation token. + /// Async enumerable of chat completion responses. + public async IAsyncEnumerable CompleteChatStreamingAsync( + IEnumerable messages, [EnumeratorCancellation] CancellationToken ct) + { + var enumerable = Utils.CallWithExceptionHandling( + () => ChatStreamingImplAsync(messages, ct), + "Error during streaming chat completion.", _logger).ConfigureAwait(false); + + await foreach (var item in enumerable) + { + yield return item; + } + } + + private async Task CompleteChatImplAsync(IEnumerable messages, + CancellationToken? ct) + { + Settings.Stream = false; + + var chatRequest = ChatCompletionCreateRequestExtended.FromUserInput(_modelId, messages, Settings); + var chatRequestJson = chatRequest.ToJson(); + + var request = new CoreInteropRequest { Params = new() { { "OpenAICreateRequest", chatRequestJson } } }; + var response = await _coreInterop.ExecuteCommandAsync("chat_completions", request, + ct ?? CancellationToken.None).ConfigureAwait(false); + + var chatCompletion = response.ToChatCompletion(_logger); + + return chatCompletion; + } + + private async IAsyncEnumerable ChatStreamingImplAsync( + IEnumerable messages, [EnumeratorCancellation] CancellationToken ct) + { + Settings.Stream = true; + + var chatRequest = ChatCompletionCreateRequestExtended.FromUserInput(_modelId, messages, Settings); + var chatRequestJson = chatRequest.ToJson(); + var request = new CoreInteropRequest { Params = new() { { "OpenAICreateRequest", chatRequestJson } } }; + + var channel = Channel.CreateUnbounded( + new UnboundedChannelOptions + { + SingleWriter = true, + SingleReader = true, + AllowSynchronousContinuations = true + }); + + // The callback will push ChatResponse objects into the channel. + // The channel reader will return the values to the user. + // This setup prevents the user from blocking the thread generating the responses. + _ = Task.Run(async () => + { + try + { + var failed = false; + + await _coreInterop.ExecuteCommandWithCallbackAsync( + "chat_completions", + request, + async (callbackData) => + { + try + { + if (!failed) + { + var chatCompletion = callbackData.ToChatCompletion(_logger); + await channel.Writer.WriteAsync(chatCompletion); + } + } + catch (Exception ex) + { + // propagate exception to reader + channel.Writer.TryComplete( + new FoundryLocalException("Error processing streaming chat completion callback data.", + ex, _logger)); + failed = true; + } + }, + ct + ).ConfigureAwait(false); + + // use TryComplete as an exception in the callback may have already closed the channel + _ = channel.Writer.TryComplete(); + } + // Ignore cancellation exceptions so we don't convert them into errors + catch (Exception ex) when (ex is not OperationCanceledException) + { + channel.Writer.TryComplete( + new FoundryLocalException("Error executing streaming chat completion.", ex, _logger)); + } + catch (OperationCanceledException) + { + // Complete the channel on cancellation but don't turn it into an error + channel.Writer.TryComplete(); + } + }, ct); + + // Start reading from the channel as items arrive. + // This will continue until ExecuteCommandWithCallbackAsync completes and closes the channel. + await foreach (var item in channel.Reader.ReadAllAsync(ct)) + { + yield return item; + } + } +} diff --git a/sdk_v2/cs/src/OpenAI/ChatCompletionRequestResponseTypes.cs b/sdk_v2/cs/src/OpenAI/ChatCompletionRequestResponseTypes.cs new file mode 100644 index 0000000..c054a28 --- /dev/null +++ b/sdk_v2/cs/src/OpenAI/ChatCompletionRequestResponseTypes.cs @@ -0,0 +1,95 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.OpenAI; + +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; + +using Microsoft.AI.Foundry.Local; +using Microsoft.AI.Foundry.Local.Detail; +using Microsoft.Extensions.Logging; + +// https://platform.openai.com/docs/api-reference/chat/create +// Using the Betalgo ChatCompletionCreateRequest and extending with the `metadata` field for additional parameters +// which is part of the OpenAI spec but for some reason not part of the Betalgo request object. +internal class ChatCompletionCreateRequestExtended : ChatCompletionCreateRequest +{ + // Valid entries: + // int top_k + // int random_seed + [JsonPropertyName("metadata")] + public Dictionary? Metadata { get; set; } + + internal static ChatCompletionCreateRequestExtended FromUserInput(string modelId, + IEnumerable messages, + OpenAIChatClient.ChatSettings settings) + { + var request = new ChatCompletionCreateRequestExtended + { + Model = modelId, + Messages = messages.ToList(), + + // apply our specific settings + FrequencyPenalty = settings.FrequencyPenalty, + MaxTokens = settings.MaxTokens, + N = settings.N, + Temperature = settings.Temperature, + PresencePenalty = settings.PresencePenalty, + Stream = settings.Stream, + TopP = settings.TopP + }; + + var metadata = new Dictionary(); + + if (settings.TopK.HasValue) + { + metadata["top_k"] = settings.TopK.Value.ToString(CultureInfo.InvariantCulture); + } + + if (settings.RandomSeed.HasValue) + { + metadata["random_seed"] = settings.RandomSeed.Value.ToString(CultureInfo.InvariantCulture); + } + + if (metadata.Count > 0) + { + request.Metadata = metadata; + } + + + return request; + } +} + +internal static class ChatCompletionsRequestResponseExtensions +{ + internal static string ToJson(this ChatCompletionCreateRequestExtended request) + { + return JsonSerializer.Serialize(request, JsonSerializationContext.Default.ChatCompletionCreateRequestExtended); + } + + internal static ChatCompletionCreateResponse ToChatCompletion(this ICoreInterop.Response response, ILogger logger) + { + if (response.Error != null) + { + logger.LogError("Error from chat_completions: {Error}", response.Error); + throw new FoundryLocalException($"Error from chat_completions command: {response.Error}"); + } + + return response.Data!.ToChatCompletion(logger); + } + + internal static ChatCompletionCreateResponse ToChatCompletion(this string responseData, ILogger logger) + { + return JsonSerializer.Deserialize(responseData, JsonSerializationContext.Default.ChatCompletionCreateResponse) + ?? throw new JsonException("Failed to deserialize ChatCompletion"); + } +} diff --git a/sdk_v2/cs/src/Utils.cs b/sdk_v2/cs/src/Utils.cs new file mode 100644 index 0000000..8300a96 --- /dev/null +++ b/sdk_v2/cs/src/Utils.cs @@ -0,0 +1,53 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local; +using System.Text.Json; +using System.Threading.Tasks; + +using Microsoft.AI.Foundry.Local.Detail; +using Microsoft.Extensions.Logging; + +internal class Utils +{ + internal static async Task GetCachedModelIdsAsync(ICoreInterop coreInterop, CancellationToken? ct = null) + { + CoreInteropRequest? input = null; + var result = await coreInterop.ExecuteCommandAsync("get_cached_models", input, ct).ConfigureAwait(false); + if (result.Error != null) + { + throw new FoundryLocalException($"Error getting cached model ids: {result.Error}"); + } + + var typeInfo = JsonSerializationContext.Default.StringArray; + var cachedModelIds = JsonSerializer.Deserialize(result.Data!, typeInfo); + if (cachedModelIds == null) + { + throw new FoundryLocalException($"Failed to deserialized cached model names. Json:'{result.Data!}'"); + } + + return cachedModelIds; + } + + // Helper to wrap function calls with consistent exception handling + internal static T CallWithExceptionHandling(Func func, string errorMsg, ILogger logger) + { + try + { + return func(); + } + // we ignore OperationCanceledException to allow proper cancellation propagation + // this also covers TaskCanceledException since it derives from OperationCanceledException + catch (Exception ex) when (ex is not OperationCanceledException) + { + if (ex is FoundryLocalException) + { + throw; + } + throw new FoundryLocalException(errorMsg, ex, logger); + } + } +} diff --git a/sdk_v2/cs/src/msbuild.binlog b/sdk_v2/cs/src/msbuild.binlog new file mode 100644 index 0000000..3beb2b7 Binary files /dev/null and b/sdk_v2/cs/src/msbuild.binlog differ diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs b/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs new file mode 100644 index 0000000..1581901 --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs @@ -0,0 +1,74 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Tests; + +using System.Text; +using System.Threading.Tasks; + +internal sealed class AudioClientTests +{ + private static Model? model; + + [Before(Class)] + public static async Task Setup() + { + var manager = FoundryLocalManager.Instance; // initialized by Utils + var catalog = await manager.GetCatalogAsync(); + var model = await catalog.GetModelAsync("whisper-tiny").ConfigureAwait(false); + await Assert.That(model).IsNotNull(); + + await model.LoadAsync().ConfigureAwait(false); + await Assert.That(await model.IsLoadedAsync()).IsTrue(); + + AudioClientTests.model = model; + } + + [Test] + public async Task AudioTranscription_NoStreaming_Succeeds() + { + var audioClient = await model!.GetAudioClientAsync(); + await Assert.That(audioClient).IsNotNull(); + + + var audioFilePath = "testdata/Recording.mp3"; + + var response = await audioClient.TranscribeAudioAsync(audioFilePath).ConfigureAwait(false); + + await Assert.That(response).IsNotNull(); + await Assert.That(response.Text).IsNotNull().And.IsNotEmpty(); + var content = response.Text; + await Assert.That(content).IsEqualTo(" And lots of times you need to give people more than one link at a time. You a band could give their fans a couple new videos from the live concert behind the scenes photo gallery and album to purchase like these next few links."); + Console.WriteLine($"Response: {content}"); + } + + [Test] + public async Task AudioTranscription_Streaming_Succeeds() + { + var audioClient = await model!.GetAudioClientAsync(); + await Assert.That(audioClient).IsNotNull(); + + + var audioFilePath = "testdata/Recording.mp3"; + + var updates = audioClient.TranscribeAudioStreamingAsync(audioFilePath, CancellationToken.None).ConfigureAwait(false); + + StringBuilder responseMessage = new(); + await foreach (var response in updates) + { + await Assert.That(response).IsNotNull(); + await Assert.That(response.Text).IsNotNull().And.IsNotEmpty(); + var content = response.Text; + responseMessage.Append(content); + } + + var fullResponse = responseMessage.ToString(); + Console.WriteLine(fullResponse); + await Assert.That(fullResponse).IsEqualTo(" And lots of times you need to give people more than one link at a time. You a band could give their fans a couple new videos from the live concert behind the scenes photo gallery and album to purchase like these next few links."); + + + } +} diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs b/sdk_v2/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs new file mode 100644 index 0000000..0f1c7c6 --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/ChatCompletionsTests.cs @@ -0,0 +1,131 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Tests; + +using System.Text; +using System.Threading.Tasks; + +using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; + +internal sealed class ChatCompletionsTests +{ + private static Model? model; + + [Before(Class)] + public static async Task Setup() + { + var manager = FoundryLocalManager.Instance; // initialized by Utils + var catalog = await manager.GetCatalogAsync(); + + // Load the specific cached model variant directly + var modelVariant = await catalog.GetModelVariantAsync("qwen2.5-0.5b-instruct-generic-cpu:4").ConfigureAwait(false); + await Assert.That(modelVariant).IsNotNull(); + + var model = new Model(modelVariant!, manager.Logger); + await model.LoadAsync().ConfigureAwait(false); + await Assert.That(await model.IsLoadedAsync()).IsTrue(); + + ChatCompletionsTests.model = model; + } + + [Test] + public async Task DirectChat_NoStreaming_Succeeds() + { + var chatClient = await model!.GetChatClientAsync(); + await Assert.That(chatClient).IsNotNull(); + + chatClient.Settings.MaxTokens = 500; + chatClient.Settings.Temperature = 0.0f; // for deterministic results + + List messages = new() + { + // System prompt is setup by GenAI + new ChatMessage { Role = "user", Content = "You are a calculator. Be precise. What is the answer to 7 multiplied by 6?" } + }; + + var response = await chatClient.CompleteChatAsync(messages).ConfigureAwait(false); + + await Assert.That(response).IsNotNull(); + await Assert.That(response.Choices).IsNotNull().And.IsNotEmpty(); + var message = response.Choices[0].Message; + await Assert.That(message).IsNotNull(); + await Assert.That(message.Role).IsEqualTo("assistant"); + await Assert.That(message.Content).IsNotNull(); + await Assert.That(message.Content).Contains("42"); + Console.WriteLine($"Response: {message.Content}"); + + messages.Add(new ChatMessage { Role = "assistant", Content = message.Content }); + + messages.Add(new ChatMessage + { + Role = "user", + Content = "Is the answer a real number?" + }); + + response = await chatClient.CompleteChatAsync(messages).ConfigureAwait(false); + message = response.Choices[0].Message; + await Assert.That(message.Content).IsNotNull(); + await Assert.That(message.Content).Contains("Yes"); + } + + [Test] + public async Task DirectChat_Streaming_Succeeds() + { + var chatClient = await model!.GetChatClientAsync(); + await Assert.That(chatClient).IsNotNull(); + + chatClient.Settings.MaxTokens = 500; + chatClient.Settings.Temperature = 0.0f; // for deterministic results + + List messages = new() + { + new ChatMessage { Role = "user", Content = "You are a calculator. Be precise. What is the answer to 7 multiplied by 6?" } + }; + + var updates = chatClient.CompleteChatStreamingAsync(messages, CancellationToken.None).ConfigureAwait(false); + + StringBuilder responseMessage = new(); + await foreach (var response in updates) + { + await Assert.That(response).IsNotNull(); + await Assert.That(response.Choices).IsNotNull().And.IsNotEmpty(); + var message = response.Choices[0].Message; + await Assert.That(message).IsNotNull(); + await Assert.That(message.Role).IsEqualTo("assistant"); + await Assert.That(message.Content).IsNotNull(); + responseMessage.Append(message.Content); + } + + var fullResponse = responseMessage.ToString(); + Console.WriteLine(fullResponse); + await Assert.That(fullResponse).Contains("42"); + + messages.Add(new ChatMessage { Role = "assistant", Content = fullResponse }); + messages.Add(new ChatMessage + { + Role = "user", + Content = "Add 25 to the previous answer. Think hard to be sure of the answer." + }); + + updates = chatClient.CompleteChatStreamingAsync(messages, CancellationToken.None).ConfigureAwait(false); + responseMessage.Clear(); + await foreach (var response in updates) + { + await Assert.That(response).IsNotNull(); + await Assert.That(response.Choices).IsNotNull().And.IsNotEmpty(); + var message = response.Choices[0].Message; + await Assert.That(message).IsNotNull(); + await Assert.That(message.Role).IsEqualTo("assistant"); + await Assert.That(message.Content).IsNotNull(); + responseMessage.Append(message.Content); + } + + fullResponse = responseMessage.ToString(); + Console.WriteLine(fullResponse); + await Assert.That(fullResponse).Contains("67"); + } +} diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/EndToEnd.cs b/sdk_v2/cs/test/FoundryLocal.Tests/EndToEnd.cs new file mode 100644 index 0000000..80ab4c0 --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/EndToEnd.cs @@ -0,0 +1,80 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Tests; +using System; +using System.Threading.Tasks; + +internal sealed class EndToEnd +{ + // end-to-end using real catalog. run manually as a standalone test as it alters the model cache. + [Test] + public async Task EndToEndTest_Succeeds() + { + var manager = FoundryLocalManager.Instance; // initialized by Utils + var catalog = await manager.GetCatalogAsync(); + + var models = await catalog.ListModelsAsync().ConfigureAwait(false); + + await Assert.That(models).IsNotNull(); + await Assert.That(models.Count).IsGreaterThan(0); + + // Load the specific cached model variant directly + var modelVariant = await catalog.GetModelVariantAsync("qwen2.5-0.5b-instruct-generic-cpu:4") + .ConfigureAwait(false); + + await Assert.That(modelVariant).IsNotNull(); + await Assert.That(modelVariant!.Alias).IsEqualTo("qwen2.5-0.5b"); + + // Create model from the specific variant + var model = new Model(modelVariant, manager.Logger); + + // uncomment this to remove the model first to test the download progress + // only do this when manually testing as other tests expect the model to be cached + //await modelVariant.RemoveFromCacheAsync().ConfigureAwait(false); + //await Assert.That(modelVariant.IsCached).IsFalse(); // check variant status matches + + var expectedCallbackUsed = !await modelVariant.IsCachedAsync(); + var progressValues = new List(); + var addProgressCallbackValue = new Action(progressValues.Add); + + await model.DownloadAsync(addProgressCallbackValue); + + if (expectedCallbackUsed) + { + await Assert.That(progressValues).IsNotEmpty(); + await Assert.That(progressValues[^1]).IsEqualTo(100.0f); + } + else + { + await Assert.That(progressValues).IsEmpty(); // no callback if already cached + } + + await Assert.That(await modelVariant.IsCachedAsync()).IsTrue(); // check variant status matches + + var path = await modelVariant.GetPathAsync().ConfigureAwait(false); + var modelPath = await model.GetPathAsync().ConfigureAwait(false); + await Assert.That(path).IsNotNull(); + await Assert.That(modelPath).IsEqualTo(path); + + await modelVariant.LoadAsync().ConfigureAwait(false); + await Assert.That(await modelVariant.IsLoadedAsync()).IsTrue(); + await Assert.That(await model.IsLoadedAsync()).IsTrue(); + + // check we get the same info from the web service + await manager.StartWebServiceAsync(); + await Assert.That(manager.Urls).IsNotNull(); + var serviceUri = new Uri(manager.Urls![0]); + + // create model load manager that queries the web service + var loadedModels = await catalog.GetLoadedModelsAsync().ConfigureAwait(false); + await Assert.That(loadedModels).Contains(modelVariant); + + // Unload happens in TestAssemblySetupCleanup so tests don't affect each other. + //await modelVariant.UnloadAsync().ConfigureAwait(false); + //await Assert.That(modelVariant.IsLoaded).IsFalse(); + } +} diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs b/sdk_v2/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs new file mode 100644 index 0000000..5227e06 --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/FoundryLocalManagerTest.cs @@ -0,0 +1,103 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Tests; + +using System; + +using Microsoft.AI.Foundry.Local; +using Microsoft.AI.Foundry.Local.Detail; + +public class FoundryLocalManagerTests +{ + [Test] + public async Task Manager_GetCatalog_Succeeds() + { + var catalog = await FoundryLocalManager.Instance.GetCatalogAsync() as Catalog; + await Assert.That(catalog).IsNotNull(); + await Assert.That(catalog!.Name).IsNotNullOrWhitespace(); + + var models = await catalog.ListModelsAsync(); + await Assert.That(models).IsNotNull().And.IsNotEmpty(); + + foreach (var model in models) + { + Console.WriteLine($"Model Alias: {model.Alias}, Variants: {model.Variants.Count}"); + Console.WriteLine($"Selected Variant Id: {model.SelectedVariant?.Id ?? "none"}"); + + // variants should be in sorted order + + DeviceType lastDeviceType = DeviceType.Invalid; + var lastName = string.Empty; + var lastVersion = int.MaxValue; + + foreach (var variant in model.Variants) + { + Console.WriteLine($" Id: {variant.Id}, Cached={variant.Info.Cached}"); + + // variants are grouped by name + // check if variants are sorted by device type and version + if ((variant.Info.Name == lastName) && + ((variant.Info.Runtime?.DeviceType > lastDeviceType) || + (variant.Info.Runtime?.DeviceType == lastDeviceType && variant.Info.Version > lastVersion))) + { + Assert.Fail($"Variant {variant.Id} is not in the expected order."); + } + + lastDeviceType = variant.Info.Runtime?.DeviceType ?? DeviceType.Invalid; + lastName = variant.Info.Name; + lastVersion = variant.Info.Version; + } + } + } + + [Test] + public async Task Catalog_ListCachedLoadUnload_Succeeds() + { + List logSink = new(); + var logger = Utils.CreateCapturingLoggerMock(logSink); + using var loadManager = new ModelLoadManager(null, Utils.CoreInterop, logger.Object); + + List intercepts = new() + { + new Utils.InteropCommandInterceptInfo + { + CommandName = "initialize", + CommandInput = null, + ResponseData = "Success", + ResponseError = null + } + }; + var coreInterop = Utils.CreateCoreInteropWithIntercept(Utils.CoreInterop, intercepts); + using var catalog = await Catalog.CreateAsync(loadManager, coreInterop.Object, logger.Object); + await Assert.That(catalog).IsNotNull(); + + var models = await catalog.ListModelsAsync(); + await Assert.That(models).IsNotNull().And.IsNotEmpty(); + + var cachedModels = await catalog.GetCachedModelsAsync(); + await Assert.That(cachedModels).IsNotNull(); + + if (cachedModels.Count == 0) + { + Console.WriteLine("No cached models found; skipping get path/load/unload test."); + return; + } + + // find smallest. pick first if no local models have size info. + var smallest = cachedModels.Where(m => m.Info.FileSizeMb > 0).OrderBy(m => m.Info.FileSizeMb).FirstOrDefault(); + var variant = smallest ?? cachedModels[0]; + + Console.WriteLine($"Testing GetPath/Load/Unload with ModelId: {variant.Id}"); + var path = await variant.GetPathAsync(); + Console.WriteLine($"Model path: {path}"); + await variant.LoadAsync(); + + // We unload any loaded models during cleanup for all tests + // await variant.UnloadAsync(); + } +} + diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/LOCAL_MODEL_TESTING.md b/sdk_v2/cs/test/FoundryLocal.Tests/LOCAL_MODEL_TESTING.md new file mode 100644 index 0000000..d3ff4da --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/LOCAL_MODEL_TESTING.md @@ -0,0 +1,20 @@ +# Running Local Model Tests + +## Configuration + +The test model cache directory name is configured in `sdk_v2/cs/test/FoundryLocal.Tests/appsettings.Test.json`: + +```json +{ + "TestModelCacheDirName": "/path/to/model/cache" +} +``` + +## Run the tests + +The tests will automatically find the models in the configured test model cache directory. + +```bash +cd /path/to/parent-dir/foundry-local-sdk/sdk_v2/cs/test/FoundryLocal.Tests +dotnet test Microsoft.AI.Foundry.Local.Tests.csproj --configuration Release +``` \ No newline at end of file diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj b/sdk_v2/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj new file mode 100644 index 0000000..15a33f7 --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj @@ -0,0 +1,55 @@ + + + + net9.0 + enable + enable + false + true + false + + + + + + + + $(NETCoreSdkRuntimeIdentifier) + + + + net9.0-windows10.0.26100.0 + 10.0.17763.0 + None + true + + + + + + PreserveNewest + + + + + + PreserveNewest + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/ModelTests.cs b/sdk_v2/cs/test/FoundryLocal.Tests/ModelTests.cs new file mode 100644 index 0000000..b5a4965 --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/ModelTests.cs @@ -0,0 +1,54 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Tests; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging.Abstractions; + +using Moq; + +internal sealed class ModelTests +{ + [Test] + public async Task GetLastestVersion_Works() + { + var loadManager = new Mock(); + var coreInterop = new Mock(); + var logger = NullLogger.Instance; + + var createModelInfo = (string name, int version) => new ModelInfo + { + Id = $"{name}:{version}", + Alias = "model", + Name = name, + Version = version, + Uri = "local://model", + ProviderType = "local", + ModelType = "test" + }; + + var variants = new List + { + new(createModelInfo("model_a", 4), loadManager.Object, coreInterop.Object, logger), + new(createModelInfo("model_b", 3), loadManager.Object, coreInterop.Object, logger), + new(createModelInfo("model_b", 2), loadManager.Object, coreInterop.Object, logger), + }; + + var model = new Model(variants[0], NullLogger.Instance); + foreach (var variant in variants.Skip(1)) + { + model.AddVariant(variant); + } + + var latestA = model.GetLatestVersion(variants[0]); + await Assert.That(latestA).IsEqualTo(variants[0]); + + var latestB = model.GetLatestVersion(variants[2]); + await Assert.That(latestB).IsEqualTo(variants[1]); + } +} diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/SkipInCIAttribute.cs b/sdk_v2/cs/test/FoundryLocal.Tests/SkipInCIAttribute.cs new file mode 100644 index 0000000..c4d17e5 --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/SkipInCIAttribute.cs @@ -0,0 +1,19 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Tests; + +using TUnit.Core; + +using System.Threading.Tasks; + +public class SkipInCIAttribute() : SkipAttribute("This test is only supported locally. Skipped on CIs.") +{ + public override Task ShouldSkip(TestRegisteredContext context) + { + return Task.FromResult(Utils.IsRunningInCI()); + } +} diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs b/sdk_v2/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs new file mode 100644 index 0000000..ac536d1 --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/TestAssemblySetupCleanup.cs @@ -0,0 +1,36 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Tests; +using System.Threading.Tasks; + +internal static class TestAssemblySetupCleanup +{ + + [After(Assembly)] + public static async Task Cleanup(AssemblyHookContext _) + { + try + { + // ensure any loaded models are unloaded + var manager = FoundryLocalManager.Instance; // initialized by Utils + var catalog = await manager.GetCatalogAsync(); + var models = await catalog.GetLoadedModelsAsync().ConfigureAwait(false); + + foreach (var model in models) + { + await Assert.That(await model.IsLoadedAsync()).IsTrue(); + await model.UnloadAsync().ConfigureAwait(false); + await Assert.That(await model.IsLoadedAsync()).IsFalse(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error during Cleanup: {ex}"); + throw; + } + } +} diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/Utils.cs b/sdk_v2/cs/test/FoundryLocal.Tests/Utils.cs new file mode 100644 index 0000000..04ee3fa --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/Utils.cs @@ -0,0 +1,451 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Tests; + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text.Json; + +using Microsoft.AI.Foundry.Local.Detail; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +using Microsoft.VisualStudio.TestPlatform.TestHost; + +using Moq; + +internal static class Utils +{ + internal struct TestCatalogInfo + { + internal readonly List TestCatalog { get; } + internal readonly string ModelListJson { get; } + + internal TestCatalogInfo(bool includeCuda) + { + + TestCatalog = Utils.BuildTestCatalog(includeCuda); + ModelListJson = JsonSerializer.Serialize(TestCatalog, JsonSerializationContext.Default.ListModelInfo); + } + } + + internal static readonly TestCatalogInfo TestCatalog = new(true); + + [Before(Assembly)] + public static void AssemblyInit(AssemblyHookContext _) + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder + .AddConsole() + .SetMinimumLevel(LogLevel.Debug); + }); + + ILogger logger = loggerFactory.CreateLogger(); + + // Read configuration from appsettings.Test.json + logger.LogDebug("Reading configuration from appsettings.Test.json"); + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.Test.json", optional: true, reloadOnChange: false) + .Build(); + + var testModelCacheDirName = configuration["TestModelCacheDirName"] ?? "test-data-shared"; + string testDataSharedPath; + if (Path.IsPathRooted(testModelCacheDirName) || + testModelCacheDirName.Contains(Path.DirectorySeparatorChar) || + testModelCacheDirName.Contains(Path.AltDirectorySeparatorChar)) + { + // It's a relative or complete filepath, resolve from current directory + testDataSharedPath = Path.GetFullPath(testModelCacheDirName); + } + else + { + // It's just a directory name, combine with repo root parent + testDataSharedPath = Path.GetFullPath(Path.Combine(GetRepoRoot(), "..", testModelCacheDirName)); + } + + logger.LogInformation("Using test model cache directory: {testDataSharedPath}", testDataSharedPath); + + if (!Directory.Exists(testDataSharedPath)) + { + throw new DirectoryNotFoundException($"Test model cache directory does not exist: {testDataSharedPath}"); + + } + + var config = new Configuration + { + AppName = "FoundryLocalSdkTest", + LogLevel = Local.LogLevel.Debug, + Web = new Configuration.WebService + { + Urls = "http://127.0.0.1:0" + }, + ModelCacheDir = testDataSharedPath + }; + + // Initialize the singleton instance. + FoundryLocalManager.CreateAsync(config, logger).GetAwaiter().GetResult(); + + // standalone instance for testing individual components that skips the 'initialize' command + CoreInterop = new CoreInterop(logger); + } + + internal static ICoreInterop CoreInterop { get; private set; } = default!; + + internal static Mock CreateCapturingLoggerMock(List sink) + { + var mock = new Mock(); + mock.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny())) + .Callback((LogLevel level, EventId id, object state, Exception? ex, Delegate formatter) => + { + var message = formatter.DynamicInvoke(state, ex) as string; + sink.Add($"{level}: {message}"); + }); + + return mock; + } + + internal sealed record InteropCommandInterceptInfo + { + public string CommandName { get; init; } = default!; + public string? CommandInput { get; init; } + public string ResponseData { get; init; } = default!; + public string? ResponseError { get; init; } + } + + internal static Mock CreateCoreInteropWithIntercept(ICoreInterop coreInterop, + List intercepts) + { + var mock = new Mock(); + var interceptNames = new HashSet(StringComparer.InvariantCulture); + + foreach (var intercept in intercepts) + { + if (!interceptNames.Add(intercept.CommandName)) + { + throw new ArgumentException($"Duplicate intercept for command {intercept.CommandName}"); + } + + mock.Setup(x => x.ExecuteCommand(It.Is(s => s == intercept.CommandName), It.IsAny())) + .Returns(new ICoreInterop.Response + { + Data = intercept.ResponseData, + Error = intercept.ResponseError + }); + + mock.Setup(x => x.ExecuteCommandAsync(It.Is(s => s == intercept.CommandName), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new ICoreInterop.Response + { + Data = intercept.ResponseData, + Error = intercept.ResponseError + }); + } + + mock.Setup(x => x.ExecuteCommand(It.Is(s => !interceptNames.Contains(s)), + It.IsAny())) + .Returns((string commandName, CoreInteropRequest? commandInput) => + coreInterop.ExecuteCommand(commandName, commandInput)); + + mock.Setup(x => x.ExecuteCommandAsync(It.Is(s => !interceptNames.Contains(s)), + It.IsAny(), + It.IsAny())) + .Returns((string commandName, CoreInteropRequest? commandInput, CancellationToken? ct) => + coreInterop.ExecuteCommandAsync(commandName, commandInput, ct)); + + return mock; + } + + internal static bool IsRunningInCI() + { + var azureDevOps = Environment.GetEnvironmentVariable("TF_BUILD"); + var githubActions = Environment.GetEnvironmentVariable("GITHUB_ACTIONS"); + var isCI = string.Equals(azureDevOps, "True", StringComparison.OrdinalIgnoreCase) || + string.Equals(githubActions, "true", StringComparison.OrdinalIgnoreCase); + + return isCI; + } + + private static List BuildTestCatalog(bool includeCuda = true) + { + // Mirrors MOCK_CATALOG_DATA ordering and fields (Python tests) + var common = new + { + ProviderType = "AzureFoundry", + Version = 1, + ModelType = "ONNX", + PromptTemplate = (PromptTemplate?)null, + Publisher = "Microsoft", + Task = "chat-completion", + FileSizeMb = 10403, + ModelSettings = new ModelSettings { Parameters = [] }, + SupportsToolCalling = false, + License = "MIT", + LicenseDescription = "License…", + MaxOutputTokens = 1024L, + MinFLVersion = "1.0.0", + }; + + var list = new List + { + // model-1 generic-gpu, generic-cpu:2, generic-cpu:1 + new() + { + Id = "model-1-generic-gpu:1", + Name = "model-1-generic-gpu", + DisplayName = "model-1-generic-gpu", + Uri = "azureml://registries/azureml/models/model-1-generic-gpu/versions/1", + Runtime = new Runtime { DeviceType = DeviceType.GPU, ExecutionProvider = "WebGpuExecutionProvider" }, + Alias = "model-1", + // ParentModelUri = "azureml://registries/azureml/models/model-1/versions/1", + ProviderType = common.ProviderType, Version = common.Version, ModelType = common.ModelType, + PromptTemplate = common.PromptTemplate, Publisher = common.Publisher, Task = common.Task, + FileSizeMb = common.FileSizeMb, ModelSettings = common.ModelSettings, + SupportsToolCalling = common.SupportsToolCalling, License = common.License, + LicenseDescription = common.LicenseDescription, MaxOutputTokens = common.MaxOutputTokens, + MinFLVersion = common.MinFLVersion + }, + new() + { + Id = "model-1-generic-cpu:2", + Name = "model-1-generic-cpu", + DisplayName = "model-1-generic-cpu", + Uri = "azureml://registries/azureml/models/model-1-generic-cpu/versions/2", + Runtime = new Runtime { DeviceType = DeviceType.CPU, ExecutionProvider = "CPUExecutionProvider" }, + Alias = "model-1", + // ParentModelUri = "azureml://registries/azureml/models/model-1/versions/2", + ProviderType = common.ProviderType, + Version = common.Version, ModelType = common.ModelType, + PromptTemplate = common.PromptTemplate, + Publisher = common.Publisher, Task = common.Task, + FileSizeMb = common.FileSizeMb - 10, // smaller so default chosen in test that sorts on this + ModelSettings = common.ModelSettings, + SupportsToolCalling = common.SupportsToolCalling, + License = common.License, + LicenseDescription = common.LicenseDescription, + MaxOutputTokens = common.MaxOutputTokens, + MinFLVersion = common.MinFLVersion + }, + new() + { + Id = "model-1-generic-cpu:1", + Name = "model-1-generic-cpu", + DisplayName = "model-1-generic-cpu", + Uri = "azureml://registries/azureml/models/model-1-generic-cpu/versions/1", + Runtime = new Runtime { DeviceType = DeviceType.CPU, ExecutionProvider = "CPUExecutionProvider" }, + Alias = "model-1", + //ParentModelUri = "azureml://registries/azureml/models/model-1/versions/1", + ProviderType = common.ProviderType, + Version = common.Version, + ModelType = common.ModelType, + PromptTemplate = common.PromptTemplate, + Publisher = common.Publisher, Task = common.Task, + FileSizeMb = common.FileSizeMb, + ModelSettings = common.ModelSettings, + SupportsToolCalling = common.SupportsToolCalling, + License = common.License, + LicenseDescription = common.LicenseDescription, + MaxOutputTokens = common.MaxOutputTokens, + MinFLVersion = common.MinFLVersion + }, + + // model-2 npu:2, npu:1, generic-cpu:1 + new() + { + Id = "model-2-npu:2", + Name = "model-2-npu", + DisplayName = "model-2-npu", + Uri = "azureml://registries/azureml/models/model-2-npu/versions/2", + Runtime = new Runtime { DeviceType = DeviceType.NPU, ExecutionProvider = "QNNExecutionProvider" }, + Alias = "model-2", + //ParentModelUri = "azureml://registries/azureml/models/model-2/versions/2", + ProviderType = common.ProviderType, + Version = common.Version, ModelType = common.ModelType, + PromptTemplate = common.PromptTemplate, + Publisher = common.Publisher, Task = common.Task, + FileSizeMb = common.FileSizeMb, + ModelSettings = common.ModelSettings, + SupportsToolCalling = common.SupportsToolCalling, + License = common.License, + LicenseDescription = common.LicenseDescription, + MaxOutputTokens = common.MaxOutputTokens, + MinFLVersion = common.MinFLVersion + }, + new() + { + Id = "model-2-npu:1", + Name = "model-2-npu", + DisplayName = "model-2-npu", + Uri = "azureml://registries/azureml/models/model-2-npu/versions/1", + Runtime = new Runtime { DeviceType = DeviceType.NPU, ExecutionProvider = "QNNExecutionProvider" }, + Alias = "model-2", + //ParentModelUri = "azureml://registries/azureml/models/model-2/versions/1", + ProviderType = common.ProviderType, + Version = common.Version, ModelType = common.ModelType, + PromptTemplate = common.PromptTemplate, + Publisher = common.Publisher, Task = common.Task, + FileSizeMb = common.FileSizeMb, + ModelSettings = common.ModelSettings, + SupportsToolCalling = common.SupportsToolCalling, + License = common.License, + LicenseDescription = common.LicenseDescription, + MaxOutputTokens = common.MaxOutputTokens, + MinFLVersion = common.MinFLVersion + }, + new() + { + Id = "model-2-generic-cpu:1", + Name = "model-2-generic-cpu", + DisplayName = "model-2-generic-cpu", + Uri = "azureml://registries/azureml/models/model-2-generic-cpu/versions/1", + Runtime = new Runtime { DeviceType = DeviceType.CPU, ExecutionProvider = "CPUExecutionProvider" }, + Alias = "model-2", + //ParentModelUri = "azureml://registries/azureml/models/model-2/versions/1", + ProviderType = common.ProviderType, + Version = common.Version, ModelType = common.ModelType, + PromptTemplate = common.PromptTemplate, + Publisher = common.Publisher, Task = common.Task, + FileSizeMb = common.FileSizeMb, + ModelSettings = common.ModelSettings, + SupportsToolCalling = common.SupportsToolCalling, + License = common.License, + LicenseDescription = common.LicenseDescription, + MaxOutputTokens = common.MaxOutputTokens, + MinFLVersion = common.MinFLVersion + }, + }; + + // model-3 cuda-gpu (optional), generic-gpu, generic-cpu + if (includeCuda) + { + list.Add(new ModelInfo + { + Id = "model-3-cuda-gpu:1", + Name = "model-3-cuda-gpu", + DisplayName = "model-3-cuda-gpu", + Uri = "azureml://registries/azureml/models/model-3-cuda-gpu/versions/1", + Runtime = new Runtime { DeviceType = DeviceType.GPU, ExecutionProvider = "CUDAExecutionProvider" }, + Alias = "model-3", + //ParentModelUri = "azureml://registries/azureml/models/model-3/versions/1", + ProviderType = common.ProviderType, + Version = common.Version, + ModelType = common.ModelType, + PromptTemplate = common.PromptTemplate, + Publisher = common.Publisher, + Task = common.Task, + FileSizeMb = common.FileSizeMb, + ModelSettings = common.ModelSettings, + SupportsToolCalling = common.SupportsToolCalling, + License = common.License, + LicenseDescription = common.LicenseDescription, + MaxOutputTokens = common.MaxOutputTokens, + MinFLVersion = common.MinFLVersion + }); + } + + list.AddRange(new[] + { + new ModelInfo + { + Id = "model-3-generic-gpu:1", + Name = "model-3-generic-gpu", + DisplayName = "model-3-generic-gpu", + Uri = "azureml://registries/azureml/models/model-3-generic-gpu/versions/1", + Runtime = new Runtime { DeviceType = DeviceType.GPU, ExecutionProvider = "WebGpuExecutionProvider" }, + Alias = "model-3", + //ParentModelUri = "azureml://registries/azureml/models/model-3/versions/1", + ProviderType = common.ProviderType, + Version = common.Version, ModelType = common.ModelType, + PromptTemplate = common.PromptTemplate, + Publisher = common.Publisher, Task = common.Task, + FileSizeMb = common.FileSizeMb, + ModelSettings = common.ModelSettings, + SupportsToolCalling = common.SupportsToolCalling, + License = common.License, + LicenseDescription = common.LicenseDescription, + MaxOutputTokens = common.MaxOutputTokens, + MinFLVersion = common.MinFLVersion + }, + new ModelInfo + { + Id = "model-3-generic-cpu:1", + Name = "model-3-generic-cpu", + DisplayName = "model-3-generic-cpu", + Uri = "azureml://registries/azureml/models/model-3-generic-cpu/versions/1", + Runtime = new Runtime { DeviceType = DeviceType.CPU, ExecutionProvider = "CPUExecutionProvider" }, + Alias = "model-3", + //ParentModelUri = "azureml://registries/azureml/models/model-3/versions/1", + ProviderType = common.ProviderType, + Version = common.Version, + ModelType = common.ModelType, + PromptTemplate = common.PromptTemplate, + Publisher = common.Publisher, Task = common.Task, + FileSizeMb = common.FileSizeMb, + ModelSettings = common.ModelSettings, + SupportsToolCalling = common.SupportsToolCalling, + License = common.License, + LicenseDescription = common.LicenseDescription, + MaxOutputTokens = common.MaxOutputTokens, + MinFLVersion = common.MinFLVersion + } + }); + + // model-4 generic-gpu (nullable prompt) + list.Add(new ModelInfo + { + Id = "model-4-generic-gpu:1", + Name = "model-4-generic-gpu", + DisplayName = "model-4-generic-gpu", + Uri = "azureml://registries/azureml/models/model-4-generic-gpu/versions/1", + Runtime = new Runtime { DeviceType = DeviceType.GPU, ExecutionProvider = "WebGpuExecutionProvider" }, + Alias = "model-4", + //ParentModelUri = "azureml://registries/azureml/models/model-4/versions/1", + ProviderType = common.ProviderType, + Version = common.Version, + ModelType = common.ModelType, + PromptTemplate = null, + Publisher = common.Publisher, + Task = common.Task, + FileSizeMb = common.FileSizeMb, + ModelSettings = common.ModelSettings, + SupportsToolCalling = common.SupportsToolCalling, + License = common.License, + LicenseDescription = common.LicenseDescription, + MaxOutputTokens = common.MaxOutputTokens, + MinFLVersion = common.MinFLVersion + }); + + return list; + } + + private static string GetSourceFilePath([CallerFilePath] string path = "") => path; + + // Gets the root directory of the foundry-local-sdk repository by finding the .git directory. + private static string GetRepoRoot() + { + var sourceFile = GetSourceFilePath(); + var dir = new DirectoryInfo(Path.GetDirectoryName(sourceFile)!); + + while (dir != null) + { + if (Directory.Exists(Path.Combine(dir.FullName, ".git"))) + return dir.FullName; + + dir = dir.Parent; + } + + throw new InvalidOperationException("Could not find git repository root from test file location"); + } +} diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/appsettings.Test.json b/sdk_v2/cs/test/FoundryLocal.Tests/appsettings.Test.json new file mode 100644 index 0000000..d42d878 --- /dev/null +++ b/sdk_v2/cs/test/FoundryLocal.Tests/appsettings.Test.json @@ -0,0 +1,3 @@ +{ + "TestModelCacheDirName": "test-data-shared" +} diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/testdata/Recording.mp3 b/sdk_v2/cs/test/FoundryLocal.Tests/testdata/Recording.mp3 new file mode 100644 index 0000000..deb3841 Binary files /dev/null and b/sdk_v2/cs/test/FoundryLocal.Tests/testdata/Recording.mp3 differ diff --git a/sdk_v2/js/LICENSE.txt b/sdk_v2/js/LICENSE.txt new file mode 100644 index 0000000..48bc6bb --- /dev/null +++ b/sdk_v2/js/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk_v2/js/README.md b/sdk_v2/js/README.md new file mode 100644 index 0000000..819f1c1 --- /dev/null +++ b/sdk_v2/js/README.md @@ -0,0 +1,98 @@ +# Foundry Local JS SDK + +The Foundry Local JS SDK provides a JavaScript/TypeScript interface for interacting with local AI models via the Foundry Local Core. It allows you to discover, download, load, and run inference on models directly on your local machine. + +## Installation + +To install the SDK, run the following command in your project directory: + +```bash +npm install foundry-local-js-sdk +``` + +*Note: Ensure you have the necessary native dependencies configured as per the main repository instructions.* + +## Usage + +### Initialization + +Initialize the `FoundryLocalManager` with your configuration. + +```typescript +import { FoundryLocalManager } from 'foundry-local-js-sdk'; + +const manager = FoundryLocalManager.create({ + libraryPath: '/path/to/core/library', + modelCacheDir: '/path/to/model/cache', + logLevel: 'info' +}); +``` + +### Discovering Models + +Use the `Catalog` to list available models. + +```typescript +const catalog = manager.catalog; +const models = catalog.models; + +models.forEach(model => { + console.log(`Model: ${model.alias}`); +}); +``` + +### Loading and Running a Model + +```typescript +const model = catalog.getModel('phi-3-mini'); + +if (model) { + await model.load(); + + const chatClient = model.createChatClient(); + const response = await chatClient.completeChat([ + { role: 'user', content: 'Hello, how are you?' } + ]); + + console.log(response.choices[0].message.content); + + await model.unload(); +} +``` + +## Documentation + +The SDK source code is documented using TSDoc. You can generate the API documentation using TypeDoc. + +### Generating Docs + +Run the following command to generate the HTML documentation in the `docs` folder: + +```bash +npm run docs +``` + +Open `docs/index.html` in your browser to view the documentation. + +## Running Tests + +To run the tests, use: + +```bash +npm test +``` + +See `test/README.md` for more details on setting up and running tests. + +## Running Examples + +The SDK includes an example script demonstrating chat completion. To run it: + +1. Ensure you have the necessary core libraries and a model available (see Tests Prerequisites). +2. Run the example command: + +```bash +npm run example +``` + +This will execute `examples/chat-completion.ts`. diff --git a/sdk_v2/js/docs/.nojekyll b/sdk_v2/js/docs/.nojekyll new file mode 100644 index 0000000..e2ac661 --- /dev/null +++ b/sdk_v2/js/docs/.nojekyll @@ -0,0 +1 @@ +TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. \ No newline at end of file diff --git a/sdk_v2/js/docs/assets/hierarchy.js b/sdk_v2/js/docs/assets/hierarchy.js new file mode 100644 index 0000000..d273811 --- /dev/null +++ b/sdk_v2/js/docs/assets/hierarchy.js @@ -0,0 +1 @@ +window.hierarchyData = "eJyNjrsOwiAYhd/lzNSWXojhDRxcXZoOpNCUSCEBnBre3VAvUQd1+Yfzf+eywjsXA3hP22Yg8Goyaoza2QC+gnX5WrEocBydVAYEZ20lOK33BBdvwDEaEYIK5Qbs5rhkahPBEYMssqO4CYmAVuwz9SS8Fjb+DL9zf3S0zUvH4X163bFHurZR+UmMKpSH7/OfZH7O2kivLHjPOkIrNqSUrtvQavw=" \ No newline at end of file diff --git a/sdk_v2/js/docs/assets/highlight.css b/sdk_v2/js/docs/assets/highlight.css new file mode 100644 index 0000000..9f769d7 --- /dev/null +++ b/sdk_v2/js/docs/assets/highlight.css @@ -0,0 +1,85 @@ +:root { + --light-hl-0: #795E26; + --dark-hl-0: #DCDCAA; + --light-hl-1: #000000; + --dark-hl-1: #D4D4D4; + --light-hl-2: #A31515; + --dark-hl-2: #CE9178; + --light-hl-3: #AF00DB; + --dark-hl-3: #C586C0; + --light-hl-4: #001080; + --dark-hl-4: #9CDCFE; + --light-hl-5: #0000FF; + --dark-hl-5: #569CD6; + --light-hl-6: #0070C1; + --dark-hl-6: #4FC1FF; + --light-hl-7: #000000FF; + --dark-hl-7: #D4D4D4; + --light-hl-8: #098658; + --dark-hl-8: #B5CEA8; + --light-code-background: #FFFFFF; + --dark-code-background: #1E1E1E; +} + +@media (prefers-color-scheme: light) { :root { + --hl-0: var(--light-hl-0); + --hl-1: var(--light-hl-1); + --hl-2: var(--light-hl-2); + --hl-3: var(--light-hl-3); + --hl-4: var(--light-hl-4); + --hl-5: var(--light-hl-5); + --hl-6: var(--light-hl-6); + --hl-7: var(--light-hl-7); + --hl-8: var(--light-hl-8); + --code-background: var(--light-code-background); +} } + +@media (prefers-color-scheme: dark) { :root { + --hl-0: var(--dark-hl-0); + --hl-1: var(--dark-hl-1); + --hl-2: var(--dark-hl-2); + --hl-3: var(--dark-hl-3); + --hl-4: var(--dark-hl-4); + --hl-5: var(--dark-hl-5); + --hl-6: var(--dark-hl-6); + --hl-7: var(--dark-hl-7); + --hl-8: var(--dark-hl-8); + --code-background: var(--dark-code-background); +} } + +:root[data-theme='light'] { + --hl-0: var(--light-hl-0); + --hl-1: var(--light-hl-1); + --hl-2: var(--light-hl-2); + --hl-3: var(--light-hl-3); + --hl-4: var(--light-hl-4); + --hl-5: var(--light-hl-5); + --hl-6: var(--light-hl-6); + --hl-7: var(--light-hl-7); + --hl-8: var(--light-hl-8); + --code-background: var(--light-code-background); +} + +:root[data-theme='dark'] { + --hl-0: var(--dark-hl-0); + --hl-1: var(--dark-hl-1); + --hl-2: var(--dark-hl-2); + --hl-3: var(--dark-hl-3); + --hl-4: var(--dark-hl-4); + --hl-5: var(--dark-hl-5); + --hl-6: var(--dark-hl-6); + --hl-7: var(--dark-hl-7); + --hl-8: var(--dark-hl-8); + --code-background: var(--dark-code-background); +} + +.hl-0 { color: var(--hl-0); } +.hl-1 { color: var(--hl-1); } +.hl-2 { color: var(--hl-2); } +.hl-3 { color: var(--hl-3); } +.hl-4 { color: var(--hl-4); } +.hl-5 { color: var(--hl-5); } +.hl-6 { color: var(--hl-6); } +.hl-7 { color: var(--hl-7); } +.hl-8 { color: var(--hl-8); } +pre, code { background: var(--code-background); } diff --git a/sdk_v2/js/docs/assets/icons.js b/sdk_v2/js/docs/assets/icons.js new file mode 100644 index 0000000..58882d7 --- /dev/null +++ b/sdk_v2/js/docs/assets/icons.js @@ -0,0 +1,18 @@ +(function() { + addIcons(); + function addIcons() { + if (document.readyState === "loading") return document.addEventListener("DOMContentLoaded", addIcons); + const svg = document.body.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); + svg.innerHTML = `MMNEPVFCICPMFPCPTTAAATR`; + svg.style.display = "none"; + if (location.protocol === "file:") updateUseElements(); + } + + function updateUseElements() { + document.querySelectorAll("use").forEach(el => { + if (el.getAttribute("href").includes("#icon-")) { + el.setAttribute("href", el.getAttribute("href").replace(/.*#/, "#")); + } + }); + } +})() \ No newline at end of file diff --git a/sdk_v2/js/docs/assets/icons.svg b/sdk_v2/js/docs/assets/icons.svg new file mode 100644 index 0000000..50ad579 --- /dev/null +++ b/sdk_v2/js/docs/assets/icons.svg @@ -0,0 +1 @@ +MMNEPVFCICPMFPCPTTAAATR \ No newline at end of file diff --git a/sdk_v2/js/docs/assets/main.js b/sdk_v2/js/docs/assets/main.js new file mode 100644 index 0000000..64b80ab --- /dev/null +++ b/sdk_v2/js/docs/assets/main.js @@ -0,0 +1,60 @@ +"use strict"; +window.translations={"copy":"Copy","copied":"Copied!","normally_hidden":"This member is normally hidden due to your filter settings.","hierarchy_expand":"Expand","hierarchy_collapse":"Collapse","folder":"Folder","search_index_not_available":"The search index is not available","search_no_results_found_for_0":"No results found for {0}","kind_1":"Project","kind_2":"Module","kind_4":"Namespace","kind_8":"Enumeration","kind_16":"Enumeration Member","kind_32":"Variable","kind_64":"Function","kind_128":"Class","kind_256":"Interface","kind_512":"Constructor","kind_1024":"Property","kind_2048":"Method","kind_4096":"Call Signature","kind_8192":"Index Signature","kind_16384":"Constructor Signature","kind_32768":"Parameter","kind_65536":"Type Literal","kind_131072":"Type Parameter","kind_262144":"Accessor","kind_524288":"Get Signature","kind_1048576":"Set Signature","kind_2097152":"Type Alias","kind_4194304":"Reference","kind_8388608":"Document"}; +"use strict";(()=>{var Ke=Object.create;var he=Object.defineProperty;var Ge=Object.getOwnPropertyDescriptor;var Ze=Object.getOwnPropertyNames;var Xe=Object.getPrototypeOf,Ye=Object.prototype.hasOwnProperty;var et=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var tt=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Ze(e))!Ye.call(t,i)&&i!==n&&he(t,i,{get:()=>e[i],enumerable:!(r=Ge(e,i))||r.enumerable});return t};var nt=(t,e,n)=>(n=t!=null?Ke(Xe(t)):{},tt(e||!t||!t.__esModule?he(n,"default",{value:t,enumerable:!0}):n,t));var ye=et((me,ge)=>{(function(){var t=function(e){var n=new t.Builder;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),n.searchPipeline.add(t.stemmer),e.call(n,n),n.build()};t.version="2.3.9";t.utils={},t.utils.warn=(function(e){return function(n){e.console&&console.warn&&console.warn(n)}})(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var n=Object.create(null),r=Object.keys(e),i=0;i0){var d=t.utils.clone(n)||{};d.position=[a,l],d.index=s.length,s.push(new t.Token(r.slice(a,o),d))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,n){n in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. +`,e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(r){var i=t.Pipeline.registeredFunctions[r];if(i)n.add(i);else throw new Error("Cannot load unregistered function: "+r)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(n){t.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");r=r+1,this._stack.splice(r,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");this._stack.splice(r,0,n)},t.Pipeline.prototype.remove=function(e){var n=this._stack.indexOf(e);n!=-1&&this._stack.splice(n,1)},t.Pipeline.prototype.run=function(e){for(var n=this._stack.length,r=0;r1&&(oe&&(r=s),o!=e);)i=r-n,s=n+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(oc?d+=2:a==c&&(n+=r[l+1]*i[d+1],l+=2,d+=2);return n},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),n=1,r=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var c=s.node.edges["*"];else{var c=new t.TokenSet;s.node.edges["*"]=c}if(s.str.length==0&&(c.final=!0),i.push({node:c,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}s.str.length==1&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var d=s.str.charAt(0),f=s.str.charAt(1),p;f in s.node.edges?p=s.node.edges[f]:(p=new t.TokenSet,s.node.edges[f]=p),s.str.length==1&&(p.final=!0),i.push({node:p,editsRemaining:s.editsRemaining-1,str:d+s.str.slice(2)})}}}return r},t.TokenSet.fromString=function(e){for(var n=new t.TokenSet,r=n,i=0,s=e.length;i=e;n--){var r=this.uncheckedNodes[n],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(n){var r=new t.QueryParser(e,n);r.parse()})},t.Index.prototype.query=function(e){for(var n=new t.Query(this.fields),r=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),c=0;c1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,n){var r=e[this._ref],i=Object.keys(this._fields);this._documents[r]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,n;do e=this.next(),n=e.charCodeAt(0);while(n>47&&n<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var n=e.next();if(n==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){e.escapeCharacter();continue}if(n==":")return t.QueryLexer.lexField;if(n=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(n=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(n=="+"&&e.width()===1||n=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(n.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,n){this.lexer=new t.QueryLexer(e),this.query=n,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var n=e.peekLexeme();if(n!=null)switch(n.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(r+=" with value '"+n.str+"'"),new t.QueryParseError(r,n.start,n.end)}},t.QueryParser.parsePresence=function(e){var n=e.consumeLexeme();if(n!=null){switch(n.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+n.str+"'";throw new t.QueryParseError(r,n.start,n.end)}var i=e.peekLexeme();if(i==null){var r="expecting term or field, found nothing";throw new t.QueryParseError(r,n.start,n.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(r,i.start,i.end)}}},t.QueryParser.parseField=function(e){var n=e.consumeLexeme();if(n!=null){if(e.query.allFields.indexOf(n.str)==-1){var r=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+n.str+"', possible fields: "+r;throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.fields=[n.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,n.start,n.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var n=e.consumeLexeme();if(n!=null){e.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(r==null){e.nextClause();return}switch(r.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+r.type+"'";throw new t.QueryParseError(i,r.start,r.end)}}},t.QueryParser.parseEditDistance=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="edit distance must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.editDistance=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="boost must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.boost=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},(function(e,n){typeof define=="function"&&define.amd?define(n):typeof me=="object"?ge.exports=n():e.lunr=n()})(this,function(){return t})})()});var M,G={getItem(){return null},setItem(){}},K;try{K=localStorage,M=K}catch{K=G,M=G}var S={getItem:t=>M.getItem(t),setItem:(t,e)=>M.setItem(t,e),disableWritingLocalStorage(){M=G},disable(){localStorage.clear(),M=G},enable(){M=K}};window.TypeDoc||={disableWritingLocalStorage(){S.disableWritingLocalStorage()},disableLocalStorage:()=>{S.disable()},enableLocalStorage:()=>{S.enable()}};window.translations||={copy:"Copy",copied:"Copied!",normally_hidden:"This member is normally hidden due to your filter settings.",hierarchy_expand:"Expand",hierarchy_collapse:"Collapse",search_index_not_available:"The search index is not available",search_no_results_found_for_0:"No results found for {0}",folder:"Folder",kind_1:"Project",kind_2:"Module",kind_4:"Namespace",kind_8:"Enumeration",kind_16:"Enumeration Member",kind_32:"Variable",kind_64:"Function",kind_128:"Class",kind_256:"Interface",kind_512:"Constructor",kind_1024:"Property",kind_2048:"Method",kind_4096:"Call Signature",kind_8192:"Index Signature",kind_16384:"Constructor Signature",kind_32768:"Parameter",kind_65536:"Type Literal",kind_131072:"Type Parameter",kind_262144:"Accessor",kind_524288:"Get Signature",kind_1048576:"Set Signature",kind_2097152:"Type Alias",kind_4194304:"Reference",kind_8388608:"Document"};var pe=[];function X(t,e){pe.push({selector:e,constructor:t})}var Z=class{alwaysVisibleMember=null;constructor(){this.createComponents(document.body),this.ensureFocusedElementVisible(),this.listenForCodeCopies(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible()),document.body.style.display||(this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}createComponents(e){pe.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r,app:this}),r.dataset.hasInstance=String(!0))})})}filterChanged(){this.ensureFocusedElementVisible()}showPage(){document.body.style.display&&(document.body.style.removeProperty("display"),this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}scrollToHash(){if(location.hash){let e=document.getElementById(location.hash.substring(1));if(!e)return;e.scrollIntoView({behavior:"instant",block:"start"})}}ensureActivePageVisible(){let e=document.querySelector(".tsd-navigation .current"),n=e?.parentElement;for(;n&&!n.classList.contains(".tsd-navigation");)n instanceof HTMLDetailsElement&&(n.open=!0),n=n.parentElement;if(e&&!rt(e)){let r=e.getBoundingClientRect().top-document.documentElement.clientHeight/4;document.querySelector(".site-menu").scrollTop=r,document.querySelector(".col-sidebar").scrollTop=r}}updateIndexVisibility(){let e=document.querySelector(".tsd-index-content"),n=e?.open;e&&(e.open=!0),document.querySelectorAll(".tsd-index-section").forEach(r=>{r.style.display="block";let i=Array.from(r.querySelectorAll(".tsd-index-link")).every(s=>s.offsetParent==null);r.style.display=i?"none":"block"}),e&&(e.open=n)}ensureFocusedElementVisible(){if(this.alwaysVisibleMember&&(this.alwaysVisibleMember.classList.remove("always-visible"),this.alwaysVisibleMember.firstElementChild.remove(),this.alwaysVisibleMember=null),!location.hash)return;let e=document.getElementById(location.hash.substring(1));if(!e)return;let n=e.parentElement;for(;n&&n.tagName!=="SECTION";)n=n.parentElement;if(!n)return;let r=n.offsetParent==null,i=n;for(;i!==document.body;)i instanceof HTMLDetailsElement&&(i.open=!0),i=i.parentElement;if(n.offsetParent==null){this.alwaysVisibleMember=n,n.classList.add("always-visible");let s=document.createElement("p");s.classList.add("warning"),s.textContent=window.translations.normally_hidden,n.prepend(s)}r&&e.scrollIntoView()}listenForCodeCopies(){document.querySelectorAll("pre > button").forEach(e=>{let n;e.addEventListener("click",()=>{e.previousElementSibling instanceof HTMLElement&&navigator.clipboard.writeText(e.previousElementSibling.innerText.trim()),e.textContent=window.translations.copied,e.classList.add("visible"),clearTimeout(n),n=setTimeout(()=>{e.classList.remove("visible"),n=setTimeout(()=>{e.textContent=window.translations.copy},100)},1e3)})})}};function rt(t){let e=t.getBoundingClientRect(),n=Math.max(document.documentElement.clientHeight,window.innerHeight);return!(e.bottom<0||e.top-n>=0)}var fe=(t,e=100)=>{let n;return()=>{clearTimeout(n),n=setTimeout(()=>t(),e)}};var Ie=nt(ye(),1);async function R(t){let e=Uint8Array.from(atob(t),s=>s.charCodeAt(0)),r=new Blob([e]).stream().pipeThrough(new DecompressionStream("deflate")),i=await new Response(r).text();return JSON.parse(i)}var Y="closing",ae="tsd-overlay";function it(){let t=Math.abs(window.innerWidth-document.documentElement.clientWidth);document.body.style.overflow="hidden",document.body.style.paddingRight=`${t}px`}function st(){document.body.style.removeProperty("overflow"),document.body.style.removeProperty("padding-right")}function xe(t,e){t.addEventListener("animationend",()=>{t.classList.contains(Y)&&(t.classList.remove(Y),document.getElementById(ae)?.remove(),t.close(),st())}),t.addEventListener("cancel",n=>{n.preventDefault(),ve(t)}),e?.closeOnClick&&document.addEventListener("click",n=>{t.open&&!t.contains(n.target)&&ve(t)},!0)}function Ee(t){if(t.open)return;let e=document.createElement("div");e.id=ae,document.body.appendChild(e),t.showModal(),it()}function ve(t){if(!t.open)return;document.getElementById(ae)?.classList.add(Y),t.classList.add(Y)}var I=class{el;app;constructor(e){this.el=e.el,this.app=e.app}};var be=document.head.appendChild(document.createElement("style"));be.dataset.for="filters";var le={};function we(t){for(let e of t.split(/\s+/))if(le.hasOwnProperty(e)&&!le[e])return!0;return!1}var ee=class extends I{key;value;constructor(e){super(e),this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),be.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } +`,this.app.updateIndexVisibility()}fromLocalStorage(){let e=S.getItem(this.key);return e?e==="true":this.el.checked}setLocalStorage(e){S.setItem(this.key,e.toString()),this.value=e,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),le[`tsd-is-${this.el.name}`]=this.value,this.app.filterChanged(),this.app.updateIndexVisibility()}};var Le=0;async function Se(t,e){if(!window.searchData)return;let n=await R(window.searchData);t.data=n,t.index=Ie.Index.load(n.index),e.innerHTML=""}function _e(){let t=document.getElementById("tsd-search-trigger"),e=document.getElementById("tsd-search"),n=document.getElementById("tsd-search-input"),r=document.getElementById("tsd-search-results"),i=document.getElementById("tsd-search-script"),s=document.getElementById("tsd-search-status");if(!(t&&e&&n&&r&&i&&s))throw new Error("Search controls missing");let o={base:document.documentElement.dataset.base};o.base.endsWith("/")||(o.base+="/"),i.addEventListener("error",()=>{let a=window.translations.search_index_not_available;Pe(s,a)}),i.addEventListener("load",()=>{Se(o,s)}),Se(o,s),ot({trigger:t,searchEl:e,results:r,field:n,status:s},o)}function ot(t,e){let{field:n,results:r,searchEl:i,status:s,trigger:o}=t;xe(i,{closeOnClick:!0});function a(){Ee(i),n.setSelectionRange(0,n.value.length)}o.addEventListener("click",a),n.addEventListener("input",fe(()=>{at(r,n,s,e)},200)),n.addEventListener("keydown",l=>{if(r.childElementCount===0||l.ctrlKey||l.metaKey||l.altKey)return;let d=n.getAttribute("aria-activedescendant"),f=d?document.getElementById(d):null;if(f){let p=!1,v=!1;switch(l.key){case"Home":case"End":case"ArrowLeft":case"ArrowRight":v=!0;break;case"ArrowDown":case"ArrowUp":p=l.shiftKey;break}(p||v)&&ke(n)}if(!l.shiftKey)switch(l.key){case"Enter":f?.querySelector("a")?.click();break;case"ArrowUp":Te(r,n,f,-1),l.preventDefault();break;case"ArrowDown":Te(r,n,f,1),l.preventDefault();break}});function c(){ke(n)}n.addEventListener("change",c),n.addEventListener("blur",c),n.addEventListener("click",c),document.body.addEventListener("keydown",l=>{if(l.altKey||l.metaKey||l.shiftKey)return;let d=l.ctrlKey&&l.key==="k",f=!l.ctrlKey&&!ut()&&l.key==="/";(d||f)&&(l.preventDefault(),a())})}function at(t,e,n,r){if(!r.index||!r.data)return;t.innerHTML="",n.innerHTML="",Le+=1;let i=e.value.trim(),s;if(i){let a=i.split(" ").map(c=>c.length?`*${c}*`:"").join(" ");s=r.index.search(a).filter(({ref:c})=>{let l=r.data.rows[Number(c)].classes;return!l||!we(l)})}else s=[];if(s.length===0&&i){let a=window.translations.search_no_results_found_for_0.replace("{0}",` "${te(i)}" `);Pe(n,a);return}for(let a=0;ac.score-a.score);let o=Math.min(10,s.length);for(let a=0;a`,f=Ce(c.name,i);globalThis.DEBUG_SEARCH_WEIGHTS&&(f+=` (score: ${s[a].score.toFixed(2)})`),c.parent&&(f=` + ${Ce(c.parent,i)}.${f}`);let p=document.createElement("li");p.id=`tsd-search:${Le}-${a}`,p.role="option",p.ariaSelected="false",p.classList.value=c.classes??"";let v=document.createElement("a");v.tabIndex=-1,v.href=r.base+c.url,v.innerHTML=d+`${f}`,p.append(v),t.appendChild(p)}}function Te(t,e,n,r){let i;if(r===1?i=n?.nextElementSibling||t.firstElementChild:i=n?.previousElementSibling||t.lastElementChild,i!==n){if(!i||i.role!=="option"){console.error("Option missing");return}i.ariaSelected="true",i.scrollIntoView({behavior:"smooth",block:"nearest"}),e.setAttribute("aria-activedescendant",i.id),n?.setAttribute("aria-selected","false")}}function ke(t){let e=t.getAttribute("aria-activedescendant");(e?document.getElementById(e):null)?.setAttribute("aria-selected","false"),t.setAttribute("aria-activedescendant","")}function Ce(t,e){if(e==="")return t;let n=t.toLocaleLowerCase(),r=e.toLocaleLowerCase(),i=[],s=0,o=n.indexOf(r);for(;o!=-1;)i.push(te(t.substring(s,o)),`${te(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(te(t.substring(s))),i.join("")}var lt={"&":"&","<":"<",">":">","'":"'",'"':"""};function te(t){return t.replace(/[&<>"'"]/g,e=>lt[e])}function Pe(t,e){t.innerHTML=e?`
${e}
`:""}var ct=["button","checkbox","file","hidden","image","radio","range","reset","submit"];function ut(){let t=document.activeElement;return t?t.isContentEditable||t.tagName==="TEXTAREA"||t.tagName==="SEARCH"?!0:t.tagName==="INPUT"&&!ct.includes(t.type):!1}var D="mousedown",Me="mousemove",$="mouseup",ne={x:0,y:0},Qe=!1,ce=!1,dt=!1,F=!1,Oe=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(Oe?"is-mobile":"not-mobile");Oe&&"ontouchstart"in document.documentElement&&(dt=!0,D="touchstart",Me="touchmove",$="touchend");document.addEventListener(D,t=>{ce=!0,F=!1;let e=D=="touchstart"?t.targetTouches[0]:t;ne.y=e.pageY||0,ne.x=e.pageX||0});document.addEventListener(Me,t=>{if(ce&&!F){let e=D=="touchstart"?t.targetTouches[0]:t,n=ne.x-(e.pageX||0),r=ne.y-(e.pageY||0);F=Math.sqrt(n*n+r*r)>10}});document.addEventListener($,()=>{ce=!1});document.addEventListener("click",t=>{Qe&&(t.preventDefault(),t.stopImmediatePropagation(),Qe=!1)});var re=class extends I{active;className;constructor(e){super(e),this.className=this.el.dataset.toggle||"",this.el.addEventListener($,n=>this.onPointerUp(n)),this.el.addEventListener("click",n=>n.preventDefault()),document.addEventListener(D,n=>this.onDocumentPointerDown(n)),document.addEventListener($,n=>this.onDocumentPointerUp(n))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let n=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(n),setTimeout(()=>document.documentElement.classList.remove(n),500)}onPointerUp(e){F||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-sidebar, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!F&&this.active&&e.target.closest(".col-sidebar")){let n=e.target.closest("a");if(n){let r=window.location.href;r.indexOf("#")!=-1&&(r=r.substring(0,r.indexOf("#"))),n.href.substring(0,r.length)==r&&setTimeout(()=>this.setActive(!1),250)}}}};var ue=new Map,de=class{open;accordions=[];key;constructor(e,n){this.key=e,this.open=n}add(e){this.accordions.push(e),e.open=this.open,e.addEventListener("toggle",()=>{this.toggle(e.open)})}toggle(e){for(let n of this.accordions)n.open=e;S.setItem(this.key,e.toString())}},ie=class extends I{constructor(e){super(e);let n=this.el.querySelector("summary"),r=n.querySelector("a");r&&r.addEventListener("click",()=>{location.assign(r.href)});let i=`tsd-accordion-${n.dataset.key??n.textContent.trim().replace(/\s+/g,"-").toLowerCase()}`,s;if(ue.has(i))s=ue.get(i);else{let o=S.getItem(i),a=o?o==="true":this.el.open;s=new de(i,a),ue.set(i,s)}s.add(this.el)}};function He(t){let e=S.getItem("tsd-theme")||"os";t.value=e,Ae(e),t.addEventListener("change",()=>{S.setItem("tsd-theme",t.value),Ae(t.value)})}function Ae(t){document.documentElement.dataset.theme=t}var se;function Ne(){let t=document.getElementById("tsd-nav-script");t&&(t.addEventListener("load",Re),Re())}async function Re(){let t=document.getElementById("tsd-nav-container");if(!t||!window.navigationData)return;let e=await R(window.navigationData);se=document.documentElement.dataset.base,se.endsWith("/")||(se+="/"),t.innerHTML="";for(let n of e)Be(n,t,[]);window.app.createComponents(t),window.app.showPage(),window.app.ensureActivePageVisible()}function Be(t,e,n){let r=e.appendChild(document.createElement("li"));if(t.children){let i=[...n,t.text],s=r.appendChild(document.createElement("details"));s.className=t.class?`${t.class} tsd-accordion`:"tsd-accordion";let o=s.appendChild(document.createElement("summary"));o.className="tsd-accordion-summary",o.dataset.key=i.join("$"),o.innerHTML='',De(t,o);let a=s.appendChild(document.createElement("div"));a.className="tsd-accordion-details";let c=a.appendChild(document.createElement("ul"));c.className="tsd-nested-navigation";for(let l of t.children)Be(l,c,i)}else De(t,r,t.class)}function De(t,e,n){if(t.path){let r=e.appendChild(document.createElement("a"));if(r.href=se+t.path,n&&(r.className=n),location.pathname===r.pathname&&!r.href.includes("#")&&(r.classList.add("current"),r.ariaCurrent="page"),t.kind){let i=window.translations[`kind_${t.kind}`].replaceAll('"',""");r.innerHTML=``}r.appendChild(Fe(t.text,document.createElement("span")))}else{let r=e.appendChild(document.createElement("span")),i=window.translations.folder.replaceAll('"',""");r.innerHTML=``,r.appendChild(Fe(t.text,document.createElement("span")))}}function Fe(t,e){let n=t.split(/(?<=[^A-Z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[_-])(?=[^_-])/);for(let r=0;r{let i=r.target;for(;i.parentElement&&i.parentElement.tagName!="LI";)i=i.parentElement;i.dataset.dropdown&&(i.dataset.dropdown=String(i.dataset.dropdown!=="true"))});let t=new Map,e=new Set;for(let r of document.querySelectorAll(".tsd-full-hierarchy [data-refl]")){let i=r.querySelector("ul");t.has(r.dataset.refl)?e.add(r.dataset.refl):i&&t.set(r.dataset.refl,i)}for(let r of e)n(r);function n(r){let i=t.get(r).cloneNode(!0);i.querySelectorAll("[id]").forEach(s=>{s.removeAttribute("id")}),i.querySelectorAll("[data-dropdown]").forEach(s=>{s.dataset.dropdown="false"});for(let s of document.querySelectorAll(`[data-refl="${r}"]`)){let o=gt(),a=s.querySelector("ul");s.insertBefore(o,a),o.dataset.dropdown=String(!!a),a||s.appendChild(i.cloneNode(!0))}}}function pt(){let t=document.getElementById("tsd-hierarchy-script");t&&(t.addEventListener("load",Ve),Ve())}async function Ve(){let t=document.querySelector(".tsd-panel.tsd-hierarchy:has(h4 a)");if(!t||!window.hierarchyData)return;let e=+t.dataset.refl,n=await R(window.hierarchyData),r=t.querySelector("ul"),i=document.createElement("ul");if(i.classList.add("tsd-hierarchy"),ft(i,n,e),r.querySelectorAll("li").length==i.querySelectorAll("li").length)return;let s=document.createElement("span");s.classList.add("tsd-hierarchy-toggle"),s.textContent=window.translations.hierarchy_expand,t.querySelector("h4 a")?.insertAdjacentElement("afterend",s),s.insertAdjacentText("beforebegin",", "),s.addEventListener("click",()=>{s.textContent===window.translations.hierarchy_expand?(r.insertAdjacentElement("afterend",i),r.remove(),s.textContent=window.translations.hierarchy_collapse):(i.insertAdjacentElement("afterend",r),i.remove(),s.textContent=window.translations.hierarchy_expand)})}function ft(t,e,n){let r=e.roots.filter(i=>mt(e,i,n));for(let i of r)t.appendChild(je(e,i,n))}function je(t,e,n,r=new Set){if(r.has(e))return;r.add(e);let i=t.reflections[e],s=document.createElement("li");if(s.classList.add("tsd-hierarchy-item"),e===n){let o=s.appendChild(document.createElement("span"));o.textContent=i.name,o.classList.add("tsd-hierarchy-target")}else{for(let a of i.uniqueNameParents||[]){let c=t.reflections[a],l=s.appendChild(document.createElement("a"));l.textContent=c.name,l.href=oe+c.url,l.className=c.class+" tsd-signature-type",s.append(document.createTextNode("."))}let o=s.appendChild(document.createElement("a"));o.textContent=t.reflections[e].name,o.href=oe+i.url,o.className=i.class+" tsd-signature-type"}if(i.children){let o=s.appendChild(document.createElement("ul"));o.classList.add("tsd-hierarchy");for(let a of i.children){let c=je(t,a,n,r);c&&o.appendChild(c)}}return r.delete(e),s}function mt(t,e,n){if(e===n)return!0;let r=new Set,i=[t.reflections[e]];for(;i.length;){let s=i.pop();if(!r.has(s)){r.add(s);for(let o of s.children||[]){if(o===n)return!0;i.push(t.reflections[o])}}}return!1}function gt(){let t=document.createElementNS("http://www.w3.org/2000/svg","svg");return t.setAttribute("width","20"),t.setAttribute("height","20"),t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("fill","none"),t.innerHTML='',t}X(re,"a[data-toggle]");X(ie,".tsd-accordion");X(ee,".tsd-filter-item input[type=checkbox]");var qe=document.getElementById("tsd-theme");qe&&He(qe);var yt=new Z;Object.defineProperty(window,"app",{value:yt});_e();Ne();$e();"virtualKeyboard"in navigator&&(navigator.virtualKeyboard.overlaysContent=!0);})(); +/*! Bundled license information: + +lunr/lunr.js: + (** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + *) + (*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + *) + (*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + *) + (*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + *) +*/ diff --git a/sdk_v2/js/docs/assets/navigation.js b/sdk_v2/js/docs/assets/navigation.js new file mode 100644 index 0000000..e30bbe2 --- /dev/null +++ b/sdk_v2/js/docs/assets/navigation.js @@ -0,0 +1 @@ +window.navigationData = "eJyNkVELgjAUhf/LfZYioQjfQgiEfO0leri4qaO1G/MKRfTfAwubOrXXfed8G2enJ7C8M0Swq4WiWCtpGAK4IZcQQaaxqmS1dOCi5KuGAC7KCIhW4fYVtI4YGTUVw/4XTHZL5LHrf2zKsKfaCPs4UIY6RYOFtEOVJzTlTElIPbQ0x7O9A6EYfUg/MWs7olXoW8el/+4Tk8mV81HKsLQ5Zr2FPrGuNFxvHGnSW8gRJZ6VuuUmkJicvP2WDhXnN2ST6pM=" \ No newline at end of file diff --git a/sdk_v2/js/docs/assets/search.js b/sdk_v2/js/docs/assets/search.js new file mode 100644 index 0000000..f4b184a --- /dev/null +++ b/sdk_v2/js/docs/assets/search.js @@ -0,0 +1 @@ +window.searchData = "eJytnN+Pm0gSx/8X9tWadf8E5m2V7EqRkruTks09WNGJsZkJim0swJNkR/nfTw3YVFHdULbnycm4q75F96eri3bDS1SV3+vofvUSfSv2m+heyGQR7bNdHt1Hfxw3RflmW+T7JlpEx2ob3UfrbVbXef07+O7ua7PbRovTV9F9FP1anPwZIc/+1uW+bqrjuimrWX+/4cbA9yI6ZJULCQc4SMqlHq6hqbJ9va6Kh3xeErV9BcWPTZVnu2L/dIl0DYwuiQEO3JusybYlle3//ioDBn2xBusUFOg2K4XWZ7X2Y1Kmb8H2D4flKW/eZOuv+eZDucm39bTQU96s28a7U+NrNd+X2YatuW0b36zZqs2K7fpWN6nMX9PrXM3nrCoyTxryqj2fG3M10eT5mjWBpDd89TpTCLvjzaIhOhD+Ug7TqM6bptg/eUZmJAcaXqCFhmdd7g7bvMldS8bldY3XXeNbNc/Z9SLxc3q9PAoIyV/lcb+pfr4v19n2Q7bPnnI6vJ42k9iM0uE6kMRDbn8bDPwX5Ys5qH6sPHM7KN23vkoXjm2+r49V/uehflt+33fZkB9EZ5wf6g00vjmmusmq5r/5w8e8ei7WdIUKxtMafs8f6rPhK8RSHq4LpTy8biTrKs+aCyI4t79GGc48/+LW/vVVkvLgiZWPu3CC0yjbFhmdR0Dj1OAq7wWdHcB1EcZ/zm/dlUqT3uv1qc1VGoes+Trlv//+Kt99ATDZ8aANUwPin202oZIEDu5mM1eLTKp0k2bqNgzS2jbOXOP1qfHVmhNFEJF0K+ktiqd0PaUE2lyhUNTv/WsJ4nlmyZhUmIv/+tirfFc+539V5a6dk1MiXdPHqtyt+6ZX6NX5Nl83DLi7hjfxfZwd9+Nlo04WCjfuofps3OD1lo+xU/5KAuMNoVbUzRTORNwZcNjmaAeHi6rOjhtDbwoQoshhxa9JsJnEv//y9XCBDvmonGK8pvZAiowSZFYrVIkgobmCZF5lsi7BWqzyZFax3U94t38sGZJt26Jre4tmuDJCcvMFkkfpiuoC03lZkcGKYK7W8ATALTlm9KcrD6TLK0Bm9KbrkBG/nJQ9o8e8tpuvi1WjIMmLSpUZ9alFAolyFgiPlrHebZ835f6xGLZnin2TV4/ZenQT3LWaXCvQ7l222RRNUe6z7cfxPt68wm+D9ezmnudSQiEdDm+zJntbVBeFcjhssibbFOFl7KIQ/gV/HeDpT/5cwBffFg9VVv38D0zLjAB6s8k0fUEQ5dP7/BnsfHAiKJ+2vc0ryNcXAuBMXmf024W1zSwXhtAadnXAqwTS75/9ud8cygIsV4xQetN8ML01mO/nfcC/4V4tI5ZhJ5C9b0tCgVnxHd6TAxG8m9+Xmyxa/a5mKtZ3/F2zgP+JQnXW+bhEDUnM1adzQodAOoIik7mHCvCKw4AWvzLkCHtqwkldRkE4LUtKwYDcbB04LUMqwCAeM+XftAznSm64ilC9F1DiFnvTokfW+MzUeFQC5LEP5GYPiJy/vKCWC+Uy7ItzA95GFZBZBxPOSIdzRzwp1E61zR/N3/viB0eva581x679lbKboj5ss5+hAnAk2reerP1mJR+Lbf6x+Cf/8MBQdI3r4p9893C9oH9NGgnN7Z9MCWyLdb6vOR04tLxN6m3uDjQd3O0IX3WDjK4MYJf9+PexORybT+W3fM+ZgbvsR9laNCeLa6WL/V/vP+dVzbvqXbF/3D6fm18r6hpM3S6OVdsjOXM3iDzZTz8PHKjatk3X9kq5PW/+3zbxD1W5OzSf8t1hC3/eDst1Bs1gcL3wc7HJK2Z/nprf1qWH48O2qL/m3puqsSJoe6Vcddw3BWsQh5ZXStXHw6GsmvpTWW7fZNstPIQZlj1ZNWW5XZ+trgyhyepvDM2+2ZUix6pgaHStrpR4ZmezCxLZl0VU7Df5j+j+5SxwH8k7dZdGi+ixyLcbdyr5NOnX5W7X1fabcn1s//mlb/Y5dz+VuMZd69+X0WK1XGh5p5L0y5fF6mTcftH+4eRj+EtrKKLFSixkfBdLiwwFMRTIUEaLlfQpSmIokaGKFivlM1TEUCFDHS1WeqHiO5kIZKiJoUaGJnSNhhgaZGijxcr4FC0xtMgwjhYr67vGmBjGyDCJFqvYZ5gQwwQZptFilfgMU2KYYgAcD6nPUlB2xAielh4vd8LDDwZIOCyE8BpThgSGSKggtxQjgTkSjg7hZVdQlARmSbQwefkVFCeBeRKOEqG9xhQpgZkSjhRhvMYUK4G5Eklo8ghKlsBoCQeM8OIsKF0C4yUdMcKLtKR8ScyXbPnyYi0pX3KUoFq+vGRLT47CfEmHjPSSLSlgEgMmHTLSS7akgEkMmAxmK0n5kpgv6YiRcqHEnV4qbEz5kpgv6YiRymtM+ZKYL+mQkXqh1F0qRmFTwCQGTDpkpPEaU8AkBkw5ZKSXTkUBUxgw5ZCRXjoVBUxhwFS7BCa+sBUFTI1WwRaw1GvsWQgxYMoho5ZeYwqYwoApx4wSXmNKmMKEKceM8hKmKGEKE6YcM0p5lSlhChOmHDPKmzsVJUxhwpRjRhlv2JQwhQnTjhnlJUxTwjQmTAcLLE0B0xgw7ZBRXjo1BUxjwLQKjpSmgOlRqaWD/aU91RYGTLeAebO2poBpDJi2wf6ifGnMl46D6U9TvjTmSyfB9KcpXxrzpdNg+tOUL435Mi1fqW9xNpQvg/kyIpg7DQXMYMBMOIMZCpjBgJlwBjMUMIMBM+EMZihgZlTPhzOY8ZT0GDATzmCGEmYwYSacwQwlzGDCTBKcVIYSZjBhxjGjvQWJoYQZTJh1zGhvQWIpYRYTZh0z2lsuW0qYxYRZx4z2lsuWEmYxYdYxo70p31LCLCbMtreL3nLZUsIsJsw6ZrQ35VtKmB3dNTpmtDdtW8+NIybMOma0N3laSpjFhNm2yveWvJYSZjFh1jFjvIRZSpjFhMXLYOqNKWExJiwWwdQbU8JiTFgsg6k3poTFmLBYBbNnTAmLMWGxDmbPmBIWY8JiE8yeMSUsxoTFNpg9Y0pYPNqbiIPZM/ZsT2DC4iSYPWNKWIwJi9Ng9owpYTEmLFkGs2dCCUswYYkILrEJJSzBhCUyyHZCCUswYYljxnhTb0IJSzBhiWPGeFNvQglLMGGJY8Z4U29CCUswYUm79eVNvQklLMGEJeE7yYQSlox2wBwzxpu3E88mGCYsaXOYN28nlLAEE5Y6Zow3b6eUsBQTljpmjH8HjhKWYsJSx4zx5u2UEpZiwlLHjPXm7ZQSlmLCUh3a4kwpYCkGLHXIWC/aKQUsxYClDhnrRTulgKUYsLTdX/WinVLAUgxY6pCxXrRTClg62mZ1yFgvnalnp3W81eqYsf6dtKVvt3W03bp02Fgvod13Y/vRjuvSkWO9kHbfje27v7W/WzznVZNv3nW/X6xWke/U7Uv0v/4HDi1OP6W8RHYZ3b/8+jX8oHH/8gv8puG+c7LwucHBkYwHR0rwHHUnS4APOfiQtrPScfcZL7vPRPJ8gwO+4GqBgGUGeTqpC7wo4IUZDjx5NnhaDo54fX86JDO4MGAAE8V00j+KDy5pcKI7K5HwfIGjbYM7AYISvB7C718ArkBnC3ORq9HbFIBPcLnCMn2CB5+AJ+CoszR996nuU/b/1/3XminXP5AO5gZgRTLHmR56BP4SMF9lH3v/GesL/PsJkClw3/eF6T9j5jDiA1oAeTCLE16o6NwV8ATgSnhRDUcsBzcKjI3q54/pP2PegHtfDwE4A9lV8lIFPPgFrhign/BCe+xOWm/dSet1/1gJyBzg2k16scPd6fFXcK0GTKqY5ZG8EWnwZgdnbF/4TUeDLzAIvOw4vMhocALmHa+7wGuKBi9gegkeD+T9Q6DHwRgK3rJYIDwlmEeyX7B10k+APvMlvO4vSIcZEJ3ldVlR04VSAvJlH5tO+xh5a9Rw7hnMfbAEqD7Zm/4z5l0yehYHzCwQsOVl/PPpSNB7YDYlPGp9px2BQzCjEt5wwGe8Qc+B6aR5A0ASL1gJ1KlgPK02/f9j5kWfH0cCIwD6zvIWmvNzRcAL6DDLW2TIiU/Q/aDXUt7Mx+c4gSuYinjTnkxOCfCXvC4aPfcEOgpcmuWtTeCpZkAFSI7mdNtwwfU5yjzrkgLjqJk9j8+ygq4HIaY89sEhVbC6gdyY8nJEaBFQgAbNw5SUU2D8OpOUh8Q49UngSfY52vTrSMy7zPHRV9BnANmUd6H4QCtwBfJPyiMWnFQFfsD6mfIWDfLYChhJ4E31q7E53UbzMvb5iCuIESxHKS+njl7CAiIEvhTX1+gRRZA3ALmW13u+OSnAYAoet+QlasAbCEryks/4PWjAGUgXkpcuvKeFwWiCGZbymOjOAgMXIB+6HTCWD/ASWzDdwdVd6Aa8kBbgBehi+TvSWzrQP6rPQPp0a9dXjwnvmtvzzaDbYthtPDK6J2EBD7DreaM3vFEM9Dtwo3gX4ykjbAIviDeA44d8wWQGqFtGaviyiA7FwRUBeXS/+vLr1/8BMckPYQ=="; \ No newline at end of file diff --git a/sdk_v2/js/docs/assets/style.css b/sdk_v2/js/docs/assets/style.css new file mode 100644 index 0000000..44328e9 --- /dev/null +++ b/sdk_v2/js/docs/assets/style.css @@ -0,0 +1,1633 @@ +@layer typedoc { + :root { + --dim-toolbar-contents-height: 2.5rem; + --dim-toolbar-border-bottom-width: 1px; + --dim-header-height: calc( + var(--dim-toolbar-border-bottom-width) + + var(--dim-toolbar-contents-height) + ); + + /* 0rem For mobile; unit is required for calculation in `calc` */ + --dim-container-main-margin-y: 0rem; + + --dim-footer-height: 3.5rem; + + --modal-animation-duration: 0.2s; + } + + :root { + /* Light */ + --light-color-background: #f2f4f8; + --light-color-background-secondary: #eff0f1; + /* Not to be confused with [:active](https://developer.mozilla.org/en-US/docs/Web/CSS/:active) */ + --light-color-background-active: #d6d8da; + --light-color-background-warning: #e6e600; + --light-color-warning-text: #222; + --light-color-accent: #c5c7c9; + --light-color-active-menu-item: var(--light-color-background-active); + --light-color-text: #222; + --light-color-contrast-text: #000; + --light-color-text-aside: #5e5e5e; + + --light-color-icon-background: var(--light-color-background); + --light-color-icon-text: var(--light-color-text); + + --light-color-comment-tag-text: var(--light-color-text); + --light-color-comment-tag: var(--light-color-background); + + --light-color-link: #1f70c2; + --light-color-focus-outline: #3584e4; + + --light-color-ts-keyword: #056bd6; + --light-color-ts-project: #b111c9; + --light-color-ts-module: var(--light-color-ts-project); + --light-color-ts-namespace: var(--light-color-ts-project); + --light-color-ts-enum: #7e6f15; + --light-color-ts-enum-member: var(--light-color-ts-enum); + --light-color-ts-variable: #4760ec; + --light-color-ts-function: #572be7; + --light-color-ts-class: #1f70c2; + --light-color-ts-interface: #108024; + --light-color-ts-constructor: var(--light-color-ts-class); + --light-color-ts-property: #9f5f30; + --light-color-ts-method: #be3989; + --light-color-ts-reference: #ff4d82; + --light-color-ts-call-signature: var(--light-color-ts-method); + --light-color-ts-index-signature: var(--light-color-ts-property); + --light-color-ts-constructor-signature: var( + --light-color-ts-constructor + ); + --light-color-ts-parameter: var(--light-color-ts-variable); + /* type literal not included as links will never be generated to it */ + --light-color-ts-type-parameter: #a55c0e; + --light-color-ts-accessor: #c73c3c; + --light-color-ts-get-signature: var(--light-color-ts-accessor); + --light-color-ts-set-signature: var(--light-color-ts-accessor); + --light-color-ts-type-alias: #d51270; + /* reference not included as links will be colored with the kind that it points to */ + --light-color-document: #000000; + + --light-color-alert-note: #0969d9; + --light-color-alert-tip: #1a7f37; + --light-color-alert-important: #8250df; + --light-color-alert-warning: #9a6700; + --light-color-alert-caution: #cf222e; + + --light-external-icon: url("data:image/svg+xml;utf8,"); + --light-color-scheme: light; + } + + :root { + /* Dark */ + --dark-color-background: #2b2e33; + --dark-color-background-secondary: #1e2024; + /* Not to be confused with [:active](https://developer.mozilla.org/en-US/docs/Web/CSS/:active) */ + --dark-color-background-active: #5d5d6a; + --dark-color-background-warning: #bebe00; + --dark-color-warning-text: #222; + --dark-color-accent: #9096a2; + --dark-color-active-menu-item: var(--dark-color-background-active); + --dark-color-text: #f5f5f5; + --dark-color-contrast-text: #ffffff; + --dark-color-text-aside: #dddddd; + + --dark-color-icon-background: var(--dark-color-background-secondary); + --dark-color-icon-text: var(--dark-color-text); + + --dark-color-comment-tag-text: var(--dark-color-text); + --dark-color-comment-tag: var(--dark-color-background); + + --dark-color-link: #00aff4; + --dark-color-focus-outline: #4c97f2; + + --dark-color-ts-keyword: #3399ff; + --dark-color-ts-project: #e358ff; + --dark-color-ts-module: var(--dark-color-ts-project); + --dark-color-ts-namespace: var(--dark-color-ts-project); + --dark-color-ts-enum: #f4d93e; + --dark-color-ts-enum-member: var(--dark-color-ts-enum); + --dark-color-ts-variable: #798dff; + --dark-color-ts-function: #a280ff; + --dark-color-ts-class: #8ac4ff; + --dark-color-ts-interface: #6cff87; + --dark-color-ts-constructor: var(--dark-color-ts-class); + --dark-color-ts-property: #ff984d; + --dark-color-ts-method: #ff4db8; + --dark-color-ts-reference: #ff4d82; + --dark-color-ts-call-signature: var(--dark-color-ts-method); + --dark-color-ts-index-signature: var(--dark-color-ts-property); + --dark-color-ts-constructor-signature: var(--dark-color-ts-constructor); + --dark-color-ts-parameter: var(--dark-color-ts-variable); + /* type literal not included as links will never be generated to it */ + --dark-color-ts-type-parameter: #e07d13; + --dark-color-ts-accessor: #ff6060; + --dark-color-ts-get-signature: var(--dark-color-ts-accessor); + --dark-color-ts-set-signature: var(--dark-color-ts-accessor); + --dark-color-ts-type-alias: #ff6492; + /* reference not included as links will be colored with the kind that it points to */ + --dark-color-document: #ffffff; + + --dark-color-alert-note: #0969d9; + --dark-color-alert-tip: #1a7f37; + --dark-color-alert-important: #8250df; + --dark-color-alert-warning: #9a6700; + --dark-color-alert-caution: #cf222e; + + --dark-external-icon: url("data:image/svg+xml;utf8,"); + --dark-color-scheme: dark; + } + + @media (prefers-color-scheme: light) { + :root { + --color-background: var(--light-color-background); + --color-background-secondary: var( + --light-color-background-secondary + ); + --color-background-active: var(--light-color-background-active); + --color-background-warning: var(--light-color-background-warning); + --color-warning-text: var(--light-color-warning-text); + --color-accent: var(--light-color-accent); + --color-active-menu-item: var(--light-color-active-menu-item); + --color-text: var(--light-color-text); + --color-contrast-text: var(--light-color-contrast-text); + --color-text-aside: var(--light-color-text-aside); + + --color-icon-background: var(--light-color-icon-background); + --color-icon-text: var(--light-color-icon-text); + + --color-comment-tag-text: var(--light-color-text); + --color-comment-tag: var(--light-color-background); + + --color-link: var(--light-color-link); + --color-focus-outline: var(--light-color-focus-outline); + + --color-ts-keyword: var(--light-color-ts-keyword); + --color-ts-project: var(--light-color-ts-project); + --color-ts-module: var(--light-color-ts-module); + --color-ts-namespace: var(--light-color-ts-namespace); + --color-ts-enum: var(--light-color-ts-enum); + --color-ts-enum-member: var(--light-color-ts-enum-member); + --color-ts-variable: var(--light-color-ts-variable); + --color-ts-function: var(--light-color-ts-function); + --color-ts-class: var(--light-color-ts-class); + --color-ts-interface: var(--light-color-ts-interface); + --color-ts-constructor: var(--light-color-ts-constructor); + --color-ts-property: var(--light-color-ts-property); + --color-ts-method: var(--light-color-ts-method); + --color-ts-reference: var(--light-color-ts-reference); + --color-ts-call-signature: var(--light-color-ts-call-signature); + --color-ts-index-signature: var(--light-color-ts-index-signature); + --color-ts-constructor-signature: var( + --light-color-ts-constructor-signature + ); + --color-ts-parameter: var(--light-color-ts-parameter); + --color-ts-type-parameter: var(--light-color-ts-type-parameter); + --color-ts-accessor: var(--light-color-ts-accessor); + --color-ts-get-signature: var(--light-color-ts-get-signature); + --color-ts-set-signature: var(--light-color-ts-set-signature); + --color-ts-type-alias: var(--light-color-ts-type-alias); + --color-document: var(--light-color-document); + + --color-alert-note: var(--light-color-alert-note); + --color-alert-tip: var(--light-color-alert-tip); + --color-alert-important: var(--light-color-alert-important); + --color-alert-warning: var(--light-color-alert-warning); + --color-alert-caution: var(--light-color-alert-caution); + + --external-icon: var(--light-external-icon); + --color-scheme: var(--light-color-scheme); + } + } + + @media (prefers-color-scheme: dark) { + :root { + --color-background: var(--dark-color-background); + --color-background-secondary: var( + --dark-color-background-secondary + ); + --color-background-active: var(--dark-color-background-active); + --color-background-warning: var(--dark-color-background-warning); + --color-warning-text: var(--dark-color-warning-text); + --color-accent: var(--dark-color-accent); + --color-active-menu-item: var(--dark-color-active-menu-item); + --color-text: var(--dark-color-text); + --color-contrast-text: var(--dark-color-contrast-text); + --color-text-aside: var(--dark-color-text-aside); + + --color-icon-background: var(--dark-color-icon-background); + --color-icon-text: var(--dark-color-icon-text); + + --color-comment-tag-text: var(--dark-color-text); + --color-comment-tag: var(--dark-color-background); + + --color-link: var(--dark-color-link); + --color-focus-outline: var(--dark-color-focus-outline); + + --color-ts-keyword: var(--dark-color-ts-keyword); + --color-ts-project: var(--dark-color-ts-project); + --color-ts-module: var(--dark-color-ts-module); + --color-ts-namespace: var(--dark-color-ts-namespace); + --color-ts-enum: var(--dark-color-ts-enum); + --color-ts-enum-member: var(--dark-color-ts-enum-member); + --color-ts-variable: var(--dark-color-ts-variable); + --color-ts-function: var(--dark-color-ts-function); + --color-ts-class: var(--dark-color-ts-class); + --color-ts-interface: var(--dark-color-ts-interface); + --color-ts-constructor: var(--dark-color-ts-constructor); + --color-ts-property: var(--dark-color-ts-property); + --color-ts-method: var(--dark-color-ts-method); + --color-ts-reference: var(--dark-color-ts-reference); + --color-ts-call-signature: var(--dark-color-ts-call-signature); + --color-ts-index-signature: var(--dark-color-ts-index-signature); + --color-ts-constructor-signature: var( + --dark-color-ts-constructor-signature + ); + --color-ts-parameter: var(--dark-color-ts-parameter); + --color-ts-type-parameter: var(--dark-color-ts-type-parameter); + --color-ts-accessor: var(--dark-color-ts-accessor); + --color-ts-get-signature: var(--dark-color-ts-get-signature); + --color-ts-set-signature: var(--dark-color-ts-set-signature); + --color-ts-type-alias: var(--dark-color-ts-type-alias); + --color-document: var(--dark-color-document); + + --color-alert-note: var(--dark-color-alert-note); + --color-alert-tip: var(--dark-color-alert-tip); + --color-alert-important: var(--dark-color-alert-important); + --color-alert-warning: var(--dark-color-alert-warning); + --color-alert-caution: var(--dark-color-alert-caution); + + --external-icon: var(--dark-external-icon); + --color-scheme: var(--dark-color-scheme); + } + } + + :root[data-theme="light"] { + --color-background: var(--light-color-background); + --color-background-secondary: var(--light-color-background-secondary); + --color-background-active: var(--light-color-background-active); + --color-background-warning: var(--light-color-background-warning); + --color-warning-text: var(--light-color-warning-text); + --color-icon-background: var(--light-color-icon-background); + --color-accent: var(--light-color-accent); + --color-active-menu-item: var(--light-color-active-menu-item); + --color-text: var(--light-color-text); + --color-contrast-text: var(--light-color-contrast-text); + --color-text-aside: var(--light-color-text-aside); + --color-icon-text: var(--light-color-icon-text); + + --color-comment-tag-text: var(--light-color-text); + --color-comment-tag: var(--light-color-background); + + --color-link: var(--light-color-link); + --color-focus-outline: var(--light-color-focus-outline); + + --color-ts-keyword: var(--light-color-ts-keyword); + --color-ts-project: var(--light-color-ts-project); + --color-ts-module: var(--light-color-ts-module); + --color-ts-namespace: var(--light-color-ts-namespace); + --color-ts-enum: var(--light-color-ts-enum); + --color-ts-enum-member: var(--light-color-ts-enum-member); + --color-ts-variable: var(--light-color-ts-variable); + --color-ts-function: var(--light-color-ts-function); + --color-ts-class: var(--light-color-ts-class); + --color-ts-interface: var(--light-color-ts-interface); + --color-ts-constructor: var(--light-color-ts-constructor); + --color-ts-property: var(--light-color-ts-property); + --color-ts-method: var(--light-color-ts-method); + --color-ts-reference: var(--light-color-ts-reference); + --color-ts-call-signature: var(--light-color-ts-call-signature); + --color-ts-index-signature: var(--light-color-ts-index-signature); + --color-ts-constructor-signature: var( + --light-color-ts-constructor-signature + ); + --color-ts-parameter: var(--light-color-ts-parameter); + --color-ts-type-parameter: var(--light-color-ts-type-parameter); + --color-ts-accessor: var(--light-color-ts-accessor); + --color-ts-get-signature: var(--light-color-ts-get-signature); + --color-ts-set-signature: var(--light-color-ts-set-signature); + --color-ts-type-alias: var(--light-color-ts-type-alias); + --color-document: var(--light-color-document); + + --color-note: var(--light-color-note); + --color-tip: var(--light-color-tip); + --color-important: var(--light-color-important); + --color-warning: var(--light-color-warning); + --color-caution: var(--light-color-caution); + + --external-icon: var(--light-external-icon); + --color-scheme: var(--light-color-scheme); + } + + :root[data-theme="dark"] { + --color-background: var(--dark-color-background); + --color-background-secondary: var(--dark-color-background-secondary); + --color-background-active: var(--dark-color-background-active); + --color-background-warning: var(--dark-color-background-warning); + --color-warning-text: var(--dark-color-warning-text); + --color-icon-background: var(--dark-color-icon-background); + --color-accent: var(--dark-color-accent); + --color-active-menu-item: var(--dark-color-active-menu-item); + --color-text: var(--dark-color-text); + --color-contrast-text: var(--dark-color-contrast-text); + --color-text-aside: var(--dark-color-text-aside); + --color-icon-text: var(--dark-color-icon-text); + + --color-comment-tag-text: var(--dark-color-text); + --color-comment-tag: var(--dark-color-background); + + --color-link: var(--dark-color-link); + --color-focus-outline: var(--dark-color-focus-outline); + + --color-ts-keyword: var(--dark-color-ts-keyword); + --color-ts-project: var(--dark-color-ts-project); + --color-ts-module: var(--dark-color-ts-module); + --color-ts-namespace: var(--dark-color-ts-namespace); + --color-ts-enum: var(--dark-color-ts-enum); + --color-ts-enum-member: var(--dark-color-ts-enum-member); + --color-ts-variable: var(--dark-color-ts-variable); + --color-ts-function: var(--dark-color-ts-function); + --color-ts-class: var(--dark-color-ts-class); + --color-ts-interface: var(--dark-color-ts-interface); + --color-ts-constructor: var(--dark-color-ts-constructor); + --color-ts-property: var(--dark-color-ts-property); + --color-ts-method: var(--dark-color-ts-method); + --color-ts-reference: var(--dark-color-ts-reference); + --color-ts-call-signature: var(--dark-color-ts-call-signature); + --color-ts-index-signature: var(--dark-color-ts-index-signature); + --color-ts-constructor-signature: var( + --dark-color-ts-constructor-signature + ); + --color-ts-parameter: var(--dark-color-ts-parameter); + --color-ts-type-parameter: var(--dark-color-ts-type-parameter); + --color-ts-accessor: var(--dark-color-ts-accessor); + --color-ts-get-signature: var(--dark-color-ts-get-signature); + --color-ts-set-signature: var(--dark-color-ts-set-signature); + --color-ts-type-alias: var(--dark-color-ts-type-alias); + --color-document: var(--dark-color-document); + + --color-note: var(--dark-color-note); + --color-tip: var(--dark-color-tip); + --color-important: var(--dark-color-important); + --color-warning: var(--dark-color-warning); + --color-caution: var(--dark-color-caution); + + --external-icon: var(--dark-external-icon); + --color-scheme: var(--dark-color-scheme); + } + + html { + color-scheme: var(--color-scheme); + @media (prefers-reduced-motion: no-preference) { + scroll-behavior: smooth; + } + } + + *:focus-visible, + .tsd-accordion-summary:focus-visible svg { + outline: 2px solid var(--color-focus-outline); + } + + .always-visible, + .always-visible .tsd-signatures { + display: inherit !important; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.2; + } + + h1 { + font-size: 1.875rem; + margin: 0.67rem 0; + } + + h2 { + font-size: 1.5rem; + margin: 0.83rem 0; + } + + h3 { + font-size: 1.25rem; + margin: 1rem 0; + } + + h4 { + font-size: 1.05rem; + margin: 1.33rem 0; + } + + h5 { + font-size: 1rem; + margin: 1.5rem 0; + } + + h6 { + font-size: 0.875rem; + margin: 2.33rem 0; + } + + dl, + menu, + ol, + ul { + margin: 1em 0; + } + + dd { + margin: 0 0 0 34px; + } + + .container { + max-width: 1700px; + padding: 0 2rem; + } + + /* Footer */ + footer { + border-top: 1px solid var(--color-accent); + padding-top: 1rem; + padding-bottom: 1rem; + max-height: var(--dim-footer-height); + } + footer > p { + margin: 0 1em; + } + + .container-main { + margin: var(--dim-container-main-margin-y) auto; + /* toolbar, footer, margin */ + min-height: calc( + 100svh - var(--dim-header-height) - var(--dim-footer-height) - + 2 * var(--dim-container-main-margin-y) + ); + } + + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + @keyframes fade-out { + from { + opacity: 1; + visibility: visible; + } + to { + opacity: 0; + } + } + @keyframes pop-in-from-right { + from { + transform: translate(100%, 0); + } + to { + transform: translate(0, 0); + } + } + @keyframes pop-out-to-right { + from { + transform: translate(0, 0); + visibility: visible; + } + to { + transform: translate(100%, 0); + } + } + body { + background: var(--color-background); + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", + Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 16px; + color: var(--color-text); + margin: 0; + } + + a { + color: var(--color-link); + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + a.external[target="_blank"] { + background-image: var(--external-icon); + background-position: top 3px right; + background-repeat: no-repeat; + padding-right: 13px; + } + a.tsd-anchor-link { + color: var(--color-text); + } + :target { + scroll-margin-block: calc(var(--dim-header-height) + 0.5rem); + } + + code, + pre { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + padding: 0.2em; + margin: 0; + font-size: 0.875rem; + border-radius: 0.8em; + } + + pre { + position: relative; + white-space: pre-wrap; + word-wrap: break-word; + padding: 10px; + border: 1px solid var(--color-accent); + margin-bottom: 8px; + } + pre code { + padding: 0; + font-size: 100%; + } + pre > button { + position: absolute; + top: 10px; + right: 10px; + opacity: 0; + transition: opacity 0.1s; + box-sizing: border-box; + } + pre:hover > button, + pre > button.visible, + pre > button:focus-visible { + opacity: 1; + } + + blockquote { + margin: 1em 0; + padding-left: 1em; + border-left: 4px solid gray; + } + + img { + max-width: 100%; + } + + * { + scrollbar-width: thin; + scrollbar-color: var(--color-accent) var(--color-icon-background); + } + + *::-webkit-scrollbar { + width: 0.75rem; + } + + *::-webkit-scrollbar-track { + background: var(--color-icon-background); + } + + *::-webkit-scrollbar-thumb { + background-color: var(--color-accent); + border-radius: 999rem; + border: 0.25rem solid var(--color-icon-background); + } + + dialog { + border: none; + outline: none; + padding: 0; + background-color: var(--color-background); + } + dialog::backdrop { + display: none; + } + #tsd-overlay { + background-color: rgba(0, 0, 0, 0.5); + position: fixed; + z-index: 9999; + top: 0; + left: 0; + right: 0; + bottom: 0; + animation: fade-in var(--modal-animation-duration) forwards; + } + #tsd-overlay.closing { + animation-name: fade-out; + } + + .tsd-typography { + line-height: 1.333em; + } + .tsd-typography ul { + list-style: square; + padding: 0 0 0 20px; + margin: 0; + } + .tsd-typography .tsd-index-panel h3, + .tsd-index-panel .tsd-typography h3, + .tsd-typography h4, + .tsd-typography h5, + .tsd-typography h6 { + font-size: 1em; + } + .tsd-typography h5, + .tsd-typography h6 { + font-weight: normal; + } + .tsd-typography p, + .tsd-typography ul, + .tsd-typography ol { + margin: 1em 0; + } + .tsd-typography table { + border-collapse: collapse; + border: none; + } + .tsd-typography td, + .tsd-typography th { + padding: 6px 13px; + border: 1px solid var(--color-accent); + } + .tsd-typography thead, + .tsd-typography tr:nth-child(even) { + background-color: var(--color-background-secondary); + } + + .tsd-alert { + padding: 8px 16px; + margin-bottom: 16px; + border-left: 0.25em solid var(--alert-color); + } + .tsd-alert blockquote > :last-child, + .tsd-alert > :last-child { + margin-bottom: 0; + } + .tsd-alert-title { + color: var(--alert-color); + display: inline-flex; + align-items: center; + } + .tsd-alert-title span { + margin-left: 4px; + } + + .tsd-alert-note { + --alert-color: var(--color-alert-note); + } + .tsd-alert-tip { + --alert-color: var(--color-alert-tip); + } + .tsd-alert-important { + --alert-color: var(--color-alert-important); + } + .tsd-alert-warning { + --alert-color: var(--color-alert-warning); + } + .tsd-alert-caution { + --alert-color: var(--color-alert-caution); + } + + .tsd-breadcrumb { + margin: 0; + margin-top: 1rem; + padding: 0; + color: var(--color-text-aside); + } + .tsd-breadcrumb a { + color: var(--color-text-aside); + text-decoration: none; + } + .tsd-breadcrumb a:hover { + text-decoration: underline; + } + .tsd-breadcrumb li { + display: inline; + } + .tsd-breadcrumb li:after { + content: " / "; + } + + .tsd-comment-tags { + display: flex; + flex-direction: column; + } + dl.tsd-comment-tag-group { + display: flex; + align-items: center; + overflow: hidden; + margin: 0.5em 0; + } + dl.tsd-comment-tag-group dt { + display: flex; + margin-right: 0.5em; + font-size: 0.875em; + font-weight: normal; + } + dl.tsd-comment-tag-group dd { + margin: 0; + } + code.tsd-tag { + padding: 0.25em 0.4em; + border: 0.1em solid var(--color-accent); + margin-right: 0.25em; + font-size: 70%; + } + h1 code.tsd-tag:first-of-type { + margin-left: 0.25em; + } + + dl.tsd-comment-tag-group dd:before, + dl.tsd-comment-tag-group dd:after { + content: " "; + } + dl.tsd-comment-tag-group dd pre, + dl.tsd-comment-tag-group dd:after { + clear: both; + } + dl.tsd-comment-tag-group p { + margin: 0; + } + + .tsd-panel.tsd-comment .lead { + font-size: 1.1em; + line-height: 1.333em; + margin-bottom: 2em; + } + .tsd-panel.tsd-comment .lead:last-child { + margin-bottom: 0; + } + + .tsd-filter-visibility h4 { + font-size: 1rem; + padding-top: 0.75rem; + padding-bottom: 0.5rem; + margin: 0; + } + .tsd-filter-item:not(:last-child) { + margin-bottom: 0.5rem; + } + .tsd-filter-input { + display: flex; + width: -moz-fit-content; + width: fit-content; + align-items: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: pointer; + } + .tsd-filter-input input[type="checkbox"] { + cursor: pointer; + position: absolute; + width: 1.5em; + height: 1.5em; + opacity: 0; + } + .tsd-filter-input input[type="checkbox"]:disabled { + pointer-events: none; + } + .tsd-filter-input svg { + cursor: pointer; + width: 1.5em; + height: 1.5em; + margin-right: 0.5em; + border-radius: 0.33em; + /* Leaving this at full opacity breaks event listeners on Firefox. + Don't remove unless you know what you're doing. */ + opacity: 0.99; + } + .tsd-filter-input input[type="checkbox"]:focus-visible + svg { + outline: 2px solid var(--color-focus-outline); + } + .tsd-checkbox-background { + fill: var(--color-accent); + } + input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { + stroke: var(--color-text); + } + .tsd-filter-input input:disabled ~ svg > .tsd-checkbox-background { + fill: var(--color-background); + stroke: var(--color-accent); + stroke-width: 0.25rem; + } + .tsd-filter-input input:disabled ~ svg > .tsd-checkbox-checkmark { + stroke: var(--color-accent); + } + + .settings-label { + font-weight: bold; + text-transform: uppercase; + display: inline-block; + } + + .tsd-filter-visibility .settings-label { + margin: 0.75rem 0 0.5rem 0; + } + + .tsd-theme-toggle .settings-label { + margin: 0.75rem 0.75rem 0 0; + } + + .tsd-hierarchy h4 label:hover span { + text-decoration: underline; + } + + .tsd-hierarchy { + list-style: square; + margin: 0; + } + .tsd-hierarchy-target { + font-weight: bold; + } + .tsd-hierarchy-toggle { + color: var(--color-link); + cursor: pointer; + } + + .tsd-full-hierarchy:not(:last-child) { + margin-bottom: 1em; + padding-bottom: 1em; + border-bottom: 1px solid var(--color-accent); + } + .tsd-full-hierarchy, + .tsd-full-hierarchy ul { + list-style: none; + margin: 0; + padding: 0; + } + .tsd-full-hierarchy ul { + padding-left: 1.5rem; + } + .tsd-full-hierarchy a { + padding: 0.25rem 0 !important; + font-size: 1rem; + display: inline-flex; + align-items: center; + color: var(--color-text); + } + .tsd-full-hierarchy svg[data-dropdown] { + cursor: pointer; + } + .tsd-full-hierarchy svg[data-dropdown="false"] { + transform: rotate(-90deg); + } + .tsd-full-hierarchy svg[data-dropdown="false"] ~ ul { + display: none; + } + + .tsd-panel-group.tsd-index-group { + margin-bottom: 0; + } + .tsd-index-panel .tsd-index-list { + list-style: none; + line-height: 1.333em; + margin: 0; + padding: 0.25rem 0 0 0; + overflow: hidden; + display: grid; + grid-template-columns: repeat(3, 1fr); + column-gap: 1rem; + grid-template-rows: auto; + } + @media (max-width: 1024px) { + .tsd-index-panel .tsd-index-list { + grid-template-columns: repeat(2, 1fr); + } + } + @media (max-width: 768px) { + .tsd-index-panel .tsd-index-list { + grid-template-columns: repeat(1, 1fr); + } + } + .tsd-index-panel .tsd-index-list li { + -webkit-page-break-inside: avoid; + -moz-page-break-inside: avoid; + -ms-page-break-inside: avoid; + -o-page-break-inside: avoid; + page-break-inside: avoid; + } + + .tsd-flag { + display: inline-block; + padding: 0.25em 0.4em; + border-radius: 4px; + color: var(--color-comment-tag-text); + background-color: var(--color-comment-tag); + text-indent: 0; + font-size: 75%; + line-height: 1; + font-weight: normal; + } + + .tsd-anchor { + position: relative; + top: -100px; + } + + .tsd-member { + position: relative; + } + .tsd-member .tsd-anchor + h3 { + display: flex; + align-items: center; + margin-top: 0; + margin-bottom: 0; + border-bottom: none; + } + + .tsd-navigation.settings { + margin: 0; + margin-bottom: 1rem; + } + .tsd-navigation > a, + .tsd-navigation .tsd-accordion-summary { + width: calc(100% - 0.25rem); + display: flex; + align-items: center; + } + .tsd-navigation a, + .tsd-navigation summary > span, + .tsd-page-navigation a { + display: flex; + width: calc(100% - 0.25rem); + align-items: center; + padding: 0.25rem; + color: var(--color-text); + text-decoration: none; + box-sizing: border-box; + } + .tsd-navigation a.current, + .tsd-page-navigation a.current { + background: var(--color-active-menu-item); + color: var(--color-contrast-text); + } + .tsd-navigation a:hover, + .tsd-page-navigation a:hover { + text-decoration: underline; + } + .tsd-navigation ul, + .tsd-page-navigation ul { + margin-top: 0; + margin-bottom: 0; + padding: 0; + list-style: none; + } + .tsd-navigation li, + .tsd-page-navigation li { + padding: 0; + max-width: 100%; + } + .tsd-navigation .tsd-nav-link { + display: none; + } + .tsd-nested-navigation { + margin-left: 3rem; + } + .tsd-nested-navigation > li > details { + margin-left: -1.5rem; + } + .tsd-small-nested-navigation { + margin-left: 1.5rem; + } + .tsd-small-nested-navigation > li > details { + margin-left: -1.5rem; + } + + .tsd-page-navigation-section > summary { + padding: 0.25rem; + } + .tsd-page-navigation-section > summary > svg { + margin-right: 0.25rem; + } + .tsd-page-navigation-section > div { + margin-left: 30px; + } + .tsd-page-navigation ul { + padding-left: 1.75rem; + } + + #tsd-sidebar-links a { + margin-top: 0; + margin-bottom: 0.5rem; + line-height: 1.25rem; + } + #tsd-sidebar-links a:last-of-type { + margin-bottom: 0; + } + + a.tsd-index-link { + padding: 0.25rem 0 !important; + font-size: 1rem; + line-height: 1.25rem; + display: inline-flex; + align-items: center; + color: var(--color-text); + } + .tsd-accordion-summary { + list-style-type: none; /* hide marker on non-safari */ + outline: none; /* broken on safari, so just hide it */ + display: flex; + align-items: center; + gap: 0.25rem; + box-sizing: border-box; + } + .tsd-accordion-summary::-webkit-details-marker { + display: none; /* hide marker on safari */ + } + .tsd-accordion-summary, + .tsd-accordion-summary a { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + + cursor: pointer; + } + .tsd-accordion-summary a { + width: calc(100% - 1.5rem); + } + .tsd-accordion-summary > * { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; + } + /* + * We need to be careful to target the arrow indicating whether the accordion + * is open, but not any other SVGs included in the details element. + */ + .tsd-accordion:not([open]) > .tsd-accordion-summary > svg:first-child { + transform: rotate(-90deg); + } + .tsd-index-content > :not(:first-child) { + margin-top: 0.75rem; + } + .tsd-index-summary { + margin-top: 1.5rem; + margin-bottom: 0.75rem; + display: flex; + align-content: center; + } + + .tsd-no-select { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + .tsd-kind-icon { + margin-right: 0.5rem; + width: 1.25rem; + height: 1.25rem; + min-width: 1.25rem; + min-height: 1.25rem; + } + .tsd-signature > .tsd-kind-icon { + margin-right: 0.8rem; + } + + .tsd-panel { + margin-bottom: 2.5rem; + } + .tsd-panel.tsd-member { + margin-bottom: 4rem; + } + .tsd-panel:empty { + display: none; + } + .tsd-panel > h1, + .tsd-panel > h2, + .tsd-panel > h3 { + margin: 1.5rem -1.5rem 0.75rem -1.5rem; + padding: 0 1.5rem 0.75rem 1.5rem; + } + .tsd-panel > h1.tsd-before-signature, + .tsd-panel > h2.tsd-before-signature, + .tsd-panel > h3.tsd-before-signature { + margin-bottom: 0; + border-bottom: none; + } + + .tsd-panel-group { + margin: 2rem 0; + } + .tsd-panel-group.tsd-index-group { + margin: 2rem 0; + } + .tsd-panel-group.tsd-index-group details { + margin: 2rem 0; + } + .tsd-panel-group > .tsd-accordion-summary { + margin-bottom: 1rem; + } + + #tsd-search[open] { + animation: fade-in var(--modal-animation-duration) ease-out forwards; + } + #tsd-search[open].closing { + animation-name: fade-out; + } + + /* Avoid setting `display` on closed dialog */ + #tsd-search[open] { + display: flex; + flex-direction: column; + padding: 1rem; + width: 32rem; + max-width: 90vw; + max-height: calc(100vh - env(keyboard-inset-height, 0px) - 25vh); + /* Anchor dialog to top */ + margin-top: 10vh; + border-radius: 6px; + will-change: max-height; + } + #tsd-search-input { + box-sizing: border-box; + width: 100%; + padding: 0 0.625rem; /* 10px */ + outline: 0; + border: 2px solid var(--color-accent); + background-color: transparent; + color: var(--color-text); + border-radius: 4px; + height: 2.5rem; + flex: 0 0 auto; + font-size: 0.875rem; + transition: border-color 0.2s, background-color 0.2s; + } + #tsd-search-input:focus-visible { + background-color: var(--color-background-active); + border-color: transparent; + color: var(--color-contrast-text); + } + #tsd-search-input::placeholder { + color: inherit; + opacity: 0.8; + } + #tsd-search-results { + margin: 0; + padding: 0; + list-style: none; + flex: 1 1 auto; + display: flex; + flex-direction: column; + overflow-y: auto; + } + #tsd-search-results:not(:empty) { + margin-top: 0.5rem; + } + #tsd-search-results > li { + background-color: var(--color-background); + line-height: 1.5; + box-sizing: border-box; + border-radius: 4px; + } + #tsd-search-results > li:nth-child(even) { + background-color: var(--color-background-secondary); + } + #tsd-search-results > li:is(:hover, [aria-selected="true"]) { + background-color: var(--color-background-active); + color: var(--color-contrast-text); + } + /* It's important that this takes full size of parent `li`, to capture a click on `li` */ + #tsd-search-results > li > a { + display: flex; + align-items: center; + padding: 0.5rem 0.25rem; + box-sizing: border-box; + width: 100%; + } + #tsd-search-results > li > a > .text { + flex: 1 1 auto; + min-width: 0; + overflow-wrap: anywhere; + } + #tsd-search-results > li > a .parent { + color: var(--color-text-aside); + } + #tsd-search-results > li > a mark { + color: inherit; + background-color: inherit; + font-weight: bold; + } + #tsd-search-status { + flex: 1; + display: grid; + place-content: center; + text-align: center; + overflow-wrap: anywhere; + } + #tsd-search-status:not(:empty) { + min-height: 6rem; + } + + .tsd-signature { + margin: 0 0 1rem 0; + padding: 1rem 0.5rem; + border: 1px solid var(--color-accent); + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 14px; + overflow-x: auto; + } + + .tsd-signature-keyword { + color: var(--color-ts-keyword); + font-weight: normal; + } + + .tsd-signature-symbol { + color: var(--color-text-aside); + font-weight: normal; + } + + .tsd-signature-type { + font-style: italic; + font-weight: normal; + } + + .tsd-signatures { + padding: 0; + margin: 0 0 1em 0; + list-style-type: none; + } + .tsd-signatures .tsd-signature { + margin: 0; + border-color: var(--color-accent); + border-width: 1px 0; + transition: background-color 0.1s; + } + .tsd-signatures .tsd-index-signature:not(:last-child) { + margin-bottom: 1em; + } + .tsd-signatures .tsd-index-signature .tsd-signature { + border-width: 1px; + } + .tsd-description .tsd-signatures .tsd-signature { + border-width: 1px; + } + + ul.tsd-parameter-list, + ul.tsd-type-parameter-list { + list-style: square; + margin: 0; + padding-left: 20px; + } + ul.tsd-parameter-list > li.tsd-parameter-signature, + ul.tsd-type-parameter-list > li.tsd-parameter-signature { + list-style: none; + margin-left: -20px; + } + ul.tsd-parameter-list h5, + ul.tsd-type-parameter-list h5 { + font-size: 16px; + margin: 1em 0 0.5em 0; + } + .tsd-sources { + margin-top: 1rem; + font-size: 0.875em; + } + .tsd-sources a { + color: var(--color-text-aside); + text-decoration: underline; + } + .tsd-sources ul { + list-style: none; + padding: 0; + } + + .tsd-page-toolbar { + position: sticky; + z-index: 1; + top: 0; + left: 0; + width: 100%; + color: var(--color-text); + background: var(--color-background-secondary); + border-bottom: var(--dim-toolbar-border-bottom-width) + var(--color-accent) solid; + transition: transform 0.3s ease-in-out; + } + .tsd-page-toolbar a { + color: var(--color-text); + } + .tsd-toolbar-contents { + display: flex; + align-items: center; + height: var(--dim-toolbar-contents-height); + margin: 0 auto; + } + .tsd-toolbar-contents > .title { + font-weight: bold; + margin-right: auto; + } + #tsd-toolbar-links { + display: flex; + align-items: center; + gap: 1.5rem; + margin-right: 1rem; + } + + .tsd-widget { + box-sizing: border-box; + display: inline-block; + opacity: 0.8; + height: 2.5rem; + width: 2.5rem; + transition: opacity 0.1s, background-color 0.1s; + text-align: center; + cursor: pointer; + border: none; + background-color: transparent; + } + .tsd-widget:hover { + opacity: 0.9; + } + .tsd-widget:active { + opacity: 1; + background-color: var(--color-accent); + } + #tsd-toolbar-menu-trigger { + display: none; + } + + .tsd-member-summary-name { + display: inline-flex; + align-items: center; + padding: 0.25rem; + text-decoration: none; + } + + .tsd-anchor-icon { + display: inline-flex; + align-items: center; + margin-left: 0.5rem; + color: var(--color-text); + vertical-align: middle; + } + + .tsd-anchor-icon svg { + width: 1em; + height: 1em; + visibility: hidden; + } + + .tsd-member-summary-name:hover > .tsd-anchor-icon svg, + .tsd-anchor-link:hover > .tsd-anchor-icon svg, + .tsd-anchor-icon:focus-visible svg { + visibility: visible; + } + + .deprecated { + text-decoration: line-through !important; + } + + .warning { + padding: 1rem; + color: var(--color-warning-text); + background: var(--color-background-warning); + } + + .tsd-kind-project { + color: var(--color-ts-project); + } + .tsd-kind-module { + color: var(--color-ts-module); + } + .tsd-kind-namespace { + color: var(--color-ts-namespace); + } + .tsd-kind-enum { + color: var(--color-ts-enum); + } + .tsd-kind-enum-member { + color: var(--color-ts-enum-member); + } + .tsd-kind-variable { + color: var(--color-ts-variable); + } + .tsd-kind-function { + color: var(--color-ts-function); + } + .tsd-kind-class { + color: var(--color-ts-class); + } + .tsd-kind-interface { + color: var(--color-ts-interface); + } + .tsd-kind-constructor { + color: var(--color-ts-constructor); + } + .tsd-kind-property { + color: var(--color-ts-property); + } + .tsd-kind-method { + color: var(--color-ts-method); + } + .tsd-kind-reference { + color: var(--color-ts-reference); + } + .tsd-kind-call-signature { + color: var(--color-ts-call-signature); + } + .tsd-kind-index-signature { + color: var(--color-ts-index-signature); + } + .tsd-kind-constructor-signature { + color: var(--color-ts-constructor-signature); + } + .tsd-kind-parameter { + color: var(--color-ts-parameter); + } + .tsd-kind-type-parameter { + color: var(--color-ts-type-parameter); + } + .tsd-kind-accessor { + color: var(--color-ts-accessor); + } + .tsd-kind-get-signature { + color: var(--color-ts-get-signature); + } + .tsd-kind-set-signature { + color: var(--color-ts-set-signature); + } + .tsd-kind-type-alias { + color: var(--color-ts-type-alias); + } + + /* if we have a kind icon, don't color the text by kind */ + .tsd-kind-icon ~ span { + color: var(--color-text); + } + + /* mobile */ + @media (max-width: 769px) { + #tsd-toolbar-menu-trigger { + display: inline-block; + /* temporary fix to vertically align, for compatibility */ + line-height: 2.5; + } + #tsd-toolbar-links { + display: none; + } + + .container-main { + display: flex; + } + .col-content { + float: none; + max-width: 100%; + width: 100%; + } + .col-sidebar { + position: fixed !important; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + z-index: 1024; + top: 0 !important; + bottom: 0 !important; + left: auto !important; + right: 0 !important; + padding: 1.5rem 1.5rem 0 0; + width: 75vw; + visibility: hidden; + background-color: var(--color-background); + transform: translate(100%, 0); + } + .col-sidebar > *:last-child { + padding-bottom: 20px; + } + .overlay { + content: ""; + display: block; + position: fixed; + z-index: 1023; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.75); + visibility: hidden; + } + + .to-has-menu .overlay { + animation: fade-in 0.4s; + } + + .to-has-menu .col-sidebar { + animation: pop-in-from-right 0.4s; + } + + .from-has-menu .overlay { + animation: fade-out 0.4s; + } + + .from-has-menu .col-sidebar { + animation: pop-out-to-right 0.4s; + } + + .has-menu body { + overflow: hidden; + } + .has-menu .overlay { + visibility: visible; + } + .has-menu .col-sidebar { + visibility: visible; + transform: translate(0, 0); + display: flex; + flex-direction: column; + gap: 1.5rem; + max-height: 100vh; + padding: 1rem 2rem; + } + .has-menu .tsd-navigation { + max-height: 100%; + } + .tsd-navigation .tsd-nav-link { + display: flex; + } + } + + /* one sidebar */ + @media (min-width: 770px) { + .container-main { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); + grid-template-areas: "sidebar content"; + --dim-container-main-margin-y: 2rem; + } + + .tsd-breadcrumb { + margin-top: 0; + } + + .col-sidebar { + grid-area: sidebar; + } + .col-content { + grid-area: content; + padding: 0 1rem; + } + } + @media (min-width: 770px) and (max-width: 1399px) { + .col-sidebar { + max-height: calc( + 100vh - var(--dim-header-height) - var(--dim-footer-height) - + 2 * var(--dim-container-main-margin-y) + ); + overflow: auto; + position: sticky; + top: calc( + var(--dim-header-height) + var(--dim-container-main-margin-y) + ); + } + .site-menu { + margin-top: 1rem; + } + } + + /* two sidebars */ + @media (min-width: 1200px) { + .container-main { + grid-template-columns: + minmax(0, 1fr) minmax(0, 2.5fr) minmax( + 0, + 20rem + ); + grid-template-areas: "sidebar content toc"; + } + + .col-sidebar { + display: contents; + } + + .page-menu { + grid-area: toc; + padding-left: 1rem; + } + .site-menu { + grid-area: sidebar; + } + + .site-menu { + margin-top: 0rem; + } + + .page-menu, + .site-menu { + max-height: calc( + 100vh - var(--dim-header-height) - var(--dim-footer-height) - + 2 * var(--dim-container-main-margin-y) + ); + overflow: auto; + position: sticky; + top: calc( + var(--dim-header-height) + var(--dim-container-main-margin-y) + ); + } + } +} diff --git a/sdk_v2/js/docs/classes/AudioClient.html b/sdk_v2/js/docs/classes/AudioClient.html new file mode 100644 index 0000000..91f4dca --- /dev/null +++ b/sdk_v2/js/docs/classes/AudioClient.html @@ -0,0 +1,16 @@ +AudioClient | foundry-local-sdk
foundry-local-sdk
    Preparing search index...

    Class AudioClient

    Client for performing audio operations (transcription, translation) with a loaded model. +Follows the OpenAI Audio API structure.

    +
    Index

    Constructors

    • Parameters

      • modelId: string
      • coreInterop: CoreInterop

      Returns AudioClient

    Methods

    • Transcribes audio into the input language.

      +

      Parameters

      • audioFile: any

        The audio file to transcribe.

        +
      • Optionaloptions: any

        Optional parameters for transcription.

        +

      Returns Promise<any>

      The transcription result.

      +

      Error - Not implemented.

      +
    • Transcribes audio into the input language using streaming.

      +

      Parameters

      • audioFile: any

        The audio file to transcribe.

        +
      • Optionaloptions: any

        Optional parameters for transcription.

        +

      Returns Promise<any>

      The transcription result.

      +

      Error - Not implemented.

      +
    diff --git a/sdk_v2/js/docs/classes/Catalog.html b/sdk_v2/js/docs/classes/Catalog.html new file mode 100644 index 0000000..621ce7c --- /dev/null +++ b/sdk_v2/js/docs/classes/Catalog.html @@ -0,0 +1,30 @@ +Catalog | foundry-local-sdk
    foundry-local-sdk
      Preparing search index...

      Class Catalog

      Represents a catalog of AI models available in the system. +Provides methods to discover, list, and retrieve models and their variants.

      +
      Index

      Constructors

      Accessors

      • get name(): string

        Gets the name of the catalog.

        +

        Returns string

        The name of the catalog.

        +

      Methods

      • Retrieves a list of all locally cached model variants. +This method is asynchronous as it may involve file I/O or querying the underlying core.

        +

        Returns Promise<ModelVariant[]>

        A Promise that resolves to an array of cached ModelVariant objects.

        +
      • Retrieves a list of all currently loaded model variants. +This operation is asynchronous because checking the loaded status may involve querying +the underlying core or an external service, which can be an I/O bound operation.

        +

        Returns Promise<ModelVariant[]>

        A Promise that resolves to an array of loaded ModelVariant objects.

        +
      • Retrieves a model by its alias. +This method is asynchronous as it may ensure the catalog is up-to-date by fetching from a remote service.

        +

        Parameters

        • alias: string

          The alias of the model to retrieve.

          +

        Returns Promise<Model | undefined>

        A Promise that resolves to the Model object if found, otherwise undefined.

        +
      • Lists all available models in the catalog. +This method is asynchronous as it may fetch the model list from a remote service or perform file I/O.

        +

        Returns Promise<Model[]>

        A Promise that resolves to an array of Model objects.

        +
      • Retrieves a specific model variant by its ID. +This method is asynchronous as it may ensure the catalog is up-to-date by fetching from a remote service.

        +

        Parameters

        • modelId: string

          The unique identifier of the model variant.

          +

        Returns Promise<ModelVariant | undefined>

        A Promise that resolves to the ModelVariant object if found, otherwise undefined.

        +
      diff --git a/sdk_v2/js/docs/classes/ChatClient.html b/sdk_v2/js/docs/classes/ChatClient.html new file mode 100644 index 0000000..ad72263 --- /dev/null +++ b/sdk_v2/js/docs/classes/ChatClient.html @@ -0,0 +1,15 @@ +ChatClient | foundry-local-sdk
      foundry-local-sdk
        Preparing search index...

        Class ChatClient

        Client for performing chat completions with a loaded model. +Follows the OpenAI Chat Completion API structure.

        +
        Index

        Constructors

        Properties

        Methods

        Constructors

        • Parameters

          • modelId: string
          • coreInterop: CoreInterop

          Returns ChatClient

        Properties

        settings: ChatClientSettings = ...

        Configuration settings for chat completions.

        +

        Methods

        • Performs a synchronous chat completion.

          +

          Parameters

          • messages: any[]

            An array of message objects (e.g., { role: 'user', content: 'Hello' }).

            +

          Returns Promise<any>

          The chat completion response object.

          +
        • Performs a streaming chat completion.

          +

          Parameters

          • messages: any[]

            An array of message objects.

            +
          • callback: (chunk: any) => void

            A callback function that receives each chunk of the streaming response.

            +

          Returns Promise<void>

          A promise that resolves when the stream is complete.

          +
        diff --git a/sdk_v2/js/docs/classes/FoundryLocalManager.html b/sdk_v2/js/docs/classes/FoundryLocalManager.html new file mode 100644 index 0000000..17cddf0 --- /dev/null +++ b/sdk_v2/js/docs/classes/FoundryLocalManager.html @@ -0,0 +1,28 @@ +FoundryLocalManager | foundry-local-sdk
        foundry-local-sdk
          Preparing search index...

          Class FoundryLocalManager

          The main entry point for the Foundry Local SDK. +Manages the initialization of the core system and provides access to the Catalog and ModelLoadManager.

          +
          Index

          Accessors

          • get catalog(): Catalog

            Gets the Catalog instance for discovering and managing models.

            +

            Returns Catalog

            The Catalog instance.

            +
          • get urls(): string[]

            Gets the URLs where the web service is listening. +Returns an empty array if the web service is not running.

            +

            Returns string[]

            An array of URLs.

            +

          Methods

          • Ensures that the necessary execution providers (EPs) are downloaded. +Also serves as a manual trigger for EP download if ManualEpDownload is enabled.

            +

            Returns void

          • Starts the local web service. +Use the urls property to retrieve the bound addresses after the service has started. +If no listener address is configured, the service defaults to 127.0.0.1:0 (binding to a random ephemeral port).

            +

            Returns void

            Error - If starting the service fails.

            +
          • Stops the local web service.

            +

            Returns void

            Error - If stopping the service fails.

            +
          • Creates the FoundryLocalManager singleton with the provided configuration.

            +

            Parameters

            Returns FoundryLocalManager

            The initialized FoundryLocalManager instance.

            +
            const manager = FoundryLocalManager.create({
            appName: 'MyApp',
            logLevel: 'info'
            }); +
            + +
          diff --git a/sdk_v2/js/docs/classes/Model.html b/sdk_v2/js/docs/classes/Model.html new file mode 100644 index 0000000..1b44d07 --- /dev/null +++ b/sdk_v2/js/docs/classes/Model.html @@ -0,0 +1,48 @@ +Model | foundry-local-sdk
          foundry-local-sdk
            Preparing search index...

            Class Model

            Represents a high-level AI model that may have multiple variants (e.g., quantized versions, different formats). +Manages the selection and interaction with a specific model variant.

            +

            Implements

            Index

            Constructors

            Accessors

            • get alias(): string

              Gets the alias of the model.

              +

              Returns string

              The model alias.

              +
            • get id(): string

              Gets the ID of the currently selected variant.

              +

              Returns string

              The ID of the selected variant.

              +
            • get isCached(): boolean

              Checks if the currently selected variant is cached locally.

              +

              Returns boolean

              True if cached, false otherwise.

              +
            • get path(): string

              Gets the local file path of the currently selected variant.

              +

              Returns string

              The local file path.

              +
            • get variants(): ModelVariant[]

              Gets all available variants for this model.

              +

              Returns ModelVariant[]

              An array of ModelVariant objects.

              +

            Methods

            • Adds a new variant to this model. +Automatically selects the new variant if it is cached and the current one is not.

              +

              Parameters

              Returns void

              Error - If the variant's alias does not match the model's alias.

              +
            • Creates a ChatClient for interacting with the model via chat completions.

              +

              Returns ChatClient

              A ChatClient instance.

              +
            • Downloads the currently selected variant.

              +

              Parameters

              • OptionalprogressCallback: (progress: number) => void

                Optional callback to report download progress.

                +

              Returns void

            • Checks if the currently selected variant is loaded in memory.

              +

              Returns Promise<boolean>

              True if loaded, false otherwise.

              +
            • Loads the currently selected variant into memory.

              +

              Returns Promise<void>

              A promise that resolves when the model is loaded.

              +
            • Removes the currently selected variant from the local cache.

              +

              Returns void

            • Selects a specific variant by its ID.

              +

              Parameters

              • modelId: string

                The ID of the variant to select.

                +

              Returns void

              Error - If the variant with the specified ID is not found.

              +
            • Unloads the currently selected variant from memory.

              +

              Returns Promise<void>

              A promise that resolves when the model is unloaded.

              +
            diff --git a/sdk_v2/js/docs/classes/ModelLoadManager.html b/sdk_v2/js/docs/classes/ModelLoadManager.html new file mode 100644 index 0000000..cb6d3d1 --- /dev/null +++ b/sdk_v2/js/docs/classes/ModelLoadManager.html @@ -0,0 +1,16 @@ +ModelLoadManager | foundry-local-sdk
            foundry-local-sdk
              Preparing search index...

              Class ModelLoadManager

              Manages the loading and unloading of models. +Handles communication with the core system or an external service (future support).

              +
              Index

              Constructors

              Methods

              Constructors

              • Parameters

                • coreInterop: CoreInterop
                • OptionalexternalServiceUrl: string

                Returns ModelLoadManager

              Methods

              • Lists the IDs of all currently loaded models.

                +

                Returns Promise<string[]>

                An array of loaded model IDs.

                +

                Error - If listing via external service fails or if JSON parsing fails.

                +
              • Loads a model into memory.

                +

                Parameters

                • modelId: string

                  The ID of the model to load.

                  +

                Returns Promise<void>

                Error - If loading via external service fails.

                +
              • Unloads a model from memory.

                +

                Parameters

                • modelId: string

                  The ID of the model to unload.

                  +

                Returns Promise<void>

                Error - If unloading via external service fails.

                +
              diff --git a/sdk_v2/js/docs/classes/ModelVariant.html b/sdk_v2/js/docs/classes/ModelVariant.html new file mode 100644 index 0000000..3a565a7 --- /dev/null +++ b/sdk_v2/js/docs/classes/ModelVariant.html @@ -0,0 +1,40 @@ +ModelVariant | foundry-local-sdk
              foundry-local-sdk
                Preparing search index...

                Class ModelVariant

                Represents a specific variant of a model (e.g., a specific quantization or format). +Contains the low-level implementation for interacting with the model.

                +

                Implements

                Index

                Constructors

                Accessors

                • get alias(): string

                  Gets the alias of the model.

                  +

                  Returns string

                  The model alias.

                  +
                • get id(): string

                  Gets the unique identifier of the model variant.

                  +

                  Returns string

                  The model ID.

                  +
                • get isCached(): boolean

                  Checks if the model variant is cached locally.

                  +

                  Returns boolean

                  True if cached, false otherwise.

                  +
                • get modelInfo(): ModelInfo

                  Gets the detailed information about the model variant.

                  +

                  Returns ModelInfo

                  The ModelInfo object.

                  +
                • get path(): string

                  Gets the local file path of the model variant.

                  +

                  Returns string

                  The local file path.

                  +

                Methods

                • Creates an AudioClient for interacting with the model via audio operations.

                  +

                  Returns AudioClient

                  An AudioClient instance.

                  +
                • Creates a ChatClient for interacting with the model via chat completions.

                  +

                  Returns ChatClient

                  A ChatClient instance.

                  +
                • Downloads the model variant.

                  +

                  Parameters

                  • OptionalprogressCallback: (progress: number) => void

                    Optional callback to report download progress.

                    +

                  Returns void

                  Error - If progress callback is provided (not implemented).

                  +
                • Checks if the model variant is loaded in memory.

                  +

                  Returns Promise<boolean>

                  True if loaded, false otherwise.

                  +
                • Loads the model variant into memory.

                  +

                  Returns Promise<void>

                  A promise that resolves when the model is loaded.

                  +
                • Removes the model variant from the local cache.

                  +

                  Returns void

                • Unloads the model variant from memory.

                  +

                  Returns Promise<void>

                  A promise that resolves when the model is unloaded.

                  +
                diff --git a/sdk_v2/js/docs/hierarchy.html b/sdk_v2/js/docs/hierarchy.html new file mode 100644 index 0000000..9d025b6 --- /dev/null +++ b/sdk_v2/js/docs/hierarchy.html @@ -0,0 +1 @@ +foundry-local-sdk
                foundry-local-sdk
                  Preparing search index...

                  foundry-local-sdk

                  Hierarchy Summary

                  diff --git a/sdk_v2/js/docs/index.html b/sdk_v2/js/docs/index.html new file mode 100644 index 0000000..68e1317 --- /dev/null +++ b/sdk_v2/js/docs/index.html @@ -0,0 +1,38 @@ +foundry-local-sdk
                  foundry-local-sdk
                    Preparing search index...

                    foundry-local-sdk

                    Foundry Local JS SDK

                    The Foundry Local JS SDK provides a JavaScript/TypeScript interface for interacting with local AI models via the Foundry Local Core. It allows you to discover, download, load, and run inference on models directly on your local machine.

                    +

                    To install the SDK, run the following command in your project directory:

                    +
                    npm install foundry-local-js-sdk
                    +
                    + +

                    Note: Ensure you have the necessary native dependencies configured as per the main repository instructions.

                    +

                    Initialize the FoundryLocalManager with your configuration.

                    +
                    import { FoundryLocalManager } from 'foundry-local-js-sdk';

                    const manager = FoundryLocalManager.create({
                    libraryPath: '/path/to/core/library',
                    modelCacheDir: '/path/to/model/cache',
                    logLevel: 'info'
                    }); +
                    + +

                    Use the Catalog to list available models.

                    +
                    const catalog = manager.catalog;
                    const models = catalog.models;

                    models.forEach(model => {
                    console.log(`Model: ${model.alias}`);
                    }); +
                    + +
                    const model = catalog.getModel('phi-3-mini');

                    if (model) {
                    await model.load();

                    const chatClient = model.createChatClient();
                    const response = await chatClient.completeChat([
                    { role: 'user', content: 'Hello, how are you?' }
                    ]);

                    console.log(response.choices[0].message.content);

                    await model.unload();
                    } +
                    + +

                    The SDK source code is documented using TSDoc. You can generate the API documentation using TypeDoc.

                    +

                    Run the following command to generate the HTML documentation in the docs folder:

                    +
                    npm run docs
                    +
                    + +

                    Open docs/index.html in your browser to view the documentation.

                    +

                    To run the tests, use:

                    +
                    npm test
                    +
                    + +

                    See test/README.md for more details on setting up and running tests.

                    +

                    The SDK includes an example script demonstrating chat completion. To run it:

                    +
                      +
                    1. Ensure you have the necessary core libraries and a model available (see Tests Prerequisites).
                    2. +
                    3. Run the example command:
                    4. +
                    +
                    npm run example
                    +
                    + +

                    This will execute examples/chat-completion.ts.

                    +
                    diff --git a/sdk_v2/js/docs/interfaces/FoundryLocalConfig.html b/sdk_v2/js/docs/interfaces/FoundryLocalConfig.html new file mode 100644 index 0000000..97cfb2a --- /dev/null +++ b/sdk_v2/js/docs/interfaces/FoundryLocalConfig.html @@ -0,0 +1,33 @@ +FoundryLocalConfig | foundry-local-sdk
                    foundry-local-sdk
                      Preparing search index...

                      Interface FoundryLocalConfig

                      Configuration options for the Foundry Local SDK. +Use a plain object with these properties to configure the SDK.

                      +
                      interface FoundryLocalConfig {
                          additionalSettings?: { [key: string]: string };
                          appDataDir?: string;
                          appName: string;
                          libraryPath?: string;
                          logLevel?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
                          logsDir?: string;
                          modelCacheDir?: string;
                          serviceEndpoint?: string;
                          webServiceUrls?: string;
                      }
                      Index

                      Properties

                      additionalSettings?: { [key: string]: string }

                      Additional settings to pass to the core. +Optional. Internal use only.

                      +
                      appDataDir?: string

                      The directory where application data should be stored. +Optional. Defaults to {user_home}/.{appName}.

                      +
                      appName: string

                      REQUIRED The name of the application using the SDK. +Used for identifying the application in logs and telemetry.

                      +
                      libraryPath?: string

                      The path to the directory containing the native Foundry Local Core libraries. +Optional. This directory must contain Microsoft.AI.Foundry.Local.Core, onnxruntime, and onnxruntime-genai binaries. +If not provided, the SDK attempts to discover them in standard locations.

                      +
                      logLevel?: "trace" | "debug" | "info" | "warn" | "error" | "fatal"

                      The logging level for the SDK. +Optional. Valid values: 'trace', 'debug', 'info', 'warn', 'error', 'fatal'. +Defaults to 'warn'.

                      +
                      logsDir?: string

                      The directory where log files are written. +Optional. Defaults to {appDataDir}/logs.

                      +
                      modelCacheDir?: string

                      The directory where models are downloaded and cached. +Optional. Defaults to {appDataDir}/cache/models.

                      +
                      serviceEndpoint?: string

                      The external URL if the web service is running in a separate process. +Optional. This is used to connect to an existing service instance.

                      +
                      webServiceUrls?: string

                      The URL(s) for the local web service to bind to. +Optional. Multiple URLs can be separated by semicolons. +Example: "http://127.0.0.1:8080"

                      +
                      diff --git a/sdk_v2/js/docs/interfaces/IModel.html b/sdk_v2/js/docs/interfaces/IModel.html new file mode 100644 index 0000000..76c2611 --- /dev/null +++ b/sdk_v2/js/docs/interfaces/IModel.html @@ -0,0 +1,12 @@ +IModel | foundry-local-sdk
                      foundry-local-sdk
                        Preparing search index...

                        Interface IModel

                        interface IModel {
                            get alias(): string;
                            get id(): string;
                            get isCached(): boolean;
                            get path(): string;
                            createAudioClient(): AudioClient;
                            createChatClient(): ChatClient;
                            download(progressCallback?: (progress: number) => void): void;
                            isLoaded(): Promise<boolean>;
                            load(): Promise<void>;
                            removeFromCache(): void;
                            unload(): Promise<void>;
                        }

                        Implemented by

                        Index

                        Accessors

                        • get alias(): string

                          Returns string

                        • get id(): string

                          Returns string

                        • get isCached(): boolean

                          Returns boolean

                        • get path(): string

                          Returns string

                        Methods

                        • Parameters

                          • OptionalprogressCallback: (progress: number) => void

                          Returns void

                        • Returns Promise<boolean>

                        • Returns Promise<void>

                        • Returns void

                        • Returns Promise<void>

                        diff --git a/sdk_v2/js/docs/interfaces/ModelInfo.html b/sdk_v2/js/docs/interfaces/ModelInfo.html new file mode 100644 index 0000000..0711f76 --- /dev/null +++ b/sdk_v2/js/docs/interfaces/ModelInfo.html @@ -0,0 +1,22 @@ +ModelInfo | foundry-local-sdk
                        foundry-local-sdk
                          Preparing search index...

                          Interface ModelInfo

                          interface ModelInfo {
                              alias: string;
                              cached: boolean;
                              createdAtUnix: number;
                              displayName?: string | null;
                              fileSizeMb?: number | null;
                              id: string;
                              license?: string | null;
                              licenseDescription?: string | null;
                              maxOutputTokens?: number | null;
                              minFLVersion?: string | null;
                              modelSettings?: ModelSettings | null;
                              modelType: string;
                              name: string;
                              promptTemplate?: PromptTemplate | null;
                              providerType: string;
                              publisher?: string | null;
                              runtime?: Runtime | null;
                              supportsToolCalling?: boolean | null;
                              task?: string | null;
                              uri: string;
                              version: number;
                          }
                          Index

                          Properties

                          alias: string
                          cached: boolean
                          createdAtUnix: number
                          displayName?: string | null
                          fileSizeMb?: number | null
                          id: string
                          license?: string | null
                          licenseDescription?: string | null
                          maxOutputTokens?: number | null
                          minFLVersion?: string | null
                          modelSettings?: ModelSettings | null
                          modelType: string
                          name: string
                          promptTemplate?: PromptTemplate | null
                          providerType: string
                          publisher?: string | null
                          runtime?: Runtime | null
                          supportsToolCalling?: boolean | null
                          task?: string | null
                          uri: string
                          version: number
                          diff --git a/sdk_v2/js/docs/modules.html b/sdk_v2/js/docs/modules.html new file mode 100644 index 0000000..62bb6d3 --- /dev/null +++ b/sdk_v2/js/docs/modules.html @@ -0,0 +1 @@ +foundry-local-sdk
                          foundry-local-sdk
                            Preparing search index...
                            diff --git a/sdk_v2/js/examples/chat-completion.ts b/sdk_v2/js/examples/chat-completion.ts new file mode 100644 index 0000000..3de70a5 --- /dev/null +++ b/sdk_v2/js/examples/chat-completion.ts @@ -0,0 +1,112 @@ +// ------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// ------------------------------------------------------------------------- + +import { FoundryLocalManager } from '../src/index.js'; + +async function main() { + try { + // Initialize the Foundry Local SDK + console.log('Initializing Foundry Local SDK...'); + + // NOTE: You must update libraryPath to point to your built DLL if not in standard location + const manager = FoundryLocalManager.create({ + appName: 'FoundryLocalExample', + serviceEndpoint: 'http://localhost:5000', + logLevel: 'info' + }); + console.log('✓ SDK initialized successfully'); + + // Explore available models + console.log('\nFetching available models...'); + const catalog = manager.catalog; + const models = await catalog.getModels(); + + console.log(`Found ${models.length} models:`); + for (const model of models) { + console.log(` - ${model.alias}`); + } + + // Explore cached models + console.log('\nFetching cached models...'); + const cachedModels = await catalog.getCachedModels(); + console.log(`Found ${cachedModels.length} cached models:`); + for (const cachedModel of cachedModels) { + console.log(` - ${cachedModel.alias}`); + } + + const modelAlias = 'MODEL_ALIAS'; // Replace with a valid model alias from the list above + + // Load the model first + console.log(`\nLoading model ${modelAlias}...`); + const modelToLoad = await catalog.getModel(modelAlias); + if (!modelToLoad) { + throw new Error(`Model ${modelAlias} not found`); + } + + await modelToLoad.load(); + console.log('✓ Model loaded'); + + // Create chat client + console.log('\nCreating chat client...'); + const chatClient = modelToLoad.createChatClient(); + + // Configure chat settings + chatClient.settings.temperature = 0.7; + chatClient.settings.maxTokens = 800; + + console.log('✓ Chat client created'); + + // Example chat completion + console.log('\nTesting chat completion...'); + const completion = await chatClient.completeChat([ + { role: 'user', content: 'What is the capital of France?' } + ]); + + console.log('\nChat completion result:'); + console.log(completion.choices[0]?.message?.content); + + // Example streaming completion + console.log('\nTesting streaming completion...'); + await chatClient.completeStreamingChat( + [{ role: 'user', content: 'Write a short poem about programming.' }], + (chunk) => { + const content = chunk.choices?.[0]?.message?.content; + if (content) { + process.stdout.write(content); + } + } + ); + console.log('\n'); + + // Model management example + const model = await catalog.getModel(modelAlias); + if (model) { + console.log('\nModel management example:'); + // Already loaded above, but let's check status + console.log(`Checking model: ${model.id}`); + console.log(`✓ Model loaded: ${await model.isLoaded()}`); + + // Unload when done + console.log('Unloading model...'); + await model.unload(); + console.log(`✓ Model loaded: ${await model.isLoaded()}`); + } + + console.log('\n✓ Example completed successfully'); + + } catch (error) { + console.log('Error running example:', error); + // Print stack trace if available + if (error instanceof Error && error.stack) { + console.log(error.stack); + } + process.exit(1); + } +} + +// Run the example +main().catch(console.error); + +export { main }; diff --git a/sdk_v2/js/how-to-publish-dev.md b/sdk_v2/js/how-to-publish-dev.md new file mode 100644 index 0000000..b7b7577 --- /dev/null +++ b/sdk_v2/js/how-to-publish-dev.md @@ -0,0 +1,49 @@ +# How to Publish a Dev Build + +This guide outlines the steps to publish a scoped development build of the Foundry Local SDK to npm. + +## Prerequisites +- An **npm account** (created at npmjs.com) +- An **Access Token** (generated in your npm account settings) + +## Instructions + +### 1. Setup Authentication +First, configure your local npm registry with your authentication token. Replace `{NPM_AUTH_TOKEN}` with your actual token. + +```bash +npm config set //registry.npmjs.org/:_authToken={NPM_AUTH_TOKEN} +``` + +### 2. Initialize Scoped Package +Initialize a new scoped package properly using your npm username. Replace `{USERNAME}` with your npm username. + +```bash +npm init --scope=@{USERNAME} +``` +> **Note:** Follow the interactive prompts to generate a custom `package.json`. + +### 3. Build the Project +Compile the TypeScript source code. + +```bash +npm run build +``` + +### 4. Pack the Artifacts +Create the distribution tarball. This will generate a `.tgz` file containing the `dist/` and `script/` directories, along with the `README.md` and `package.json`. + +```bash +npm pack +``` + +### 5. Publish to npm +Publish the generated tarball to the public registry. Replace `{TGZ_FILEPATH}` with the path to the file generated in the previous step. + +```bash +npm publish {TGZ_FILEPATH} --access public +``` + +--- + + **Reference:** For more details, see the [npm documentation on creating and publishing scoped public packages](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages). diff --git a/sdk_v2/js/package-lock.json b/sdk_v2/js/package-lock.json new file mode 100644 index 0000000..38b195b --- /dev/null +++ b/sdk_v2/js/package-lock.json @@ -0,0 +1,2017 @@ +{ + "name": "@prathikrao/foundry-local-sdk", + "version": "0.0.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@prathikrao/foundry-local-sdk", + "version": "0.0.2", + "hasInstallScript": true, + "dependencies": { + "koffi": "^2.9.0" + }, + "devDependencies": { + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "@types/node": "^24.10.1", + "chai": "^6.2.1", + "mocha": "^11.7.5", + "tsx": "^4.7.0", + "typedoc": "^0.28.15", + "typescript": "^5.9.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.19.0.tgz", + "integrity": "sha512-ZSlWfLvr8Nl0T4iA3FF/8VH8HivYF82xQts2DY0tJxZd4wtXJ8AA0nmdW9lmO4hlrh3f9xNwEPtOgqETPqKwDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.19.0", + "@shikijs/langs": "^3.19.0", + "@shikijs/themes": "^3.19.0", + "@shikijs/types": "^3.19.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.19.0.tgz", + "integrity": "sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.19.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.19.0.tgz", + "integrity": "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.19.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.19.0.tgz", + "integrity": "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.19.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.19.0.tgz", + "integrity": "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/koffi": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.14.1.tgz", + "integrity": "sha512-IMFL3IbRDXacSIjs7pPbPxgNlJ2hUtawQXU2QPdr6iw38jmv5AesAUG8HPX00xl0PPA2BbEa3noTw1YdHY+gHg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://buymeacoffee.com/koromix" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typedoc": { + "version": "0.28.15", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.15.tgz", + "integrity": "sha512-mw2/2vTL7MlT+BVo43lOsufkkd2CJO4zeOSuWQQsiXoV2VuEn7f6IZp2jsUDPmBMABpgR0R5jlcJ2OGEFYmkyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.17.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.8.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/sdk_v2/js/package.json b/sdk_v2/js/package.json new file mode 100644 index 0000000..07e4939 --- /dev/null +++ b/sdk_v2/js/package.json @@ -0,0 +1,31 @@ +{ + "name": "foundry-local-sdk", + "version": "0.8.0", + "description": "Foundry Local JavaScript SDK", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "files": [ + "dist", "script" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "docs": "typedoc --out docs src", + "example": "tsx examples/chat-completion.ts", + "install": "node script/install.cjs", + "test": "mocha --import=tsx test/**/*.test.ts" + }, + "dependencies": { + "koffi": "^2.9.0" + }, + "devDependencies": { + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "@types/node": "^24.10.1", + "chai": "^6.2.1", + "mocha": "^11.7.5", + "tsx": "^4.7.0", + "typedoc": "^0.28.15", + "typescript": "^5.9.3" + } +} diff --git a/sdk_v2/js/script/install.cjs b/sdk_v2/js/script/install.cjs new file mode 100644 index 0000000..124d0c1 --- /dev/null +++ b/sdk_v2/js/script/install.cjs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { execSync } = require('child_process'); + +// Determine platform +const PLATFORM_MAP = { + 'win32-x64': 'win-x64', + 'win32-arm64': 'win-arm64', + 'linux-x64': 'linux-x64', + 'darwin-arm64': 'osx-arm64', +}; +const platformKey = `${os.platform()}-${os.arch()}`; +const RID = PLATFORM_MAP[platformKey]; + +if (!RID) { + console.warn(`[foundry-local] Unsupported platform: ${platformKey}. Skipping native library installation.`); + process.exit(0); +} + +const BIN_DIR = path.join(__dirname, '..', 'node_modules', 'Microsoft.AI.Foundry.Local'); +const REQUIRED_FILES = [ + 'Microsoft.AI.Foundry.Local.Core.dll', + 'onnxruntime.dll', + 'onnxruntime-genai.dll', +].map(f => f.replace('.dll', os.platform() === 'win32' ? '.dll' : os.platform() === 'darwin' ? '.dylib' : '.so')); + +// When you run npm install --winml, npm does not pass --winml as a command-line argument to your script. +// Instead, it sets an environment variable named npm_config_winml to 'true'. +const useWinML = process.env.npm_config_winml === 'true'; + +console.log(`[foundry-local] WinML enabled: ${useWinML}`); + +// NuGet package definitions +const MAIN_PACKAGE = { + name: useWinML ? 'Microsoft.AI.Foundry.Local.Core.WinML' : 'Microsoft.AI.Foundry.Local.Core', + version: '0.8.2.2', + feed: 'https://pkgs.dev.azure.com/microsoft/windows.ai.toolkit/_packaging/Neutron/nuget/v3/index.json' +}; + +// Check if already installed +if (fs.existsSync(BIN_DIR) && REQUIRED_FILES.every(f => fs.existsSync(path.join(BIN_DIR, f)))) { + console.log(`[foundry-local] Native libraries already installed.`); + process.exit(0); +} + +console.log(`[foundry-local] Installing native libraries for ${RID}...`); + +// Ensure bin directory exists +fs.mkdirSync(BIN_DIR, { recursive: true }); + +const ARTIFACTS = [ + { name: MAIN_PACKAGE.name, files: ['Microsoft.AI.Foundry.Local.Core'] }, + { name: 'Microsoft.ML.OnnxRuntime.Foundry', files: ['onnxruntime'] }, + { name: useWinML ? 'Microsoft.ML.OnnxRuntimeGenAI.WinML' : 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', files: ['onnxruntime-genai'] }, +]; + +// Download and extract using NuGet CLI +function installPackages() { + const tempDir = path.join(__dirname, '..', 'node_modules', 'FoundryLocalCorePath', 'temp'); + + // Clean temp dir + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + fs.mkdirSync(tempDir, { recursive: true }); + + console.log(` Installing ${MAIN_PACKAGE.name} version ${MAIN_PACKAGE.version}...`); + + try { + // We install only the main package, dependencies are automatically installed + const cmd = `nuget install ${MAIN_PACKAGE.name} -Version ${MAIN_PACKAGE.version} -Source "${MAIN_PACKAGE.feed}" -OutputDirectory "${tempDir}" -Prerelease -NonInteractive`; + execSync(cmd, { stdio: 'inherit' }); + + // Copy files from installed packages + for (const artifact of ARTIFACTS) { + findAndCopyArtifact(tempDir, artifact); + } + + // Cleanup + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch (e) { + console.warn(` ⚠ Warning: Failed to clean up temporary files: ${e.message}`); + } + + } catch (err) { + throw new Error(`Failed to install packages: ${err.message}`); + } +} + +function findAndCopyArtifact(tempDir, artifact) { + // Find directory starting with package name (e.g. Microsoft.AI.Foundry.Local.Core.0.8.2.2) + const entries = fs.readdirSync(tempDir); + // Sort to get latest version if multiple (though we expect one) + const pkgDirName = entries + .filter(e => e.toLowerCase().startsWith(artifact.name.toLowerCase()) && fs.statSync(path.join(tempDir, e)).isDirectory()) + .sort().pop(); + + if (!pkgDirName) { + console.warn(` ⚠ Package folder not found for ${artifact.name}`); + return; + } + + const installedDir = path.join(tempDir, pkgDirName); + const ext = os.platform() === 'win32' ? '.dll' : os.platform() === 'darwin' ? '.dylib' : '.so'; + const nativeDir = path.join(installedDir, 'runtimes', RID, 'native'); + + for (const fileBase of artifact.files) { + const srcPath = path.join(nativeDir, `${fileBase}${ext}`); + const destPath = path.join(BIN_DIR, `${fileBase}${ext}`); + + if (fs.existsSync(srcPath)) { + fs.copyFileSync(srcPath, destPath); + console.log(` ✓ Installed ${fileBase}${ext} from ${pkgDirName}`); + } else { + console.warn(` ⚠ File not found: ${srcPath}`); + } + } +} + +// Install all packages +try { + installPackages(); + console.log('[foundry-local] ✓ Installation complete.'); +} catch (err) { + console.error('[foundry-local] Installation failed:', err.message); + process.exit(1); +} \ No newline at end of file diff --git a/sdk_v2/js/src/catalog.ts b/sdk_v2/js/src/catalog.ts new file mode 100644 index 0000000..7c14b39 --- /dev/null +++ b/sdk_v2/js/src/catalog.ts @@ -0,0 +1,152 @@ +import { CoreInterop } from './detail/coreInterop.js'; +import { ModelLoadManager } from './detail/modelLoadManager.js'; +import { Model } from './model.js'; +import { ModelVariant } from './modelVariant.js'; +import { ModelInfo } from './types.js'; + +/** + * Represents a catalog of AI models available in the system. + * Provides methods to discover, list, and retrieve models and their variants. + */ +export class Catalog { + private _name: string; + private coreInterop: CoreInterop; + private modelLoadManager: ModelLoadManager; + private _models: Model[] = []; + private modelAliasToModel: Map = new Map(); + private modelIdToModelVariant: Map = new Map(); + private lastFetch: number = 0; + + constructor(coreInterop: CoreInterop, modelLoadManager: ModelLoadManager) { + this.coreInterop = coreInterop; + this.modelLoadManager = modelLoadManager; + this._name = this.coreInterop.executeCommand("get_catalog_name"); + } + + /** + * Gets the name of the catalog. + * @returns The name of the catalog. + */ + public get name(): string { + return this._name; + } + + private async updateModels(): Promise { + // TODO: make this configurable + if ((Date.now() - this.lastFetch) < 6 * 60 * 60 * 1000) { // 6 hours + return; + } + + // Potential network call to fetch model list + const modelListJson = this.coreInterop.executeCommand("get_model_list"); + let modelsInfo: ModelInfo[] = []; + try { + modelsInfo = JSON.parse(modelListJson); + } catch (error) { + throw new Error(`Failed to parse model list JSON: ${error}`); + } + + this.modelAliasToModel.clear(); + this.modelIdToModelVariant.clear(); + this._models = []; + + for (const info of modelsInfo) { + const variant = new ModelVariant(info, this.coreInterop, this.modelLoadManager); + let model = this.modelAliasToModel.get(info.alias); + + if (!model) { + model = new Model(variant); + this.modelAliasToModel.set(info.alias, model); + this._models.push(model); + } else { + model.addVariant(variant); + } + + this.modelIdToModelVariant.set(variant.id, variant); + } + + this.lastFetch = Date.now(); + } + + /** + * Lists all available models in the catalog. + * This method is asynchronous as it may fetch the model list from a remote service or perform file I/O. + * @returns A Promise that resolves to an array of Model objects. + */ + public async getModels(): Promise { + await this.updateModels(); + return this._models; + } + + /** + * Retrieves a model by its alias. + * This method is asynchronous as it may ensure the catalog is up-to-date by fetching from a remote service. + * @param alias - The alias of the model to retrieve. + * @returns A Promise that resolves to the Model object if found, otherwise undefined. + */ + public async getModel(alias: string): Promise { + await this.updateModels(); + return this.modelAliasToModel.get(alias); + } + + /** + * Retrieves a specific model variant by its ID. + * This method is asynchronous as it may ensure the catalog is up-to-date by fetching from a remote service. + * @param modelId - The unique identifier of the model variant. + * @returns A Promise that resolves to the ModelVariant object if found, otherwise undefined. + */ + public async getModelVariant(modelId: string): Promise { + await this.updateModels(); + return this.modelIdToModelVariant.get(modelId); + } + + /** + * Retrieves a list of all locally cached model variants. + * This method is asynchronous as it may involve file I/O or querying the underlying core. + * @returns A Promise that resolves to an array of cached ModelVariant objects. + */ + public async getCachedModels(): Promise { + await this.updateModels(); + const cachedModelListJson = this.coreInterop.executeCommand("get_cached_models"); + let cachedModelIds: string[] = []; + try { + cachedModelIds = JSON.parse(cachedModelListJson); + } catch (error) { + throw new Error(`Failed to parse cached model list JSON: ${error}`); + } + const cachedModels: Set = new Set(); + + for (const modelId of cachedModelIds) { + const variant = this.modelIdToModelVariant.get(modelId); + if (variant) { + cachedModels.add(variant); + } + } + return Array.from(cachedModels); + } + + /** + * Retrieves a list of all currently loaded model variants. + * This operation is asynchronous because checking the loaded status may involve querying + * the underlying core or an external service, which can be an I/O bound operation. + * @returns A Promise that resolves to an array of loaded ModelVariant objects. + */ + public async getLoadedModels(): Promise { + await this.updateModels(); + let loadedModelIds: string[] = []; + try { + loadedModelIds = await this.modelLoadManager.listLoaded(); + } catch (error) { + throw new Error(`Failed to list loaded models: ${error}`); + } + const loadedModels: ModelVariant[] = []; + + for (const modelId of loadedModelIds) { + const variant = this.modelIdToModelVariant.get(modelId); + if (variant) { + loadedModels.push(variant); + } + } + return loadedModels; + } +} \ No newline at end of file diff --git a/sdk_v2/js/src/configuration.ts b/sdk_v2/js/src/configuration.ts new file mode 100644 index 0000000..916c5da --- /dev/null +++ b/sdk_v2/js/src/configuration.ts @@ -0,0 +1,107 @@ +/** + * Configuration options for the Foundry Local SDK. + * Use a plain object with these properties to configure the SDK. + */ +export interface FoundryLocalConfig { + /** + * **REQUIRED** The name of the application using the SDK. + * Used for identifying the application in logs and telemetry. + */ + appName: string; + + /** + * The directory where application data should be stored. + * Optional. Defaults to `{user_home}/.{appName}`. + */ + appDataDir?: string; + + /** + * The directory where models are downloaded and cached. + * Optional. Defaults to `{appDataDir}/cache/models`. + */ + modelCacheDir?: string; + + /** + * The directory where log files are written. + * Optional. Defaults to `{appDataDir}/logs`. + */ + logsDir?: string; + + /** + * The logging level for the SDK. + * Optional. Valid values: 'trace', 'debug', 'info', 'warn', 'error', 'fatal'. + * Defaults to 'warn'. + */ + logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'; + + /** + * The URL(s) for the local web service to bind to. + * Optional. Multiple URLs can be separated by semicolons. + * Example: "http://127.0.0.1:8080" + */ + webServiceUrls?: string; + + /** + * The external URL if the web service is running in a separate process. + * Optional. This is used to connect to an existing service instance. + */ + serviceEndpoint?: string; + + /** + * The path to the directory containing the native Foundry Local Core libraries. + * Optional. This directory must contain `Microsoft.AI.Foundry.Local.Core`, `onnxruntime`, and `onnxruntime-genai` binaries. + * If not provided, the SDK attempts to discover them in standard locations. + */ + libraryPath?: string; + + /** + * Additional settings to pass to the core. + * Optional. Internal use only. + */ + additionalSettings?: { [key: string]: string }; +} + +// Log level mapping from JS-style to C#-style +const LOG_LEVEL_MAP: { [key: string]: string } = { + 'trace': 'Verbose', + 'debug': 'Debug', + 'info': 'Information', + 'warn': 'Warning', + 'error': 'Error', + 'fatal': 'Fatal' +}; + +// Internal Configuration class (not exported) +export class Configuration { + public params: { [key: string]: string }; + + constructor(config: FoundryLocalConfig) { + if (!config) { + throw new Error("Configuration must be provided."); + } + + if (!config.appName || config.appName.trim() === "") { + throw new Error("appName must be set to a valid application name."); + } + + this.params = { + 'AppName': config.appName + }; + + if (config.appDataDir) this.params['AppDataDir'] = config.appDataDir; + if (config.modelCacheDir) this.params['ModelCacheDir'] = config.modelCacheDir; + if (config.logsDir) this.params['LogsDir'] = config.logsDir; + if (config.logLevel) this.params['LogLevel'] = LOG_LEVEL_MAP[config.logLevel] || config.logLevel; + if (config.webServiceUrls) this.params['WebServiceUrls'] = config.webServiceUrls; + if (config.serviceEndpoint) this.params['WebServiceExternalUrl'] = config.serviceEndpoint; + if (config.libraryPath) this.params['FoundryLocalCorePath'] = config.libraryPath; + + // Flatten additional settings into params + if (config.additionalSettings) { + for (const key in config.additionalSettings) { + this.params[key] = config.additionalSettings[key]; + } + } + } +} + diff --git a/sdk_v2/js/src/detail/coreInterop.ts b/sdk_v2/js/src/detail/coreInterop.ts new file mode 100644 index 0000000..498341f --- /dev/null +++ b/sdk_v2/js/src/detail/coreInterop.ts @@ -0,0 +1,151 @@ +import koffi from 'koffi'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; +import { Configuration } from '../configuration.js'; + +koffi.struct('RequestBuffer', { + Command: 'char*', + CommandLength: 'int32_t', + Data: 'char*', + DataLength: 'int32_t', +}); + +koffi.struct('ResponseBuffer', { + Data: 'void*', + DataLength: 'int32_t', + Error: 'void*', + ErrorLength: 'int32_t', +}); + +const CallbackType = koffi.proto('void CallbackType(void *data, int32_t length, void *userData)'); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export class CoreInterop { + private lib: any; + private execute_command: any; + private execute_command_with_callback: any; + + private static _getLibraryExtension(): string { + const platform = process.platform; + if (platform === 'win32') return '.dll'; + if (platform === 'linux') return '.so'; + if (platform === 'darwin') return '.dylib'; + throw new Error(`Unsupported platform: ${platform}`); + } + + private static _resolveDefaultCorePath(): string | null { + // Default path under node_modules/Microsoft.AI.Foundry.Local + const ext = CoreInterop._getLibraryExtension(); + // Get current module's directory path (ES module equivalent of __dirname) + const corePath = path.join(__dirname, '..', '..', 'node_modules', 'Microsoft.AI.Foundry.Local', `Microsoft.AI.Foundry.Local.Core${ext}`); + + if (fs.existsSync(corePath)) { + return corePath; + } + + return null; + } + + constructor(config: Configuration) { + const corePath = config.params['FoundryLocalCorePath'] || CoreInterop._resolveDefaultCorePath(); + + if (!corePath) { + throw new Error("FoundryLocalCorePath not specified in configuration and could not auto-discover binaries. Please run 'npm install' to download native libraries."); + } + + const coreDir = path.dirname(corePath); + const ext = CoreInterop._getLibraryExtension(); + + koffi.load(path.join(coreDir, `onnxruntime${ext}`)); + koffi.load(path.join(coreDir, `onnxruntime-genai${ext}`)); + this.lib = koffi.load(corePath); + + this.execute_command = this.lib.func('void execute_command(RequestBuffer *request, _Inout_ ResponseBuffer *response)'); + this.execute_command_with_callback = this.lib.func('void execute_command_with_callback(RequestBuffer *request, _Inout_ ResponseBuffer *response, CallbackType *callback, void *userData)'); + } + + public executeCommand(command: string, params?: any): string { + const cmdBuf = koffi.alloc('char', command.length + 1); + koffi.encode(cmdBuf, 'char', command, command.length + 1); + + const dataStr = params ? JSON.stringify(params) : ''; + const dataBuf = koffi.alloc('char', dataStr.length + 1); + koffi.encode(dataBuf, 'char', dataStr, dataStr.length + 1); + + const req = { + Command: koffi.address(cmdBuf), + CommandLength: command.length, + Data: koffi.address(dataBuf), + DataLength: dataStr.length + }; + const res = { Data: 0, DataLength: 0, Error: 0, ErrorLength: 0 }; + + this.execute_command(req, res); + + try { + if (res.Error) { + const errorMsg = koffi.decode(res.Error, 'char', res.ErrorLength); + throw new Error(`Command '${command}' failed: ${errorMsg}`); + } + + return res.Data ? koffi.decode(res.Data, 'char', res.DataLength) : ""; + } finally { + // Free the heap-allocated response strings using koffi.free() + // Docs: https://koffi.dev/pointers/#disposable-types + if (res.Data) koffi.free(res.Data); + if (res.Error) koffi.free(res.Error); + } + } + + public executeCommandStreaming(command: string, params: any, callback: (chunk: string) => void): Promise { + const cmdBuf = koffi.alloc('char', command.length + 1); + koffi.encode(cmdBuf, 'char', command, command.length + 1); + + const dataStr = params ? JSON.stringify(params) : ''; + const dataBuf = koffi.alloc('char', dataStr.length + 1); + koffi.encode(dataBuf, 'char', dataStr, dataStr.length + 1); + + const cb = koffi.register((data: any, length: number, userData: any) => { + const chunk = koffi.decode(data, 'char', length); + callback(chunk); + }, koffi.pointer(CallbackType)); + + return new Promise((resolve, reject) => { + const req = { + Command: koffi.address(cmdBuf), + CommandLength: command.length, + Data: koffi.address(dataBuf), + DataLength: dataStr.length + }; + const res = { Data: 0, DataLength: 0, Error: 0, ErrorLength: 0 }; + + this.execute_command_with_callback.async(req, res, cb, null, (err: any) => { + koffi.unregister(cb); + koffi.free(cmdBuf); + koffi.free(dataBuf); + + if (err) { + reject(err); + return; + } + + try { + if (res.Error) { + const errorMsg = koffi.decode(res.Error, 'char', res.ErrorLength); + reject(new Error(`Command '${command}' failed: ${errorMsg}`)); + } else { + resolve(); + } + } finally { + // Free the heap-allocated response strings using koffi.free() + if (res.Data) koffi.free(res.Data); + if (res.Error) koffi.free(res.Error); + } + }); + }); + } + +} diff --git a/sdk_v2/js/src/detail/modelLoadManager.ts b/sdk_v2/js/src/detail/modelLoadManager.ts new file mode 100644 index 0000000..e66f327 --- /dev/null +++ b/sdk_v2/js/src/detail/modelLoadManager.ts @@ -0,0 +1,82 @@ +import { CoreInterop } from './coreInterop.js'; +import packageJson from '../../package.json' with { type: "json" }; +const { version } = packageJson; + +/** + * Manages the loading and unloading of models. + * Handles communication with the core system or an external service (future support). + */ +export class ModelLoadManager { + private coreInterop: CoreInterop; + private externalServiceUrl?: string; + private headers: HeadersInit; + + constructor(coreInterop: CoreInterop, externalServiceUrl?: string) { + this.coreInterop = coreInterop; + this.externalServiceUrl = externalServiceUrl; + this.headers = { + 'User-Agent': `foundry-local-js-sdk/${version}` + }; + } + + /** + * Loads a model into memory. + * @param modelId - The ID of the model to load. + * @throws Error - If loading via external service fails. + */ + public async load(modelId: string): Promise { + if (this.externalServiceUrl) { + const url = new URL(`models/load/${encodeURIComponent(modelId)}`, this.externalServiceUrl); + try { + const response = await fetch(url.toString(), { headers: this.headers }); + if (!response.ok) { + throw new Error(`Error loading model ${modelId} from ${this.externalServiceUrl}: ${response.statusText}`); + } + } catch (error: any) { + throw new Error(`Network error occurred while loading model ${modelId} from ${this.externalServiceUrl}: ${error.message}`); + } + return; + } + this.coreInterop.executeCommand("load_model", { Params: { Model: modelId } }); + } + + /** + * Unloads a model from memory. + * @param modelId - The ID of the model to unload. + * @throws Error - If unloading via external service fails. + */ + public async unload(modelId: string): Promise { + if (this.externalServiceUrl) { + const url = new URL(`models/unload/${encodeURIComponent(modelId)}`, this.externalServiceUrl); + const response = await fetch(url.toString(), { headers: this.headers }); + if (!response.ok) { + throw new Error(`Error unloading model ${modelId} from ${this.externalServiceUrl}: ${response.statusText}`); + } + return; + } + this.coreInterop.executeCommand("unload_model", { Params: { Model: modelId } }); + } + + /** + * Lists the IDs of all currently loaded models. + * @returns An array of loaded model IDs. + * @throws Error - If listing via external service fails or if JSON parsing fails. + */ + public async listLoaded(): Promise { + if (this.externalServiceUrl) { + const url = new URL('models/loaded', this.externalServiceUrl); + const response = await fetch(url.toString(), { headers: this.headers }); + if (!response.ok) { + throw new Error(`Error listing loaded models from ${this.externalServiceUrl}: ${response.statusText}`); + } + const list = await response.json(); + return list || []; + } + const response = this.coreInterop.executeCommand("list_loaded_models"); + try { + return JSON.parse(response); + } catch (error) { + throw new Error(`Failed to decode JSON response: ${error}. Response was: ${response}`); + } + } +} diff --git a/sdk_v2/js/src/foundryLocalManager.ts b/sdk_v2/js/src/foundryLocalManager.ts new file mode 100644 index 0000000..586474a --- /dev/null +++ b/sdk_v2/js/src/foundryLocalManager.ts @@ -0,0 +1,103 @@ +import { Configuration, FoundryLocalConfig } from './configuration.js'; +import { CoreInterop } from './detail/coreInterop.js'; +import { ModelLoadManager } from './detail/modelLoadManager.js'; +import { Catalog } from './catalog.js'; +import { ChatClient } from './openai/chatClient.js'; +import { AudioClient } from './openai/audioClient.js'; + +/** + * The main entry point for the Foundry Local SDK. + * Manages the initialization of the core system and provides access to the Catalog and ModelLoadManager. + */ +export class FoundryLocalManager { + private static instance: FoundryLocalManager; + private config: Configuration; + private coreInterop: CoreInterop; + private _modelLoadManager: ModelLoadManager; + private _catalog: Catalog; + private _urls: string[] = []; + + private constructor(config: Configuration) { + this.config = config; + this.coreInterop = new CoreInterop(this.config); + this.coreInterop.executeCommand("initialize", { Params: this.config.params }); + this._modelLoadManager = new ModelLoadManager(this.coreInterop); + this._catalog = new Catalog(this.coreInterop, this._modelLoadManager); + } + + /** + * Creates the FoundryLocalManager singleton with the provided configuration. + * @param config - The configuration settings for the SDK (plain object). + * @returns The initialized FoundryLocalManager instance. + * @example + * ```typescript + * const manager = FoundryLocalManager.create({ + * appName: 'MyApp', + * logLevel: 'info' + * }); + * ``` + */ + public static create(config: FoundryLocalConfig): FoundryLocalManager { + if (!FoundryLocalManager.instance) { + const internalConfig = new Configuration(config); + FoundryLocalManager.instance = new FoundryLocalManager(internalConfig); + } + return FoundryLocalManager.instance; + } + + /** + * Gets the Catalog instance for discovering and managing models. + * @returns The Catalog instance. + */ + public get catalog(): Catalog { + return this._catalog; + } + + /** + * Gets the URLs where the web service is listening. + * Returns an empty array if the web service is not running. + * @returns An array of URLs. + */ + public get urls(): string[] { + return this._urls; + } + + /** + * Ensures that the necessary execution providers (EPs) are downloaded. + * Also serves as a manual trigger for EP download if ManualEpDownload is enabled. + */ + public ensureEpsDownloaded(): void { + const manualEpDownload = this.config.params["ManualEpDownload"]; + if (manualEpDownload && manualEpDownload.toLowerCase() === "true") { + this.coreInterop.executeCommand("ensure_eps_downloaded"); + } else { + throw new Error("Manual EP download is not enabled in the configuration."); + } + } + + /** + * Starts the local web service. + * Use the `urls` property to retrieve the bound addresses after the service has started. + * If no listener address is configured, the service defaults to `127.0.0.1:0` (binding to a random ephemeral port). + * @throws Error - If starting the service fails. + */ + public startWebService(): void { + const response = this.coreInterop.executeCommand("start_service"); + try { + this._urls = JSON.parse(response); + } catch (error) { + throw new Error(`Failed to decode JSON response from start_service: ${error}. Response was: ${response}`); + } + } + + /** + * Stops the local web service. + * @throws Error - If stopping the service fails. + */ + public stopWebService(): void { + if (this._urls.length > 0) { + this.coreInterop.executeCommand("stop_service"); + this._urls = []; + } + } +} diff --git a/sdk_v2/js/src/imodel.ts b/sdk_v2/js/src/imodel.ts new file mode 100644 index 0000000..3d0844d --- /dev/null +++ b/sdk_v2/js/src/imodel.ts @@ -0,0 +1,18 @@ +import { ChatClient } from './openai/chatClient.js'; +import { AudioClient } from './openai/audioClient.js'; + +export interface IModel { + get id(): string; + get alias(): string; + get isCached(): boolean; + isLoaded(): Promise; + + download(progressCallback?: (progress: number) => void): void; + get path(): string; + load(): Promise; + removeFromCache(): void; + unload(): Promise; + + createChatClient(): ChatClient; + createAudioClient(): AudioClient; +} diff --git a/sdk_v2/js/src/index.ts b/sdk_v2/js/src/index.ts new file mode 100644 index 0000000..2ad30b3 --- /dev/null +++ b/sdk_v2/js/src/index.ts @@ -0,0 +1,10 @@ +export { FoundryLocalManager } from './foundryLocalManager.js'; +export type { FoundryLocalConfig } from './configuration.js'; +export { Catalog } from './catalog.js'; +export { Model } from './model.js'; +export { ModelVariant } from './modelVariant.js'; +export type { IModel } from './imodel.js'; +export { ChatClient } from './openai/chatClient.js'; +export { AudioClient } from './openai/audioClient.js'; +export { ModelLoadManager } from './detail/modelLoadManager.js'; +export type { ModelInfo } from './types.js'; diff --git a/sdk_v2/js/src/model.ts b/sdk_v2/js/src/model.ts new file mode 100644 index 0000000..975ed95 --- /dev/null +++ b/sdk_v2/js/src/model.ts @@ -0,0 +1,149 @@ +import { ModelVariant } from './modelVariant.js'; +import { ChatClient } from './openai/chatClient.js'; +import { AudioClient } from './openai/audioClient.js'; +import { IModel } from './imodel.js'; + +/** + * Represents a high-level AI model that may have multiple variants (e.g., quantized versions, different formats). + * Manages the selection and interaction with a specific model variant. + */ +export class Model implements IModel { + private _alias: string; + + private _variants: ModelVariant[]; + private selectedVariant: ModelVariant; + + constructor(variant: ModelVariant) { + this._alias = variant.alias; + this._variants = [variant]; + this.selectedVariant = variant; + } + + /** + * Adds a new variant to this model. + * Automatically selects the new variant if it is cached and the current one is not. + * @param variant - The model variant to add. + * @throws Error - If the variant's alias does not match the model's alias. + */ + public addVariant(variant: ModelVariant): void { + if (variant.alias !== this._alias) { + throw new Error("Variant alias does not match model alias."); + } + this._variants.push(variant); + + // prefer the highest priority locally cached variant + if (variant.isCached && !this.selectedVariant.isCached) { + this.selectedVariant = variant; + } + } + + /** + * Selects a specific variant by its ID. + * @param modelId - The ID of the variant to select. + * @throws Error - If the variant with the specified ID is not found. + */ + public selectVariant(modelId: string): void { + for (const variant of this._variants) { + if (variant.id === modelId) { + this.selectedVariant = variant; + return; + } + } + throw new Error(`Model variant with id ${modelId} not found.`); + } + + /** + * Gets the ID of the currently selected variant. + * @returns The ID of the selected variant. + */ + public get id(): string { + return this.selectedVariant.id; + } + + /** + * Gets the alias of the model. + * @returns The model alias. + */ + public get alias(): string { + return this._alias; + } + + /** + * Checks if the currently selected variant is cached locally. + * @returns True if cached, false otherwise. + */ + public get isCached(): boolean { + return this.selectedVariant.isCached; + } + + /** + * Checks if the currently selected variant is loaded in memory. + * @returns True if loaded, false otherwise. + */ + public async isLoaded(): Promise { + return await this.selectedVariant.isLoaded(); + } + + /** + * Gets all available variants for this model. + * @returns An array of ModelVariant objects. + */ + public get variants(): ModelVariant[] { + return this._variants; + } + + /** + * Downloads the currently selected variant. + * @param progressCallback - Optional callback to report download progress. + */ + public download(progressCallback?: (progress: number) => void): void { + this.selectedVariant.download(progressCallback); + } + + /** + * Gets the local file path of the currently selected variant. + * @returns The local file path. + */ + public get path(): string { + return this.selectedVariant.path; + } + + /** + * Loads the currently selected variant into memory. + * @returns A promise that resolves when the model is loaded. + */ + public async load(): Promise { + await this.selectedVariant.load(); + } + + /** + * Removes the currently selected variant from the local cache. + */ + public removeFromCache(): void { + this.selectedVariant.removeFromCache(); + } + + /** + * Unloads the currently selected variant from memory. + * @returns A promise that resolves when the model is unloaded. + */ + public async unload(): Promise { + this.selectedVariant.unload(); + } + + /** + * Creates a ChatClient for interacting with the model via chat completions. + * @returns A ChatClient instance. + */ + public createChatClient(): ChatClient { + return this.selectedVariant.createChatClient(); + } + + /** + * Creates an AudioClient for interacting with the model via audio operations. + * @returns An AudioClient instance. + */ + public createAudioClient(): AudioClient { + return this.selectedVariant.createAudioClient(); + } +} diff --git a/sdk_v2/js/src/modelVariant.ts b/sdk_v2/js/src/modelVariant.ts new file mode 100644 index 0000000..a34134e --- /dev/null +++ b/sdk_v2/js/src/modelVariant.ts @@ -0,0 +1,126 @@ +import { CoreInterop } from './detail/coreInterop.js'; +import { ModelLoadManager } from './detail/modelLoadManager.js'; +import { ModelInfo } from './types.js'; +import { ChatClient } from './openai/chatClient.js'; +import { AudioClient } from './openai/audioClient.js'; +import { IModel } from './imodel.js'; + +/** + * Represents a specific variant of a model (e.g., a specific quantization or format). + * Contains the low-level implementation for interacting with the model. + */ +export class ModelVariant implements IModel { + private _modelInfo: ModelInfo; + private coreInterop: CoreInterop; + private modelLoadManager: ModelLoadManager; + + constructor(modelInfo: ModelInfo, coreInterop: CoreInterop, modelLoadManager: ModelLoadManager) { + this._modelInfo = modelInfo; + this.coreInterop = coreInterop; + this.modelLoadManager = modelLoadManager; + } + + /** + * Gets the unique identifier of the model variant. + * @returns The model ID. + */ + public get id(): string { + return this._modelInfo.id; + } + + /** + * Gets the alias of the model. + * @returns The model alias. + */ + public get alias(): string { + return this._modelInfo.alias; + } + + /** + * Gets the detailed information about the model variant. + * @returns The ModelInfo object. + */ + public get modelInfo(): ModelInfo { + return this._modelInfo; + } + + /** + * Checks if the model variant is cached locally. + * @returns True if cached, false otherwise. + */ + public get isCached(): boolean { + const cachedModels: string[] = JSON.parse(this.coreInterop.executeCommand("get_cached_models")); + return cachedModels.includes(this._modelInfo.id); + } + + /** + * Checks if the model variant is loaded in memory. + * @returns True if loaded, false otherwise. + */ + public async isLoaded(): Promise { + const loadedModels = await this.modelLoadManager.listLoaded(); + return loadedModels.includes(this._modelInfo.id); + } + + /** + * Downloads the model variant. + * @param progressCallback - Optional callback to report download progress. + * @throws Error - If progress callback is provided (not implemented). + */ + public download(progressCallback?: (progress: number) => void): void { + const request = { Params: { Model: this._modelInfo.id } }; + if (!progressCallback) { + this.coreInterop.executeCommand("download_model", request); + } else { + throw new Error("Download with progress callback is not implemented yet."); + } + } + + /** + * Gets the local file path of the model variant. + * @returns The local file path. + */ + public get path(): string { + const request = { Params: { Model: this._modelInfo.id } }; + return this.coreInterop.executeCommand("get_model_path", request); + } + + /** + * Loads the model variant into memory. + * @returns A promise that resolves when the model is loaded. + */ + public async load(): Promise { + await this.modelLoadManager.load(this._modelInfo.id); + } + + /** + * Removes the model variant from the local cache. + */ + public removeFromCache(): void { + this.coreInterop.executeCommand("remove_cached_model", { Params: { Model: this._modelInfo.id } }); + } + + /** + * Unloads the model variant from memory. + * @returns A promise that resolves when the model is unloaded. + */ + public async unload(): Promise { + await this.modelLoadManager.unload(this._modelInfo.id); + } + + /** + * Creates a ChatClient for interacting with the model via chat completions. + * @returns A ChatClient instance. + */ + public createChatClient(): ChatClient { + return new ChatClient(this._modelInfo.id, this.coreInterop); + } + + /** + * Creates an AudioClient for interacting with the model via audio operations. + * @returns An AudioClient instance. + */ + public createAudioClient(): AudioClient { + return new AudioClient(this._modelInfo.id, this.coreInterop); + } +} diff --git a/sdk_v2/js/src/openai/audioClient.ts b/sdk_v2/js/src/openai/audioClient.ts new file mode 100644 index 0000000..eec7e4f --- /dev/null +++ b/sdk_v2/js/src/openai/audioClient.ts @@ -0,0 +1,37 @@ +import { CoreInterop } from '../detail/coreInterop.js'; + +/** + * Client for performing audio operations (transcription, translation) with a loaded model. + * Follows the OpenAI Audio API structure. + */ +export class AudioClient { + private modelId: string; + private coreInterop: CoreInterop; + + constructor(modelId: string, coreInterop: CoreInterop) { + this.modelId = modelId; + this.coreInterop = coreInterop; + } + + /** + * Transcribes audio into the input language. + * @param audioFile - The audio file to transcribe. + * @param options - Optional parameters for transcription. + * @returns The transcription result. + * @throws Error - Not implemented. + */ + public async transcribe(audioFile: any, options?: any): Promise { + throw new Error("Synchronous audio transcription is not implemented."); + } + + /** + * Transcribes audio into the input language using streaming. + * @param audioFile - The audio file to transcribe. + * @param options - Optional parameters for transcription. + * @returns The transcription result. + * @throws Error - Not implemented. + */ + public async transcribeStreaming(audioFile: any, options?: any): Promise { + throw new Error("Streaming audio transcription is not implemented yet."); + } +} diff --git a/sdk_v2/js/src/openai/chatClient.ts b/sdk_v2/js/src/openai/chatClient.ts new file mode 100644 index 0000000..3ee08dc --- /dev/null +++ b/sdk_v2/js/src/openai/chatClient.ts @@ -0,0 +1,111 @@ +import { CoreInterop } from '../detail/coreInterop.js'; + +export class ChatClientSettings { + frequencyPenalty?: number; + maxTokens?: number; + n?: number; + temperature?: number; + presencePenalty?: number; + randomSeed?: number; + topK?: number; + topP?: number; + + /** + * Serializes the settings into an OpenAI-compatible request object. + * @internal + */ + _serialize() { + // Standard OpenAI properties + const result: any = { + frequency_penalty: this.frequencyPenalty, + max_tokens: this.maxTokens, + n: this.n, + presence_penalty: this.presencePenalty, + temperature: this.temperature, + top_p: this.topP, + }; + + // Foundry specific metadata properties + const metadata: Record = {}; + if (this.topK !== undefined) { + metadata["top_k"] = this.topK.toString(); + } + if (this.randomSeed !== undefined) { + metadata["random_seed"] = this.randomSeed.toString(); + } + + if (Object.keys(metadata).length > 0) { + result.metadata = metadata; + } + + // Filter out undefined properties + return Object.fromEntries(Object.entries(result).filter(([_, v]) => v !== undefined)); + } +} + +/** + * Client for performing chat completions with a loaded model. + * Follows the OpenAI Chat Completion API structure. + */ +export class ChatClient { + private modelId: string; + private coreInterop: CoreInterop; + + /** + * Configuration settings for chat completions. + */ + public settings = new ChatClientSettings(); + + constructor(modelId: string, coreInterop: CoreInterop) { + this.modelId = modelId; + this.coreInterop = coreInterop; + } + + /** + * Performs a synchronous chat completion. + * @param messages - An array of message objects (e.g., { role: 'user', content: 'Hello' }). + * @returns The chat completion response object. + */ + public async completeChat(messages: any[]): Promise { + const request = { + model: this.modelId, + messages, + // stream is undefined (false) by default + ...this.settings._serialize() + }; + + const response = this.coreInterop.executeCommand("chat_completions", { Params: { OpenAICreateRequest: JSON.stringify(request) } }); + return JSON.parse(response); + } + + /** + * Performs a streaming chat completion. + * @param messages - An array of message objects. + * @param callback - A callback function that receives each chunk of the streaming response. + * @returns A promise that resolves when the stream is complete. + */ + public async completeStreamingChat(messages: any[], callback: (chunk: any) => void): Promise { + const request = { + model: this.modelId, + messages, + stream: true, + ...this.settings._serialize() + }; + + await this.coreInterop.executeCommandStreaming( + "chat_completions", + { Params: { OpenAICreateRequest: JSON.stringify(request) } }, + (chunkStr: string) => { + if (chunkStr) { + try { + const chunk = JSON.parse(chunkStr); + callback(chunk); + } catch (e) { + throw new Error(`Failed completeStreamingChat: ${e}`); + } + } + } + ); + } +} + diff --git a/sdk_v2/js/src/types.ts b/sdk_v2/js/src/types.ts new file mode 100644 index 0000000..f7d8eac --- /dev/null +++ b/sdk_v2/js/src/types.ts @@ -0,0 +1,53 @@ +// adapted from sdk_v2\cs\src\FoundryModelInfo.cs + +export enum DeviceType { + Invalid = 'Invalid', + CPU = 'CPU', + GPU = 'GPU', + NPU = 'NPU' +} + +export interface PromptTemplate { + system?: string | null; + user?: string | null; + assistant: string; + prompt: string; +} + +export interface Runtime { + deviceType: DeviceType; + executionProvider: string; +} + +export interface Parameter { + name: string; + value?: string | null; +} + +export interface ModelSettings { + parameters?: Parameter[] | null; +} + +export interface ModelInfo { + id: string; + name: string; + version: number; + alias: string; + displayName?: string | null; + providerType: string; + uri: string; + modelType: string; + promptTemplate?: PromptTemplate | null; + publisher?: string | null; + modelSettings?: ModelSettings | null; + license?: string | null; + licenseDescription?: string | null; + cached: boolean; + task?: string | null; + runtime?: Runtime | null; + fileSizeMb?: number | null; + supportsToolCalling?: boolean | null; + maxOutputTokens?: number | null; + minFLVersion?: string | null; + createdAtUnix: number; +} diff --git a/sdk_v2/js/test/README.md b/sdk_v2/js/test/README.md new file mode 100644 index 0000000..ebebc78 --- /dev/null +++ b/sdk_v2/js/test/README.md @@ -0,0 +1,41 @@ +# Foundry Local JS SDK Tests + +This directory contains the test suite for the Foundry Local JS SDK. The tests use `mocha` as the test runner and `chai` for assertions. + +## Running Tests + +To run all tests: + +```bash +npm install && npm test +``` + +To run a specific test file: + +```bash +npx mocha --import=tsx test/model.test.ts +``` + +## Adding Local Model Tests + +To add tests that require specific local models: + +1. Ensure the model is available in your configured model cache and set the `modelCacheDir` configuration option. +2. Use the `TEST_MODEL_ALIAS` constant in `testUtils.ts` or define your own alias. +3. In your test, use `catalog.getCachedModels()` to verify the model is available before attempting to load it. + +Example: + +```typescript +it('should run inference on my-model', async function() { + const manager = getTestManager(); + const catalog = manager.getCatalog(); + const model = catalog.getModel('my-model'); + + if (!model) this.skip(); // Skip if model not found + + await model.load(); + // ... run inference ... + await model.unload(); +}); +``` diff --git a/sdk_v2/js/test/catalog.test.ts b/sdk_v2/js/test/catalog.test.ts new file mode 100644 index 0000000..ebb38ef --- /dev/null +++ b/sdk_v2/js/test/catalog.test.ts @@ -0,0 +1,48 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import { getTestManager, TEST_MODEL_ALIAS } from './testUtils.js'; + +describe('Catalog Tests', () => { + it('should initialize with catalog name', function() { + const manager = getTestManager(); + const catalog = manager.catalog; + expect(catalog.name).to.be.a('string'); + expect(catalog.name.length).to.be.greaterThan(0); + }); + + it('should list models', async function() { + this.timeout(10000); + const manager = getTestManager(); + const catalog = manager.catalog; + const models = await catalog.getModels(); + + expect(models).to.be.an('array'); + expect(models.length).to.be.greaterThan(0); + + // Verify we can find our test model + const testModel = models.find(m => m.alias === TEST_MODEL_ALIAS); + expect(testModel).to.not.be.undefined; + }); + + it('should get model by alias', async function() { + const manager = getTestManager(); + const catalog = manager.catalog; + const model = await catalog.getModel(TEST_MODEL_ALIAS); + + expect(model).to.not.be.undefined; + expect(model?.alias).to.equal(TEST_MODEL_ALIAS); + }); + + it('should get cached models', async function() { + const manager = getTestManager(); + const catalog = manager.catalog; + const cachedModels = await catalog.getCachedModels(); + + expect(cachedModels).to.be.an('array'); + // We expect at least one cached model since we are pointing to a populated cache + expect(cachedModels.length).to.be.greaterThan(0); + + const testModel = cachedModels.find(m => m.alias === TEST_MODEL_ALIAS); + expect(testModel).to.not.be.undefined; + }); +}); diff --git a/sdk_v2/js/test/detail/modelLoadManager.test.ts b/sdk_v2/js/test/detail/modelLoadManager.test.ts new file mode 100644 index 0000000..32d46f1 --- /dev/null +++ b/sdk_v2/js/test/detail/modelLoadManager.test.ts @@ -0,0 +1,124 @@ +import { expect } from 'chai'; +import { ModelLoadManager } from '../../src/detail/modelLoadManager.js'; +import { getTestManager, TEST_MODEL_ALIAS, IS_RUNNING_IN_CI } from '../testUtils.js'; + +describe('ModelLoadManager', function() { + let coreInterop: any; + let modelId: string; + let managerInstance: any; + let serviceUrl: string; + + before(async function() { + managerInstance = getTestManager(); + // Access private coreInterop using any cast + coreInterop = (managerInstance as any).coreInterop; + + const catalog = managerInstance.catalog; + const model = await catalog.getModel(TEST_MODEL_ALIAS); + if (!model) { + throw new Error(`Model ${TEST_MODEL_ALIAS} not found in catalog`); + } + modelId = model.id; + + // Start the real web service if not in CI + if (!IS_RUNNING_IN_CI) { + try { + managerInstance.startWebService(); + const urls = managerInstance.urls; + if (!urls || urls.length === 0) { + console.warn("Web service started but no URLs returned"); + } else { + serviceUrl = urls[0]; + } + } catch (e: any) { + console.warn(`Skipping web service tests: Failed to start web service (${e.message})`); + // If start_web_service is not supported by the local core binary, we can't run these tests. + } + } + }); + + after(function() { + if (!IS_RUNNING_IN_CI && managerInstance) { + try { + managerInstance.stopWebService(); + } catch (e) { + console.warn("Failed to stop web service:", e); + } + } + }); + + it('should load model using core interop when no external url is provided', async function() { + this.timeout(30000); + const manager = new ModelLoadManager(coreInterop); + + await manager.load(modelId); + + const loaded = await manager.listLoaded(); + expect(loaded).to.include(modelId); + }); + + it('should unload model using core interop when no external url is provided', async function() { + this.timeout(30000); + const manager = new ModelLoadManager(coreInterop); + + await manager.load(modelId); + let loaded = await manager.listLoaded(); + expect(loaded).to.include(modelId); + + await manager.unload(modelId); + + loaded = await manager.listLoaded(); + expect(loaded).to.not.include(modelId); + }); + + it('should list loaded models using core interop when no external url is provided', async function() { + this.timeout(30000); + const manager = new ModelLoadManager(coreInterop); + + await manager.load(modelId); + + const loaded = await manager.listLoaded(); + expect(loaded).to.be.an('array'); + expect(loaded).to.include(modelId); + }); + + it('should load and unload model using external service when url is provided', async function() { + if (IS_RUNNING_IN_CI || !serviceUrl) { + this.skip(); + } + + const manager = new ModelLoadManager(coreInterop, serviceUrl); + + // Load it first so we can unload it (can use core interop to setup state) + // Creating a manager WITHOUT serviceUrl to force core interop usage for setup + const setupManager = new ModelLoadManager(coreInterop); + await setupManager.load(modelId); + + let loaded = await setupManager.listLoaded(); + expect(loaded).to.include(modelId); + + // Unload via external service + await manager.unload(modelId); + + // Verify via core interop + loaded = await setupManager.listLoaded(); + expect(loaded).to.not.include(modelId); + }); + + it('should list loaded models using external service when url is provided', async function() { + if (IS_RUNNING_IN_CI || !serviceUrl) { + this.skip(); + } + + const manager = new ModelLoadManager(coreInterop, serviceUrl); + const setupManager = new ModelLoadManager(coreInterop); + + // Setup: Load model via core + await setupManager.load(modelId); + + // Verify: List via external service + const loaded = await manager.listLoaded(); + expect(loaded).to.be.an('array'); + expect(loaded).to.include(modelId); + }); +}); diff --git a/sdk_v2/js/test/foundryLocalManager.test.ts b/sdk_v2/js/test/foundryLocalManager.test.ts new file mode 100644 index 0000000..5ab4004 --- /dev/null +++ b/sdk_v2/js/test/foundryLocalManager.test.ts @@ -0,0 +1,19 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import { getTestManager } from './testUtils.js'; + +describe('Foundry Local Manager Tests', () => { + it('should initialize successfully', function() { + const manager = getTestManager(); + expect(manager).to.not.be.undefined; + }); + + it('should return catalog', function() { + const manager = getTestManager(); + const catalog = manager.catalog; + + expect(catalog).to.not.be.undefined; + // We don't assert the exact name as it might change, but we ensure it exists + expect(catalog.name).to.be.a('string'); + }); +}); diff --git a/sdk_v2/js/test/model.test.ts b/sdk_v2/js/test/model.test.ts new file mode 100644 index 0000000..fda5e9e --- /dev/null +++ b/sdk_v2/js/test/model.test.ts @@ -0,0 +1,172 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import { getTestManager, TEST_MODEL_ALIAS } from './testUtils.js'; + +describe('Model Tests', () => { + it('should verify cached models from test-data-shared', async function() { + const manager = getTestManager(); + const catalog = manager.catalog; + const cachedModels = await catalog.getCachedModels(); + + expect(cachedModels).to.be.an('array'); + expect(cachedModels.length).to.be.greaterThan(0); + + // Check for qwen model + const qwenModel = cachedModels.find(m => m.alias === 'qwen2.5-0.5b'); + expect(qwenModel, 'qwen2.5-0.5b should be cached').to.not.be.undefined; + expect(qwenModel?.isCached).to.be.true; + + // Check for whisper model + const whisperModel = cachedModels.find(m => m.alias === 'whisper-tiny'); + expect(whisperModel, 'whisper-tiny should be cached').to.not.be.undefined; + expect(whisperModel?.isCached).to.be.true; + }); + + it('should load and unload model', async function() { + this.timeout(10000); + const manager = getTestManager(); + const catalog = manager.catalog; + + // Ensure cache is populated first + const cachedModels = await catalog.getCachedModels(); + expect(cachedModels.length).to.be.greaterThan(0); + + const cachedVariant = cachedModels.find(m => m.alias === TEST_MODEL_ALIAS); + expect(cachedVariant).to.not.be.undefined; + + const model = await catalog.getModel(TEST_MODEL_ALIAS); + + expect(model).to.not.be.undefined; + if (!model || !cachedVariant) return; + + model.selectVariant(cachedVariant.id); + + // Ensure it's not loaded initially (or unload if it is) + if (await model.isLoaded()) { + await model.unload(); + } + expect(await model.isLoaded()).to.be.false; + + await model.load(); + expect(await model.isLoaded()).to.be.true; + + await model.unload(); + expect(await model.isLoaded()).to.be.false; + }); + + it('should perform chat completion', async function() { + this.timeout(10000); + const manager = getTestManager(); + const catalog = manager.catalog; + + // Ensure cache is populated first + const cachedModels = await catalog.getCachedModels(); + expect(cachedModels.length).to.be.greaterThan(0); + + const cachedVariant = cachedModels.find(m => m.alias === TEST_MODEL_ALIAS); + expect(cachedVariant).to.not.be.undefined; + + const model = await catalog.getModel(TEST_MODEL_ALIAS); + + expect(model).to.not.be.undefined; + if (!model || !cachedVariant) return; + + model.selectVariant(cachedVariant.id); + + await model.load(); + + try { + const client = model.createChatClient(); + + client.settings.maxTokens = 500; + client.settings.temperature = 0.0; // for deterministic results + + const result = await client.completeChat([ + { role: 'user', content: 'You are a calculator. Be precise. What is the answer to 7 multiplied by 6?' } + ]); + + expect(result).to.not.be.undefined; + expect(result.choices).to.be.an('array'); + expect(result.choices.length).to.be.greaterThan(0); + expect(result.choices[0].message).to.not.be.undefined; + expect(result.choices[0].message.content).to.be.a('string'); + console.log(`Response: ${result.choices[0].message.content}`); + expect(result.choices[0].message.content).to.include('42'); + } finally { + await model.unload(); + } + }); + + it('should perform streaming chat completion', async function() { + this.timeout(10000); + const manager = getTestManager(); + const catalog = manager.catalog; + + // Ensure cache is populated first + const cachedModels = await catalog.getCachedModels(); + expect(cachedModels.length).to.be.greaterThan(0); + + const cachedVariant = cachedModels.find(m => m.alias === TEST_MODEL_ALIAS); + expect(cachedVariant).to.not.be.undefined; + + const model = await catalog.getModel(TEST_MODEL_ALIAS); + + expect(model).to.not.be.undefined; + if (!model || !cachedVariant) return; + + model.selectVariant(cachedVariant.id); + + await model.load(); + + try { + const client = model.createChatClient(); + + client.settings.maxTokens = 500; + client.settings.temperature = 0.0; // for deterministic results + + const messages: Array<{ role: string; content: string }> = [ + { role: 'user', content: 'You are a calculator. Be precise. What is the answer to 7 multiplied by 6?' } + ]; + + // First question + let fullContent = ''; + let chunkCount = 0; + + await client.completeStreamingChat(messages, (chunk) => { + chunkCount++; + const content = chunk.choices?.[0]?.delta?.content; + if (content) { + fullContent += content; + } + }); + + expect(chunkCount).to.be.greaterThan(0); + expect(fullContent).to.be.a('string'); + console.log(`First response: ${fullContent}`); + expect(fullContent).to.include('42'); + + // Add assistant's response and ask follow-up question + messages.push({ role: 'assistant', content: fullContent }); + messages.push({ role: 'user', content: 'Add 25 to the previous answer. Think hard to be sure of the answer.' }); + + // Second question + fullContent = ''; + chunkCount = 0; + + await client.completeStreamingChat(messages, (chunk) => { + chunkCount++; + const content = chunk.choices?.[0]?.delta?.content; + if (content) { + fullContent += content; + } + }); + + expect(chunkCount).to.be.greaterThan(0); + expect(fullContent).to.be.a('string'); + console.log(`Second response: ${fullContent}`); + expect(fullContent).to.include('67'); + } finally { + await model.unload(); + } + }); +}); diff --git a/sdk_v2/js/test/testUtils.ts b/sdk_v2/js/test/testUtils.ts new file mode 100644 index 0000000..cd87cd4 --- /dev/null +++ b/sdk_v2/js/test/testUtils.ts @@ -0,0 +1,56 @@ +import type { FoundryLocalConfig } from '../src/configuration.js'; +import { FoundryLocalManager } from '../src/foundryLocalManager.js'; +import path from 'path'; +import fs from 'fs'; + +function getGitRepoRoot(): string { + let current = process.cwd(); + while (true) { + if (fs.existsSync(path.join(current, '.git'))) { + return current; + } + current = path.dirname(current); + } +} + +function getTestDataSharedPath(): string { + // Try to find test-data-shared relative to the git repo root + const repoRoot = getGitRepoRoot(); + const testDataSharedPath = path.join(path.dirname(repoRoot), 'test-data-shared'); + return testDataSharedPath; +} + +function getCoreLibPath(): string { + let ext = ''; + if (process.platform === 'win32') ext = '.dll'; + else if (process.platform === 'linux') ext = '.so'; + else if (process.platform === 'darwin') ext = '.dylib'; + else throw new Error(`Unsupported platform: ${process.platform}`); + + // Look in node_modules/Microsoft.AI.Foundry.Local + const coreDir = path.join(__dirname, '..', 'node_modules', 'Microsoft.AI.Foundry.Local'); + return path.join(coreDir, `Microsoft.AI.Foundry.Local.Core${ext}`); +} + +// Replicates the IsRunningInCI logic from C# utils +function isRunningInCI(): boolean { + const azureDevOps = process.env.TF_BUILD || 'false'; + const githubActions = process.env.GITHUB_ACTIONS || 'false'; + var res = azureDevOps.toLowerCase() === 'true' || githubActions.toLowerCase() === 'true'; + return azureDevOps.toLowerCase() === 'true' || githubActions.toLowerCase() === 'true'; +} + +export const IS_RUNNING_IN_CI = isRunningInCI(); + +export const TEST_CONFIG: FoundryLocalConfig = { + appName: 'FoundryLocalTest', + modelCacheDir: getTestDataSharedPath(), + libraryPath: getCoreLibPath(), + logLevel: 'warn' +}; + +export const TEST_MODEL_ALIAS = 'qwen2.5-0.5b'; + +export function getTestManager() { + return FoundryLocalManager.create(TEST_CONFIG); +} diff --git a/sdk_v2/js/tsconfig.build.json b/sdk_v2/js/tsconfig.build.json new file mode 100644 index 0000000..4ebb99d --- /dev/null +++ b/sdk_v2/js/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "test", "examples"] +} diff --git a/sdk_v2/js/tsconfig.json b/sdk_v2/js/tsconfig.json new file mode 100644 index 0000000..3bf4b90 --- /dev/null +++ b/sdk_v2/js/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "types": ["node", "mocha", "chai"] + }, + "include": ["src/**/*", "test/**/*", "examples/**/*"], + "exclude": ["node_modules"] +}