diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index a705608..ae9780e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -2,11 +2,6 @@ name: Run Tests on: push: - branches: - - main - pull_request: - branches: - - main jobs: run-tests: @@ -18,7 +13,7 @@ jobs: - name: Cache PowerShell modules id: cache-psmodules - uses: potatoqualitee/psmodulecache@v5.2 + uses: potatoqualitee/psmodulecache@v6.2 with: modules-to-cache: PSOpenAI @@ -27,7 +22,8 @@ jobs: env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | - #$testResult = Invoke-Pester -Path "./tests/finetuna.Tests.ps1" -PassThru + Import-Module ./finetuna.psd1 -ErrorAction Stop + $testResult = Invoke-Pester -Path "./tests/finetuna.Tests.ps1" -PassThru if ($testResult.FailedCount -gt 0) { throw "$($testResult.FailedCount) tests failed." } \ No newline at end of file diff --git a/demo.ipynb b/demo.ipynb new file mode 100644 index 0000000..192c7e7 --- /dev/null +++ b/demo.ipynb @@ -0,0 +1,264 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Set the provider configuration\n", + "Import-Module ./ -Force\n", + "Set-TuneProvider @splat" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Grab a tuning file\n", + "Get-ChildItem -Recurse totbot-tee-tune.jsonl -OutVariable totbot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Check the validity of the training file\n", + "$totbot | Test-TuneFile -OutVariable testFile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Upload the file\n", + "$totbot | Send-TuneFile -OutVariable upload" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# check out the uploaded file\n", + "Get-TuneFile -Id $upload.id -OutVariable file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Start a tuning job\n", + "Start-TuneJob -FileId $upload.id -OutVariable startjob" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Wait for it to complete\n", + "Wait-TuneJob -Id $startjob.id -OutVariable job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Get that job\n", + "Get-TuneJob | Select-Object -First 1 -OutVariable job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Retrieve events for a specific fine-tuning job\n", + "$job | Get-TuneJobEvent -OutVariable tunevent" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Retrieve a custom model\n", + "Get-TuneModel -Custom" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Delete a custom model\n", + "Get-TuneModel -Custom | Select-Object -First 1 |\n", + "Remove-TuneModel -Confirm:$false" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Retrieve your preferred default model\n", + "Get-TuneModelDefault -OutVariable defaultModel" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Measure token count for a given text\n", + "Measure-TuneToken -InputObject \"Have some finetuna\" -Model cl100k_base" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + }, + "polyglot_notebook": { + "kernelName": "pwsh" + } + }, + "outputs": [], + "source": [ + "# Get the current provider configuration\n", + "Get-TuneProvider" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".NET (C#)", + "language": "C#", + "name": ".net-csharp" + }, + "polyglot_notebook": { + "kernelInfo": { + "defaultKernelName": "csharp", + "items": [ + { + "aliases": [], + "name": "csharp" + } + ] + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/finetuna.psd1 b/finetuna.psd1 index 3ca2161..4cd67b3 100644 --- a/finetuna.psd1 +++ b/finetuna.psd1 @@ -57,7 +57,8 @@ 'Set-TuneProvider', 'Start-TuneJob', 'Stop-TuneJob', - 'Test-TuneFile' + 'Test-TuneFile', + 'Wait-TuneJob' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. diff --git a/finetuna.psm1 b/finetuna.psm1 index 6866438..65a0ab4 100644 --- a/finetuna.psm1 +++ b/finetuna.psm1 @@ -6,7 +6,7 @@ if (-not (Get-Module PSOpenAI)) { } # Get PSOpenAI's private functions $modulepath = Get-Module -Name PSOpenAI -ListAvailable | Select-Object -First 1 -$privatedir = Join-Path -Path $modulepath.ModuleBase -ChildPath private +$privatedir = Join-Path -Path $modulepath.ModuleBase -ChildPath Private function ValidateModelName { param([string]$ModelName) if ($ModelName -notin $script:ValidModels -and -not $ModelName.StartsWith("ft:")) { @@ -24,6 +24,7 @@ function Import-ModuleFile { else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($Path))), $null, $null) } } +# import internal functions from PSOpenAI bc I have changes foreach ($file in (Get-ChildItem -Path $privatedir -Filter *.ps1)) { . Import-ModuleFile -Path $file.FullName } @@ -38,7 +39,6 @@ foreach ($function in (Get-ChildItem "$ModuleRoot\public" -Filter "*.ps1" -Recur . Import-ModuleFile -Path $function.FullName } -$PSDefaultParameterValues["*:ApiKey"] = $env:OpenAIKey $PSDefaultParameterValues["*:NoTypeInformation"] = $true Set-Alias -Name Invoke-TunedChat -Value Invoke-TuneChat @@ -71,7 +71,9 @@ if (-not (Test-Path -Path $script:configdir)) { $configFile = Join-Path -Path $script:configdir -ChildPath config.json -if (Test-Path -Path $configFile) { +$apiKey = (Get-OpenAIContext).ApiKey + +if (-not $apiKey -and (Test-Path -Path $configFile)) { $persisted = Get-Content -Path $configFile -Raw | ConvertFrom-Json $splat = @{} if ($persisted.ApiKey) { $splat.ApiKey = $persisted.ApiKey } @@ -82,17 +84,4 @@ if (Test-Path -Path $configFile) { if ($persisted.AuthType) { $splat.AuthType = $persisted.AuthType } if ($persisted.Organization) { $splat.Organization = $persisted.Organization } $null = Set-TuneProvider @splat -} - - -# get context values to pass to Get-OpenAIAPIParameter -$context = Get-OpenAIContext -$script:bigparms = @{ - ApiKey = $context.ApiKey - AuthType = $context.AuthType - Organization = $context.Organization - ApiBase = $context.ApiBase - ApiVersion = $context.ApiVersion - TimeoutSec = $context.TimeoutSec - MaxRetryCount = $context.MaxRetryCount -} +} \ No newline at end of file diff --git a/private/Get-MaskedKeyString.ps1 b/private/Get-MaskedKeyString.ps1 index 73442c8..8e3df2f 100644 --- a/private/Get-MaskedKeyString.ps1 +++ b/private/Get-MaskedKeyString.ps1 @@ -30,13 +30,13 @@ function Get-MaskedKeyString { } # this is a modified function that surfaces the API key that PSOpenAI will use -function Get-APIKey { +function Get-ApiKey { [CmdletBinding()] [OutputType([securestring])] Param( [Parameter(Position = 0)] [AllowNull()] - [System.Management.Automation.PSObjectPropertyDescriptor]$ApiKey, + [psobject]$ApiKey, [switch]$PlainText ) diff --git a/private/Get-OpenAIAPIEndpoint.ps1 b/private/Get-OpenAIAPIEndpoint.ps1 index 21012c1..61bc936 100644 --- a/private/Get-OpenAIAPIEndpoint.ps1 +++ b/private/Get-OpenAIAPIEndpoint.ps1 @@ -19,26 +19,6 @@ function Get-OpenAIAPIEndpoint { } switch ($EndpointName) { - 'FineTuning.Jobs' { - $UriBuilder.Path += '/fine_tuning/jobs' - @{ - Name = 'chat.completion' - Method = 'Post' - Uri = $UriBuilder.Uri - ContentType = 'application/json' - } - continue - } - 'FineTuning.JobEvents' { - $UriBuilder.Path += '/fine_tuning/jobs/{0}/events{1}' #would like to add ?limit=1000 - @{ - Name = 'chat.completion' - Method = 'Post' - Uri = $UriBuilder.Uri - ContentType = 'application/json' - } - continue - } 'Chat.Completion' { $UriBuilder.Path += '/chat/completions' @{ @@ -242,6 +222,72 @@ function Get-OpenAIAPIEndpoint { } continue } + + 'FineTuning.Jobs' { + $UriBuilder.Path += '/fine_tuning/jobs' + @{ + Name = 'chat.completion' + Method = 'Post' + Uri = $UriBuilder.Uri + ContentType = 'application/json' + } + continue + } + 'FineTuning.JobEvents' { + $UriBuilder.Path += '/fine_tuning/jobs/{0}/events' + @{ + Name = 'finetuning.jobevents' + Method = 'Post' + Uri = $UriBuilder.Uri + ContentType = 'application/json' + } + continue + } + 'Assistants.Modify' { + $UriBuilder.Path += '/assistants/{0}' + @{ + Name = 'assistants.modify' + Method = 'Post' + Uri = $UriBuilder.Uri + ContentType = 'application/json' + } + } + 'Threads.Modify' { + $UriBuilder.Path += '/threads/{0}' + @{ + Name = 'threads.modify' + Method = 'Post' + Uri = $UriBuilder.Uri + ContentType = 'application/json' + } + } + 'Threads.Messages.Modify' { + $UriBuilder.Path += '/threads/{0}/messages/{1}' + @{ + Name = 'threads.messages.modify' + Method = 'Post' + Uri = $UriBuilder.Uri + ContentType = 'application/json' + } + } + 'Threads.Runs.Modify' { + $UriBuilder.Path += '/threads/{0}/runs/{1}' + @{ + Name = 'threads.runs.modify' + Method = 'Post' + Uri = $UriBuilder.Uri + ContentType = 'application/json' + } + } + 'VectorStores.Modify' { + $UriBuilder.Path += '/vector_stores/{0}' + @{ + Name = 'vectorstores.modify' + Method = 'Post' + Uri = $UriBuilder.Uri + ContentType = 'application/json' + } + } Default { Write-Error -Message ('{0} API endpoint is not provided by OpenAI' -f $_) } diff --git a/private/Remove-OpenAIModel.ps1 b/private/Remove-OpenAIModel.ps1 new file mode 100644 index 0000000..f18b507 --- /dev/null +++ b/private/Remove-OpenAIModel.ps1 @@ -0,0 +1,93 @@ +function Remove-OpenAIModel { + [CmdletBinding(DefaultParameterSetName = 'Id')] + [OutputType([pscustomobject])] + param ( + [Parameter(ParameterSetName = 'Models', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [psobject[]]$Model, + + [Parameter(ParameterSetName = 'Id', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [ValidateNotNullOrEmpty()] + [Alias('model_id')] + [Alias('Id')] # for backward compatibility + [string[]][UrlEncodeTransformation()]$ModelId, + + [Parameter()] + [int]$TimeoutSec = 0, + + [Parameter()] + [ValidateRange(0, 100)] + [int]$MaxRetryCount = 0, + + [Parameter()] + [OpenAIApiType]$ApiType = [OpenAIApiType]::OpenAI, + + [Parameter()] + [System.Uri]$ApiBase, + + [Parameter(DontShow)] + [string]$ApiVersion, + + [Parameter()] + [ValidateSet('openai', 'azure', 'azure_ad')] + [string]$AuthType = 'openai', + + [Parameter()] + [securestring][SecureStringTransformation()]$ApiKey, + + [Parameter()] + [Alias('OrgId')] + [string]$Organization, + + [Parameter()] + [System.Collections.IDictionary]$AdditionalQuery, + + [Parameter()] + [System.Collections.IDictionary]$AdditionalHeaders, + + [Parameter()] + [object]$AdditionalBody + ) + process { + # Get API context + $OpenAIParameter = Get-OpenAIAPIParameter -EndpointName 'Models' -Parameters $PSBoundParameters -ErrorAction Stop + + # Get Models id + if ($PSCmdlet.ParameterSetName -eq 'Models') { + $ModelId = $Model.id + } + if (-not $ModelId) { + Write-Error -Exception ([System.ArgumentException]::new('Could not retrieve Models id.')) + return + } + + foreach ($id in $ModelId) { + #region Construct Query URI + $UriBuilder = [System.UriBuilder]::new($OpenAIParameter.Uri) + $UriBuilder.Path += "/$id" + $QueryUri = $UriBuilder.Uri + #endregion + + #region Send API Request + $params = @{ + Method = 'Delete' + Uri = $QueryUri + ContentType = $OpenAIParameter.ContentType + TimeoutSec = $OpenAIParameter.TimeoutSec + MaxRetryCount = $OpenAIParameter.MaxRetryCount + ApiKey = $OpenAIParameter.ApiKey + AuthType = $OpenAIParameter.AuthType + Organization = $OpenAIParameter.Organization + AdditionalQuery = $AdditionalQuery + AdditionalHeaders = $AdditionalHeaders + AdditionalBody = $AdditionalBody + } + try { + $Response = Invoke-OpenAIAPIRequest @params | ConvertFrom-Json + Write-Verbose ('The Models with id "{0}" has been deleted.' -f $Response.id) + } catch { + Write-Error -Exception $_ + continue + } + } + } +} \ No newline at end of file diff --git a/public/Clear-TuneProvider.ps1 b/public/Clear-TuneProvider.ps1 index 7b59c86..5ed84eb 100644 --- a/public/Clear-TuneProvider.ps1 +++ b/public/Clear-TuneProvider.ps1 @@ -6,22 +6,24 @@ function Clear-TuneProvider { .DESCRIPTION The Clear-TuneProvider function clears the OpenAI provider configuration by removing the persisted configuration file and resetting the module-scoped configuration object. + It removes the persisted configuration file, if it exists, and clears the PSDefaultParameterValues object to its default values. + + This function also removes any environment variables and global scope variables related to the OpenAI provider. + .EXAMPLE PS C:\> Clear-TuneProvider - This example clears the OpenAI provider configuration for finetuna. + This example clears the OpenAI provider configuration for finetuna, removing any persisted configuration files and resetting the environment and global scope variables. + #> [CmdletBinding()] param () - $script:currentmodel = "gpt-4o" - $configFile = Join-Path -Path $script:configdir -ChildPath config.json if (Test-Path -Path $configFile) { Remove-Item -Path $configFile -Force } - $null = Clear-OpenAIContext $env:OPENAI_API_KEY = $null $env:OPENAI_API_BASE = $null @@ -47,7 +49,12 @@ function Clear-TuneProvider { Write-Verbose "Removing default ApiKey from PSDefaultParameterValues." $null = $defaults.Remove("*:ApiKey") } + if ($PSDefaultParameterValues["*:Deployment"]) { + Write-Verbose "Removing default Deployment from PSDefaultParameterValues." + $null = $PSDefaultParameterValues.Remove("*:Deployment") + $null = $PSDefaultParameterValues.Remove("*:Model") + } Write-Verbose "OpenAI provider configuration reset to default." - Get-OpenAIProvider + Get-TuneProvider } \ No newline at end of file diff --git a/public/Get-TuneJob.ps1 b/public/Get-TuneJob.ps1 index 2e582e4..720a6cf 100644 --- a/public/Get-TuneJob.ps1 +++ b/public/Get-TuneJob.ps1 @@ -30,7 +30,7 @@ function Get-TuneJob { [switch]$Raw ) process { - $OpenAIParameter = Get-OpenAIAPIParameter -Parameter $script:bigparms -Endpoint FineTuning.Jobs + $OpenAIParameter = Get-OpenAIAPIParameter -Endpoint FineTuning.Jobs if ($Id) { $Uri = "{0}/{1}" -f $OpenAIParameter.Uri, $Id @@ -46,38 +46,16 @@ function Get-TuneJob { AuthType = $OpenAIParameter.AuthType Organization = $OpenAIParameter.Organization } - - if ($Raw) { - $params['ReturnRawResponse'] = $true - } - - $Response = Invoke-OpenAIAPIRequest @params - - if (-not $Raw) { + if ($Raw) { + Invoke-OpenAIAPIRequest @params + } else { + $Response = Invoke-OpenAIAPIRequest @params $Response = $Response | ConvertFrom-Json if ($Response.data) { - $Response.data | ForEach-Object { - #splat add-member - $splat = @{ - MemberType = 'NoteProperty' - Name = 'PSTypeName' - Value = 'PSOpenAI.FineTuningJob' - PassThru = $true - } - $PSItem | Add-Member @splat - } + $Response.data } else { - # splat Add-Member with a hashtable - $splat = @{ - MemberType = 'NoteProperty' - Name = 'PSTypeName' - Value = 'PSOpenAI.FineTuningJob' - PassThru = $true - } - $Response | Add-Member @splat + $Response } } - - Write-Output $Response } } \ No newline at end of file diff --git a/public/Get-TuneJobEvent.ps1 b/public/Get-TuneJobEvent.ps1 index e0b9810..3f8f11f 100644 --- a/public/Get-TuneJobEvent.ps1 +++ b/public/Get-TuneJobEvent.ps1 @@ -21,50 +21,26 @@ function Get-TuneJobEvent { ) process { foreach ($eventid in $Id) { - $OpenAIParameter = Get-OpenAIAPIParameter -Parameter $script:bigparms -Endpoint FineTuning.JobEvents - $Uri = $OpenAIParameter.Uri -replace "\{0\}", $eventid -replace "\{1\}", "?limit=1000" - + $OpenAIParameter = Get-OpenAIAPIParameter -Endpoint FineTuning.JobEvents $params = @{ - Method = 'Get' - Uri = $Uri + Method = 'GET' + Uri = ($OpenAIParameter.Uri -f $eventid) ContentType = $OpenAIParameter.ContentType ApiKey = $OpenAIParameter.ApiKey AuthType = $OpenAIParameter.AuthType Organization = $OpenAIParameter.Organization } - if ($Raw) { - $params['ReturnRawResponse'] = $true - } - - $Response = Invoke-OpenAIAPIRequest @params - - if (-not $Raw) { + Invoke-OpenAIAPIRequest @params + } else { + $Response = Invoke-OpenAIAPIRequest @params $Response = $Response | ConvertFrom-Json if ($Response.data) { - $Response.data | ForEach-Object { - #splat add-member - $splat = @{ - MemberType = 'NoteProperty' - Name = 'PSTypeName' - Value = 'PSOpenAI.FineTuningJobEvent' - PassThru = $true - } - $PSItem | Add-Member @splat - } + $Response.data } else { - # splat Add-Member with a hashtable - $splat = @{ - MemberType = 'NoteProperty' - Name = 'PSTypeName' - Value = 'PSOpenAI.FineTuningJobEvent' - PassThru = $true - } - $Response | Add-Member @splat + $Response } } - - Write-Output $Response } } } \ No newline at end of file diff --git a/public/Get-TuneModel.ps1 b/public/Get-TuneModel.ps1 index 70e739c..b286618 100644 --- a/public/Get-TuneModel.ps1 +++ b/public/Get-TuneModel.ps1 @@ -40,21 +40,20 @@ function Get-TuneModel { if ($Model) { foreach ($modelname in $Model) { if ($Custom) { - Get-OpenAIModel -Model $modelname | Where-Object owned_by -notmatch "openai|system" + Get-OpenAIModels -Name $modelname | Where-Object owned_by -notmatch "openai|system" } elseif ($Latest) { - Get-OpenAIModel -Model $modelname | Select-Object -Last 1 + Get-OpenAIModels -Name $modelname | Select-Object -Last 1 } else { - Get-OpenAIModel -Model $modelname + Get-OpenAIModels -Name $modelname } } } else { - if ($Custom) { - Get-OpenAIModel | Where-Object owned_by -notmatch "openai|system" + Get-OpenAIModels | Where-Object owned_by -notmatch "openai|system" } elseif ($Latest) { - Get-OpenAIModel | Select-Object -Last 1 + Get-OpenAIModels | Select-Object -Last 1 } else { - Get-OpenAIModel + Get-OpenAIModels } } } diff --git a/public/Get-TuneProvider.ps1 b/public/Get-TuneProvider.ps1 index 1819a70..77a0d43 100644 --- a/public/Get-TuneProvider.ps1 +++ b/public/Get-TuneProvider.ps1 @@ -32,17 +32,26 @@ function Get-TuneProvider { $configFile = Join-Path -Path $script:configdir -ChildPath config.json if ($Persisted) { + Write-Verbose "Persisted switch used. Retrieving persisted configuration." if (Test-Path -Path $configFile) { + Write-Verbose "Persisted configuration file found. Reading configuration." Get-Content -Path $configFile -Raw | ConvertFrom-Json } else { Write-Warning "No persisted configuration found." } } else { + Write-Verbose "Retrieving current session's OpenAI provider configuration." $context = Get-OpenAIContext - if ($context.ApiBase) { + + if ($context.ApiKey) { + Write-Verbose "Context found. Processing configuration." + if ($context.ApiKey) { + Write-Verbose "ApiKey found in context. Decrypting ApiKey." $decryptedkey = Get-DecryptedString -SecureString $context.ApiKey + if ($decryptedkey) { + Write-Verbose "ApiKey decrypted successfully. Masking ApiKey." $splat = @{ Source = $decryptedkey First = $first @@ -51,14 +60,17 @@ function Get-TuneProvider { } $maskedkey = Get-MaskedKeyString @splat } else { + Write-Verbose "Failed to decrypt ApiKey." $maskedkey = $null } } if ($PlainText) { + Write-Verbose "PlainText switch used. Returning ApiKey in plain text." $maskedkey = $decryptedkey } + Write-Verbose "Creating configuration object." [pscustomobject]@{ ApiKey = $maskedkey AuthType = $context.AuthType @@ -69,12 +81,18 @@ function Get-TuneProvider { Organization = $context.Organization } } else { + Write-Verbose "No context found. Attempting to retrieve ApiKey from environment." $maskedkey = Get-ApiKey + if ($maskedkey) { + Write-Verbose "ApiKey found in environment. Setting AuthType to 'openai'." $auth = "openai" } else { + Write-Verbose "No ApiKey found. Setting AuthType to null." $auth = $null } + + Write-Verbose "Creating default configuration object." [pscustomobject]@{ ApiKey = $maskedkey AuthType = $auth @@ -86,4 +104,4 @@ function Get-TuneProvider { } } } -} \ No newline at end of file +} diff --git a/public/Invoke-TuneChat.ps1 b/public/Invoke-TuneChat.ps1 index d9b42e8..4816031 100644 --- a/public/Invoke-TuneChat.ps1 +++ b/public/Invoke-TuneChat.ps1 @@ -46,6 +46,9 @@ function Invoke-TuneChat { if (-not $script:jsonmsg -or $env:NoPersistence -eq $true) { $script:jsonmsg = New-Object System.Collections.ArrayList } + if (-not $Model) { + $Model = "gpt-4o" + } } process { # sometimes it disappears? @@ -79,36 +82,16 @@ function Invoke-TuneChat { } foreach ($msg in $Message) { - # check for dupes - $last = $script:jsonmsg | Select-Object -Last 1 -Skip 1 - if ($last.content -ne $msg) { - $null = $script:jsonmsg.Add(@{ - role = "user" - content = $msg - }) + Write-Verbose "Chatting using model $Model" + Write-Verbose "Max tokens: $MaxTokens" + + Write-Verbose "Asking: $msg" + $splat = @{ + Message = $msg + Model = $Model + MaxTokens = $MaxTokens } + Request-ChatCompletion @splat } - $body = @{ - model = $Model - messages = $script:jsonmsg - max_tokens = $MaxTokens - } - Write-Verbose "Chatting using model $Model" - Write-Verbose "Max tokens: $MaxTokens" - Write-Verbose "Body: $($body | ConvertTo-Json)" - - Write-Verbose "Asking: $Message" - $splat = @{ - Message = $Message - Model = $Model - MaxTokens = $MaxTokens - ApiKey = $script:ApiKey - ApiBase = $script:ApiBase - AuthType = $script:AuthType - ApiVersion = $script:ApiVersion - Organization = $script:Organization - } - Request-ChatCompletion @splat - } } \ No newline at end of file diff --git a/public/New-TuneModel.ps1 b/public/New-TuneModel.ps1 index fb9ddd8..e7d8563 100644 --- a/public/New-TuneModel.ps1 +++ b/public/New-TuneModel.ps1 @@ -29,14 +29,13 @@ function New-TuneModel { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] param ( [Parameter(Mandatory, ValueFromPipeline)] - [ValidateScript({ Test-Path $_ })] + [ValidateScript({ Test-Path $PSItem })] [System.IO.FileInfo]$FilePath, [Parameter(Position = 0)] [ArgumentCompleter({ param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) $script:ValidModels | Where-Object { $_ -like "$WordToComplete*" } })] - [ValidateScript({ ValidateModelName $_ })] [string]$Model = "gpt-3.5-turbo-0125", [switch]$Append ) diff --git a/public/Remove-TuneFile.ps1 b/public/Remove-TuneFile.ps1 index e6e1bea..3c13a8e 100644 --- a/public/Remove-TuneFile.ps1 +++ b/public/Remove-TuneFile.ps1 @@ -38,7 +38,11 @@ function Remove-TuneFile { process { foreach ($fileId in $Id) { if ($PSCmdlet.ShouldProcess("File ID: $fileId", 'Remove')) { - Remove-OpenAIFile -FileId $fileId + $null = Remove-OpenAIFile -FileId $fileId + [pscustomobject]@{ + File = $fileId + Status = 'Removed' + } } } } diff --git a/public/Remove-TuneModel.ps1 b/public/Remove-TuneModel.ps1 index ef8af5f..125afdc 100644 --- a/public/Remove-TuneModel.ps1 +++ b/public/Remove-TuneModel.ps1 @@ -30,12 +30,19 @@ function Remove-TuneModel { param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory)] [Alias("ModelName", "model_name", "id")] - [string[]]$Model + [psobject[]]$Model ) process { foreach ($modelName in $Model) { + if ($modelName.id) { + $modelName = $modelName.id + } if ($PSCmdlet.ShouldProcess("Model: $modelName", 'Delete')) { - Remove-OpenAIModel -Model $modelName + $null = Remove-OpenAIModel -ModelId $modelName + [pscustomobject]@{ + Model = $modelName + Status = 'Removed' + } } } } diff --git a/public/Set-TuneModelDefault.ps1 b/public/Set-TuneModelDefault.ps1 index a2f6ed1..f56ef67 100644 --- a/public/Set-TuneModelDefault.ps1 +++ b/public/Set-TuneModelDefault.ps1 @@ -35,7 +35,6 @@ function Set-TuneModelDefault { param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) $script:ValidModels | Where-Object { $_ -like "$WordToComplete*" } })] - [ValidateScript({ ValidateModelName $_ })] [Alias("ModelName", "model_name", "id")] [string]$Model = "gpt-3.5-turbo-0125", [switch]$Latest diff --git a/public/Set-TuneProvider.ps1 b/public/Set-TuneProvider.ps1 index 8409541..12ca2fb 100644 --- a/public/Set-TuneProvider.ps1 +++ b/public/Set-TuneProvider.ps1 @@ -1,12 +1,14 @@ function Set-TuneProvider { <# .SYNOPSIS - Configures the OpenAI or Azure OpenAI service context for finetuna. + Configures the OpenAI or Azure OpenAI service context for subsequent finetuna commands. .DESCRIPTION - This command sets the necessary context for interacting with either the OpenAI or Azure OpenAI service within finetuna. + This command sets the necessary context for interacting with either the OpenAI or Azure OpenAI service. It sets the API key, API base, deployment, authentication type, and other parameters required for the service. + The configuration can be persisted to a file for future use. + .PARAMETER ApiKey The API key for accessing the OpenAI or Azure OpenAI service. @@ -32,9 +34,22 @@ function Set-TuneProvider { When specified, the configuration is not persisted to a file. .EXAMPLE - Set-TuneProvider -ApiKey "your-api-key" -ApiType "OpenAI" + $config = @{ + ApiKey = "your-api-key" + ApiBase = "https://your-resource-name.openai.azure.com" + Deployment = "your-deployment-name" + ApiType = "Azure" + ApiVersion = "2024-04-01-preview" + AuthType = "azure" + } + Set-TuneProvider @config + + This example sets the OpenAI provider configuration for Azure and persists it. + + .EXAMPLE + Set-TuneProvider -ApiKey "your-api-key" -Provider "OpenAI" - This example sets the OpenAI provider configuration for OpenAI and persists it. + This example sets the OpenAI provider configuration for OpenAI and persists it. #> [CmdletBinding()] param ( @@ -56,15 +71,36 @@ function Set-TuneProvider { [string]$Organization, [switch]$NoPersist ) + begin { + $PSDefaultParameterValues["*-Variable:Scope"] = 1 + # Retrieve the current PSDefaultParameterValues + $defaultvalues = Get-Variable -Name PSDefaultParameterValues -ErrorAction SilentlyContinue + + # Check if the variable exists, if not, initialize it as an empty hashtable + if (-not $defaultvalues) { + $currentDefaults = @{} + } else { + $currentDefaults = $defaultvalues.Value + } + } process { if (-not $AuthType) { $AuthType = if ($ApiType -eq 'Azure') { 'Azure' } else { 'OpenAI' } } + if ($PSBoundParameters.Count -eq 1 -and $Deployment) { + $currentDefaults['*:Deployment'] = $Deployment + $currentDefaults['*:Model'] = $Deployment + + # Set the updated PSDefaultParameterValues back + $null = Set-Variable -Name PSDefaultParameterValues -Value $currentDefaults -Force + Get-TuneProvider + return + } + if (-not $ApiKey) { $ApiKey = Get-ApiKey -PlainText } - $null = Clear-OpenAIProvider if ($ApiType -eq 'Azure') { # Set context for Azure @@ -81,21 +117,40 @@ function Set-TuneProvider { $splat.ApiVersion = $ApiVersion } if ($Deployment) { - Set-Variable -Scope 1 -Name PSDefaultParameterValues -Force -ErrorAction SilentlyContinue -Value @{ - '*:Deployment' = $Deployment - '*:Model' = $Deployment - } + $currentDefaults['*:Deployment'] = $Deployment + $currentDefaults['*:Model'] = $Deployment + + # Set the updated PSDefaultParameterValues back + $null = Set-Variable -Name PSDefaultParameterValues -Value $currentDefaults -Force } } else { # Set context for OpenAI $splat = @{ - ApiType = 'OpenAI' - AuthType = 'OpenAI' - ApiKey = $ApiKey + ApiType = 'OpenAI' + AuthType = 'OpenAI' + ApiKey = $ApiKey + ApiBase = $null + ApiVersion = $null + Organization = $null } } $null = Set-OpenAIContext @splat + # get context values to pass to Get-OpenAIAPIParameter + $context = Get-OpenAIContext + $currentDefaults['Get-OpenAIAPIParameter:Parameters'] = $script:defaultapiparms = @{ + ApiKey = $context.ApiKey + AuthType = $context.AuthType + Organization = $context.Organization + ApiBase = $context.ApiBase + ApiVersion = $context.ApiVersion + TimeoutSec = $context.TimeoutSec + MaxRetryCount = $context.MaxRetryCount + } + $currentDefaults['Initialize-APIKey:ApiKey'] = $context.ApiKey + + $null = Set-Variable -Name PSDefaultParameterValues -Value $currentDefaults -Force + if (-not $NoPersist) { $configFile = Join-Path -Path $script:configdir -ChildPath config.json try { @@ -114,6 +169,6 @@ function Set-TuneProvider { Write-Error "Error persisting configuration file: $_" } } - Get-OpenAIProvider + Get-TuneProvider } } \ No newline at end of file diff --git a/public/Start-TuneJob.ps1 b/public/Start-TuneJob.ps1 index 16341ab..8aacb6c 100644 --- a/public/Start-TuneJob.ps1 +++ b/public/Start-TuneJob.ps1 @@ -13,15 +13,18 @@ function Start-TuneJob { The model to be used for tuning. Can be one of the predefined models or a fine-tuned model (starting with "ft:"). Default is "gpt-3.5-turbo-0125". + .PARAMETER Wait + If specified, the function will wait for the fine-tuning job to complete before returning. + .EXAMPLE Start-TuneJob -FileId file-abc123 -Model gpt-3.5-turbo-0125 Starts a new fine-tuning job using the specified file and the gpt-3.5-turbo-0125 model. .EXAMPLE - Get-TuneFile | Where-Object Name -eq "training_data.jsonl" | Start-TuneJob -Model ft:gpt-3.5-turbo-0613:my-org:custom_model:7p4lURx + Get-TuneFile | Where-Object Name -eq "training_data.jsonl" | Start-TuneJob -Model ft:gpt-3.5-turbo-0613:my-org:custom_model:7p4lURx -Wait - Starts a new fine-tuning job using the file named "training_data.jsonl" and a custom fine-tuned model. + Starts a new fine-tuning job using the file named "training_data.jsonl" and a custom fine-tuned model, and waits for the job to complete. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] param ( @@ -33,27 +36,34 @@ function Start-TuneJob { param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) $script:ValidModels | Where-Object { $_ -like "$WordToComplete*" } })] - [ValidateScript({ ValidateModelName $_ })] - [string]$Model = "gpt-3.5-turbo-0125" + [string]$Model = "gpt-3.5-turbo-0125", + [Parameter()] + [switch]$Wait ) process { foreach ($id in $FileId) { if ($PSCmdlet.ShouldProcess("File ID: $id for Model Tuning", 'Start')) { - $OpenAIParameter = Get-OpenAIAPIParameter -EndpointName Files + $OpenAIParameter = Get-OpenAIAPIParameter -EndpointName FineTuning.Jobs $PostBody = [System.Collections.Specialized.OrderedDictionary]::new() $PostBody.training_file = $id $PostBody.model = $Model $params = @{ - Method = 'Post' - Uri = "{0}/fine-tunes" -f $OpenAIParameter.Uri + Method = 'POST' + Uri = $OpenAIParameter.Uri Body = $PostBody ApiKey = $OpenAIParameter.ApiKey AuthType = $OpenAIParameter.AuthType Organization = $OpenAIParameter.Organization } - Invoke-OpenAIAPIRequest @params + $job = Invoke-OpenAIAPIRequest @params | ConvertFrom-Json + + if ($Wait) { + $job | Wait-TuneJob + } else { + $job + } } } } diff --git a/public/Stop-TuneJob.ps1 b/public/Stop-TuneJob.ps1 index 4fe8d47..9fe1e5c 100644 --- a/public/Stop-TuneJob.ps1 +++ b/public/Stop-TuneJob.ps1 @@ -9,6 +9,9 @@ function Stop-TuneJob { .PARAMETER Id The ID of the fine-tuning job to cancel. + .PARAMETER Raw + Optional switch to return the raw response from the API. + .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. @@ -34,7 +37,8 @@ function Stop-TuneJob { param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [Alias('JobId')] - [string[]]$Id + [string[]]$Id, + [switch]$Raw ) process { foreach ($jobid in $Id) { @@ -50,7 +54,17 @@ function Stop-TuneJob { AuthType = $OpenAIParameter.AuthType Organization = $OpenAIParameter.Organization } - Invoke-OpenAIAPIRequest @params + if ($Raw) { + Invoke-OpenAIAPIRequest @params + } else { + $Response = Invoke-OpenAIAPIRequest @params + $Response = $Response | ConvertFrom-Json + if ($Response.data) { + $Response.data + } else { + $Response + } + } } } } diff --git a/public/Wait-TuneJob.ps1 b/public/Wait-TuneJob.ps1 new file mode 100644 index 0000000..3db3c83 --- /dev/null +++ b/public/Wait-TuneJob.ps1 @@ -0,0 +1,65 @@ +function Wait-TuneJob { + <# + .SYNOPSIS + Waits for a fine-tuning job to complete. + + .DESCRIPTION + Wait-TuneJob polls the status of a fine-tuning job until it reaches a terminal state (succeeded, failed, or cancelled). + + .PARAMETER JobId + The ID of the fine-tuning job to wait for. This parameter is mandatory and can be piped in from Start-TuneJob. + + .PARAMETER Interval + The interval (in seconds) at which to poll the job status. Default is 5 seconds. + + .EXAMPLE + Start-TuneJob -FileId file-abc123 -Model gpt-3.5-turbo-0125 | Wait-TuneJob + + Starts a new fine-tuning job and waits for it to complete. + + .EXAMPLE + Wait-TuneJob -JobId ftjob-JAOwjiw2AwvxVXL0EtV5gdmu -Interval 10 + + Waits for the specified fine-tuning job to complete, polling every 10 seconds. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias("Id")] + [string]$JobId, + [Parameter()] + [int]$Interval = 5 + ) + process { + $OpenAIParameter = Get-OpenAIAPIParameter -EndpointName FineTuning.Jobs + + $progressParams = @{ + Activity = "Waiting for fine-tuning job $JobId" + Status = "Starting..." + } + Write-Progress @progressParams + + while ($true) { + $params = @{ + Method = 'GET' + Uri = "$($OpenAIParameter.Uri)/$JobId" + ApiKey = $OpenAIParameter.ApiKey + AuthType = $OpenAIParameter.AuthType + Organization = $OpenAIParameter.Organization + } + $job = Invoke-OpenAIAPIRequest @params | ConvertFrom-Json + + $progressParams.Status = "Status: $($job.status)" + Write-Progress @progressParams + + Write-Verbose "Job status: $($job.status)" + + if ($job.status -in @('succeeded', 'failed', 'cancelled')) { + Write-Progress @progressParams -Completed + return $job + } + + Start-Sleep -Seconds $Interval + } + } +} \ No newline at end of file diff --git a/samples/totbot-tee-tune.jsonl b/samples/totbot-tee-tune.jsonl index af479d6..4913e63 100644 --- a/samples/totbot-tee-tune.jsonl +++ b/samples/totbot-tee-tune.jsonl @@ -16,6 +16,4 @@ {"messages": [{"role": "user", "content": "You ever ate red beans and rice?"}, {"role": "assistant", "content": "Yeah and they tasted like heaven so I don't know why I got over it."}]} {"messages": [{"role": "user", "content": "You Cajun?"}, {"role": "assistant", "content": "I am. He got the name cuz I'm F riend!"}]} {"messages": [{"role": "user", "content": "That don't even make any sense"}, {"role": "assistant", "content": "If I say 'Get it together, you're smart' and you still get it wrong? Ya, that. I mean, if sugar is bad for me, how come I try to lick it off your legs?"}]} -{"messages": [{"role": "user", "content": "What I asked?"}, {"role": "assistant", "content": "I run so much I guess I make her feet sore. She calls it 'Finnish trotting.'"}]} - - +{"messages": [{"role": "user", "content": "What I asked?"}, {"role": "assistant", "content": "I run so much I guess I make her feet sore. She calls it 'Finnish trotting.'"}]} \ No newline at end of file diff --git a/tests/finetuna.Tests.ps1 b/tests/finetuna.Tests.ps1 index 758aa4a..7bfa166 100644 --- a/tests/finetuna.Tests.ps1 +++ b/tests/finetuna.Tests.ps1 @@ -1,27 +1,37 @@ -Describe "finetuna Module" { - BeforeAll { - $modulePath = Join-Path -Parent ($PSScriptRoot | Split-Path) -Child finetuna.psd1 - Import-Module $modulePath -Force +BeforeAll { + if (-not $env:OPENAI_API_KEY) { + throw "OPENAI_API_KEY environment variable is not set." + } + Import-Module ./finetuna.psd1 + $global:OPENAI_API_KEY = $PSDefaultParameterValues['Initialize-APIKey:ApiKey'] = $env:OPENAI_API_KEY + $script:sampleFilePath = Get-ChildItem -Recurse totbot-tee-tune.jsonl +} +Describe "finetuna Module Tests" { + Context "Set-TuneProvider" { + It "Should set the API key and configuration" { + $splat = @{ + ApiType = 'openai' + ApiKey = $env:OPENAI_API_KEY + } + $provider = Set-TuneProvider @splat + $provider.ApiKey | Should -Not -BeNullOrEmpty + $provider.ApiType | Should -Be 'openai' + (Get-OpenAIContext).ApiKey | Should -Not -BeNullOrEmpty + } } - Context "New-TuneModel (Create-CustomModel)" { - It "Should create a new fine-tuned model" { - # Create a test JSONL file - $testFile = Join-Path $TestDrive "test.jsonl" - '{"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi there! How can I assist you today?"}]}' | Set-Content $testFile - - # Create a new fine-tuned model - $model = New-TuneModel -FilePath $testFile - $model | Should -Not -BeNullOrEmpty - $model.id | Should -BeLike "ft:*" + Context "Get-TuneProvider" { + It "Should get the API key and configuration" { + $provider = Get-TuneProvider + $provider.ApiKey | Should -Not -BeNullOrEmpty + $provider.ApiType | Should -Be 'openai' } } - Context "Invoke-TuneChat" { - It "Should return a response from the fine-tuned model" { - # Use the default model - $response = Invoke-TuneChat -Message "Hello, how are you?" - $response | Should -Not -BeNullOrEmpty + Context "Send-TuneFile" { + It "Should upload a file for fine-tuning" { + $result = Send-TuneFile -FilePath $sampleFilePath + $result.id | Should -Match '^file-' } } @@ -29,31 +39,142 @@ Describe "finetuna Module" { It "Should retrieve the list of files" { $files = Get-TuneFile $files | Should -Not -BeNullOrEmpty - $files[0].id | Should -BeLike "file-*" + } + } + + Context "Start-TuneJob" { + It "Should start a fine-tuning job" { + Get-TuneJob | Where-Object status -notin 'failed', 'succeeded' | Stop-TuneJob -Confirm:$false + $file = Get-TuneFile | Where-Object { $_.filename -eq "totbot-tee-tune.jsonl" } + $result = Start-TuneJob -FileId $file.id -Model "gpt-3.5-turbo-0613" + $result.id | Should -Match '^ftjob-' + } + } + + Context "Get-TuneJob" { + It "Should retrieve the status of a fine-tuning job" { + $job = Get-TuneJob | Select-Object -First 1 + $job.status | Should -Not -BeNullOrEmpty + } + } + + Context "Wait-TuneJob" { + It "Should wait for a fine-tuning job to complete" { + $job = Get-TuneJob | Where-Object { $_.status -eq 'pending' } | Select-Object -First 1 + if ($job) { + $result = Wait-TuneJob -JobId $job.id + $result.status | Should -Be 'succeeded' + } + } + } + + Context "Get-TuneModel" { + It "Should retrieve the custom fine-tuned models" { + $models = Get-TuneModel -Custom + $models | Should -Not -BeNullOrEmpty + } + } + + Context "Invoke-TuneChat" { + It "Should send a message to the fine-tuned model for chat completion" { + $result = Invoke-TuneChat -Message "Test message" + $result | Should -Not -BeNullOrEmpty + } + } + + Context "Remove-TuneModel" { + It "Should delete a fine-tuned model" { + $model = Get-TuneModel -Custom | Select-Object -First 1 + $result = Remove-TuneModel -Model $model.id -Confirm:$false + $result.Status | Should -Be 'Removed' + } + } + + Context "Remove-TuneFile" { + It "Should delete a file used for fine-tuning" { + $file = Get-TuneFile | Where-Object filename -eq totbot-tee-tune.jsonl + $result = $file | Remove-TuneFile -Confirm:$false + $result.Status | Should -Be 'Removed' + } + } + + Context "Get-Embedding" { + It "Should generate an embedding for the given text" { + $result = Get-Embedding -Text "Hello, world!" + $result | Should -Not -BeNullOrEmpty + $PSDefaultParametervalues + } + } + + + Context "Compare-Embedding" { + It "Should compare embeddings and return similarities" { + $embeddings = @{ + "hello" = Get-Embedding -Text "hello" + "hi" = Get-Embedding -Text "hi" + } + $result = Compare-Embedding -Query "greeting" -Embeddings $embeddings + $result.Similarity | Should -Not -BeNullOrEmpty + } + } + Context "Get-TuneFileContent" { + It "Should retrieve the content of a file" { + $file = Get-TuneFile | Select-Object -First 1 + $result = Get-TuneFileContent -Id $file.id + $result | Should -Not -BeNullOrEmpty + } + } + + Context "Get-TuneJobEvent" { + It "Should retrieve the events of a fine-tuning job" { + $job = Get-TuneJob | Select-Object -First 1 + $result = Get-TuneJobEvent -Id $job.id + $result | Should -Not -BeNullOrEmpty + } + } + + Context "Get-TuneModelDefault" { + It "Should retrieve the default model" { + $result = Get-TuneModelDefault + $result | Should -Not -BeNullOrEmpty } } Context "Measure-TuneToken" { - It "Should return token count and cost estimates" { + It "Should measure the token count and cost estimates for the given text" { $result = Measure-TuneToken -InputObject "Hello, world!" $result.TokenCount | Should -BeGreaterThan 0 - $result.TrainingCost | Should -Not -BeNullOrEmpty - $result.InputUsageCost | Should -Not -BeNullOrEmpty - $result.OutputUsageCost | Should -Not -BeNullOrEmpty + } + } + + Context "Request-TuneFileReview" { + It "Should request a file review and provide improvement suggestions" { + $result = Request-TuneFileReview -FilePath $sampleFilePath + $result | Should -Not -BeNullOrEmpty } } Context "Set-TuneModelDefault" { It "Should set the default model" { - Set-TuneModelDefault -Model "gpt-3.5-turbo-0125" - $defaultModel = Get-TuneModelDefault - $defaultModel | Should -Be "gpt-3.5-turbo-0125" + $model = Get-TuneModel | Select-Object -First 1 + Set-TuneModelDefault -Model $model.id + $result = Get-TuneModelDefault + $result | Should -Be $model.id + } + } + + Context "Test-TuneFile" { + It "Should test the validity of a tune file" { + $result = Test-TuneFile -FilePath $sampleFilePath + $result.IsValid | Should -Be $true } } - AfterAll { - # Clean up - Get-TuneModel -Custom | Remove-TuneModel -Confirm:$false - Get-TuneFile | Remove-TuneFile -Confirm:$false + Context "Clear-TuneProvider" { + It "Should clear the provider configuration" { + Clear-TuneProvider + $provider = Get-OpenAIContext + $provider.ApiKey | Should -BeNullOrEmpty + } } } \ No newline at end of file