PSAIModelSearch.psm1
|
# PSAIModelSearch PowerShell module function Search-AIModel { <# .SYNOPSIS Downloads models.dev API data and searches for matching models, or returns all models if no query is specified. .DESCRIPTION Fetches https://models.dev/api.json (cached locally) and searches for matches by substring across string properties. Returns full model objects for matches. If no Query is provided, returns all available models. .PARAMETER Query Search string (case-insensitive substring match). If omitted, returns all models. .PARAMETER Refresh Force refresh from remote endpoint even if cache exists. .PARAMETER PassThru Return full model objects instead of the summary table. .PARAMETER Table Output a formatted table similar to the models.dev UI. .PARAMETER Deep Perform deep recursive search across all nested fields (slower). .PARAMETER Fields Top-level fields to search when not using -Deep (faster). .PARAMETER FlatCachePath Path to flattened cache (CLIXML). Defaults to model-flat.clixml next to this module. .PARAMETER NoFlatCache Disable flattened cache usage. .PARAMETER CachePath Path to cache file. Defaults to model.json alongside this module. #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [AllowEmptyString()] [string]$Query = "", [switch]$Refresh, [switch]$PassThru, [switch]$Table, [switch]$Deep, [string[]]$Fields = @('id', 'name', 'provider_name', 'provider_id', 'family'), [string]$FlatCachePath = (Join-Path $PSScriptRoot 'model-flat.clixml'), [switch]$NoFlatCache, [string]$CachePath = (Join-Path $PSScriptRoot 'model.json') ) function Convert-ToDateTimeIfPossible { param($Value) if ($Value -is [string]) { try { $parsed = [DateTime]::Parse($Value) return $parsed } catch { return $Value } } return $Value } function Get-ModelsData { param( [string]$Path, [switch]$ForceRefresh ) $needsFetch = $ForceRefresh -or -not (Test-Path $Path) if (-not $needsFetch) { $content = Get-Content -Path $Path -Raw -ErrorAction SilentlyContinue if (-not $content) { $needsFetch = $true } } if ($needsFetch) { Write-Host "Fetching data from https://models.dev/api.json..." -ForegroundColor Cyan $data = Invoke-RestMethod -Uri 'https://models.dev/api.json' -Method Get $json = $data | ConvertTo-Json -Depth 100 $json | Set-Content -Path $Path -Encoding UTF8 return $data } return Get-Content -Path $Path -Raw | ConvertFrom-Json } function Get-ModelsFromProvider { param( $Provider, [string]$ProviderId ) if ($null -eq $Provider -or -not ($Provider.PSObject.Properties.Name -contains 'models')) { return @() } $providerName = $Provider.name $providerApi = $Provider.api $providerNpm = $Provider.npm $providerDoc = $Provider.doc $providerEnv = $Provider.env $models = $Provider.models $items = @() foreach ($prop in $models.PSObject.Properties) { $model = $prop.Value if ($null -eq $model) { continue } $model | Add-Member -NotePropertyName provider_id -NotePropertyValue $ProviderId -Force $model | Add-Member -NotePropertyName provider_name -NotePropertyValue $providerName -Force $model | Add-Member -NotePropertyName provider_api -NotePropertyValue $providerApi -Force $model | Add-Member -NotePropertyName provider_npm -NotePropertyValue $providerNpm -Force $model | Add-Member -NotePropertyName provider_doc -NotePropertyValue $providerDoc -Force $model | Add-Member -NotePropertyName provider_env -NotePropertyValue $providerEnv -Force # Convert date-like strings to DateTime foreach ($prop in $model.PSObject.Properties) { $model.($prop.Name) = Convert-ToDateTimeIfPossible $prop.Value } $items += $model } return $items } function Get-ModelCollection { param( $Data ) if ($null -eq $Data) { return @() } if ($Data.PSObject.Properties.Name -contains 'models') { return Get-ModelsFromProvider -Provider $Data -ProviderId $Data.id } if ($Data -is [psobject]) { $all = @() foreach ($prop in $Data.PSObject.Properties) { $provider = $prop.Value $all += Get-ModelsFromProvider -Provider $provider -ProviderId $prop.Name } if ($all.Count -gt 0) { return $all } } if ($Data -is [System.Collections.IDictionary]) { $all = @() foreach ($key in $Data.Keys) { $provider = $Data[$key] $all += Get-ModelsFromProvider -Provider $provider -ProviderId $key } return $all } if ($Data -is [System.Collections.IEnumerable] -and -not ($Data -is [string])) { return @($Data) } return @($Data) } function Test-ValueMatch { param( $Value, [string]$Search ) if ($null -eq $Value) { return $false } if ($Value -is [string]) { return $Value.IndexOf($Search, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 } if ($Value -is [System.Collections.IDictionary]) { foreach ($key in $Value.Keys) { if (Test-ValueMatch -Value $key -Search $Search) { return $true } if (Test-ValueMatch -Value $Value[$key] -Search $Search) { return $true } } return $false } if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) { foreach ($v in $Value) { if (Test-ValueMatch -Value $v -Search $Search) { return $true } } return $false } foreach ($prop in $Value.PSObject.Properties) { if (Test-ValueMatch -Value $prop.Value -Search $Search) { return $true } } return $false } function Test-ModelMatch { param( $Item, [string]$Search ) return (Test-ValueMatch -Value $Item -Search $Search) } function Test-ModelMatchShallow { param( $Item, [string]$Search, [string[]]$FieldList ) foreach ($field in $FieldList) { if ($null -eq $Item) { continue } if (-not ($Item.PSObject.Properties.Name -contains $field)) { continue } $value = $Item.$field if ($null -ne $value -and $value.ToString().IndexOf($Search, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { return $true } } return $false } function Format-Modalities { param( $Modalities, [string]$Key ) if ($null -eq $Modalities) { return '' } $value = $Modalities.$Key if ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) { return ($value | ForEach-Object { $_.ToString() }) -join ', ' } if ($null -ne $value) { return $value.ToString() } return '' } function Format-ModelsTable { param( $Models ) $Models | Sort-Object provider_name, id | ForEach-Object { [pscustomobject]@{ Provider = $_.provider_name Model = $_.name Family = $_.family ProviderId = $_.provider_id ModelId = $_.id ToolCall = if ($null -ne $_.tool_call) { $_.tool_call } else { '' } Reasoning = if ($null -ne $_.reasoning) { $_.reasoning } else { '' } Input = Format-Modalities -Modalities $_.modalities -Key 'input' Output = Format-Modalities -Modalities $_.modalities -Key 'output' InputCost = if ($null -ne $_.cost -and $null -ne $_.cost.input) { $_.cost.input } else { '' } } } | Format-Table -AutoSize } function Get-ModelsList { param( [string]$Path, [string]$FlatPath, [switch]$ForceRefresh, [switch]$DisableFlat ) if (-not $DisableFlat -and -not $ForceRefresh -and (Test-Path $FlatPath)) { if (Test-Path $Path) { $flatTime = (Get-Item $FlatPath).LastWriteTimeUtc $srcTime = (Get-Item $Path).LastWriteTimeUtc if ($flatTime -ge $srcTime) { return Import-Clixml -Path $FlatPath } } else { return Import-Clixml -Path $FlatPath } } $data = Get-ModelsData -Path $Path -ForceRefresh:$ForceRefresh Write-Host "Processing models..." -ForegroundColor Cyan $models = Get-ModelCollection -Data $data if (-not $DisableFlat) { Write-Host "Saving cache..." -ForegroundColor Cyan $models | Export-Clixml -Path $FlatPath } return $models } $models = Get-ModelsList -Path $CachePath -FlatPath $FlatCachePath -ForceRefresh:$Refresh -DisableFlat:$NoFlatCache $matches = if ([string]::IsNullOrEmpty($Query)) { $models } else { if ($Deep) { $models | Where-Object { Test-ModelMatch -Item $_ -Search $Query } } else { $models | Where-Object { Test-ModelMatchShallow -Item $_ -Search $Query -FieldList $Fields } } } if (-not $matches -and -not [string]::IsNullOrEmpty($Query)) { Write-Host "No matches found for '$Query'." -ForegroundColor Yellow return } if ($Table -and -not $PassThru) { Format-ModelsTable -Models $matches return } $matches } Export-ModuleMember -Function Search-AIModel |