[CmdletBinding()] param( [Parameter(Position = 0)] [ValidatePattern('^[A-Za-z0-9_-]+$')] [string]$ProviderId = 'openai_http', [string]$CodexHome, [string]$CodexExecutable ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 function Fail([string]$Message) { throw $Message } if (-not $CodexHome) { if ($env:CODEX_HOME) { $CodexHome = $env:CODEX_HOME } elseif ($env:USERPROFILE) { $CodexHome = Join-Path $env:USERPROFILE '.codex' } else { Fail 'Cannot determine CODEX_HOME; pass -CodexHome explicitly.' } } $CodexHome = [System.IO.Path]::GetFullPath($CodexHome) $ConfigFile = Join-Path $CodexHome 'config.toml' $LockDirectory = Join-Path $CodexHome '.force-http-provider.lock' $TempFile = $null $BackupFile = $null $Changed = $false if (-not (Test-Path -LiteralPath $CodexHome -PathType Container)) { Fail "CODEX_HOME does not exist: $CodexHome" } if (-not (Test-Path -LiteralPath $ConfigFile -PathType Leaf)) { Fail "config.toml does not exist: $ConfigFile" } if (-not $CodexExecutable) { $Command = Get-Command codex -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($Command) { $CodexExecutable = $Command.Source } } if (-not $CodexExecutable -or -not (Test-Path -LiteralPath $CodexExecutable -PathType Leaf)) { Fail 'Codex executable not found; pass -CodexExecutable with the real Windows Codex executable.' } try { New-Item -ItemType Directory -Path $LockDirectory -ErrorAction Stop | Out-Null } catch { Fail 'Another provider patch is already running.' } function Restore-Backup { if ($BackupFile -and (Test-Path -LiteralPath $BackupFile -PathType Leaf)) { Copy-Item -LiteralPath $BackupFile -Destination $ConfigFile -Force } } function Test-CodexConfig { $PreviousCodexHome = $env:CODEX_HOME try { $env:CODEX_HOME = $CodexHome $DoctorText = (& $CodexExecutable --strict-config doctor --json 2>$null | Out-String) if (-not $DoctorText.Trim()) { return $false } $Doctor = $DoctorText | ConvertFrom-Json $LoadCheck = $Doctor.checks.'config.load' if (-not $LoadCheck -or $LoadCheck.status -ne 'ok') { return $false } return ($LoadCheck.details.'model provider' -eq $ProviderId) } catch { return $false } finally { $env:CODEX_HOME = $PreviousCodexHome } } try { $Raw = [System.IO.File]::ReadAllText($ConfigFile) $NewLine = if ($Raw.Contains("`r`n")) { "`r`n" } else { "`n" } $HadTrailingNewLine = $Raw.EndsWith("`n") $Lines = [System.Text.RegularExpressions.Regex]::Split($Raw, '\r\n|\n') if ($HadTrailingNewLine -and $Lines.Count -gt 0 -and $Lines[-1] -eq '') { $Lines = $Lines[0..($Lines.Count - 2)] } $InRoot = $true $InTarget = $false $RootCount = 0 $TargetTableCount = 0 $WebSocketCount = 0 foreach ($Line in $Lines) { $Header = [regex]::Match($Line, '^\s*\[([^\]]+)\]\s*(?:#.*)?$') if ($Header.Success) { $InRoot = $false $InTarget = ($Header.Groups[1].Value -eq "model_providers.$ProviderId") if ($InTarget) { $TargetTableCount++ } continue } if ($InRoot -and $Line -match '^\s*model_provider\s*=') { $RootCount++ } if ($InTarget -and $Line -match '^\s*supports_websockets\s*=') { $WebSocketCount++ } } if ($RootCount -gt 1) { Fail 'Duplicate root model_provider keys; refusing to guess.' } if ($TargetTableCount -ne 1) { Fail "Expected exactly one [model_providers.$ProviderId] table; found $TargetTableCount." } if ($WebSocketCount -gt 1) { Fail 'Duplicate supports_websockets keys in target provider; refusing to guess.' } $Output = [System.Collections.Generic.List[string]]::new() if ($RootCount -eq 0) { $Output.Add("model_provider = `"$ProviderId`"") } $InRoot = $true $InTarget = $false $InsertedWebSocket = $false foreach ($Line in $Lines) { $Header = [regex]::Match($Line, '^\s*\[([^\]]+)\]\s*(?:#.*)?$') if ($Header.Success) { if ($InTarget -and $WebSocketCount -eq 0 -and -not $InsertedWebSocket) { $Output.Add('supports_websockets = false') $InsertedWebSocket = $true } $InRoot = $false $InTarget = ($Header.Groups[1].Value -eq "model_providers.$ProviderId") $Output.Add($Line) if ($InTarget -and $WebSocketCount -eq 0 -and -not $InsertedWebSocket) { $Output.Add('supports_websockets = false') $InsertedWebSocket = $true } continue } if ($InRoot -and $Line -match '^\s*model_provider\s*=') { $Comment = if ($Line -match '(\s+#.*)$') { $Matches[1] } else { '' } $Output.Add("model_provider = `"$ProviderId`"$Comment") continue } if ($InTarget -and $Line -match '^\s*supports_websockets\s*=') { $Comment = if ($Line -match '(\s+#.*)$') { $Matches[1] } else { '' } $Output.Add("supports_websockets = false$Comment") continue } $Output.Add($Line) } $Candidate = [string]::Join($NewLine, $Output) if ($HadTrailingNewLine) { $Candidate += $NewLine } if ($Candidate -eq $Raw) { if (-not (Test-CodexConfig)) { Fail 'Existing config failed strict Codex validation; no file was changed.' } Write-Output 'OK: already configured; no file changed' return } $Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' $BackupFile = Join-Path $CodexHome "config.toml.before-http-$Timestamp.bak" if (Test-Path -LiteralPath $BackupFile) { $BackupFile = Join-Path $CodexHome "config.toml.before-http-$Timestamp-$PID.bak" } Copy-Item -LiteralPath $ConfigFile -Destination $BackupFile $TempFile = Join-Path $CodexHome ("config.toml.force-http.{0}.tmp" -f [guid]::NewGuid().ToString('N')) [System.IO.File]::WriteAllText($TempFile, $Candidate, [System.Text.UTF8Encoding]::new($false)) Move-Item -LiteralPath $TempFile -Destination $ConfigFile -Force $TempFile = $null $Changed = $true if (-not (Test-CodexConfig)) { Restore-Backup Fail 'Strict config validation failed; backup restored.' } Write-Output 'OK: config updated and strictly loaded by Codex' Write-Output "Backup: $BackupFile" Write-Output 'Next: run one real Codex request before installing any recurring trigger' } catch { if ($Changed) { Restore-Backup } throw } finally { if ($TempFile -and (Test-Path -LiteralPath $TempFile)) { Remove-Item -LiteralPath $TempFile -Force -ErrorAction SilentlyContinue } Remove-Item -LiteralPath $LockDirectory -Force -ErrorAction SilentlyContinue }