PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 All 49 releases
← All changes | includes/ai/class-claude-client.php +113 -10 2.0.22.7.0 View file →
@@ -167,17 +167,22 @@
167 167
168 168 /**
169 169 * Whether the given model rejects sampling params (temperature/top_p/top_k).
170 170 *
171 - * Anthropic removed these on Opus 4.7+, Sonnet 5, and Fable 5 — including any
172 - * date-suffixed or "-latest" alias of them — so they must be omitted from the
173 - * request body or the API returns a 400.
171 + * Anthropic removed these on Opus 4.7+, Opus 5, Sonnet 5, and Fable 5 —
172 + * including any date-suffixed or "-latest" alias of them — so they must be
173 + * omitted from the request body or the API returns a 400.
174 174 *
175 + * Every generate_* method below sends a temperature, so a model missing from
176 + * this list fails on its first real call rather than at save time. `claude-opus-5`
177 + * was absent while being offered in the UI, which made the flagship model
178 + * unusable (#572).
179 + *
175 180 * @param string $model Model ID
176 181 * @return bool
177 182 */
178 183 private function model_rejects_sampling_params(string $model): bool {
179 - foreach (['claude-opus-4-7', 'claude-opus-4-8', 'claude-sonnet-5', 'claude-fable-5', 'claude-mythos-5'] as $prefix) {
184 + foreach (['claude-opus-4-7', 'claude-opus-4-8', 'claude-opus-5', 'claude-sonnet-5', 'claude-fable-5', 'claude-mythos-5'] as $prefix) {
180 185 if (strpos($model, $prefix) === 0) {
181 186 return true;
182 187 }
183 188 }
@@ -238,19 +243,113 @@
238 243 return $this->model;
239 244 }
240 245
241 246 /**
247 + * Output-token ceiling per model family, longest prefix wins.
248 + *
249 + * Matched by prefix so a dated snapshot (`claude-haiku-4-5-20251001`) and a
250 + * point release (`claude-fable-5-1`) resolve to their family. Order matters
251 + * only in that lookup walks longest-first, which is what keeps
252 + * `claude-fable-5-1` from matching `claude-fable-5`.
253 + *
254 + * @since 2.7.0
255 + * @var array<string, int>
256 + */
257 + private const MODEL_OUTPUT_LIMITS = [
258 + // 128K output.
259 + 'claude-fable-5-1' => 128000,
260 + 'claude-fable-5' => 128000,
261 + 'claude-mythos-5-1' => 128000,
262 + 'claude-mythos-5' => 128000,
263 + 'claude-opus-5' => 128000,
264 + 'claude-opus-4-8' => 128000,
265 + 'claude-opus-4-7' => 128000,
266 + 'claude-opus-4-6' => 128000,
267 + 'claude-sonnet-5' => 128000,
268 + 'claude-sonnet-4-6' => 128000,
269 + // 64K output.
270 + 'claude-haiku-4-5' => 64000,
271 + ];
272 +
273 + /**
274 + * Upper bound per use case, applied after the percentage.
275 + *
276 + * Two reasons these exist rather than letting the percentage run against a
277 + * 128K ceiling.
278 + *
279 + * Requests here are a single blocking HTTP call with a 120s timeout and no
280 + * streaming, so 0.9 x 128000 would risk running past the timeout instead of
281 + * returning — trading a truncation failure for a timeout failure. 16000
282 + * leaves room for the brief's JSON plus reasoning tokens while staying
283 + * answerable; raise it only alongside streaming.
284 + *
285 + * And correcting the ceiling would otherwise inflate every other use case
286 + * as a side effect — seo_metadata would jump from ~1,229 tokens to ~19,200
287 + * purely because this bug was fixed. Metadata generation already works, so
288 + * it keeps its cost profile (#665).
289 + *
290 + * @since 2.7.0
291 + * @var array<string, int>
292 + */
293 + private const USE_CASE_TOKEN_CAPS = [
294 + 'content_brief' => 16000,
295 + 'llms_txt' => 16000,
296 + 'analysis' => 8000,
297 + 'seo_metadata' => 4000,
298 + 'optimization' => 4000,
299 + 'default' => 4000,
300 + ];
301 +
302 + /**
303 + * Ceiling for a model this table does not know.
304 + *
305 + * The previous behaviour for every model, kept for older and unrecognised
306 + * ones: 8192 is accepted without an extended-output beta header, so it is
307 + * the safe answer when we cannot identify the family.
308 + *
309 + * @since 2.7.0
310 + * @var int
311 + */
312 + private const FALLBACK_OUTPUT_LIMIT = 8192;
313 +
314 + /**
242 315 * Maximum completion (output) tokens accepted for a single Claude request.
243 316 *
244 - * 8192 is accepted by every current Claude model without the extended-output
245 - * beta header, so it is a safe per-request ceiling. Kept as a method (rather
246 - * than a constant) to mirror the other clients and allow per-model tuning.
317 + * This returned a flat 8192 for every model and ignored $model entirely, so
318 + * Content Brief was capped at a fraction of the available budget and
319 + * truncated before its structured JSON completed — on every Claude model,
320 + * every time. Current models also emit reasoning tokens from the same
321 + * output budget, which is why it failed so reliably rather than
322 + * intermittently (#665).
247 323 *
248 - * @param string $model Model ID (reserved for future per-model limits).
324 + * @param string $model Model ID.
249 325 * @return int Maximum output tokens.
250 326 */
251 327 private function get_max_completion_tokens(string $model): int {
252 - return 8192;
328 + $model = strtolower(trim($model));
329 +
330 + if ('' === $model) {
331 + return self::FALLBACK_OUTPUT_LIMIT;
332 + }
333 +
334 + $limits = self::MODEL_OUTPUT_LIMITS;
335 +
336 + // Longest prefix first, so a point release never matches the shorter
337 + // family id that is a prefix of it.
338 + uksort(
339 + $limits,
340 + static function (string $a, string $b): int {
341 + return strlen($b) <=> strlen($a);
342 + }
343 + );
344 +
345 + foreach ($limits as $prefix => $limit) {
346 + if (0 === strpos($model, $prefix)) {
347 + return $limit;
348 + }
349 + }
350 +
351 + return self::FALLBACK_OUTPUT_LIMIT;
253 352 }
254 353
255 354 /**
256 355 * Recommended output-token budget for a given use case.
@@ -273,9 +372,13 @@
273 372 'optimization' => 0.15,
274 373 ];
275 374 $percentage = $recommendations[$use_case] ?? 0.15;
276 375
277 - return (int) ($max_tokens * $percentage);
376 + $budget = (int) ($max_tokens * $percentage);
377 +
378 + $cap = self::USE_CASE_TOKEN_CAPS[$use_case] ?? self::USE_CASE_TOKEN_CAPS['default'];
379 +
380 + return max(1, min($budget, $cap));
278 381 }
279 382
280 383 /**
281 384 * Test API connection