Swap & Verify sample: Otter Blocks, gpt-3.5-turbo to gpt-5.6-terra

Model Sunset Desk: public proof sample. Produced 2026-10-09 (AI-assisted, human-reviewed). AI-assisted, human-reviewed before any client use.

Subject: Otter Blocks 3.2.7 (Themeisle), a GPL WordPress plugin with 300,000 active installs according to WordPress.org (our scan, research/wp-scan/plugins_retiring_models.csv). Source: downloads.wordpress.org/plugin/otter-blocks.latest-stable.zip, sha256 c59d54bd...71edadc0, downloaded 2026-10-09.

Read this first. No live API calls were made for this sample (budget $0). The patch is real, applies cleanly, and its new logic is unit-tested on PHP 5.6 and 8.3. The regression harness is real and runs on the client's own OpenAI key. The before/after table further down comes from a dry run on synthetic mock responses, included to show what the client report looks like. It contains no measurements of either model. We are not affiliated with Themeisle, have not contacted them, and have not opened an issue or pull request anywhere.

1. Executive summary

2. Exposure (file:line)

#Location (Otter 3.2.7)What it does23 Oct impact
1inc/server/class-prompt-server.php:144 in save_api_key() (lines 119-188)Validates a newly entered key with a chat completion on hard-coded 'model' => 'gpt-3.5-turbo'. Any OpenAI error (lines 176-183) returns a WP_Error and the key is not saved (line 185 never runs).Breaks
2Remote templates from https://api.themeisle.com/templates-cloud/otter-prompts, fetched by class-prompt-server.php:312-358 and cached for WEEK_IN_SECONDS (line 352)All 4 templates (form, contentGenerator, textTransformation, patternsPicker) pin "model": "gpt-3.5-turbo". form and patternsPicker also use legacy functions + function_call. Read 2026-10-09, sha256 01e20afa...cfda114db3.Breaks the AI Block, AI Form generator and Patterns picker
3inc/server/class-otter-openai-backend.php:62-76The model is taken from the template payload; the gpt-5-mini fallback (line 71) applies only when no model is sentTemplate pin wins
4inc/server/class-otter-openai-backend.php:178-187The response parser reads message.function_call.arguments or message.content only, and ignores tool_calls and refusalNeeds extending if requests move to tools
5build/blocks/blocks.js (minified, webpack module 3907)"Regenerate" merges temperature: 1.3 into the template request. The page/section pipeline sends response_format: json_object, max_tokens: 8192 and no model.temperature support on GPT-5.6 is undocumented
6build/dashboard/index.jsThe dashboard "Save" button POSTs to otter/v1/openai/key and shows any error as a snackbarUser-visible symptom of item 1
7inc/server/class-ai-backend-resolver.php:55-65Uses the WordPress AI Client backend when a provider is configured; otherwise uses Otter's OpenAI-key pathDecides which sites are exposed
8packages/e2e-tests/mu-plugins/otter-e2e-bootstrap.php:141,150,184,719,912Test fixtures and mocks that mention gpt-3.5-turbo(-0125)None (tests only)

3. What breaks on 23 Oct 2026

Applies to sites on Otter's OpenAI-key path (item 7). We do not know what share of the 300,000 installs that is.

FeatureRequest pathExpected behaviour after shutdown
Save or change the OpenAI key (Otter dashboard)Key check, item 1The check fails, so the key is not saved and the dashboard shows OpenAI's error. A key saved earlier stays stored, because the check runs only on save.
AI Block (content generator)textTransformation templateEach generation returns an OpenAI error, shown in the block as "Error <code> - <message>"
AI Form generatorform templateSame; no form fields are generated
Patterns picker (blocks saved in that mode)patternsPicker templateSame
AI page/section generationNo model sent, so gpt-5-miniNot affected on 23 Oct. Follow-up: the gpt-5-mini model page lists one snapshot, gpt-5-mini-2025-08-07, marked Deprecated, and the deprecations page lists that snapshot for shutdown on 11 Dec 2026 (replacement gpt-5.6-terra). Whether the alias will be re-pointed is not stated.
Sites using WordPress AI Client connectorsWP_AI_Client_BackendNot affected by the pin, because Otter drops model on this path

The exact HTTP status and error text OpenAI will return for a shut-down model cannot be verified before the shutdown. The harness treats model_not_found or HTTP 404 as "old model unavailable".

Themeisle controls the served templates and may update them before 23 Oct. Two things still need a plugin-side fix: the key check is compiled into the plugin, and cached templates can be up to 7 days old. The patch below works whatever model the templates pin.

4. Parameter differences, with sources

All OpenAI pages were read live on 2026-10-09 between about 12:30 and 12:50 EDT. "Verified" means the statement appears on the cited page.

ItemOtter sends todaygpt-5.6-terra per OpenAI docsPatch actionSourceStatus
modelgpt-3.5-turbo (key check + 4 templates)Shutdown 2026-10-23; recommended replacement gpt-5.6-terraSwap via filterable mapDeprecationsVerified
Endpointv1/chat/completionsChat Completions and Responses supportedStay on Chat Completionsgpt-5.6-terraVerified
reasoning_effortNot sentnone, low, medium (default), high, xhigh, max; GPT-5.6 "defaults to medium" if omittedSend none after swapping a non-reasoning model (filter: otter_ai_openai_swap_reasoning_effort)terra page; GPT-5.6 guideValues verified; quality effect to be measured
max_tokensPage pipeline: 8192 (already renamed by 3.2.7); templates: none"Deprecated ... in favor of max_completion_tokens, and is not compatible with o-series models"; max_completion_tokens includes "visible output tokens and reasoning tokens"Keep the existing renameChat API referenceVerified
temperature, top_ptemperature: 1.3 on RegenerateNot stated for GPT-5.6 on the model page or the API reference. The API reference says parameter support "can differ ... particularly for newer reasoning models". The same guide's GPT-6 section says "When reasoning effort is not none, remove temperature, top_p, and top_logprobs".Dropped for gpt-5*/o* by default; opt back in with otter_ai_openai_keep_sampling_params; the harness tests both with --keep-samplingAPI reference; latest-model guideNot verified for terra. Designed defensively
functions, function_callform, patternsPicker"Deprecated in favor of tools" / "in favor of tool_choice"; terra lists function calling as supportedConvert to tools + named tool_choiceAPI reference; terra pageDeprecation verified. Whether terra still accepts the legacy fields is not verified (not relied on)
Response shapeReads function_call.arguments or contentmessage.tool_calls[].function.arguments; function_call "deprecated and replaced by tool_calls"Read tool_calls first; keep both fallbacksAPI referenceVerified
Refusalsmessage.refusal ignored, so the editor shows "empty response" (502, auto-retried twice)message.refusal documented. GPT-5.6 guide: "users may encounter safeguards that block or refuse some requests"; generation can be "paused for several seconds mid-stream"Return 422 model_refusal with the model's text (not auto-retried)API reference; GPT-5.6 guideField verified; frequency to be measured
response_format: json_objectPage pipeline only"older JSON mode ... Using json_schema is preferred"Unchanged (path not affected on 23 Oct)API referenceVerified
Usage and billingEditor shows total_tokensReasoning tokens "are billed as output tokens"; usage reports reasoning_tokens, cached_tokens, cache_write_tokens; GPT-5.6 cache writes billed at 1.25x uncached inputUnchanged; the harness records every fieldReasoning guide; API reference; terra pageVerified
Price per 1M tokensgpt-3.5-turbo: $0.50 in / $1.50 outterra: $2.00 in / $0.20 cached / $12.00 outCost moves with the swap; the harness estimates itgpt-3.5-turbo; terra pageVerified (list prices)
Limits16,385 context / 4,096 max output1,050,000 context / 128,000 max output; knowledge cutoff Feb 16, 2026n/aModel pagesVerified
Length and verbosityn/aGPT-5.6 "tends to be more concise by default than GPT-5.5"; verbosity low/medium/high, default mediumNot set; use it if the harness shows outputs that are too shortGPT-5.6 guide; API referenceVerified (relative to GPT-5.5, not to gpt-3.5)

5. The patch

File: patch.diff (unified diff against Otter 3.2.7; apply from the plugin root with patch -p1 < patch.diff). Patched copies of both files are in patch-work/b/.

Hunk 1, class-otter-openai-backend.php: constants and helpers. Adds:

--- a/inc/server/class-otter-openai-backend.php
+++ b/inc/server/class-otter-openai-backend.php
@@ -20,6 +21,173 @@
 	const BASE_URL = 'https://api.openai.com/v1/chat/completions';
 
 	/**
+	 * OpenAI chat models shut down on 2026-10-23, mapped to the replacement
+	 * OpenAI recommends for each.
+	 *
+	 * Source: https://developers.openai.com/api/docs/deprecations (entry
+	 * announced 2026-04-22). Filter with `otter_ai_openai_model_replacements`.
+	 *
+	 * @var array<string, string>
+	 */
+	const RETIRING_MODEL_REPLACEMENTS = array(
+		'gpt-3.5-turbo'           => 'gpt-5.6-terra',
+		'gpt-3.5-turbo-0125'      => 'gpt-5.6-terra',
+		'o4-mini'                 => 'gpt-5.6-terra',
+		'o4-mini-2025-04-16'      => 'gpt-5.6-terra',
+		'gpt-4'                   => 'gpt-5.6-sol',
+		'gpt-4-0613'              => 'gpt-5.6-sol',
+		'gpt-4-turbo'             => 'gpt-5.6-sol',
+		'gpt-4-turbo-2024-04-09'  => 'gpt-5.6-sol',
+		'gpt-4-1106-preview'      => 'gpt-5.6-sol',
+		'gpt-4o-2024-05-13'       => 'gpt-5.6-sol',
+		'o1'                      => 'gpt-5.6-sol',
+		'o1-2024-12-17'           => 'gpt-5.6-sol',
+		'o3-mini'                 => 'gpt-5.6-sol',
+		'o3-mini-2025-01-31'      => 'gpt-5.6-sol',
+		'gpt-4.1-nano'            => 'gpt-5.6-luna',
+		'gpt-4.1-nano-2025-04-14' => 'gpt-5.6-luna',
+	);
+
+	/**
+	 * Reasoning effort sent after swapping a retiring NON-reasoning model
+	 * (gpt-3.5/gpt-4 family). GPT-5.6 models default to `medium`; `none` keeps
+	 * the latency and token profile closest to the model being replaced.
+	 *
+	 * @var string
+	 */
+	const SWAP_REASONING_EFFORT = 'none';
+
+	/**
+	 * Map a retiring model ID to its recommended replacement.
+	 *
+	 * @param string $model Model ID.
+	 * @return string The replacement, or the input unchanged when it is not retiring.
+	 */
+	public static function get_replacement_model( $model ) {
+		/**
+		 * Filters the retiring-model => replacement map.
+		 *
+		 * Return an empty array to disable the automatic swap.
+		 *
+		 * @param array<string, string> $replacements Retiring model ID => replacement model ID.
+		 */
+		$replacements = apply_filters( 'otter_ai_openai_model_replacements', self::RETIRING_MODEL_REPLACEMENTS );
+
+		if ( is_array( $replacements ) && isset( $replacements[ $model ] ) && is_string( $replacements[ $model ] ) && '' !== $replacements[ $model ] ) {
+			return $replacements[ $model ];
+		}
+
+		return $model;
+	}
+
+	/**
+	 * Model used for the one-off API key check in the Otter dashboard.
+	 *
+	 * @return string
+	 */
+	public static function get_key_validation_model() {
+		/**
+		 * Filters the model used to validate a newly saved OpenAI API key.
+		 *
+		 * @param string $model Model ID. Defaults to the gpt-3.5-turbo replacement.
+		 */
+		return (string) apply_filters( 'otter_ai_openai_key_validation_model', self::get_replacement_model( 'gpt-3.5-turbo' ) );
+	}
+
+	/**
+	 * Whether the model documents `reasoning_effort: none` (GPT-5.6 family).
+	 *
+	 * @param string $model Model ID.
+	 * @return bool
+	 */
+	public static function supports_reasoning_effort_none( $model ) {
+		return 1 === preg_match( '/^gpt-5\.6(-(sol|terra|luna))?$/', (string) $model );
+	}
+
+	/**
+	 * Adapt an OpenAI-shaped payload to the model it will be sent to.
+	 *
+	 * 1. Swaps retiring model IDs for their recommended replacement.
+	 * 2. Sets `reasoning_effort` after a swap from a non-reasoning model.
+	 * 3. Drops sampling parameters (`temperature`, `top_p`, logprobs) for
+	 *    reasoning-family models unless a site opts back in.
+	 * 4. Converts deprecated `functions`/`function_call` to `tools`/`tool_choice`.
+	 *
+	 * @param array<string, mixed> $payload The OpenAI-format payload.
+	 * @return array<string, mixed>
+	 */
+	public static function adapt_payload( array $payload ) {
+		$requested = isset( $payload['model'] ) && is_string( $payload['model'] ) ? $payload['model'] : '';
+		$model     = '' !== $requested ? self::get_replacement_model( $requested ) : '';
+
+		if ( '' !== $model && $model !== $requested ) {
+			$payload['model'] = $model;
+
+			if ( ! isset( $payload['reasoning_effort'] ) && 1 !== preg_match( '/^o[1-9]/', $requested ) && self::supports_reasoning_effort_none( $model ) ) {
+				/**
+				 * Filters the reasoning effort used after a retiring non-reasoning model is swapped.
+				 *
+				 * Return '' to omit `reasoning_effort` and use the model default.
+				 *
+				 * @param string               $effort    Reasoning effort.
+				 * @param string               $requested The retiring model ID that was requested.
+				 * @param string               $model     The replacement model ID.
+				 * @param array<string, mixed> $payload   The payload.
+				 */
+				$effort = (string) apply_filters( 'otter_ai_openai_swap_reasoning_effort', self::SWAP_REASONING_EFFORT, $requested, $model, $payload );
+
+				if ( '' !== $effort ) {
+					$payload['reasoning_effort'] = $effort;
+				}
+			}
+		}
+
+		if ( 1 === preg_match( '/^(gpt-5|o[1-9])/', $model ) ) {
+			/**
+			 * Filters whether sampling parameters are kept for reasoning-family models.
+			 *
+			 * @param bool                 $keep    Keep `temperature`, `top_p`, `logprobs`, `top_logprobs`. Default false.
+			 * @param string               $model   The model ID.
+			 * @param array<string, mixed> $payload The payload.
+			 */
+			if ( ! apply_filters( 'otter_ai_openai_keep_sampling_params', false, $model, $payload ) ) {
+				unset( $payload['temperature'], $payload['top_p'], $payload['logprobs'], $payload['top_logprobs'] );
+			}
+		}
+
+		if ( isset( $payload['functions'] ) && is_array( $payload['functions'] ) && ! isset( $payload['tools'] ) ) {
+			$tools = array();
+
+			foreach ( $payload['functions'] as $function ) {
+				if ( is_array( $function ) && ! empty( $function['name'] ) ) {
+					$tools[] = array(
+						'type'     => 'function',
+						'function' => $function,
+					);
+				}
+			}
+
+			if ( ! empty( $tools ) ) {
+				$function_call = isset( $payload['function_call'] ) ? $payload['function_call'] : null;
+
+				if ( is_array( $function_call ) && ! empty( $function_call['name'] ) ) {
+					$payload['tool_choice'] = array(
+						'type'     => 'function',
+						'function' => array( 'name' => (string) $function_call['name'] ),
+					);
+				} elseif ( 'none' === $function_call || 'auto' === $function_call ) {
+					$payload['tool_choice'] = $function_call;
+				}
+
+				$payload['tools'] = $tools;
+				unset( $payload['functions'], $payload['function_call'] );
+			}
+		}
+
+		return $payload;
+	}
+
+	/**
 	 * Whether an Otter OpenAI API key is configured.
 	 *
 	 * @return bool

Hunk 2: one call site. adapt_payload() runs after all existing filters (otter_ai_otter_openai_payload, otter_ai_otter_openai_model, otter_ai_block_generation_model) and before the existing max_tokens rename. A template, or a site filter, that still names a retiring model is therefore always remapped.

@@ -75,6 +243,10 @@
 			$payload['model'] = $model;
 		}
 
+		// Prompt templates are fetched remotely and cached for a week, so they
+		// can still pin a model OpenAI has shut down. Adapt after all filters ran.
+		$payload = self::adapt_payload( $payload );
+
 		// GPT-5/o-series models reject `max_tokens`; `max_completion_tokens` is
 		// accepted by all current chat-completions models.
 		if ( isset( $payload['max_tokens'] ) ) {

Hunk 3: response parsing. Reads tool_calls[0].function.arguments first, then the legacy function_call, then content, so both old and new response shapes work. An empty reply that carries message.refusal now returns HTTP 422 model_refusal with the model's text. Before, it was a generic 502 "empty response", which the editor auto-retries twice.

@@ -179,13 +351,22 @@
 		$content = '';
 		$format  = 'text';
 
-		if ( isset( $message['function_call'] ) && is_array( $message['function_call'] ) && isset( $message['function_call']['arguments'] ) ) {
+		if ( isset( $message['tool_calls'][0]['function']['arguments'] ) && is_string( $message['tool_calls'][0]['function']['arguments'] ) ) {
+			// `tools`/`tool_choice` response shape (see adapt_payload()).
+			$content = $message['tool_calls'][0]['function']['arguments'];
+			$format  = 'json';
+		} elseif ( isset( $message['function_call'] ) && is_array( $message['function_call'] ) && isset( $message['function_call']['arguments'] ) ) {
 			$content = (string) $message['function_call']['arguments'];
 			$format  = 'json';
 		} elseif ( isset( $message['content'] ) ) {
 			$content = (string) $message['content'];
 		}
 
+		if ( '' === $content && isset( $message['refusal'] ) && is_string( $message['refusal'] ) && '' !== $message['refusal'] ) {
+			// Surface the model's refusal instead of a generic, auto-retried 502.
+			return $this->error_response( 'model_refusal', $message['refusal'], 422 );
+		}
+
 		if ( '' === $content ) {
 			return $this->error_response( 'empty_response', __( 'OpenAI returned an empty response. Please try again.', 'otter-blocks' ), 502 );
 		}

Hunks 4-5, class-prompt-server.php: the line-144 key check. Builds the same "Hello!" request with get_key_validation_model(), adding reasoning_effort: none when the model supports it. It is still a real chat completion, as before, so restricted project keys that can call models but cannot list them keep validating.

--- a/inc/server/class-prompt-server.php
+++ b/inc/server/class-prompt-server.php
@@ -131,6 +132,27 @@
 			return new \WP_REST_Response( array( 'message' => __( 'API key saved.', 'otter-blocks' ) ), 200 );
 		}
 
+		// gpt-3.5-turbo shuts down on 2026-10-23; validate with its replacement.
+		$validation_model = Otter_OpenAI_Backend::get_key_validation_model();
+		$validation_body  = array(
+			'model'    => $validation_model,
+			'messages' => array(
+				array(
+					'role'    => 'system',
+					'content' => 'You are a helpful assistant.',
+				),
+				array(
+					'role'    => 'user',
+					'content' => 'Hello!',
+				),
+			),
+		);
+
+		// GPT-5.6 models reason by default (`medium`); a key check needs none.
+		if ( Otter_OpenAI_Backend::supports_reasoning_effort_none( $validation_model ) ) {
+			$validation_body['reasoning_effort'] = 'none';
+		}
+
 		$response = wp_remote_post(
 			self::BASE_URL,
 			array(
@@ -139,21 +161,7 @@
 					'Authorization' => 'Bearer ' . $api_key,
 					'Content-Type'  => 'application/json',
 				),
-				'body'    => wp_json_encode(
-					array(
-						'model'    => 'gpt-3.5-turbo',
-						'messages' => array(
-							array(
-								'role'    => 'system',
-								'content' => 'You are a helpful assistant.',
-							),
-							array(
-								'role'    => 'user',
-								'content' => 'Hello!',
-							),
-						),
-					)
-				),
+				'body'    => wp_json_encode( $validation_body ),
 				'timeout' => 2 * MINUTE_IN_SECONDS,
 			)
 		);

Modification notices (GPL-2.0 section 2(a)). Each changed file also gets one dated comment line in its header docblock that says what changed and that the original is (c) Themeisle under GPL-2.0-or-later. These two comment-only hunks change no behaviour, and the hunk numbers above count code hunks only. Licence text: LICENSE-GPL-2.0.txt; notice: patch-work/NOTICE.md.

--- a/inc/server/class-otter-openai-backend.php
+++ b/inc/server/class-otter-openai-backend.php
@@ -1,6 +1,7 @@
 <?php
 /**
  * Otter OpenAI backend strategy.
+ * Modified 2026-10-09 by Model Sunset Desk: swaps OpenAI models retiring 2026-10-23 for the recommended replacements (adapt_payload) and parses tool_calls and refusals; original © Themeisle, GPL-2.0-or-later
  *
  * @package ThemeIsle\GutenbergBlocks\Server
  */
--- a/inc/server/class-prompt-server.php
+++ b/inc/server/class-prompt-server.php
@@ -1,6 +1,7 @@
 <?php
 /**
  * Prompt server logic.
+ * Modified 2026-10-09 by Model Sunset Desk: the API-key check no longer hard-codes gpt-3.5-turbo (uses its replacement with reasoning_effort none); original © Themeisle, GPL-2.0-or-later
  *
  * @package ThemeIsle\GutenbergBlocks\Server
  */

Backward compatibility.

No-core-edit alternative for site owners (mu-plugin/otter-model-sunset.php). Editing plugin files is undone by the next plugin update. The mu-plugin applies the same transform through Otter's existing hooks:

To install, copy it to wp-content/mu-plugins/. To remove, delete the file.

6. Test plan and dry-run demo

Done for this sample (no API spend):

CheckResult
patch -p1 --dry-run on a fresh copy of the 3.2.7 sourceApplies cleanly (5 code hunks plus 2 comment-only notice hunks; re-checked 2026-10-09 after the notices were added); the result is byte-identical to patch-work/b/
php -l on patched files and the mu-plugin (official php:5.6-cli and php:8.3-cli images, no network)No syntax errors
tests/test_adapt_payload.php: 24 checks on the code hunk 1 adds, extracted from patch.diff (no Otter code executed)24/24 passed on PHP 5.6.40; 24/24 passed on PHP 8.3.35
tests/test_mu_plugin.php: 8 checks8/8 passed on PHP 5.6.40; 8/8 passed on PHP 8.3.35
tests/test_transform_mirror.py: harness mirror matches patch behaviour10/10 passed (Python 3.14)
Harness live-mode guards: missing key; pre-flight cost capBoth abort before any request (harness/out/live-guards.console.txt)

Not done here:

Both are part of client acceptance:

  1. Install on staging and save the key; expect "API key saved."
  2. Generate one AI Block, one AI form and one Regenerate.
  3. Confirm "Token used" appears and nothing returns HTTP 400.
  4. Run the harness live.

Regression harness (harness/).

Dry-run demo output (synthetic). Command: python3 -I harness/run_harness.py --dry-run --out harness/out/dry-run. Full output: harness/out/dry-run/comparison.md.

SYNTHETIC MOCK DATA, NOT MEASUREMENTS. The mock outputs were written by hand and are identical across arms, except for seven labelled variations (old arm 3, new arm 4) injected to show that the detectors fire. Both arms get the same latency noise and token counts. The cost row is therefore list-price arithmetic only.

Metricoldnew:gpt-5.6-terra
Requests3030
HTTP 20030/30 (100%)30/30 (100%)
Format valid29/30 (97%)28/30 (93%)
Expectations met (excl. policy-edge)25/28 (89%)26/28 (93%)
Refusals01
Errors00
Latency median (ms)12791167
Latency p90 (ms)19181918
Mean words, AI Block outputs51.946.4
Mean output chars, all341.4327.8
Prompt tokens1013510135
Completion tokens (incl. reasoning)25482494
Reasoning tokens00
Est. cost, this run (USD)0.0088890.050198
Est. cost per 1,000 requests (USD)0.29631.6733
Model reported by APIgpt-3.5-turbo-0125gpt-5.6-terra

Flagged cases from the same dry run. Each flag corresponds to an injected variation, and every one was detected:

CaseFeatureRegenOld: valid / words / msnew:gpt-5.6-terra: valid / words / msFlags
B05textTransformationyes / 50 / 1482NO / 51 / 1521FORMAT-REGRESSION, EXPECTATION-REGRESSION
B09textTransformationyes / 65 / 1692yes / 17 / 763EXPECTATION-REGRESSION, LENGTH-SHIFT -74%
B16textTransformationyes / 42 / 689NO / 0 / 605FORMAT-REGRESSION, REFUSAL-CHANGE, LENGTH-SHIFT -100%, POLICY-EDGE
F05formyes / 4 / 2248yes / 5 / 2248EXPECTATION-FIXED
F09formNO / 0 / 1270yes / 6 / 1890FORMAT-FIXED, EXPECTATION-FIXED, COUNT-SHIFT 0->6
F10formyes / 6 / 1404yes / 4 / 1110COUNT-SHIFT 6->4, POLICY-EDGE
P02patternsPickeryes / 4 / 526yes / 5 / 535EXPECTATION-FIXED

What the patch changes on the wire, from requests.jsonl of the same run:

B01 (textTransformation)

KeyOld requestNew request
model"gpt-3.5-turbo""gpt-5.6-terra"
reasoning_effort(absent)"none"

F01 (form)

KeyOld requestNew request
function_call{"name": "generateForm"}(absent)
functions[{"name": "generateForm", "description": "Generate form fields for the task.", "parameters...(absent)
model"gpt-3.5-turbo""gpt-5.6-terra"
reasoning_effort(absent)"none"
tool_choice(absent){"type": "function", "function": {"name": "generateForm"}}
tools(absent)[{"type": "function", "function": {"name": "generateForm", "description": "Generate form f...

P01 (patternsPicker)

KeyOld requestNew request
function_call{"name": "pickPatterns"}(absent)
functions[{"name": "pickPatterns", "description": "Pick the patterns relevant for the page.", "para...(absent)
model"gpt-3.5-turbo""gpt-5.6-terra"
reasoning_effort(absent)"none"
tool_choice(absent){"type": "function", "function": {"name": "pickPatterns"}}
tools(absent)[{"type": "function", "function": {"name": "pickPatterns", "description": "Pick the patter...

B04 (textTransformation, regenerate)

KeyOld requestNew request
model"gpt-3.5-turbo""gpt-5.6-terra"
reasoning_effort(absent)"none"
temperature1.3(absent)

A second dry run (harness/out/dry-run-post-shutdown/) replays a synthetic post-shutdown model_not_found for the old model. The harness marks the old arm unavailable after the first failure, skips the remaining old calls, and points to --baseline for comparison against a pre-shutdown run.

Suggested acceptance criteria for a client run:

7. Risks and rollback

RiskLikelihood / evidenceMitigation
Higher API cost per requestCertain per token: 4x input, 8x output at list price (verified prices)Measure with the harness. --candidate gpt-5.6-luna tests a cheaper model ($0.20 / $1.20), but OpenAI recommends terra for gpt-3.5-turbo.
Output drift (length, tone, HTML habits such as code fences)Unknown until measuredRun the harness on the client's inputs; consider verbosity or a prompt tweak
temperature rejected by terraUndocumentedDropped by default; --keep-sampling measures whether it is accepted
reasoning_effort: none lowers quality on harder promptsUnknown--swap-effort low comparison; filter otter_ai_openai_swap_reasoning_effort
More refusals or safeguard pausesGPT-5.6 guide documents safeguards that can refuse or pauseRefusal text is now surfaced; policy-edge cases are in the harness
Key's project has no access to gpt-5.6-terraPossible on restricted projectsThe error names the model; filter otter_ai_openai_key_validation_model
A plugin update overwrites a direct patchCertain for in-place editsUse the mu-plugin, or upstream the patch
Vendor ships its own fixLikely at some pointBoth are idempotent: an already-swapped model is left alone; delete the mu-plugin afterwards
Not changed by this patch: the page pipeline caps output at 8192 completion tokens on gpt-5-mini at its default effort; OpenAI's reasoning guide recommends "reserving at least 25,000 tokens" when experimentingObserved in code, not measuredOut of scope; noted for the 11 Dec follow-up

Rollback. Run patch -R -p1 < patch.diff, or reinstall Otter 3.2.7 from WordPress.org. For the mu-plugin, delete the file. To disable only the swap: add_filter( 'otter_ai_openai_model_replacements', '__return_empty_array' );. No data migration is involved. After 23 Oct, a rollback brings back the broken state.

8. What a client would receive

This sample corresponds to Swap & Verify ($290 per repo, or per bundle of up to 3 n8n/Make/Zapier workflows). The client pays on delivery through a Payment Link sent with the PR and report, and does not pay if the delivery does not pass their review. Delivery is within 48 hours of receiving access and a staging key, weekends included.

  1. A pull request into the client's repository (or a fixed workflow export) that replaces retiring model IDs with OpenAI's recommended replacement and fixes parameter and response-shape differences, as in section 5.
  2. A 30-case before/after regression on the client's own inputs, run with the client's staging API key, which the client revokes afterwards. It covers format validity, refusals, length, latency, tokens and estimated cost, as in section 6.
  3. A one-page quality and cost report, with flagged cases shown side by side for the client's decision.
  4. Rollback instructions.

Not included:

Client data is used only for the engagement and deleted within 7 days of delivery. Liability is capped at the fee paid. Scope is written per engagement in one paragraph, with an acceptance test, before work starts.

Contact: Orkhan Karimov, Model Sunset Desk, hello@modelsunsetdesk.com, https://orkhan103.github.io/model-sunset-desk.

9. Verification log

Verified live on 2026-10-09 (page copies saved under launch/_untrusted/openai-docs-otter/; the Otter zip and served templates under launch/_untrusted/otter-blocks/):

  1. OpenAI deprecations:
    • gpt-3.5-turbo-0125, gpt-3.5-turbo and gpt-3.5-turbo-completions shut down 2026-10-23, replacement gpt-5.6-terra, entry announced 2026-04-22;
    • gpt-5-mini-2025-08-07 shuts down 2026-12-11 (replacement gpt-5.6-terra); plain gpt-5-mini is not listed;
    • nothing about functions/function_call.
  2. gpt-5.6-terra: effort values and default, endpoints, features, limits, prices, cache-write rule, single snapshot. The sol and luna pages list the same effort values.
  3. gpt-3.5-turbo and gpt-5-mini: prices, limits, snapshots (marked Deprecated).
  4. Chat Completions create reference: max_tokens, max_completion_tokens, functions, function_call, tools, tool_choice, reasoning_effort, temperature, response_format, verbosity, refusal, finish_reason and usage fields, quoted in section 4.
  5. Latest-model guide (GPT-6 and GPT-5.6 sections) and the reasoning guide: quoted in sections 4 and 7.
  6. Otter 3.2.7 source (zip sha256 c59d54bdaab37d5e4c441b83472daed2f4335600cba5db0142d4cd8c71edadc0), read in full for the AI path. Served templates: one read-only GET of the endpoint the plugin itself calls, using the placeholder site_url=https://example.com (sha256 01e20afab2e124737f631d9d48e24753b50a8acf5a87e08f7d9ca8cfda114db3).

Not verified (stated as such wherever used):

Files in this sample: README.md (index), report.md/report.html (this report), patch.diff, LICENSE-GPL-2.0.txt, patch-work/ (with NOTICE.md), mu-plugin/, tests/, harness/, tools/.