param( [switch]$Help ) $ErrorActionPreference = 'Stop' $OmniRouterBaseUrl = 'https://api.omnirouter.ru/v1' function Show-Usage { @' Установщик OmniRouter для Codex Desktop и Codex CLI на Windows. Использование: irm "https://api.omnirouter.ru/install/codex.ps1" | iex Что делает скрипт: - Без отображения запрашивает действующий omni-ключ - Сохраняет OMNIROUTER_API_KEY в переменных текущего пользователя Windows - Не изменяет настоящий API-ключ OpenAI - Удаляет устаревший omni-ключ из кеша Codex, если он там остался - Настраивает пользовательский config.toml для Codex Desktop и Codex CLI - Сохраняет остальные настройки и обычную авторизацию OpenAI/ChatGPT Omni-ключ не нужно и нельзя вводить в экран входа OpenAI. '@ } function Write-Utf8NoBom([string]$Path, [string]$Value) { $encoding = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($Path, $Value, $encoding) } function Remove-TomlTable([string]$Text, [string]$TableName) { $escaped = [regex]::Escape($TableName) $pattern = "(?ms)^[ `t]*\[$escaped\][ `t]*\r?\n.*?(?=^[ `t]*\[|\z)" return [regex]::Replace($Text, $pattern, '') } function Set-CodexConfig([string]$CodexDir) { $configPath = Join-Path $CodexDir 'config.toml' $profilePath = Join-Path $CodexDir 'omnirouter.config.toml' $startMarker = '# >>> omnirouter codex >>>' $endMarker = '# <<< omnirouter codex <<<' $newLine = [Environment]::NewLine $text = if (Test-Path -LiteralPath $configPath) { [System.IO.File]::ReadAllText($configPath) } else { '' } $managedPattern = '(?ms)^[ \t]*' + [regex]::Escape($startMarker) + '\r?\n.*?^[ \t]*' + [regex]::Escape($endMarker) + '\r?\n?' $text = [regex]::Replace($text, $managedPattern, '') $text = Remove-TomlTable $text 'model_providers.omnirouter' $text = Remove-TomlTable $text 'profiles.omnirouter' $firstTable = [regex]::Match($text, '(?m)^[ \t]*\[') if ($firstTable.Success) { $prefix = $text.Substring(0, $firstTable.Index) $suffix = $text.Substring($firstTable.Index) } else { $prefix = $text $suffix = '' } $prefix = [regex]::Replace( $prefix, '(?m)^[ \t]*(?:model_provider[ \t]*=.*|profile[ \t]*=[ \t]*["'']omnirouter["''])[ \t]*\r?\n?', '' ) $text = ($prefix + $suffix).Trim() $managedBlock = @" $startMarker [model_providers.omnirouter] name = "OmniRouter" base_url = "$OmniRouterBaseUrl" wire_api = "responses" env_key = "OMNIROUTER_API_KEY" $endMarker "@ $managedBlock = $managedBlock.Trim() $configText = 'model_provider = "omnirouter"' + $newLine if ($text) { $configText += $newLine + $text + $newLine } $configText += $newLine + $managedBlock + $newLine Write-Utf8NoBom $configPath $configText $profileText = if (Test-Path -LiteralPath $profilePath) { [System.IO.File]::ReadAllText($profilePath) } else { '' } $profileText = [regex]::Replace( $profileText, '(?m)^[ \t]*(?:model_provider|web_search)[ \t]*=.*\r?\n?', '' ) $profileText = [regex]::Replace( $profileText, '(?m)^[ \t]*(?:multi_agent|browser_use|image_generation)[ \t]*=.*\r?\n?', '' ).Trim() $featureConfig = 'multi_agent = false' + $newLine + 'browser_use = false' + $newLine + 'image_generation = false' + $newLine if ([regex]::IsMatch($profileText, '(?m)^[ \t]*\[features\][ \t]*$')) { $profileText = [regex]::Replace( $profileText, '(?m)^([ \t]*\[features\][ \t]*)$', '$1' + $newLine + $featureConfig, 1 ) } else { if ($profileText) { $profileText += $newLine + $newLine } $profileText += '[features]' + $newLine + $featureConfig } $profileConfig = 'model_provider = "omnirouter"' + $newLine + 'web_search = "disabled"' + $newLine if ($profileText) { $profileConfig += $profileText + $newLine } Write-Utf8NoBom $profilePath $profileConfig } function Remove-LegacyOmniAuth([string]$CodexDir) { $authPath = Join-Path $CodexDir 'auth.json' if (-not (Test-Path -LiteralPath $authPath)) { return } try { $auth = [System.IO.File]::ReadAllText($authPath) | ConvertFrom-Json } catch { throw "Не удалось прочитать кеш авторизации Codex $authPath. Исправьте JSON или временно переместите файл и повторите установку." } $keyProperty = $auth.PSObject.Properties['OPENAI_API_KEY'] if ($null -eq $keyProperty -or $keyProperty.Value -isnot [string] -or -not $keyProperty.Value.StartsWith('omni_')) { return } $auth.PSObject.Properties.Remove('OPENAI_API_KEY') $modeProperty = $auth.PSObject.Properties['auth_mode'] if ($null -ne $modeProperty -and $modeProperty.Value -eq 'apikey') { $auth.PSObject.Properties.Remove('auth_mode') } Write-Utf8NoBom $authPath (($auth | ConvertTo-Json -Depth 100) + [Environment]::NewLine) } if ($Help) { Show-Usage return } $secureKey = Read-Host 'Введите omni-ключ' -AsSecureString $keyPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureKey) $omniApiKey = $null try { $omniApiKey = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($keyPointer) if ($omniApiKey -cnotmatch '^omni_[0-9a-f]{32}$') { throw 'Некорректный omni-ключ. Ожидается префикс omni_ и 32 строчные шестнадцатеричные цифры.' } $codexDir = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $env:USERPROFILE '.codex' } [System.IO.Directory]::CreateDirectory($codexDir) | Out-Null Set-CodexConfig $codexDir Remove-LegacyOmniAuth $codexDir [Environment]::SetEnvironmentVariable('OMNIROUTER_API_KEY', $omniApiKey, 'User') $env:OMNIROUTER_API_KEY = $omniApiKey Write-Host 'Codex Desktop и Codex CLI настроены для OmniRouter.' Write-Host "Обновлена конфигурация: $(Join-Path $codexDir 'config.toml')" Write-Host "Обновлён профиль CLI: $(Join-Path $codexDir 'omnirouter.config.toml')" Write-Host 'Полностью закройте ChatGPT/Codex, включая значок в системном трее, и откройте приложение снова.' Write-Host 'Omni-ключ не вводится в экран входа OpenAI.' } finally { if ($keyPointer -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($keyPointer) } $omniApiKey = $null $secureKey = $null }