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.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 1.10.0 All 48 releases
← All changes | includes/ai/class-claude-client.php +104 -6 2.2.02.7.0 View file →
@@ -243,19 +243,113 @@
243 243 return $this->model;
244 244 }
245 245
246 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 + /**
247 315 * Maximum completion (output) tokens accepted for a single Claude request.
248 316 *
249 - * 8192 is accepted by every current Claude model without the extended-output
250 - * beta header, so it is a safe per-request ceiling. Kept as a method (rather
251 - * 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).
252 323 *
253 - * @param string $model Model ID (reserved for future per-model limits).
324 + * @param string $model Model ID.
254 325 * @return int Maximum output tokens.
255 326 */
256 327 private function get_max_completion_tokens(string $model): int {
257 - 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;
258 352 }
259 353
260 354 /**
261 355 * Recommended output-token budget for a given use case.
@@ -278,9 +372,13 @@
278 372 'optimization' => 0.15,
279 373 ];
280 374 $percentage = $recommendations[$use_case] ?? 0.15;
281 375
282 - 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));
283 381 }
284 382
285 383 /**
286 384 * Test API connection